diff --git a/api/current.txt b/api/current.txt index c628933147a22..723764830cf88 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5123,6 +5123,7 @@ package android.app { method public int getBadgeIconType(); method public java.lang.String getChannel(); method public java.lang.String getGroup(); + method public int getGroupAlertBehavior(); method public android.graphics.drawable.Icon getLargeIcon(); method public java.lang.CharSequence getSettingsText(); method public java.lang.String getShortcutId(); @@ -5198,6 +5199,9 @@ package android.app { field public static final int FLAG_ONGOING_EVENT = 2; // 0x2 field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8 field public static final deprecated int FLAG_SHOW_LIGHTS = 1; // 0x1 + field public static final int GROUP_ALERT_ALL = 0; // 0x0 + field public static final int GROUP_ALERT_CHILDREN = 2; // 0x2 + field public static final int GROUP_ALERT_SUMMARY = 1; // 0x1 field public static final java.lang.String INTENT_CATEGORY_NOTIFICATION_PREFERENCES = "android.intent.category.NOTIFICATION_PREFERENCES"; field public static final deprecated int PRIORITY_DEFAULT = 0; // 0x0 field public static final deprecated int PRIORITY_HIGH = 1; // 0x1 @@ -5344,6 +5348,7 @@ package android.app { method public android.app.Notification.Builder setExtras(android.os.Bundle); method public android.app.Notification.Builder setFullScreenIntent(android.app.PendingIntent, boolean); method public android.app.Notification.Builder setGroup(java.lang.String); + method public android.app.Notification.Builder setGroupAlertBehavior(int); method public android.app.Notification.Builder setGroupSummary(boolean); method public android.app.Notification.Builder setLargeIcon(android.graphics.Bitmap); method public android.app.Notification.Builder setLargeIcon(android.graphics.drawable.Icon); diff --git a/api/removed.txt b/api/removed.txt index dc9c54e8b1cd4..73dd096507506 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -1,7 +1,6 @@ package android.app { public class Notification implements android.os.Parcelable { - method public deprecated int getBadgeIcon(); method public deprecated void setLatestEventInfo(android.content.Context, java.lang.CharSequence, java.lang.CharSequence, android.app.PendingIntent); } diff --git a/api/system-current.txt b/api/system-current.txt index 0c341f0cb06e3..31865b7644823 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5303,6 +5303,7 @@ package android.app { method public int getBadgeIconType(); method public java.lang.String getChannel(); method public java.lang.String getGroup(); + method public int getGroupAlertBehavior(); method public android.graphics.drawable.Icon getLargeIcon(); method public static java.lang.Class extends android.app.Notification.Style> getNotificationStyleClass(java.lang.String); method public java.lang.CharSequence getSettingsText(); @@ -5382,6 +5383,9 @@ package android.app { field public static final int FLAG_ONGOING_EVENT = 2; // 0x2 field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8 field public static final deprecated int FLAG_SHOW_LIGHTS = 1; // 0x1 + field public static final int GROUP_ALERT_ALL = 0; // 0x0 + field public static final int GROUP_ALERT_CHILDREN = 2; // 0x2 + field public static final int GROUP_ALERT_SUMMARY = 1; // 0x1 field public static final java.lang.String INTENT_CATEGORY_NOTIFICATION_PREFERENCES = "android.intent.category.NOTIFICATION_PREFERENCES"; field public static final deprecated int PRIORITY_DEFAULT = 0; // 0x0 field public static final deprecated int PRIORITY_HIGH = 1; // 0x1 @@ -5528,6 +5532,7 @@ package android.app { method public android.app.Notification.Builder setExtras(android.os.Bundle); method public android.app.Notification.Builder setFullScreenIntent(android.app.PendingIntent, boolean); method public android.app.Notification.Builder setGroup(java.lang.String); + method public android.app.Notification.Builder setGroupAlertBehavior(int); method public android.app.Notification.Builder setGroupSummary(boolean); method public android.app.Notification.Builder setLargeIcon(android.graphics.Bitmap); method public android.app.Notification.Builder setLargeIcon(android.graphics.drawable.Icon); diff --git a/api/system-removed.txt b/api/system-removed.txt index 4862bb76f0c2e..ed813631e394b 100644 --- a/api/system-removed.txt +++ b/api/system-removed.txt @@ -1,7 +1,6 @@ package android.app { public class Notification implements android.os.Parcelable { - method public deprecated int getBadgeIcon(); method public deprecated void setLatestEventInfo(android.content.Context, java.lang.CharSequence, java.lang.CharSequence, android.app.PendingIntent); } diff --git a/api/test-current.txt b/api/test-current.txt index 25ec0746c899f..086883e788577 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -5136,6 +5136,7 @@ package android.app { method public int getBadgeIconType(); method public java.lang.String getChannel(); method public java.lang.String getGroup(); + method public int getGroupAlertBehavior(); method public android.graphics.drawable.Icon getLargeIcon(); method public java.lang.CharSequence getSettingsText(); method public java.lang.String getShortcutId(); @@ -5211,6 +5212,9 @@ package android.app { field public static final int FLAG_ONGOING_EVENT = 2; // 0x2 field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8 field public static final deprecated int FLAG_SHOW_LIGHTS = 1; // 0x1 + field public static final int GROUP_ALERT_ALL = 0; // 0x0 + field public static final int GROUP_ALERT_CHILDREN = 2; // 0x2 + field public static final int GROUP_ALERT_SUMMARY = 1; // 0x1 field public static final java.lang.String INTENT_CATEGORY_NOTIFICATION_PREFERENCES = "android.intent.category.NOTIFICATION_PREFERENCES"; field public static final deprecated int PRIORITY_DEFAULT = 0; // 0x0 field public static final deprecated int PRIORITY_HIGH = 1; // 0x1 @@ -5357,6 +5361,7 @@ package android.app { method public android.app.Notification.Builder setExtras(android.os.Bundle); method public android.app.Notification.Builder setFullScreenIntent(android.app.PendingIntent, boolean); method public android.app.Notification.Builder setGroup(java.lang.String); + method public android.app.Notification.Builder setGroupAlertBehavior(int); method public android.app.Notification.Builder setGroupSummary(boolean); method public android.app.Notification.Builder setLargeIcon(android.graphics.Bitmap); method public android.app.Notification.Builder setLargeIcon(android.graphics.drawable.Icon); diff --git a/api/test-removed.txt b/api/test-removed.txt index dc9c54e8b1cd4..73dd096507506 100644 --- a/api/test-removed.txt +++ b/api/test-removed.txt @@ -1,7 +1,6 @@ package android.app { public class Notification implements android.os.Parcelable { - method public deprecated int getBadgeIcon(); method public deprecated void setLatestEventInfo(android.content.Context, java.lang.CharSequence, java.lang.CharSequence, android.app.PendingIntent); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 2b4fb1956c424..e53e3da4d2160 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1106,6 +1106,45 @@ public class Notification implements Parcelable private String mShortcutId; private CharSequence mSettingsText; + /** @hide */ + @IntDef(prefix = { "GROUP_ALERT_" }, value = { + GROUP_ALERT_ALL, GROUP_ALERT_CHILDREN, GROUP_ALERT_SUMMARY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface GroupAlertBehavior {} + + /** + * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that all notifications in a + * group with sound or vibration ought to make sound or vibrate (respectively), so this + * notification will not be muted when it is in a group. + */ + public static final int GROUP_ALERT_ALL = 0; + + /** + * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that all children + * notification in a group should be silenced (no sound or vibration) even if they are posted + * to a {@link NotificationChannel} that has sound and/or vibration. Use this constant to + * mute this notification if this notification is a group child. + * + *
For example, you might want to use this constant if you post a number of children + * notifications at once (say, after a periodic sync), and only need to notify the user + * audibly once. + */ + public static final int GROUP_ALERT_SUMMARY = 1; + + /** + * Constant for {@link Builder#setGroupAlertBehavior(int)}, meaning that the summary + * notification in a group should be silenced (no sound or vibration) even if they are + * posted to a {@link NotificationChannel} that has sound and/or vibration. Use this constant + * to mute this notification if this notification is a group summary. + * + *
For example, you might want to use this constant if only the children notifications + * in your group have content and the summary is only used to visually group notifications. + */ + public static final int GROUP_ALERT_CHILDREN = 2; + + private int mGroupAlertBehavior = GROUP_ALERT_ALL; + /** * If this notification is being shown as a badge, always show as a number. */ @@ -1878,6 +1917,8 @@ public class Notification implements Parcelable if (parcel.readInt() != 0) { mSettingsText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); } + + mGroupAlertBehavior = parcel.readInt(); } @Override @@ -1990,6 +2031,7 @@ public class Notification implements Parcelable that.mShortcutId = this.mShortcutId; that.mBadgeIcon = this.mBadgeIcon; that.mSettingsText = this.mSettingsText; + that.mGroupAlertBehavior = this.mGroupAlertBehavior; if (!heavy) { that.lightenPayload(); // will clean out extras @@ -2266,6 +2308,8 @@ public class Notification implements Parcelable } else { parcel.writeInt(0); } + + parcel.writeInt(mGroupAlertBehavior); } /** @@ -2470,17 +2514,6 @@ public class Notification implements Parcelable return mTimeout; } - /** - * @removed - * Returns what icon should be shown for this notification if it is being displayed in a - * Launcher that supports badging. Will be one of {@link #BADGE_ICON_NONE}, - * {@link #BADGE_ICON_SMALL}, or {@link #BADGE_ICON_LARGE}. - */ - @Deprecated - public int getBadgeIcon() { - return mBadgeIcon; - } - /** * Returns what icon should be shown for this notification if it is being displayed in a * Launcher that supports badging. Will be one of {@link #BADGE_ICON_NONE}, @@ -2505,6 +2538,15 @@ public class Notification implements Parcelable return mSettingsText; } + /** + * Returns which type of notifications in a group are responsible for audibly alerting the + * user. See {@link #GROUP_ALERT_ALL}, {@link #GROUP_ALERT_CHILDREN}, + * {@link #GROUP_ALERT_SUMMARY}. + */ + public @GroupAlertBehavior int getGroupAlertBehavior() { + return mGroupAlertBehavior; + } + /** * The small icon representing this notification in the status bar and content view. * @@ -2752,6 +2794,19 @@ public class Notification implements Parcelable return this; } + /** + * Sets the group alert behavior for this notification. Use this method to mute this + * notification if alerts for this notification's group should be handled by a different + * notification. This is only applicable for notifications that belong to a + * {@link #setGroup(String) group}. + * + *
The default value is {@link #GROUP_ALERT_ALL}.
+ */ + public Builder setGroupAlertBehavior(@GroupAlertBehavior int groupAlertBehavior) { + mN.mGroupAlertBehavior = groupAlertBehavior; + return this; + } + /** * Specifies the channel the notification should be delivered on. */ diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 02b93a232bb19..1e7d076267825 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3659,10 +3659,7 @@ public class NotificationManagerService extends SystemService { } hasValidVibrate = vibration != null; - // 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. - if (!(record.isUpdate - && (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)) { + if (!shouldMuteNotificationLocked(record)) { sendAccessibilityEvent(notification, record.sbn.getPackageName()); if (hasValidSound) { @@ -3716,6 +3713,24 @@ public class NotificationManagerService extends SystemService { } } + boolean shouldMuteNotificationLocked(final NotificationRecord record) { + final Notification notification = record.getNotification(); + if(record.isUpdate + && (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) { + return true; + } + if (record.sbn.isGroup()) { + if (notification.isGroupSummary() + && notification.getGroupAlertBehavior() == Notification.GROUP_ALERT_CHILDREN) { + return true; + } else if (notification.isGroupChild() + && notification.getGroupAlertBehavior() == Notification.GROUP_ALERT_SUMMARY) { + return true; + } + } + return false; + } + private boolean playSound(final NotificationRecord record, Uri soundUri) { boolean looping = (record.getNotification().flags & Notification.FLAG_INSISTENT) != 0; // do not play notifications if there is a user of exclusive audio focus diff --git a/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java index 1106006352aaf..d4904f5aecf71 100644 --- a/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ b/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java @@ -15,6 +15,9 @@ */ package com.android.server.notification; +import static android.app.Notification.GROUP_ALERT_ALL; +import static android.app.Notification.GROUP_ALERT_CHILDREN; +import static android.app.Notification.GROUP_ALERT_SUMMARY; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static junit.framework.Assert.assertFalse; @@ -188,17 +191,24 @@ public class BuzzBeepBlinkTest { private NotificationRecord getCustomLightsNotification() { return getNotificationRecord(mId, false /* insistent */, true /* once */, false /* noisy */, true /* buzzy*/, true /* lights */, - true /* defaultVibration */, true /* defaultSound */, false /* defaultLights */); + true /* defaultVibration */, true /* defaultSound */, false /* defaultLights */, + null, Notification.GROUP_ALERT_ALL); } 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); + return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, true, true, true, + null, Notification.GROUP_ALERT_ALL); + } + + private NotificationRecord getBeepyNotificationRecord(String groupKey, int groupAlertBehavior) { + return getNotificationRecord(mId, false, false, true, false, false, true, true, true, + groupKey, groupAlertBehavior); } private NotificationRecord getNotificationRecord(int id, boolean insistent, boolean once, boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, - boolean defaultSound, boolean defaultLights) { + boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior) { NotificationChannel channel = new NotificationChannel("test", "test", IMPORTANCE_HIGH); final Builder builder = new Builder(getContext()) @@ -239,6 +249,9 @@ public class BuzzBeepBlinkTest { } builder.setDefaults(defaults); + builder.setGroup(groupKey); + builder.setGroupAlertBehavior(groupAlertBehavior); + Notification n = builder.build(); if (insistent) { n.flags |= Notification.FLAG_INSISTENT; @@ -546,7 +559,7 @@ public class BuzzBeepBlinkTest { } @Test - public void testInsistenteVibrate() throws Exception { + public void testInsistentVibrate() throws Exception { NotificationRecord r = getInsistentBuzzyNotification(); mService.buzzBeepBlinkLocked(r); @@ -567,6 +580,71 @@ public class BuzzBeepBlinkTest { verifyVibrate(); } + @Test + public void testGroupAlertSummarySilenceChild() throws Exception { + NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); + + mService.buzzBeepBlinkLocked(child); + + verifyNeverBeep(); + } + + @Test + public void testGroupAlertSummaryNoSilenceSummary() throws Exception { + NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + + mService.buzzBeepBlinkLocked(summary); + + verifyBeepLooped(); + } + + @Test + public void testGroupAlertSummaryNoSilenceNonGroupChild() throws Exception { + NotificationRecord nonGroup = getBeepyNotificationRecord(null, GROUP_ALERT_SUMMARY); + + mService.buzzBeepBlinkLocked(nonGroup); + + verifyBeepLooped(); + } + + @Test + public void testGroupAlertChildSilenceSummary() throws Exception { + NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + + mService.buzzBeepBlinkLocked(summary); + + verifyNeverBeep(); + } + + @Test + public void testGroupAlertChildNoSilenceChild() throws Exception { + NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); + + mService.buzzBeepBlinkLocked(child); + + verifyBeepLooped(); + } + + @Test + public void testGroupAlertChildNoSilenceNonGroupSummary() throws Exception { + NotificationRecord nonGroup = getBeepyNotificationRecord(null, GROUP_ALERT_CHILDREN); + + mService.buzzBeepBlinkLocked(nonGroup); + + verifyBeepLooped(); + } + + @Test + public void testGroupAlertAllNoSilenceGroup() throws Exception { + NotificationRecord group = getBeepyNotificationRecord("a", GROUP_ALERT_ALL); + + mService.buzzBeepBlinkLocked(group); + + verifyBeepLooped(); + } + @Test public void testHonorAlertOnlyOnceForBuzz() throws Exception { NotificationRecord r = getBuzzyNotification();