summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt26
-rw-r--r--api/system-current.txt35
-rw-r--r--api/test-current.txt26
-rw-r--r--core/java/android/app/INotificationManager.aidl10
-rw-r--r--core/java/android/app/Notification.aidl1
-rw-r--r--core/java/android/app/Notification.java30
-rw-r--r--core/java/android/app/NotificationChannel.aidl19
-rw-r--r--core/java/android/app/NotificationChannel.java382
-rw-r--r--core/java/android/app/NotificationManager.java60
-rw-r--r--core/java/android/service/notification/NotificationRankerService.java3
-rw-r--r--core/res/res/values/strings.xml2
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--services/core/java/com/android/server/notification/ImportanceExtractor.java16
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java196
-rw-r--r--services/core/java/com/android/server/notification/NotificationRecord.java17
-rw-r--r--services/core/java/com/android/server/notification/PriorityExtractor.java8
-rw-r--r--services/core/java/com/android/server/notification/RankingConfig.java9
-rw-r--r--services/core/java/com/android/server/notification/RankingHelper.java128
-rw-r--r--services/core/java/com/android/server/notification/VisibilityExtractor.java9
-rw-r--r--services/tests/servicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java198
-rw-r--r--services/tests/servicestests/src/com/android/server/notification/ImportanceExtractorTest.java171
-rw-r--r--services/tests/servicestests/src/com/android/server/notification/RankingHelperTest.java74
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));
+ }
}