diff options
22 files changed, 1344 insertions, 77 deletions
diff --git a/api/current.txt b/api/current.txt index 43801a3dedf5..dea5cec7f60e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4923,6 +4923,7 @@ package android.app { method public int describeContents(); method public java.lang.String getGroup(); method public android.graphics.drawable.Icon getLargeIcon(); + method public java.lang.String getNotificationChannel(); method public android.graphics.drawable.Icon getSmallIcon(); method public java.lang.String getSortKey(); method public void writeToParcel(android.os.Parcel, int); @@ -5111,6 +5112,7 @@ package android.app { method public android.app.Notification.Builder setActions(android.app.Notification.Action...); method public android.app.Notification.Builder setAutoCancel(boolean); method public android.app.Notification.Builder setCategory(java.lang.String); + method public android.app.Notification.Builder setChannel(java.lang.String); method public android.app.Notification.Builder setChronometerCountDown(boolean); method public android.app.Notification.Builder setColor(int); method public deprecated android.app.Notification.Builder setContent(android.widget.RemoteViews); @@ -5306,17 +5308,40 @@ package android.app { field public static final int UNSET_ACTION_INDEX = -1; // 0xffffffff } + public final class NotificationChannel implements android.os.Parcelable { + ctor public NotificationChannel(java.lang.String, java.lang.CharSequence); + ctor protected NotificationChannel(android.os.Parcel); + method public boolean canBypassDnd(); + method public int describeContents(); + method public android.net.Uri getDefaultRingtone(); + method public java.lang.String getId(); + method public int getImportance(); + method public java.lang.CharSequence getName(); + method public void setDefaultRingtone(android.net.Uri); + method public void setLights(boolean); + method public void setVibration(boolean); + method public boolean shouldShowLights(); + method public boolean shouldVibrate(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.NotificationChannel> CREATOR; + field public static final java.lang.String DEFAULT_CHANNEL_ID = "miscellaneous"; + } + public class NotificationManager { method public java.lang.String addAutomaticZenRule(android.app.AutomaticZenRule); method public boolean areNotificationsEnabled(); method public void cancel(int); method public void cancel(java.lang.String, int); method public void cancelAll(); + method public void createNotificationChannel(android.app.NotificationChannel); + method public void deleteNotificationChannel(java.lang.String); method public android.service.notification.StatusBarNotification[] getActiveNotifications(); method public android.app.AutomaticZenRule getAutomaticZenRule(java.lang.String); method public java.util.Map<java.lang.String, android.app.AutomaticZenRule> getAutomaticZenRules(); method public final int getCurrentInterruptionFilter(); method public int getImportance(); + method public android.app.NotificationChannel getNotificationChannel(java.lang.String); + method public java.util.List<android.app.NotificationChannel> getNotificationChannels(); method public android.app.NotificationManager.Policy getNotificationPolicy(); method public boolean isNotificationPolicyAccessGranted(); method public void notify(int, android.app.Notification); @@ -5325,6 +5350,7 @@ package android.app { method public final void setInterruptionFilter(int); method public void setNotificationPolicy(android.app.NotificationManager.Policy); method public boolean updateAutomaticZenRule(java.lang.String, android.app.AutomaticZenRule); + method public void updateNotificationChannel(android.app.NotificationChannel); field public static final java.lang.String ACTION_INTERRUPTION_FILTER_CHANGED = "android.app.action.INTERRUPTION_FILTER_CHANGED"; field public static final java.lang.String ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED"; field public static final java.lang.String ACTION_NOTIFICATION_POLICY_CHANGED = "android.app.action.NOTIFICATION_POLICY_CHANGED"; diff --git a/api/system-current.txt b/api/system-current.txt index 350c4b848369..0911f0ac08d9 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5068,6 +5068,7 @@ package android.app { method public int describeContents(); method public java.lang.String getGroup(); method public android.graphics.drawable.Icon getLargeIcon(); + method public java.lang.String getNotificationChannel(); method public android.graphics.drawable.Icon getSmallIcon(); method public java.lang.String getSortKey(); method public void writeToParcel(android.os.Parcel, int); @@ -5258,6 +5259,7 @@ package android.app { method public android.app.Notification.Builder setActions(android.app.Notification.Action...); method public android.app.Notification.Builder setAutoCancel(boolean); method public android.app.Notification.Builder setCategory(java.lang.String); + method public android.app.Notification.Builder setChannel(java.lang.String); method public android.app.Notification.Builder setChronometerCountDown(boolean); method public android.app.Notification.Builder setColor(int); method public deprecated android.app.Notification.Builder setContent(android.widget.RemoteViews); @@ -5453,17 +5455,48 @@ package android.app { field public static final int UNSET_ACTION_INDEX = -1; // 0xffffffff } + public final class NotificationChannel implements android.os.Parcelable { + ctor public NotificationChannel(java.lang.String, java.lang.CharSequence); + ctor protected NotificationChannel(android.os.Parcel); + method public boolean canBypassDnd(); + method public int describeContents(); + method public android.net.Uri getDefaultRingtone(); + method public java.lang.String getId(); + method public int getImportance(); + method public int getLockscreenVisibility(); + method public java.lang.CharSequence getName(); + method public void populateFromXml(org.xmlpull.v1.XmlPullParser); + method public void setBypassDnd(boolean); + method public void setDefaultRingtone(android.net.Uri); + method public void setImportance(int); + method public void setLights(boolean); + method public void setLockscreenVisibility(int); + method public void setName(java.lang.CharSequence); + method public void setVibration(boolean); + method public boolean shouldShowLights(); + method public boolean shouldVibrate(); + method public org.json.JSONObject toJson() throws org.json.JSONException; + method public void writeToParcel(android.os.Parcel, int); + method public void writeXml(org.xmlpull.v1.XmlSerializer) throws java.io.IOException; + field public static final android.os.Parcelable.Creator<android.app.NotificationChannel> CREATOR; + field public static final java.lang.String DEFAULT_CHANNEL_ID = "miscellaneous"; + } + public class NotificationManager { method public java.lang.String addAutomaticZenRule(android.app.AutomaticZenRule); method public boolean areNotificationsEnabled(); method public void cancel(int); method public void cancel(java.lang.String, int); method public void cancelAll(); + method public void createNotificationChannel(android.app.NotificationChannel); + method public void deleteNotificationChannel(java.lang.String); method public android.service.notification.StatusBarNotification[] getActiveNotifications(); method public android.app.AutomaticZenRule getAutomaticZenRule(java.lang.String); method public java.util.Map<java.lang.String, android.app.AutomaticZenRule> getAutomaticZenRules(); method public final int getCurrentInterruptionFilter(); method public int getImportance(); + method public android.app.NotificationChannel getNotificationChannel(java.lang.String); + method public java.util.List<android.app.NotificationChannel> getNotificationChannels(); method public android.app.NotificationManager.Policy getNotificationPolicy(); method public boolean isNotificationPolicyAccessGranted(); method public void notify(int, android.app.Notification); @@ -5472,6 +5505,7 @@ package android.app { method public final void setInterruptionFilter(int); method public void setNotificationPolicy(android.app.NotificationManager.Policy); method public boolean updateAutomaticZenRule(java.lang.String, android.app.AutomaticZenRule); + method public void updateNotificationChannel(android.app.NotificationChannel); field public static final java.lang.String ACTION_INTERRUPTION_FILTER_CHANGED = "android.app.action.INTERRUPTION_FILTER_CHANGED"; field public static final java.lang.String ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED"; field public static final java.lang.String ACTION_NOTIFICATION_POLICY_CHANGED = "android.app.action.NOTIFICATION_POLICY_CHANGED"; @@ -37651,6 +37685,7 @@ package android.service.notification { method public void onNotificationVisibilityChanged(java.lang.String, long, boolean); field public static final int REASON_APP_CANCEL = 8; // 0x8 field public static final int REASON_APP_CANCEL_ALL = 9; // 0x9 + field public static final int REASON_CHANNEL_BANNED = 17; // 0x11 field public static final int REASON_DELEGATE_CANCEL = 2; // 0x2 field public static final int REASON_DELEGATE_CANCEL_ALL = 3; // 0x3 field public static final int REASON_DELEGATE_CLICK = 1; // 0x1 diff --git a/api/test-current.txt b/api/test-current.txt index 4e644ec8a964..5c065896cb97 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -4926,6 +4926,7 @@ package android.app { method public int describeContents(); method public java.lang.String getGroup(); method public android.graphics.drawable.Icon getLargeIcon(); + method public java.lang.String getNotificationChannel(); method public android.graphics.drawable.Icon getSmallIcon(); method public java.lang.String getSortKey(); method public void writeToParcel(android.os.Parcel, int); @@ -5114,6 +5115,7 @@ package android.app { method public android.app.Notification.Builder setActions(android.app.Notification.Action...); method public android.app.Notification.Builder setAutoCancel(boolean); method public android.app.Notification.Builder setCategory(java.lang.String); + method public android.app.Notification.Builder setChannel(java.lang.String); method public android.app.Notification.Builder setChronometerCountDown(boolean); method public android.app.Notification.Builder setColor(int); method public deprecated android.app.Notification.Builder setContent(android.widget.RemoteViews); @@ -5309,17 +5311,40 @@ package android.app { field public static final int UNSET_ACTION_INDEX = -1; // 0xffffffff } + public final class NotificationChannel implements android.os.Parcelable { + ctor public NotificationChannel(java.lang.String, java.lang.CharSequence); + ctor protected NotificationChannel(android.os.Parcel); + method public boolean canBypassDnd(); + method public int describeContents(); + method public android.net.Uri getDefaultRingtone(); + method public java.lang.String getId(); + method public int getImportance(); + method public java.lang.CharSequence getName(); + method public void setDefaultRingtone(android.net.Uri); + method public void setLights(boolean); + method public void setVibration(boolean); + method public boolean shouldShowLights(); + method public boolean shouldVibrate(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.NotificationChannel> CREATOR; + field public static final java.lang.String DEFAULT_CHANNEL_ID = "miscellaneous"; + } + public class NotificationManager { method public java.lang.String addAutomaticZenRule(android.app.AutomaticZenRule); method public boolean areNotificationsEnabled(); method public void cancel(int); method public void cancel(java.lang.String, int); method public void cancelAll(); + method public void createNotificationChannel(android.app.NotificationChannel); + method public void deleteNotificationChannel(java.lang.String); method public android.service.notification.StatusBarNotification[] getActiveNotifications(); method public android.app.AutomaticZenRule getAutomaticZenRule(java.lang.String); method public java.util.Map<java.lang.String, android.app.AutomaticZenRule> getAutomaticZenRules(); method public final int getCurrentInterruptionFilter(); method public int getImportance(); + method public android.app.NotificationChannel getNotificationChannel(java.lang.String); + method public java.util.List<android.app.NotificationChannel> getNotificationChannels(); method public android.app.NotificationManager.Policy getNotificationPolicy(); method public boolean isNotificationPolicyAccessGranted(); method public void notify(int, android.app.Notification); @@ -5328,6 +5353,7 @@ package android.app { method public final void setInterruptionFilter(int); method public void setNotificationPolicy(android.app.NotificationManager.Policy); method public boolean updateAutomaticZenRule(java.lang.String, android.app.AutomaticZenRule); + method public void updateNotificationChannel(android.app.NotificationChannel); field public static final java.lang.String ACTION_INTERRUPTION_FILTER_CHANGED = "android.app.action.INTERRUPTION_FILTER_CHANGED"; field public static final java.lang.String ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED"; field public static final java.lang.String ACTION_NOTIFICATION_POLICY_CHANGED = "android.app.action.NOTIFICATION_POLICY_CHANGED"; diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 532e4b063c4a..28224e8e2e2f 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -19,6 +19,7 @@ package android.app; import android.app.ITransientNotification; import android.app.Notification; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.ComponentName; import android.content.Intent; @@ -57,6 +58,15 @@ interface INotificationManager int getImportance(String pkg, int uid); int getPackageImportance(String pkg); + void createNotificationChannel(String pkg, in NotificationChannel channel); + void updateNotificationChannel(String pkg, in NotificationChannel channel); + void updateNotificationChannelForPackage(String pkg, int uid, in NotificationChannel channel); + NotificationChannel getNotificationChannel(String pkg, String channelId); + NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId); + void deleteNotificationChannel(String pkg, String channelId); + ParceledListSlice getNotificationChannels(String pkg); + ParceledListSlice getNotificationChannelsForPackage(String pkg, int uid); + // TODO: Remove this when callers have been migrated to the equivalent // INotificationListener method. StatusBarNotification[] getActiveNotifications(String callingPkg); diff --git a/core/java/android/app/Notification.aidl b/core/java/android/app/Notification.aidl index 3f1d1130b936..9d8129ca601a 100644 --- a/core/java/android/app/Notification.aidl +++ b/core/java/android/app/Notification.aidl @@ -17,4 +17,3 @@ package android.app; parcelable Notification; -parcelable Notification.Topic; diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 861274298444..2595c455e0be 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -992,6 +992,8 @@ public class Notification implements Parcelable private Icon mSmallIcon; private Icon mLargeIcon; + private String mChannelId; + /** * Structure to encapsulate a named action that can be shown as part of this notification. * It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is @@ -1675,6 +1677,10 @@ public class Notification implements Parcelable } color = parcel.readInt(); + + if (parcel.readInt() != 0) { + mChannelId = parcel.readString(); + } } @Override @@ -1780,6 +1786,8 @@ public class Notification implements Parcelable that.color = this.color; + that.mChannelId = this.mChannelId; + if (!heavy) { that.lightenPayload(); // will clean out extras } @@ -2028,6 +2036,13 @@ public class Notification implements Parcelable } parcel.writeInt(color); + + if (mChannelId != null) { + parcel.writeInt(1); + parcel.writeString(mChannelId); + } else { + parcel.writeInt(0); + } } /** @@ -2218,6 +2233,13 @@ public class Notification implements Parcelable } /** + * Returns the id of the channel this notification posts to. + */ + public String getNotificationChannel() { + return mChannelId; + } + + /** * The small icon representing this notification in the status bar and content view. * * @return the small icon representing this notification. @@ -2406,6 +2428,14 @@ public class Notification implements Parcelable } /** + * Specifies the channel the notification should be delivered on. + */ + public Builder setChannel(String channelId) { + mN.mChannelId = channelId; + return this; + } + + /** * Add a timestamp pertaining to the notification (usually the time the event occurred). * * For apps targeting {@link android.os.Build.VERSION_CODES#N} and above, this time is not diff --git a/core/java/android/app/NotificationChannel.aidl b/core/java/android/app/NotificationChannel.aidl new file mode 100644 index 000000000000..53e6863bac1f --- /dev/null +++ b/core/java/android/app/NotificationChannel.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +parcelable NotificationChannel;
\ No newline at end of file diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java new file mode 100644 index 000000000000..530b8bb7f3eb --- /dev/null +++ b/core/java/android/app/NotificationChannel.java @@ -0,0 +1,382 @@ +package android.app; + +import org.json.JSONException; +import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +import android.annotation.SystemApi; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.notification.NotificationListenerService; +import android.text.TextUtils; + +import java.io.IOException; + +/** + * A representation of settings that apply to a collection of similarly themed notifications. + */ +public final class NotificationChannel implements Parcelable { + + /** + * The id of the default channel for an app. All notifications posted without a notification + * channel specified are posted to this channel. + */ + public static final String DEFAULT_CHANNEL_ID = "miscellaneous"; + + private static final String TAG_CHANNEL = "channel"; + private static final String ATT_NAME = "name"; + private static final String ATT_ID = "id"; + private static final String ATT_PRIORITY = "priority"; + private static final String ATT_VISIBILITY = "visibility"; + private static final String ATT_IMPORTANCE = "importance"; + private static final String ATT_LIGHTS = "lights"; + private static final String ATT_VIBRATION = "vibration"; + private static final String ATT_DEFAULT_RINGTONE = "ringtone"; + + private static final int DEFAULT_VISIBILITY = + NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE; + private static final int DEFAULT_IMPORTANCE = + NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED; + + private final String mId; + private CharSequence mName; + private int mImportance = DEFAULT_IMPORTANCE; + private boolean mBypassDnd; + private int mLockscreenVisibility = DEFAULT_VISIBILITY; + private Uri mRingtone; + private boolean mLights; + private boolean mVibration; + + /** + * Creates a notification channel. + * + * @param id The id of the channel. Must be unique per package. + * @param name The user visible name of the channel. + */ + public NotificationChannel(String id, CharSequence name) { + this.mId = id; + this.mName = name; + } + + protected NotificationChannel(Parcel in) { + if (in.readByte() != 0) { + mId = in.readString(); + } else { + mId = null; + } + mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mImportance = in.readInt(); + mBypassDnd = in.readByte() != 0; + mLockscreenVisibility = in.readInt(); + if (in.readByte() != 0) { + mRingtone = Uri.CREATOR.createFromParcel(in); + } else { + mRingtone = null; + } + mLights = in.readByte() != 0; + mVibration = in.readByte() != 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mId != null) { + dest.writeByte((byte) 1); + dest.writeString(mId); + } else { + dest.writeByte((byte) 0); + } + TextUtils.writeToParcel(mName, dest, flags); + dest.writeInt(mImportance); + dest.writeByte(mBypassDnd ? (byte) 1 : (byte) 0); + dest.writeInt(mLockscreenVisibility); + if (mRingtone != null) { + dest.writeByte((byte) 1); + mRingtone.writeToParcel(dest, 0); + } else { + dest.writeByte((byte) 0); + } + dest.writeByte(mLights ? (byte) 1 : (byte) 0); + dest.writeByte(mVibration ? (byte) 1 : (byte) 0); + } + + // Only modifiable by users. + /** + * @hide + */ + @SystemApi + public void setName(CharSequence name) { + this.mName = name; + } + + /** + * @hide + */ + @SystemApi + public void setImportance(int importance) { + this.mImportance = importance; + } + + /** + * @hide + */ + @SystemApi + public void setBypassDnd(boolean bypassDnd) { + this.mBypassDnd = bypassDnd; + } + + /** + * @hide + */ + @SystemApi + public void setLockscreenVisibility(int lockscreenVisibility) { + this.mLockscreenVisibility = lockscreenVisibility; + } + + // Modifiable by apps. + + /** + * Sets the ringtone that should be played for notifications posted to this channel if + * the notifications don't supply a ringtone. + */ + public void setDefaultRingtone(Uri defaultRingtone) { + this.mRingtone = defaultRingtone; + } + + /** + * Sets whether notifications posted to this channel should display notification lights, + * on devices that support that feature. + */ + public void setLights(boolean lights) { + this.mLights = lights; + } + + /** + * Sets whether notification posted to this channel should vibrate, even if individual + * notifications are marked as having vibration. + */ + public void setVibration(boolean vibration) { + this.mVibration = vibration; + } + + /** + * Returns the id of this channel. + */ + public String getId() { + return mId; + } + + /** + * Returns the user visible name of this channel. + */ + public CharSequence getName() { + return mName; + } + + /** + * Returns the user specified importance {e.g. @link NotificationManager#IMPORTANCE_LOW} for + * notifications posted to this channel. + */ + public int getImportance() { + return mImportance; + } + + /** + * Whether or not notifications posted to this channel can bypass the Do Not Disturb + * {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY} mode. + */ + public boolean canBypassDnd() { + return mBypassDnd; + } + + /** + * Returns the notification sound for this channel. + */ + public Uri getDefaultRingtone() { + return mRingtone; + } + + /** + * Returns whether notifications posted to this channel trigger notification lights. + */ + public boolean shouldShowLights() { + return mLights; + } + + /** + * Returns whether notifications posted to this channel always vibrate. + */ + public boolean shouldVibrate() { + return mVibration; + } + + /** + * @hide + */ + @SystemApi + public int getLockscreenVisibility() { + return mLockscreenVisibility; + } + + /** + * @hide + */ + @SystemApi + public void populateFromXml(XmlPullParser parser) { + // Name and id are set in the constructor. + setImportance(safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE)); + setBypassDnd(Notification.PRIORITY_DEFAULT + != safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT)); + setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY)); + setDefaultRingtone(safeUri(parser, ATT_DEFAULT_RINGTONE)); + setLights(safeBool(parser, ATT_LIGHTS, false)); + setVibration(safeBool(parser, ATT_VIBRATION, false)); + } + + /** + * @hide + */ + @SystemApi + public void writeXml(XmlSerializer out) throws IOException { + out.startTag(null, TAG_CHANNEL); + out.attribute(null, ATT_ID, getId()); + out.attribute(null, ATT_NAME, getName().toString()); + if (getImportance() != DEFAULT_IMPORTANCE) { + out.attribute( + null, ATT_IMPORTANCE, Integer.toString(getImportance())); + } + if (canBypassDnd()) { + out.attribute( + null, ATT_PRIORITY, Integer.toString(Notification.PRIORITY_MAX)); + } + if (getLockscreenVisibility() != DEFAULT_VISIBILITY) { + out.attribute(null, ATT_VISIBILITY, + Integer.toString(getLockscreenVisibility())); + } + if (getDefaultRingtone() != null) { + out.attribute(null, ATT_DEFAULT_RINGTONE, getDefaultRingtone().toString()); + } + if (shouldShowLights()) { + out.attribute(null, ATT_LIGHTS, Boolean.toString(shouldShowLights())); + } + if (shouldVibrate()) { + out.attribute(null, ATT_VIBRATION, Boolean.toString(shouldVibrate())); + } + out.endTag(null, TAG_CHANNEL); + } + + /** + * @hide + */ + @SystemApi + public JSONObject toJson() throws JSONException { + JSONObject record = new JSONObject(); + record.put(ATT_ID, getId()); + record.put(ATT_NAME, getName()); + if (getImportance() != DEFAULT_IMPORTANCE) { + record.put(ATT_IMPORTANCE, + NotificationListenerService.Ranking.importanceToString(getImportance())); + } + if (canBypassDnd()) { + record.put(ATT_PRIORITY, Notification.PRIORITY_MAX); + } + if (getLockscreenVisibility() != DEFAULT_VISIBILITY) { + record.put(ATT_VISIBILITY, Notification.visibilityToString(getLockscreenVisibility())); + } + if (getDefaultRingtone() != null) { + record.put(ATT_DEFAULT_RINGTONE, getDefaultRingtone().toString()); + } + record.put(ATT_LIGHTS, Boolean.toString(shouldShowLights())); + record.put(ATT_VIBRATION, Boolean.toString(shouldVibrate())); + + return record; + } + + private static Uri safeUri(XmlPullParser parser, String att) { + final String val = parser.getAttributeValue(null, att); + return val == null ? null : Uri.parse(val); + } + + private static int safeInt(XmlPullParser parser, String att, int defValue) { + final String val = parser.getAttributeValue(null, att); + return tryParseInt(val, defValue); + } + + private static int tryParseInt(String value, int defValue) { + if (TextUtils.isEmpty(value)) return defValue; + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defValue; + } + } + + private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) { + final String value = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(value)) return defValue; + return Boolean.parseBoolean(value); + } + + public static final Creator<NotificationChannel> CREATOR = new Creator<NotificationChannel>() { + @Override + public NotificationChannel createFromParcel(Parcel in) { + return new NotificationChannel(in); + } + + @Override + public NotificationChannel[] newArray(int size) { + return new NotificationChannel[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NotificationChannel that = (NotificationChannel) o; + + if (mImportance != that.mImportance) return false; + if (mBypassDnd != that.mBypassDnd) return false; + if (mLockscreenVisibility != that.mLockscreenVisibility) return false; + if (mLights != that.mLights) return false; + if (mVibration != that.mVibration) return false; + if (!mId.equals(that.mId)) return false; + if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false; + return mRingtone != null ? mRingtone.equals( + that.mRingtone) : that.mRingtone == null; + } + + @Override + public String toString() { + return "NotificationChannel{" + + "mId='" + mId + '\'' + + ", mName=" + mName + + ", mImportance=" + mImportance + + ", mBypassDnd=" + mBypassDnd + + ", mLockscreenVisibility=" + mLockscreenVisibility + + ", mRingtone='" + mRingtone + '\'' + + ", mLights=" + mLights + + ", mVibration=" + mVibration + + '}'; + } + + @Override + public int hashCode() { + int result = mId.hashCode(); + result = 31 * result + (mName != null ? mName.hashCode() : 0); + result = 31 * result + mImportance; + result = 31 * result + (mBypassDnd ? 1 : 0); + result = 31 * result + mLockscreenVisibility; + result = 31 * result + (mRingtone != null ? mRingtone.hashCode() : 0); + result = 31 * result + (mLights ? 1 : 0); + result = 31 * result + (mVibration ? 1 : 0); + return result; + } +} diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index ff514bd7c81b..39cd2b5e901d 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -378,6 +378,66 @@ public class NotificationManager } /** + * Creates a notification channel that notifications can be posted to. + */ + public void createNotificationChannel(NotificationChannel channel) { + INotificationManager service = getService(); + try { + service.createNotificationChannel(mContext.getPackageName(), channel); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the notification channel settings for a given channel id. + */ + public NotificationChannel getNotificationChannel(String channelId) { + INotificationManager service = getService(); + try { + return service.getNotificationChannel(mContext.getPackageName(), channelId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns all notification channels created by the calling app. + */ + public List<NotificationChannel> getNotificationChannels() { + INotificationManager service = getService(); + try { + return service.getNotificationChannels(mContext.getPackageName()).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Updates settings for a given channel. + */ + public void updateNotificationChannel(NotificationChannel channel) { + INotificationManager service = getService(); + try { + service.updateNotificationChannel(mContext.getPackageName(), channel); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Deletes the given notification channel. + */ + public void deleteNotificationChannel(String channelId) { + INotificationManager service = getService(); + try { + service.deleteNotificationChannel(mContext.getPackageName(), channelId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @hide */ public ComponentName getEffectsSuppressor() { diff --git a/core/java/android/service/notification/NotificationRankerService.java b/core/java/android/service/notification/NotificationRankerService.java index ee5361aa9fe6..261d82de13c3 100644 --- a/core/java/android/service/notification/NotificationRankerService.java +++ b/core/java/android/service/notification/NotificationRankerService.java @@ -99,6 +99,9 @@ public abstract class NotificationRankerService extends NotificationListenerServ /** Autobundled summary notification was canceled because its group was unbundled */ public static final int REASON_UNAUTOBUNDLED = 16; + /** Notification was canceled by the user banning the channel. */ + public static final int REASON_CHANNEL_BANNED = 17; + private Handler mHandler; /** @hide */ diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index e20a70bfa282..f1118d76d732 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4344,6 +4344,8 @@ <item quantity="other"><xliff:g id="count" example="3">%1$d</xliff:g> selected</item> </plurals> + <string name="default_notification_channel_label">Miscellaneous</string> + <string name="importance_from_user">You set the importance of these notifications.</string> <string name="importance_from_person">This is important because of the people involved.</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 7ccbb9ec9e5f..cf6fdc8113fa 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2470,6 +2470,7 @@ <java-symbol type="dimen" name="notification_content_margin_top" /> <java-symbol type="dimen" name="notification_content_margin_bottom" /> <java-symbol type="dimen" name="notification_header_background_height" /> + <java-symbol type="string" name="default_notification_channel_label" /> <java-symbol type="string" name="importance_from_user" /> <java-symbol type="string" name="importance_from_person" /> diff --git a/services/core/java/com/android/server/notification/ImportanceExtractor.java b/services/core/java/com/android/server/notification/ImportanceExtractor.java index 885b9b7919a7..3bdc22c17383 100644 --- a/services/core/java/com/android/server/notification/ImportanceExtractor.java +++ b/services/core/java/com/android/server/notification/ImportanceExtractor.java @@ -15,6 +15,7 @@ */ package com.android.server.notification; +import android.app.NotificationManager; import android.content.Context; import android.util.Slog; @@ -41,10 +42,17 @@ public class ImportanceExtractor implements NotificationSignalExtractor { if (DBG) Slog.d(TAG, "missing config"); return null; } - - record.setUserImportance( - mConfig.getImportance(record.sbn.getPackageName(), record.sbn.getUid())); - + int importance = NotificationManager.IMPORTANCE_UNSPECIFIED; + int appImportance = mConfig.getImportance( + record.sbn.getPackageName(), record.sbn.getUid()); + int channelImportance = record.getChannel().getImportance(); + if (appImportance == NotificationManager.IMPORTANCE_UNSPECIFIED) { + record.setUserImportance(channelImportance); + } else if (channelImportance == NotificationManager.IMPORTANCE_UNSPECIFIED) { + record.setUserImportance(appImportance); + } else { + record.setUserImportance(Math.min(appImportance, channelImportance)); + } return null; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 14728ac30d3f..6b2df73de8a6 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -18,6 +18,7 @@ package com.android.server.notification; import static android.service.notification.NotificationRankerService.REASON_APP_CANCEL; import static android.service.notification.NotificationRankerService.REASON_APP_CANCEL_ALL; +import static android.service.notification.NotificationRankerService.REASON_CHANNEL_BANNED; import static android.service.notification.NotificationRankerService.REASON_DELEGATE_CANCEL; import static android.service.notification.NotificationRankerService.REASON_DELEGATE_CANCEL_ALL; import static android.service.notification.NotificationRankerService.REASON_DELEGATE_CLICK; @@ -55,6 +56,7 @@ import android.app.IActivityManager; import android.app.INotificationManager; import android.app.ITransientNotification; import android.app.Notification; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.NotificationManager.Policy; import android.app.PendingIntent; @@ -746,8 +748,8 @@ public class NotificationManagerService extends SystemService { if (pkgList != null && (pkgList.length > 0)) { for (String pkgName : pkgList) { if (cancelNotifications) { - cancelAllNotificationsInt(MY_UID, MY_PID, pkgName, 0, 0, !queryRestart, - changeUserId, reason, null); + cancelAllNotificationsInt(MY_UID, MY_PID, pkgName, null, 0, 0, + !queryRestart, changeUserId, reason, null); } } } @@ -779,13 +781,13 @@ public class NotificationManagerService extends SystemService { } else if (action.equals(Intent.ACTION_USER_STOPPED)) { int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); if (userHandle >= 0) { - cancelAllNotificationsInt(MY_UID, MY_PID, null, 0, 0, true, userHandle, + cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, true, userHandle, REASON_USER_STOPPED, null); } } else if (action.equals(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); if (userHandle >= 0) { - cancelAllNotificationsInt(MY_UID, MY_PID, null, 0, 0, true, userHandle, + cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, true, userHandle, REASON_PROFILE_TURNED_OFF, null); } } else if (action.equals(Intent.ACTION_USER_PRESENT)) { @@ -907,6 +909,28 @@ public class NotificationManagerService extends SystemService { } @VisibleForTesting + void setStatusBarManager(StatusBarManagerInternal statusBar) { + mStatusBar = statusBar; + } + + @VisibleForTesting + void setLights(Light light) { + mNotificationLight = light; + mAttentionLight = light; + } + + @VisibleForTesting + void setScreenOn(boolean on) { + mScreenOn = on; + } + + @VisibleForTesting + void addNotification(NotificationRecord r) { + mNotificationList.add(r); + mNotificationsByKey.put(r.sbn.getKey(), r); + } + + @VisibleForTesting void setSystemReady(boolean systemReady) { mSystemReady = systemReady; } @@ -1149,8 +1173,8 @@ public class NotificationManagerService extends SystemService { // Now, cancel any outstanding notifications that are part of a just-disabled app if (ENABLE_BLOCKED_NOTIFICATIONS && !enabled) { - cancelAllNotificationsInt(MY_UID, MY_PID, pkg, 0, 0, true, UserHandle.getUserId(uid), - REASON_PACKAGE_BANNED, null); + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, null, 0, 0, true, + UserHandle.getUserId(uid), REASON_PACKAGE_BANNED, null); } } @@ -1408,7 +1432,7 @@ public class NotificationManagerService extends SystemService { // Calling from user space, don't allow the canceling of actively // running foreground services. cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(), - pkg, 0, Notification.FLAG_FOREGROUND_SERVICE, true, userId, + pkg, null, 0, Notification.FLAG_FOREGROUND_SERVICE, true, userId, REASON_APP_CANCEL_ALL, null); } @@ -1486,6 +1510,87 @@ public class NotificationManagerService extends SystemService { return mRankingHelper.getImportance(pkg, uid); } + @Override + public void createNotificationChannel(String pkg, NotificationChannel channel) { + Preconditions.checkNotNull(channel); + Preconditions.checkNotNull(channel.getId()); + Preconditions.checkNotNull(channel.getName()); + checkCallerIsSystemOrSameApp(pkg); + mRankingHelper.createNotificationChannel(pkg, Binder.getCallingUid(), channel); + savePolicyFile(); + } + + @Override + public void updateNotificationChannel(String pkg, NotificationChannel channel) { + Preconditions.checkNotNull(channel); + Preconditions.checkNotNull(channel.getId()); + checkCallerIsSystemOrSameApp(pkg); + if (channel.getImportance() == NotificationManager.IMPORTANCE_NONE) { + // cancel + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channel.getId(), 0, 0, true, + UserHandle.getUserId(Binder.getCallingUid()), REASON_CHANNEL_BANNED, null); + } + mRankingHelper.updateNotificationChannel(Binder.getCallingUid(), pkg, + Binder.getCallingUid(), channel); + savePolicyFile(); + } + + @Override + public NotificationChannel getNotificationChannel(String pkg, String channelId) { + Preconditions.checkNotNull(channelId); + checkCallerIsSystemOrSameApp(pkg); + return mRankingHelper.getNotificationChannel(pkg, Binder.getCallingUid(), channelId); + } + + @Override + public NotificationChannel getNotificationChannelForPackage(String pkg, int uid, + String channelId) { + Preconditions.checkNotNull(channelId); + checkCallerIsSystem(); + return mRankingHelper.getNotificationChannel(pkg, uid, channelId); + } + + @Override + public void deleteNotificationChannel(String pkg, String channelId) { + Preconditions.checkNotNull(channelId); + checkCallerIsSystemOrSameApp(pkg); + if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) { + throw new IllegalArgumentException("Cannot delete default channel"); + } + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true, + UserHandle.getUserId(Binder.getCallingUid()), REASON_CHANNEL_BANNED, null); + mRankingHelper.deleteNotificationChannel(pkg, Binder.getCallingUid(), channelId); + savePolicyFile(); + } + + @Override + public void updateNotificationChannelForPackage(String pkg, int uid, + NotificationChannel channel) { + Preconditions.checkNotNull(channel); + Preconditions.checkNotNull(channel.getId()); + checkCallerIsSystem(); + if (channel.getImportance() == NotificationManager.IMPORTANCE_NONE) { + // cancel + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channel.getId(), 0, 0, true, + UserHandle.getUserId(Binder.getCallingUid()), REASON_CHANNEL_BANNED, null); + } + mRankingHelper.updateNotificationChannel(Binder.getCallingUid(), pkg, uid, channel); + savePolicyFile(); + } + + @Override + public ParceledListSlice<NotificationChannel> getNotificationChannelsForPackage(String pkg, + int uid) { + checkCallerIsSystem(); + return mRankingHelper.getNotificationChannels(pkg, uid); + } + + @Override + public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg) { + checkCallerIsSystemOrSameApp(pkg); + return mRankingHelper.getNotificationChannels(pkg, Binder.getCallingUid()); + } + /** * System-only API for getting a list of current (i.e. not cleared) notifications. * @@ -2335,7 +2440,10 @@ public class NotificationManagerService extends SystemService { summaryNotification, adjustedSbn.getUser(), newAutoBundleKey, System.currentTimeMillis()); - summaryRecord = new NotificationRecord(getContext(), summarySbn); + summaryRecord = new NotificationRecord(getContext(), summarySbn, + mRankingHelper.getNotificationChannel(adjustedSbn.getPackageName(), + adjustedSbn.getUid(), + adjustedSbn.getNotification().getNotificationChannel())); summaries.put(adjustment.getPackage(), summarySbn.getKey()); } } @@ -2639,7 +2747,9 @@ public class NotificationManagerService extends SystemService { Notification.PRIORITY_MAX); // setup local book-keeping - final NotificationRecord r = new NotificationRecord(getContext(), n); + final NotificationRecord r = new NotificationRecord(getContext(), n, + mRankingHelper.getNotificationChannel(pkg, callingUid, + n.getNotification().getNotificationChannel())); mHandler.post(new EnqueueNotificationRunnable(userId, r)); idOut[0] = id; @@ -2697,7 +2807,8 @@ public class NotificationManagerService extends SystemService { final boolean isPackageSuspended = isPackageSuspendedForUser(pkg, callingUid); // blocked apps - if (r.getImportance() == NotificationListenerService.Ranking.IMPORTANCE_NONE + if (r.getImportance() == NotificationManager.IMPORTANCE_NONE + || r.getChannel().getImportance() == NotificationManager.IMPORTANCE_NONE || !noteNotificationOp(pkg, callingUid) || isPackageSuspended) { if (!isSystemNotification) { if (isPackageSuspended) { @@ -2865,9 +2976,8 @@ public class NotificationManagerService extends SystemService { // DEFAULT_SOUND or because notification.sound is pointing at // Settings.System.NOTIFICATION_SOUND) final boolean useDefaultSound = - (notification.defaults & Notification.DEFAULT_SOUND) != 0 || - Settings.System.DEFAULT_NOTIFICATION_URI - .equals(notification.sound); + (notification.defaults & Notification.DEFAULT_SOUND) != 0 + || Settings.System.DEFAULT_NOTIFICATION_URI.equals(notification.sound); Uri soundUri = null; if (useDefaultSound) { @@ -2878,6 +2988,9 @@ public class NotificationManagerService extends SystemService { } else if (notification.sound != null) { soundUri = notification.sound; hasValidSound = (soundUri != null); + } else if (record.getChannel().getDefaultRingtone() != null) { + soundUri = record.getChannel().getDefaultRingtone(); + hasValidSound = (soundUri != null); } // Does the notification want to specify its own vibration? @@ -2894,8 +3007,10 @@ public class NotificationManagerService extends SystemService { final boolean useDefaultVibrate = (notification.defaults & Notification.DEFAULT_VIBRATE) != 0; + final boolean hasChannelVibration = record.getChannel().shouldVibrate(); + hasValidVibrate = useDefaultVibrate || convertSoundToVibration || - hasCustomVibrate; + hasCustomVibrate || hasChannelVibration; // We can alert, and we're allowed to alert, but if the developer asked us to only do // it once, and we already have, then don't. @@ -2931,26 +3046,13 @@ public class NotificationManagerService extends SystemService { } } } - if (hasValidVibrate && !(mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT)) { mVibrateNotificationKey = key; if (useDefaultVibrate || convertSoundToVibration) { - // Escalate privileges so we can use the vibrator even if the - // notifying app does not have the VIBRATE permission. - long identity = Binder.clearCallingIdentity(); - try { - mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), - useDefaultVibrate ? mDefaultVibrationPattern - : mFallbackVibrationPattern, - ((notification.flags & Notification.FLAG_INSISTENT) != 0) - ? 0: -1, audioAttributesForNotification(notification)); - buzz = true; - } finally { - Binder.restoreCallingIdentity(identity); - } - } else if (notification.vibrate.length > 1) { + playNonCustomVibration(record, useDefaultVibrate); + } else if (notification.vibrate != null && notification.vibrate.length > 1) { // If you want your own vibration pattern, you need the VIBRATE // permission mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), @@ -2958,6 +3060,8 @@ public class NotificationManagerService extends SystemService { ((notification.flags & Notification.FLAG_INSISTENT) != 0) ? 0: -1, audioAttributesForNotification(notification)); buzz = true; + } else if (hasChannelVibration) { + playNonCustomVibration(record, useDefaultVibrate); } } } @@ -2975,7 +3079,7 @@ public class NotificationManagerService extends SystemService { // light // release the light boolean wasShowLights = mLights.remove(key); - if ((notification.flags & Notification.FLAG_SHOW_LIGHTS) != 0 && aboveThreshold + if (shouldShowLights(record) && aboveThreshold && ((record.getSuppressedVisualEffects() & NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF) == 0)) { mLights.add(key); @@ -2999,6 +3103,28 @@ public class NotificationManagerService extends SystemService { } } + private boolean shouldShowLights(final NotificationRecord record) { + return record.getChannel().shouldShowLights() + || (record.getNotification().flags & Notification.FLAG_SHOW_LIGHTS) != 0; + } + + private boolean playNonCustomVibration(final NotificationRecord record, + boolean useDefaultVibrate) { + // Escalate privileges so we can use the vibrator even if the + // notifying app does not have the VIBRATE permission. + long identity = Binder.clearCallingIdentity(); + try { + mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), + useDefaultVibrate ? mDefaultVibrationPattern + : mFallbackVibrationPattern, + ((record.getNotification().flags & Notification.FLAG_INSISTENT) != 0) + ? 0: -1, audioAttributesForNotification(record.getNotification())); + return true; + } finally{ + Binder.restoreCallingIdentity(identity); + } + } + private static AudioAttributes audioAttributesForNotification(Notification n) { if (n.audioAttributes != null && !Notification.AUDIO_ATTRIBUTES_DEFAULT.equals(n.audioAttributes)) { @@ -3489,8 +3615,8 @@ public class NotificationManagerService extends SystemService { * Cancels all notifications from a given package that have all of the * {@code mustHaveFlags}. */ - boolean cancelAllNotificationsInt(int callingUid, int callingPid, String pkg, int mustHaveFlags, - int mustNotHaveFlags, boolean doit, int userId, int reason, + boolean cancelAllNotificationsInt(int callingUid, int callingPid, String pkg, String channelId, + int mustHaveFlags, int mustNotHaveFlags, boolean doit, int userId, int reason, ManagedServiceInfo listener) { String listenerName = listener == null ? null : listener.component.toShortString(); EventLogTags.writeNotificationCancelAll(callingUid, callingPid, @@ -3518,6 +3644,9 @@ public class NotificationManagerService extends SystemService { if (pkg != null && !r.sbn.getPackageName().equals(pkg)) { continue; } + if (channelId == null || !channelId.equals(r.getChannel().getId())) { + continue; + } if (canceledNotifications == null) { canceledNotifications = new ArrayList<>(); } @@ -3636,7 +3765,8 @@ public class NotificationManagerService extends SystemService { int ledARGB = ledno.ledARGB; int ledOnMS = ledno.ledOnMS; int ledOffMS = ledno.ledOffMS; - if ((ledno.defaults & Notification.DEFAULT_LIGHTS) != 0) { + if ((ledno.defaults & Notification.DEFAULT_LIGHTS) != 0 + || (ledno.flags & Notification.FLAG_SHOW_LIGHTS) == 0) { ledARGB = mDefaultNotificationColor; ledOnMS = mDefaultNotificationLedOn; ledOffMS = mDefaultNotificationLedOff; diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index b2198d7d77c2..c5de93e921b4 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -23,12 +23,14 @@ import static android.service.notification.NotificationListenerService.Ranking.I import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_MAX; import android.app.Notification; +import android.app.NotificationChannel; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Icon; import android.media.AudioAttributes; +import android.net.Uri; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; @@ -101,8 +103,11 @@ public final class NotificationRecord { private String mUserExplanation; private String mPeopleExplanation; + private NotificationChannel mNotificationChannel; + @VisibleForTesting - public NotificationRecord(Context context, StatusBarNotification sbn) + public NotificationRecord(Context context, StatusBarNotification sbn, + NotificationChannel channel) { this.sbn = sbn; mOriginalFlags = sbn.getNotification().flags; @@ -111,6 +116,7 @@ public final class NotificationRecord { mUpdateTimeMs = mCreationTimeMs; mContext = context; stats = new NotificationUsageStats.SingleNotificationStats(); + mNotificationChannel = channel; mImportance = defaultImportance(); } @@ -145,7 +151,9 @@ public final class NotificationRecord { boolean isNoisy = (n.defaults & Notification.DEFAULT_SOUND) != 0 || (n.defaults & Notification.DEFAULT_VIBRATE) != 0 || n.sound != null - || n.vibrate != null; + || n.vibrate != null + || mNotificationChannel.shouldVibrate() + || mNotificationChannel.getDefaultRingtone() != null; stats.isNoisy = isNoisy; if (!isNoisy && importance > IMPORTANCE_LOW) { @@ -283,6 +291,7 @@ public final class NotificationRecord { pw.println(prefix + " mVisibleSinceMs=" + mVisibleSinceMs); pw.println(prefix + " mUpdateTimeMs=" + mUpdateTimeMs); pw.println(prefix + " mSuppressedVisualEffects= " + mSuppressedVisualEffects); + pw.println(prefix + " mNotificationChannel= " + mNotificationChannel); } @@ -527,4 +536,8 @@ public final class NotificationRecord { public boolean isImportanceFromUser() { return mImportance == mUserImportance; } + + public NotificationChannel getChannel() { + return mNotificationChannel; + } } diff --git a/services/core/java/com/android/server/notification/PriorityExtractor.java b/services/core/java/com/android/server/notification/PriorityExtractor.java index 6c764761d738..666cf00dc2ce 100644 --- a/services/core/java/com/android/server/notification/PriorityExtractor.java +++ b/services/core/java/com/android/server/notification/PriorityExtractor.java @@ -15,6 +15,7 @@ */ package com.android.server.notification; +import android.app.Notification; import android.content.Context; import android.util.Slog; @@ -42,8 +43,11 @@ public class PriorityExtractor implements NotificationSignalExtractor { return null; } - record.setPackagePriority( - mConfig.getPriority(record.sbn.getPackageName(), record.sbn.getUid())); + int priority = mConfig.getPriority(record.sbn.getPackageName(), record.sbn.getUid()); + if (priority == Notification.PRIORITY_DEFAULT && record.getChannel().canBypassDnd()){ + priority = Notification.PRIORITY_MAX; + } + record.setPackagePriority(priority); return null; } diff --git a/services/core/java/com/android/server/notification/RankingConfig.java b/services/core/java/com/android/server/notification/RankingConfig.java index b5cc2efcabc2..2df4043190ac 100644 --- a/services/core/java/com/android/server/notification/RankingConfig.java +++ b/services/core/java/com/android/server/notification/RankingConfig.java @@ -15,6 +15,9 @@ */ package com.android.server.notification; +import android.app.NotificationChannel; +import android.content.pm.ParceledListSlice; + public interface RankingConfig { int getPriority(String packageName, int uid); @@ -28,4 +31,10 @@ public interface RankingConfig { void setImportance(String packageName, int uid, int importance); int getImportance(String packageName, int uid); + + void createNotificationChannel(String pkg, int uid, NotificationChannel channel); + void updateNotificationChannel(int callingUid, String pkg, int uid, NotificationChannel channel); + NotificationChannel getNotificationChannel(String pkg, int uid, String channelId); + void deleteNotificationChannel(String pkg, int uid, String channelId); + ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid); } diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java index 32c5b13d4813..7182da151441 100644 --- a/services/core/java/com/android/server/notification/RankingHelper.java +++ b/services/core/java/com/android/server/notification/RankingHelper.java @@ -16,16 +16,19 @@ package com.android.server.notification; import android.app.Notification; +import android.app.NotificationChannel; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ParceledListSlice; +import android.os.Process; import android.os.UserHandle; import android.service.notification.NotificationListenerService.Ranking; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Slog; -import com.android.server.notification.NotificationManagerService.DumpFilter; +import com.android.internal.R; import org.json.JSONArray; import org.json.JSONException; @@ -38,6 +41,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -48,15 +52,15 @@ public class RankingHelper implements RankingConfig { private static final String TAG_RANKING = "ranking"; private static final String TAG_PACKAGE = "package"; - private static final String ATT_VERSION = "version"; + private static final String TAG_CHANNEL = "channel"; + private static final String ATT_VERSION = "version"; private static final String ATT_NAME = "name"; private static final String ATT_UID = "uid"; + private static final String ATT_ID = "id"; private static final String ATT_PRIORITY = "priority"; private static final String ATT_VISIBILITY = "visibility"; private static final String ATT_IMPORTANCE = "importance"; - private static final String ATT_TOPIC_ID = "id"; - private static final String ATT_TOPIC_LABEL = "label"; private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT; private static final int DEFAULT_VISIBILITY = Ranking.VISIBILITY_NO_OVERRIDE; @@ -166,6 +170,28 @@ public class RankingHelper implements RankingConfig { r.importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); r.priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY); r.visibility = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY); + + final int innerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > innerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (TAG_CHANNEL.equals(tagName)) { + String id = parser.getAttributeValue(null, ATT_ID); + CharSequence channelName = parser.getAttributeValue(null, ATT_NAME); + + if (!TextUtils.isEmpty(id)) { + final NotificationChannel channel = + new NotificationChannel(id, channelName); + channel.populateFromXml(parser); + r.channels.put(id, channel); + } + } + } } } } @@ -184,11 +210,18 @@ public class RankingHelper implements RankingConfig { r = new Record(); r.pkg = pkg; r.uid = uid; + NotificationChannel defaultChannel = createDefaultChannel(); + r.channels.put(defaultChannel.getId(), defaultChannel); mRecords.put(key, r); } return r; } + private NotificationChannel createDefaultChannel() { + return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, + mContext.getString(R.string.default_notification_channel_label)); + } + public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { out.startTag(null, TAG_RANKING); out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); @@ -201,7 +234,8 @@ public class RankingHelper implements RankingConfig { continue; } final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE - || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY; + || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY + || r.channels.size() > 0; if (hasNonDefaultSettings) { out.startTag(null, TAG_PACKAGE); out.attribute(null, ATT_NAME, r.pkg); @@ -219,6 +253,10 @@ public class RankingHelper implements RankingConfig { out.attribute(null, ATT_UID, Integer.toString(r.uid)); } + for (NotificationChannel channel : r.channels.values()) { + channel.writeXml(out); + } + out.endTag(null, TAG_PACKAGE); } } @@ -309,11 +347,6 @@ public class RankingHelper implements RankingConfig { } } - private static boolean tryParseBool(String value, boolean defValue) { - if (TextUtils.isEmpty(value)) return defValue; - return Boolean.parseBoolean(value); - } - /** * Gets priority. */ @@ -356,6 +389,65 @@ public class RankingHelper implements RankingConfig { return getOrCreateRecord(packageName, uid).importance; } + @Override + public void createNotificationChannel(String pkg, int uid, NotificationChannel channel) { + Record r = getOrCreateRecord(pkg, uid); + if (r.channels.containsKey(channel.getId()) || channel.getName().equals( + mContext.getString(R.string.default_notification_channel_label))) { + throw new IllegalArgumentException("Channel already exists"); + } + if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { + channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); + } + r.channels.put(channel.getId(), channel); + updateConfig(); + } + + @Override + public void updateNotificationChannel(int callingUid, String pkg, int uid, + NotificationChannel updatedChannel) { + Record r = getOrCreateRecord(pkg, uid); + NotificationChannel channel = r.channels.get(updatedChannel.getId()); + if (channel == null) { + throw new IllegalArgumentException("Channel does not exist"); + } + if (!isUidSystem(callingUid)) { + updatedChannel.setImportance(channel.getImportance()); + updatedChannel.setName(channel.getName()); + } + if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { + updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); + } + r.channels.put(updatedChannel.getId(), updatedChannel); + updateConfig(); + } + + @Override + public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId) { + Record r = getOrCreateRecord(pkg, uid); + if (channelId == null) { + channelId = NotificationChannel.DEFAULT_CHANNEL_ID; + } + return r.channels.get(channelId); + } + + @Override + public void deleteNotificationChannel(String pkg, int uid, String channelId) { + Record r = getOrCreateRecord(pkg, uid); + r.channels.remove(channelId); + } + + @Override + public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid) { + List<NotificationChannel> channels = new ArrayList<>(); + Record r = getOrCreateRecord(pkg, uid); + int N = r.channels.size(); + for (int i = 0; i < N; i++) { + channels.add(r.channels.valueAt(i)); + } + return new ParceledListSlice<NotificationChannel>(channels); + } + /** * Sets importance. */ @@ -420,6 +512,12 @@ public class RankingHelper implements RankingConfig { pw.print(Notification.visibilityToString(r.visibility)); } pw.println(); + for (NotificationChannel channel : r.channels.values()) { + pw.print(prefix); + pw.print(" "); + pw.print(" "); + pw.println(channel); + } } } } @@ -449,6 +547,9 @@ public class RankingHelper implements RankingConfig { if (r.visibility != DEFAULT_VISIBILITY) { record.put("visibility", Notification.visibilityToString(r.visibility)); } + for (NotificationChannel channel : r.channels.values()) { + record.put("channel", channel.toJson()); + } } catch (JSONException e) { // pass } @@ -530,6 +631,11 @@ public class RankingHelper implements RankingConfig { } } + private static boolean isUidSystem(int uid) { + final int appid = UserHandle.getAppId(uid); + return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0); + } + private static class Record { static int UNKNOWN_UID = UserHandle.USER_NULL; @@ -538,5 +644,7 @@ public class RankingHelper implements RankingConfig { int importance = DEFAULT_IMPORTANCE; int priority = DEFAULT_PRIORITY; int visibility = DEFAULT_VISIBILITY; + + ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); } } diff --git a/services/core/java/com/android/server/notification/VisibilityExtractor.java b/services/core/java/com/android/server/notification/VisibilityExtractor.java index 2da2b2f3c264..9d0e506fd68b 100644 --- a/services/core/java/com/android/server/notification/VisibilityExtractor.java +++ b/services/core/java/com/android/server/notification/VisibilityExtractor.java @@ -16,6 +16,7 @@ package com.android.server.notification; import android.content.Context; +import android.service.notification.NotificationListenerService; import android.util.Slog; /** @@ -42,8 +43,12 @@ public class VisibilityExtractor implements NotificationSignalExtractor { return null; } - record.setPackageVisibilityOverride( - mConfig.getVisibilityOverride(record.sbn.getPackageName(), record.sbn.getUid())); + int visibility = + mConfig.getVisibilityOverride(record.sbn.getPackageName(), record.sbn.getUid()); + if (visibility == NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) { + visibility = record.getChannel().getLockscreenVisibility(); + } + record.setPackageVisibilityOverride(visibility); return null; } diff --git a/services/tests/servicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/servicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java index f1f708c486d9..5fe000aa62de 100644 --- a/services/tests/servicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ b/services/tests/servicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java @@ -15,6 +15,9 @@ */ package com.android.server.notification; +import com.android.server.lights.Light; +import com.android.server.statusbar.StatusBarManagerInternal; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -22,7 +25,10 @@ import org.junit.runner.RunWith; import android.app.ActivityManager; import android.app.Notification; import android.app.Notification.Builder; +import android.app.NotificationManager; import android.content.Context; +import android.app.NotificationChannel; +import android.graphics.Color; import android.media.AudioAttributes; import android.media.AudioManager; import android.net.Uri; @@ -30,6 +36,7 @@ import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.os.Vibrator; +import android.provider.Settings; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.StatusBarNotification; import android.support.test.InstrumentationRegistry; @@ -57,6 +64,8 @@ public class BuzzBeepBlinkTest { @Mock AudioManager mAudioManager; @Mock Vibrator mVibrator; @Mock android.media.IRingtonePlayer mRingtonePlayer; + @Mock StatusBarManagerInternal mStatusBar; + @Mock Light mLight; @Mock Handler mHandler; private NotificationManagerService mService; @@ -69,6 +78,15 @@ public class BuzzBeepBlinkTest { private int mScore = 10; private android.os.UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser()); + private static final long[] CUSTOM_VIBRATION = new long[] { + 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, + 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, + 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400 }; + private static final Uri CUSTOM_SOUND = Settings.System.DEFAULT_ALARM_ALERT_URI; + private static final int CUSTOM_LIGHT_COLOR = Color.BLACK; + private static final int CUSTOM_LIGHT_ON = 10000; + private static final int CUSTOM_LIGHT_OFF = 10000; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -83,6 +101,9 @@ public class BuzzBeepBlinkTest { mService.setVibrator(mVibrator); mService.setSystemReady(true); mService.setHandler(mHandler); + mService.setStatusBarManager(mStatusBar); + mService.setLights(mLight); + mService.setScreenOn(false); mService.setSystemNotificationSound("beep!"); } @@ -92,56 +113,85 @@ public class BuzzBeepBlinkTest { private NotificationRecord getNoisyOtherNotification() { return getNotificationRecord(mOtherId, false /* insistent */, false /* once */, - true /* noisy */, true /* buzzy*/); + true /* noisy */, true /* buzzy*/, false /* lights */); } private NotificationRecord getBeepyNotification() { return getNotificationRecord(mId, false /* insistent */, false /* once */, - true /* noisy */, false /* buzzy*/); + true /* noisy */, false /* buzzy*/, false /* lights */); } private NotificationRecord getBeepyOnceNotification() { return getNotificationRecord(mId, false /* insistent */, true /* once */, - true /* noisy */, false /* buzzy*/); + true /* noisy */, false /* buzzy*/, false /* lights */); } private NotificationRecord getQuietNotification() { return getNotificationRecord(mId, false /* insistent */, false /* once */, - false /* noisy */, false /* buzzy*/); + false /* noisy */, false /* buzzy*/, false /* lights */); } private NotificationRecord getQuietOtherNotification() { return getNotificationRecord(mOtherId, false /* insistent */, false /* once */, - false /* noisy */, false /* buzzy*/); + false /* noisy */, false /* buzzy*/, false /* lights */); } private NotificationRecord getQuietOnceNotification() { return getNotificationRecord(mId, false /* insistent */, true /* once */, - false /* noisy */, false /* buzzy*/); + false /* noisy */, false /* buzzy*/, false /* lights */); } private NotificationRecord getInsistentBeepyNotification() { return getNotificationRecord(mId, true /* insistent */, false /* once */, - true /* noisy */, false /* buzzy*/); + true /* noisy */, false /* buzzy*/, false /* lights */); } private NotificationRecord getBuzzyNotification() { return getNotificationRecord(mId, false /* insistent */, false /* once */, - false /* noisy */, true /* buzzy*/); + false /* noisy */, true /* buzzy*/, false /* lights */); } private NotificationRecord getBuzzyOnceNotification() { return getNotificationRecord(mId, false /* insistent */, true /* once */, - false /* noisy */, true /* buzzy*/); + false /* noisy */, true /* buzzy*/, false /* lights */); } private NotificationRecord getInsistentBuzzyNotification() { return getNotificationRecord(mId, true /* insistent */, false /* once */, - false /* noisy */, true /* buzzy*/); + false /* noisy */, true /* buzzy*/, false /* lights */); + } + + private NotificationRecord getLightsNotification() { + return getNotificationRecord(mId, false /* insistent */, true /* once */, + false /* noisy */, true /* buzzy*/, true /* lights */); + } + + private NotificationRecord getCustomBuzzyOnceNotification() { + return getNotificationRecord(mId, false /* insistent */, true /* once */, + false /* noisy */, true /* buzzy*/, false /* lights */, + false /* defaultVibration */, true /* defaultSound */, true /* defaultLights */); + } + + private NotificationRecord getCustomBeepyNotification() { + return getNotificationRecord(mId, false /* insistent */, false /* once */, + true /* noisy */, false /* buzzy*/, false /* lights */, + true /* defaultVibration */, false /* defaultSound */, true /* defaultLights */); + } + + private NotificationRecord getCustomLightsNotification() { + return getNotificationRecord(mId, false /* insistent */, true /* once */, + false /* noisy */, true /* buzzy*/, true /* lights */, + true /* defaultVibration */, true /* defaultSound */, false /* defaultLights */); + } + + private NotificationRecord getNotificationRecord(int id, boolean insistent, boolean once, + boolean noisy, boolean buzzy, boolean lights) { + return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, true, true, true); } private NotificationRecord getNotificationRecord(int id, boolean insistent, boolean once, - boolean noisy, boolean buzzy) { + boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, + boolean defaultSound, boolean defaultLights) { final Builder builder = new Builder(getContext()) .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -150,10 +200,25 @@ public class BuzzBeepBlinkTest { int defaults = 0; if (noisy) { - defaults |= Notification.DEFAULT_SOUND; + if (defaultSound) { + defaults |= Notification.DEFAULT_SOUND; + } else { + builder.setSound(CUSTOM_SOUND); + } } if (buzzy) { - defaults |= Notification.DEFAULT_VIBRATE; + if (defaultVibration) { + defaults |= Notification.DEFAULT_VIBRATE; + } else { + builder.setVibrate(CUSTOM_VIBRATION); + } + } + if (lights) { + if (defaultLights) { + defaults |= Notification.DEFAULT_LIGHTS; + } else { + builder.setLights(CUSTOM_LIGHT_COLOR, CUSTOM_LIGHT_ON, CUSTOM_LIGHT_OFF); + } } builder.setDefaults(defaults); @@ -163,7 +228,10 @@ public class BuzzBeepBlinkTest { } StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, id, mTag, mUid, mPid, mScore, n, mUser, System.currentTimeMillis()); - return new NotificationRecord(getContext(), sbn); + NotificationRecord r = new NotificationRecord(getContext(), sbn, + new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, "misc")); + mService.addNotification(r); + return r; } // @@ -185,6 +253,11 @@ public class BuzzBeepBlinkTest { eq(false), (AudioAttributes) anyObject()); } + private void verifyCustomBeep() throws RemoteException { + verify(mRingtonePlayer, times(1)).playAsync(eq(CUSTOM_SOUND), (UserHandle) anyObject(), + eq(false), (AudioAttributes) anyObject()); + } + private void verifyNeverStopAudio() throws RemoteException { verify(mRingtonePlayer, never()).stopAsync(); } @@ -208,6 +281,11 @@ public class BuzzBeepBlinkTest { eq(0), (AudioAttributes) anyObject()); } + private void verifyCustomVibrate() { + verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), eq(CUSTOM_VIBRATION), eq(-1), + (AudioAttributes) anyObject()); + } + private void verifyStopVibrate() { verify(mVibrator, times(1)).cancel(); } @@ -216,10 +294,33 @@ public class BuzzBeepBlinkTest { verify(mVibrator, never()).cancel(); } + private void verifyLights() { + verify(mStatusBar, times(1)).notificationLightPulse(anyInt(), anyInt(), anyInt()); + } + + private void verifyCustomLights() { + verify(mStatusBar, times(1)).notificationLightPulse( + eq(CUSTOM_LIGHT_COLOR), eq(CUSTOM_LIGHT_ON), eq(CUSTOM_LIGHT_OFF)); + } + private Context getContext() { return InstrumentationRegistry.getTargetContext(); } + // + // Tests + // + + @Test + public void testLights() throws Exception { + NotificationRecord r = getLightsNotification(); + r.setImportance(NotificationManager.IMPORTANCE_DEFAULT, "for testing"); + + mService.buzzBeepBlinkLocked(r); + + verifyLights(); + } + @Test public void testBeep() throws Exception { NotificationRecord r = getBeepyNotification(); @@ -230,9 +331,40 @@ public class BuzzBeepBlinkTest { verifyNeverVibrate(); } - // - // Tests - // + @Test + public void testBeepFromChannel() throws Exception { + NotificationRecord r = getQuietNotification(); + r.getChannel().setDefaultRingtone(Settings.System.DEFAULT_NOTIFICATION_URI); + r.setImportance(NotificationManager.IMPORTANCE_DEFAULT, "for testing"); + + mService.buzzBeepBlinkLocked(r); + + verifyBeepLooped(); + verifyNeverVibrate(); + } + + @Test + public void testVibrateFromChannel() throws Exception { + NotificationRecord r = getQuietNotification(); + r.getChannel().setVibration(true); + r.setImportance(NotificationManager.IMPORTANCE_DEFAULT, "for testing"); + + mService.buzzBeepBlinkLocked(r); + + verifyNeverBeep(); + verifyVibrate(); + } + + @Test + public void testLightsFromChannel() throws Exception { + NotificationRecord r = getQuietNotification(); + r.setImportance(NotificationManager.IMPORTANCE_DEFAULT, "for testing"); + r.getChannel().setLights(true); + + mService.buzzBeepBlinkLocked(r); + + verifyLights(); + } @Test public void testBeepInsistently() throws Exception { @@ -244,6 +376,36 @@ public class BuzzBeepBlinkTest { } @Test + public void testChannelNoOverwriteCustomVibration() throws Exception { + NotificationRecord r = getCustomBuzzyOnceNotification(); + r.getChannel().setVibration(true); + + mService.buzzBeepBlinkLocked(r); + + verifyCustomVibrate(); + } + + @Test + public void testChannelNoOverwriteCustomBeep() throws Exception { + NotificationRecord r = getCustomBeepyNotification(); + r.getChannel().setDefaultRingtone(Settings.System.DEFAULT_RINGTONE_URI); + + mService.buzzBeepBlinkLocked(r); + + verifyCustomBeep(); + } + + @Test + public void testChannelNoOverwriteCustomLights() throws Exception { + NotificationRecord r = getCustomLightsNotification(); + r.getChannel().setLights(true); + + mService.buzzBeepBlinkLocked(r); + + verifyCustomLights(); + } + + @Test public void testNoInterruptionForMin() throws Exception { NotificationRecord r = getBeepyNotification(); r.setImportance(Ranking.IMPORTANCE_MIN, "foo"); @@ -393,7 +555,7 @@ public class BuzzBeepBlinkTest { } @Test - public void testDemotInsistenteSoundToVibrate() throws Exception { + public void testDemoteInsistenteSoundToVibrate() throws Exception { NotificationRecord r = getInsistentBeepyNotification(); // the phone is quiet diff --git a/services/tests/servicestests/src/com/android/server/notification/ImportanceExtractorTest.java b/services/tests/servicestests/src/com/android/server/notification/ImportanceExtractorTest.java new file mode 100644 index 000000000000..3cbde1d3cf0f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/notification/ImportanceExtractorTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.notification; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import android.app.ActivityManager; +import android.app.Notification; +import android.app.Notification.Builder; +import android.app.NotificationManager; +import android.app.NotificationChannel; +import android.content.Context; +import android.os.UserHandle; +import android.service.notification.StatusBarNotification; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; + +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; + +import static org.junit.Assert.assertEquals; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ImportanceExtractorTest { + + @Mock RankingConfig mConfig; + + private String mPkg = "com.android.server.notification"; + private int mId = 1001; + private int mOtherId = 1002; + private String mTag = null; + private int mUid = 1000; + private int mPid = 2000; + private int mScore = 10; + private android.os.UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser()); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + private NotificationRecord getNotificationRecord(NotificationChannel channel) { + final Builder builder = new Builder(getContext()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setPriority(Notification.PRIORITY_HIGH) + .setDefaults(Notification.DEFAULT_SOUND); + + Notification n = builder.build(); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, mId, mTag, mUid, + mPid, mScore, n, mUser, System.currentTimeMillis()); + NotificationRecord r = new NotificationRecord(getContext(), sbn, channel); + return r; + } + + private Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + // + // Tests + // + + @Test + public void testAppPreferenceChannelNone() throws Exception { + ImportanceExtractor extractor = new ImportanceExtractor(); + extractor.setConfig(mConfig); + + when(mConfig.getImportance(anyString(), anyInt())).thenReturn( + NotificationManager.IMPORTANCE_MIN); + NotificationChannel channel = new NotificationChannel("a", "a"); + channel.setImportance(NotificationManager.IMPORTANCE_UNSPECIFIED); + + NotificationRecord r = getNotificationRecord(channel); + + extractor.process(r); + + assertEquals(r.getUserImportance(), NotificationManager.IMPORTANCE_MIN); + } + + @Test + public void testAppPreferenceChannelPermissive() throws Exception { + ImportanceExtractor extractor = new ImportanceExtractor(); + extractor.setConfig(mConfig); + + when(mConfig.getImportance(anyString(), anyInt())).thenReturn( + NotificationManager.IMPORTANCE_MIN); + NotificationChannel channel = new NotificationChannel("a", "a"); + channel.setImportance(NotificationManager.IMPORTANCE_HIGH); + + NotificationRecord r = getNotificationRecord(channel); + + extractor.process(r); + + assertEquals(r.getUserImportance(), NotificationManager.IMPORTANCE_MIN); + } + + @Test + public void testAppPreferenceChannelStrict() throws Exception { + ImportanceExtractor extractor = new ImportanceExtractor(); + extractor.setConfig(mConfig); + + when(mConfig.getImportance(anyString(), anyInt())).thenReturn( + NotificationManager.IMPORTANCE_HIGH); + NotificationChannel channel = new NotificationChannel("a", "a"); + channel.setImportance(NotificationManager.IMPORTANCE_MIN); + + NotificationRecord r = getNotificationRecord(channel); + + extractor.process(r); + + assertEquals(r.getUserImportance(), NotificationManager.IMPORTANCE_MIN); + } + + @Test + public void testNoAppPreferenceChannelPreference() throws Exception { + ImportanceExtractor extractor = new ImportanceExtractor(); + extractor.setConfig(mConfig); + + when(mConfig.getImportance(anyString(), anyInt())).thenReturn( + NotificationManager.IMPORTANCE_UNSPECIFIED); + NotificationChannel channel = new NotificationChannel("a", "a"); + channel.setImportance(NotificationManager.IMPORTANCE_MIN); + + NotificationRecord r = getNotificationRecord(channel); + + extractor.process(r); + + assertEquals(r.getUserImportance(), NotificationManager.IMPORTANCE_MIN); + } + + @Test + public void testNoPreferences() throws Exception { + ImportanceExtractor extractor = new ImportanceExtractor(); + extractor.setConfig(mConfig); + + when(mConfig.getImportance(anyString(), anyInt())).thenReturn( + NotificationManager.IMPORTANCE_UNSPECIFIED); + NotificationChannel channel = new NotificationChannel("a", "a"); + channel.setImportance(NotificationManager.IMPORTANCE_UNSPECIFIED); + + NotificationRecord r = getNotificationRecord(channel); + + extractor.process(r); + + assertEquals(r.getUserImportance(), + NotificationManager.IMPORTANCE_UNSPECIFIED); + } +} diff --git a/services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java index e890a48e429a..ee33dcca175d 100644 --- a/services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java @@ -18,19 +18,33 @@ package com.android.server.notification; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import com.android.internal.util.FastXmlSerializer; + import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; import android.app.Notification; import android.content.Context; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.net.Uri; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import android.util.Xml; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.util.ArrayList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @SmallTest @@ -70,7 +84,8 @@ public class RankingHelperTest { .setWhen(1205) .build(); mRecordGroupGSortA = new NotificationRecord(getContext(), new StatusBarNotification( - "package", "package", 1, null, 0, 0, 0, mNotiGroupGSortA, user)); + "package", "package", 1, null, 0, 0, 0, mNotiGroupGSortA, user), + getDefaultChannel()); mNotiGroupGSortB = new Notification.Builder(getContext()) .setContentTitle("B") @@ -79,21 +94,24 @@ public class RankingHelperTest { .setWhen(1200) .build(); mRecordGroupGSortB = new NotificationRecord(getContext(), new StatusBarNotification( - "package", "package", 1, null, 0, 0, 0, mNotiGroupGSortB, user)); + "package", "package", 1, null, 0, 0, 0, mNotiGroupGSortB, user), + getDefaultChannel()); mNotiNoGroup = new Notification.Builder(getContext()) .setContentTitle("C") .setWhen(1201) .build(); mRecordNoGroup = new NotificationRecord(getContext(), new StatusBarNotification( - "package", "package", 1, null, 0, 0, 0, mNotiNoGroup, user)); + "package", "package", 1, null, 0, 0, 0, mNotiNoGroup, user), + getDefaultChannel()); mNotiNoGroup2 = new Notification.Builder(getContext()) .setContentTitle("D") .setWhen(1202) .build(); mRecordNoGroup2 = new NotificationRecord(getContext(), new StatusBarNotification( - "package", "package", 1, null, 0, 0, 0, mNotiNoGroup2, user)); + "package", "package", 1, null, 0, 0, 0, mNotiNoGroup2, user), + getDefaultChannel()); mNotiNoGroupSortA = new Notification.Builder(getContext()) .setContentTitle("E") @@ -101,9 +119,14 @@ public class RankingHelperTest { .setSortKey("A") .build(); mRecordNoGroupSortA = new NotificationRecord(getContext(), new StatusBarNotification( - "package", "package", 1, null, 0, 0, 0, mNotiNoGroupSortA, user)); + "package", "package", 1, null, 0, 0, 0, mNotiNoGroupSortA, user), + getDefaultChannel()); } + private NotificationChannel getDefaultChannel() { + return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, "name"); + } + @Test public void testFindAfterRankingWithASplitGroup() throws Exception { ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(3); @@ -153,4 +176,45 @@ public class RankingHelperTest { ArrayList<NotificationRecord> notificationList = new ArrayList<NotificationRecord>(); mHelper.sort(notificationList); } + + @Test + public void testChannelXml() throws Exception { + String pkg = "com.android.server.notification"; + int uid = 0; + NotificationChannel channel1 = new NotificationChannel("id1", "name1"); + NotificationChannel channel2 = new NotificationChannel("id2", "name2"); + channel2.setImportance(NotificationManager.IMPORTANCE_LOW); + channel2.setDefaultRingtone(new Uri.Builder().scheme("test").build()); + channel2.setLights(true); + channel2.setBypassDnd(true); + channel2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + + mHelper.createNotificationChannel(pkg, uid, channel1); + mHelper.createNotificationChannel(pkg, uid, channel2); + + byte[] data; + XmlSerializer serializer = new FastXmlSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + serializer.startTag(null, "ranking"); + mHelper.writeXml(serializer, false); + serializer.endTag(null, "ranking"); + serializer.endDocument(); + serializer.flush(); + + mHelper.deleteNotificationChannel(pkg, uid, channel1.getId()); + mHelper.deleteNotificationChannel(pkg, uid, channel2.getId()); + mHelper.deleteNotificationChannel(pkg, uid, NotificationChannel.DEFAULT_CHANNEL_ID); + + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mHelper.readXml(parser, false); + + assertEquals(channel1, mHelper.getNotificationChannel(pkg, uid, channel1.getId())); + assertEquals(channel2, mHelper.getNotificationChannel(pkg, uid, channel2.getId())); + assertNotNull( + mHelper.getNotificationChannel(pkg, uid, NotificationChannel.DEFAULT_CHANNEL_ID)); + } } |