diff options
20 files changed, 3710 insertions, 509 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index aea15e13e5d6..db979a5dd30b 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -762,6 +762,16 @@ public class Notification implements Parcelable @FlaggedApi(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public static final int FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY = 0x00010000; + /** + * Bit to be bitwise-ored into the {@link #flags} field that should be + * set by the system if this notification is silent. + * + * This flag is for internal use only; applications cannot set this flag directly. + * @hide + */ + @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + public static final int FLAG_SILENT = 1 << 17; //0x00020000 + private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList( BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, @@ -784,7 +794,8 @@ public class Notification implements Parcelable FLAG_BUBBLE, FLAG_NO_DISMISS, FLAG_FSI_REQUESTED_BUT_DENIED, - FLAG_USER_INITIATED_JOB + FLAG_USER_INITIATED_JOB, + FLAG_SILENT }) @Retention(RetentionPolicy.SOURCE) public @interface NotificationFlags{}; @@ -1692,6 +1703,7 @@ public class Notification implements Parcelable * * @hide */ + @Deprecated public static final String GROUP_KEY_SILENT = "silent"; private int mGroupAlertBehavior = GROUP_ALERT_ALL; @@ -3984,6 +3996,13 @@ public class Notification implements Parcelable } } + if (android.service.notification.Flags.notificationSilentFlag()) { + if ((flags & FLAG_SILENT) != 0) { + flagStrings.add("SILENT"); + flags &= ~FLAG_SILENT; + } + } + if (flagStrings.isEmpty()) { return "0"; } @@ -4123,6 +4142,17 @@ public class Notification implements Parcelable } /** + * Sets 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}. + * @param groupAlertBehavior + * @hide + */ + public void setGroupAlertBehavior(@GroupAlertBehavior int groupAlertBehavior) { + mGroupAlertBehavior = groupAlertBehavior; + } + + /** * Returns the bubble metadata that will be used to display app content in a floating window * over the existing foreground activity. */ @@ -4309,6 +4339,31 @@ public class Notification implements Parcelable } /** + * Sets the FLAG_SILENT flag to mark the notification as silent and clears the group key. + * @hide + */ + public void fixSilentGroup() { + if (android.service.notification.Flags.notificationSilentFlag()) { + if (GROUP_KEY_SILENT.equals(mGroupKey)) { + mGroupKey = null; + flags |= FLAG_SILENT; + } + } + } + + /** + * @return whether this notification is silent. See {@link Builder#setSilent()} + * @hide + */ + public boolean isSilent() { + if (android.service.notification.Flags.notificationSilentFlag()) { + return (flags & Notification.FLAG_SILENT) != 0; + } else { + return GROUP_KEY_SILENT.equals(getGroup()) && suppressAlertingDueToGrouping(); + } + } + + /** * Builder class for {@link Notification} objects. * * Provides a convenient way to set the various fields of a {@link Notification} and generate @@ -4759,8 +4814,12 @@ public class Notification implements Parcelable mN.defaults &= ~DEFAULT_VIBRATE; setDefaults(mN.defaults); - if (TextUtils.isEmpty(mN.mGroupKey)) { - setGroup(GROUP_KEY_SILENT); + if (android.service.notification.Flags.notificationSilentFlag()) { + mN.flags |= FLAG_SILENT; + } else { + if (TextUtils.isEmpty(mN.mGroupKey)) { + setGroup(GROUP_KEY_SILENT); + } } return this; } diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java index 264b53c6ee40..146c2b6fa46e 100644 --- a/core/java/android/service/notification/StatusBarNotification.java +++ b/core/java/android/service/notification/StatusBarNotification.java @@ -176,7 +176,11 @@ public class StatusBarNotification implements Parcelable { private String groupKey() { if (overrideGroupKey != null) { - return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey; + if (Flags.notificationForceGrouping()) { + return overrideGroupKey; + } else { + return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey; + } } final String group = getNotification().getGroup(); final String sortKey = getNotification().getSortKey(); diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig index bdef04164b36..51961a85d307 100644 --- a/core/java/android/service/notification/flags.aconfig +++ b/core/java/android/service/notification/flags.aconfig @@ -43,4 +43,18 @@ flag { namespace: "systemui" description: "Allows the NAS to classify notifications" bug: "343988084" +} + +flag { + name: "notification_force_grouping" + namespace: "systemui" + description: "This flag controls the forced auto-grouping feature" + bug: "336488844" +} + +flag { + name: "notification_silent_flag" + namespace: "systemui" + description: "Guards the new FLAG_SILENT Notification flag" + bug: "336488844" }
\ No newline at end of file diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index e9ad1c28578a..ef6ff0518dac 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -84,6 +84,8 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.text.Spannable; @@ -545,12 +547,26 @@ public class NotificationTest { } @Test + @DisableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) public void testBuilder_setSilent_emptyGroupKey_groupKeySilent() { Notification emptyGroupKeyNotif = new Notification.Builder(mContext, "channelId") .setGroup("") .setSilent(true) .build(); - assertEquals(GROUP_KEY_SILENT, emptyGroupKeyNotif.getGroup()); + assertThat(emptyGroupKeyNotif.getGroup()).isEqualTo(GROUP_KEY_SILENT); + assertThat(emptyGroupKeyNotif.isSilent()).isTrue(); + } + + @Test + @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + public void testBuilder_setSilent_flagSilent() { + final String groupKey = "groupKey"; + Notification emptyGroupKeyNotif = new Notification.Builder(mContext, "channelId") + .setGroup(groupKey) + .setSilent(true) + .build(); + assertThat(emptyGroupKeyNotif.getGroup()).isEqualTo(groupKey); + assertThat(emptyGroupKeyNotif.isSilent()).isTrue(); } @Test diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 5e91786e4160..e04e0facc766 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -39,6 +39,7 @@ import android.os.SystemProperties import android.provider.Settings import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED import android.provider.Settings.Global.HEADS_UP_OFF +import android.service.notification.Flags import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger import com.android.internal.messages.nano.SystemMessageProto.SystemMessage @@ -221,6 +222,15 @@ class HunGroupAlertBehaviorSuppressor() : entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() } } +class HunSilentNotificationSuppressor() : + VisualInterruptionFilter( + types = setOf(PEEK, PULSE), + reason = "notification isSilent" + ) { + override fun shouldSuppress(entry: NotificationEntry) = + entry.sbn.let { Flags.notificationSilentFlag() && it.notification.isSilent } +} + class HunJustLaunchedFsiSuppressor() : VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "just launched FSI") { override fun shouldSuppress(entry: NotificationEntry) = entry.hasJustLaunchedFullScreenIntent() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt index b77748e2990b..576c7ad2ca3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt @@ -39,6 +39,7 @@ import com.android.systemui.statusbar.notification.interruption.FullScreenIntent import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_ONLY_BY_DND import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR +import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_SILENT_NOTIFICATION import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR @@ -94,6 +95,7 @@ class FullScreenIntentDecisionProvider( uiEventId = FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA, eventLogData = EventLogData("274759612", "bubbleMetadata") ), + NO_FSI_SUPPRESSIVE_SILENT_NOTIFICATION(false, "suppressive setSilent notification"), NO_FSI_PACKAGE_SUSPENDED(false, "package suspended"), FSI_DEVICE_NOT_INTERACTIVE(true, "device is not interactive"), FSI_DEVICE_DREAMING(true, "device is dreaming"), @@ -154,6 +156,12 @@ class FullScreenIntentDecisionProvider( return NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR } + if (android.service.notification.Flags.notificationSilentFlag()) { + if (sbn.notification.isSilent) { + return NO_FSI_SUPPRESSIVE_SILENT_NOTIFICATION + } + } + val bubbleMetadata = notification.bubbleMetadata if (bubbleMetadata != null && bubbleMetadata.isNotificationSuppressed) { return NO_FSI_SUPPRESSIVE_BUBBLE_METADATA diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt index a2f97bdbc7a1..e9efa56399b1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt @@ -193,6 +193,14 @@ class NotificationInterruptLogger @Inject constructor( }) } + fun logNoAlertingSilentNotification(entry: NotificationEntry) { + buffer.log(TAG, DEBUG, { + str1 = entry.logKey + }, { + "No alerting: suppressed due to silent notification: $str1" + }) + } + fun logNoAlertingSuppressedBy( entry: NotificationEntry, suppressor: NotificationInterruptSuppressor, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java index c084482bec9d..2b027907fa62 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java @@ -67,6 +67,11 @@ public interface NotificationInterruptStateProvider { */ NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(false), /** + * Notification should not FSI due to being explicitly silent. + * see {@link android.app.Notification#isSilent} + */ + NO_FSI_SUPPRESSIVE_SILENT_NOTIFICATION(false), + /** * Device screen is off, so the FSI should launch. */ FSI_DEVICE_NOT_INTERACTIVE(true), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index fea360d4e02a..450067a969e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -295,8 +295,17 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter // b/231322873: Detect and report an event when a notification has both an FSI and a // suppressive groupAlertBehavior, and now correctly block the FSI from firing. return getDecisionGivenSuppression( - FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, + FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, + suppressedByDND); + } + + // If the notification is explicitly silent, block FSI and warn. + if (android.service.notification.Flags.notificationSilentFlag()) { + if (sbn.getNotification().isSilent()) { + return getDecisionGivenSuppression( + FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_SILENT_NOTIFICATION, suppressedByDND); + } } // If the notification has suppressive BubbleMetadata, block FSI and warn. @@ -587,8 +596,18 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter StatusBarNotification sbn = entry.getSbn(); // Don't alert notifications that are suppressed due to group alert behavior + if (android.service.notification.Flags.notificationSilentFlag()) { + if (sbn.getNotification().isSilent()) { + if (log) { + mLogger.logNoAlertingSilentNotification(entry); + } + return false; + } + } + if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) { - if (log) mLogger.logNoAlertingGroupAlertBehavior(entry); + if (log) + mLogger.logNoAlertingGroupAlertBehavior(entry); return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index 96f94ca2a254..1c476ce0362b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -176,6 +176,7 @@ constructor( addFilter(BubbleNotAllowedSuppressor()) addFilter(BubbleNoMetadataSuppressor()) addFilter(HunGroupAlertBehaviorSuppressor()) + addFilter(HunSilentNotificationSuppressor()) addFilter(HunJustLaunchedFsiSuppressor()) addFilter(AlertAppSuspendedSuppressor()) addFilter(AlertKeyguardVisibilitySuppressor(keyguardNotificationVisibilityProvider)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java index bfe5c6e233d3..61d14b73204a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java @@ -60,6 +60,7 @@ import android.os.Handler; import android.os.PowerManager; import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -244,6 +245,50 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { } @Test + @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + public void testShouldNotHeadsUp_silentNotification() { + // GIVEN state for "heads up when awake" is true + ensureStateForHeadsUpWhenAwake(); + + // WHEN the alert for a grouped notification is suppressed + // see {@link android.app.Notification#GROUP_ALERT_CHILDREN} + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg("a") + .setOpPkg("a") + .setTag("a") + .setNotification(new Notification.Builder(getContext(), "a") + .setSilent(true) + .build()) + .setImportance(IMPORTANCE_DEFAULT) + .build(); + + // THEN this entry shouldn't HUN + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + @Test + @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + public void testShouldNotHeadsUp_silentNotificationFalse() { + // GIVEN state for "heads up when awake" is true + ensureStateForHeadsUpWhenAwake(); + + // WHEN the alert for a grouped notification is suppressed + // see {@link android.app.Notification#GROUP_ALERT_CHILDREN} + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg("a") + .setOpPkg("a") + .setTag("a") + .setNotification(new Notification.Builder(getContext(), "a") + .setSilent(false) + .build()) + .setImportance(IMPORTANCE_HIGH) + .build(); + + // THEN this entry shouldn't HUN + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); + } + + @Test public void testShouldHeadsUpWhenDozing() { ensureStateForHeadsUpWhenDozing(); @@ -685,6 +730,26 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { } @Test + @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + public void testShouldNotFullScreen_silentNotification() { + Notification n = new Notification.Builder(getContext(), "a") + .setContentTitle("title") + .setContentText("content text") + .setFullScreenIntent(mPendingIntent, true) + .setSilent(true) + .build(); + NotificationEntry entry = createNotification(IMPORTANCE_HIGH, n); + + assertThat(mNotifInterruptionStateProvider.getFullScreenIntentDecision(entry)) + .isEqualTo(FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_SILENT_NOTIFICATION); + assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) + .isFalse(); + verify(mLogger).logNoFullscreen(entry, "NO_FSI_SUPPRESSIVE_SILENT_NOTIFICATION"); + verify(mLogger, never()).logFullscreen(any(), any()); + verify(mLogger, never()).logNoFullscreenWarning(any(), any()); + } + + @Test public void testShouldFullScreen_notInteractive() { NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false); Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder("foo") diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt index 378705a3c1a3..d5ab62b43866 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt @@ -49,6 +49,7 @@ import android.graphics.drawable.Icon import android.hardware.display.FakeAmbientDisplayConfiguration import android.os.Looper import android.os.PowerManager +import android.platform.test.annotations.EnableFlags import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED import android.provider.Settings.Global.HEADS_UP_OFF import android.provider.Settings.Global.HEADS_UP_ON @@ -532,6 +533,32 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } @Test + @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + fun testShouldNotHeadsUp_silentNotification() { + withPeekAndPulseEntry({ + isGrouped = false + isGroupSummary = false + isSilent = true + }) { + assertShouldNotHeadsUp(it) + assertNoEventsLogged() + } + } + + @Test + @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + fun testShouldHeadsUp_silentNotificationFalse() { + withPeekAndPulseEntry({ + isGrouped = false + isGroupSummary = false + isSilent = false + }) { + assertShouldHeadsUp(it) + assertNoEventsLogged() + } + } + + @Test fun testShouldNotHeadsUp_justLaunchedFsi() { withPeekAndPulseEntry({ hasJustLaunchedFsi = true }) { assertShouldNotHeadsUp(it) @@ -1163,6 +1190,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { var groupAlertBehavior: Int? = null var hasBubbleMetadata = false var hasFsi = false + var isSilent = false // Set on Notification: var isForegroundService = false @@ -1233,6 +1261,8 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } groupAlertBehavior?.let { nb.setGroupAlertBehavior(it) } + nb.setSilent(isSilent) + if (hasBubbleMetadata) { nb.setBubbleMetadata(buildBubbleMetadata()) } diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index 13cc99c804f6..1cdab44a5b1b 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -24,9 +24,14 @@ import static android.app.Notification.FLAG_NO_CLEAR; import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.Notification.VISIBILITY_PRIVATE; import static android.app.Notification.VISIBILITY_PUBLIC; +import static android.service.notification.Flags.notificationForceGrouping; +import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -34,7 +39,9 @@ import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.service.notification.StatusBarNotification; +import android.text.TextUtils; import android.util.ArrayMap; +import android.util.Log; import android.util.Slog; import com.android.internal.R; @@ -42,14 +49,20 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; /** * NotificationManagerService helper for auto-grouping notifications. */ public class GroupHelper { private static final String TAG = "GroupHelper"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); protected static final String AUTOGROUP_KEY = "ranker_group"; @@ -63,8 +76,16 @@ public class GroupHelper { // Flags that autogroup summaries inherits if any child has them private static final int ANY_CHILDREN_FLAGS = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; + protected static final String AGGREGATE_GROUP_KEY = "Aggregate_"; + + // If an app posts more than NotificationManagerService.AUTOGROUP_SPARSE_GROUPS_AT_COUNT groups + // with less than this value, they will be forced grouped + private static final int MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING = 3; + + private final Callback mCallback; private final int mAutoGroupAtCount; + private final int mAutogroupSparseGroupsAtCount; private final Context mContext; private final PackageManager mPackageManager; @@ -75,12 +96,41 @@ public class GroupHelper { private final ArrayMap<String, ArrayMap<String, NotificationAttributes>> mUngroupedNotifications = new ArrayMap<>(); + // Contains the list of notifications that should be aggregated (forced grouping) + // but there are less than mAutoGroupAtCount per section for a package. + // The primary map's key is the full aggregated group key: userId|pkgName|g:groupName + // The internal map's key is the notification record key + @GuardedBy("mAggregatedNotifications") + private final ArrayMap<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>> + mUngroupedAbuseNotifications = new ArrayMap<>(); + + // Contains the list of group summaries that were canceled when "singleton groups" were + // force grouped. Used to remove the original group's children when an app cancels the + // already removed summary. Key is userId|packageName|g:OriginalGroupName + @GuardedBy("mAggregatedNotifications") + private final ArrayMap<FullyQualifiedGroupKey, CachedSummary> + mCanceledSummaries = new ArrayMap<>(); + + // Represents the current state of the aggregated (forced grouped) notifications + // Key is the full aggregated group key: userId|pkgName|g:groupName + // And groupName is "Aggregate_"+sectionName + @GuardedBy("mAggregatedNotifications") + private final ArrayMap<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>> + mAggregatedNotifications = new ArrayMap<>(); + + private static final List<NotificationSectioner> NOTIFICATION_SHADE_SECTIONS = List.of( + new NotificationSectioner("AlertingSection", 0, (record) -> + record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT), + new NotificationSectioner("SilentSection", 1, (record) -> + record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT)); + public GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount, - Callback callback) { + int autoGroupSparseGroupsAtCount, Callback callback) { mAutoGroupAtCount = autoGroupAtCount; mCallback = callback; mContext = context; mPackageManager = packageManager; + mAutogroupSparseGroupsAtCount = autoGroupSparseGroupsAtCount; } private String generatePackageKey(int userId, String pkg) { @@ -88,40 +138,50 @@ public class GroupHelper { } @VisibleForTesting - @GuardedBy("mUngroupedNotifications") - protected int getAutogroupSummaryFlags( - @NonNull final ArrayMap<String, NotificationAttributes> children) { + protected static int getAutogroupSummaryFlags( + @NonNull final ArrayMap<String, NotificationAttributes> childrenMap) { + final Collection<NotificationAttributes> children = childrenMap.values(); boolean allChildrenHasFlag = children.size() > 0; int anyChildFlagSet = 0; - for (int i = 0; i < children.size(); i++) { - if (!hasAnyFlag(children.valueAt(i).flags, ALL_CHILDREN_FLAG)) { + for (NotificationAttributes childAttr: children) { + if (!hasAnyFlag(childAttr.flags, ALL_CHILDREN_FLAG)) { allChildrenHasFlag = false; } - if (hasAnyFlag(children.valueAt(i).flags, ANY_CHILDREN_FLAGS)) { - anyChildFlagSet |= (children.valueAt(i).flags & ANY_CHILDREN_FLAGS); + if (hasAnyFlag(childAttr.flags, ANY_CHILDREN_FLAGS)) { + anyChildFlagSet |= (childAttr.flags & ANY_CHILDREN_FLAGS); } } return BASE_FLAGS | (allChildrenHasFlag ? ALL_CHILDREN_FLAG : 0) | anyChildFlagSet; } - private boolean hasAnyFlag(int flags, int mask) { + private static boolean hasAnyFlag(int flags, int mask) { return (flags & mask) != 0; } /** * Called when a notification is newly posted. Checks whether that notification, and all other * active notifications should be grouped or ungrouped atuomatically, and returns whether. - * @param sbn The posted notification. + * @param record The posted notification. * @param autogroupSummaryExists Whether a summary for this notification already exists. * @return Whether the provided notification should be autogrouped synchronously. */ - public boolean onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) { + public boolean onNotificationPosted(NotificationRecord record, boolean autogroupSummaryExists) { boolean sbnToBeAutogrouped = false; try { - if (!sbn.isAppGroup()) { - sbnToBeAutogrouped = maybeGroup(sbn, autogroupSummaryExists); + if (notificationForceGrouping()) { + final StatusBarNotification sbn = record.getSbn(); + if (!sbn.isAppGroup()) { + sbnToBeAutogrouped = maybeGroupWithSections(record, autogroupSummaryExists); + } else { + maybeUngroupWithSections(record); + } } else { - maybeUngroup(sbn, false, sbn.getUserId()); + final StatusBarNotification sbn = record.getSbn(); + if (!sbn.isAppGroup()) { + sbnToBeAutogrouped = maybeGroup(sbn, autogroupSummaryExists); + } else { + maybeUngroup(sbn, false, sbn.getUserId()); + } } } catch (Exception e) { Slog.e(TAG, "Failure processing new notification", e); @@ -129,9 +189,20 @@ public class GroupHelper { return sbnToBeAutogrouped; } - public void onNotificationRemoved(StatusBarNotification sbn) { + /** + * Called when a notification was removed. Checks if that notification was part of an autogroup + * and triggers any necessary cleanups: summary removal, clearing caches etc. + * + * @param record The removed notification. + */ + public void onNotificationRemoved(NotificationRecord record) { try { - maybeUngroup(sbn, true, sbn.getUserId()); + if (notificationForceGrouping()) { + onNotificationRemoved(record, new ArrayList<>()); + } else { + final StatusBarNotification sbn = record.getSbn(); + maybeUngroup(sbn, true, sbn.getUserId()); + } } catch (Exception e) { Slog.e(TAG, "Error processing canceled notification", e); } @@ -156,10 +227,10 @@ public class GroupHelper { String packageKey = generatePackageKey(sbn.getUserId(), sbn.getPackageName()); final ArrayMap<String, NotificationAttributes> children = mUngroupedNotifications.getOrDefault(packageKey, new ArrayMap<>()); - NotificationAttributes attr = new NotificationAttributes(sbn.getNotification().flags, sbn.getNotification().getSmallIcon(), sbn.getNotification().color, - sbn.getNotification().visibility); + sbn.getNotification().visibility, Notification.GROUP_ALERT_CHILDREN, + sbn.getNotification().getChannelId()); children.put(sbn.getKey(), attr); mUngroupedNotifications.put(packageKey, children); @@ -173,17 +244,20 @@ public class GroupHelper { if (autogroupSummaryExists) { NotificationAttributes attr = new NotificationAttributes(flags, sbn.getNotification().getSmallIcon(), sbn.getNotification().color, - VISIBILITY_PRIVATE); + VISIBILITY_PRIVATE, Notification.GROUP_ALERT_CHILDREN, + sbn.getNotification().getChannelId()); if (Flags.autogroupSummaryIconUpdate()) { attr = updateAutobundledSummaryAttributes(sbn.getPackageName(), childrenAttr, attr); } - mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), attr); + mCallback.updateAutogroupSummary(sbn.getUserId(), sbn.getPackageName(), + AUTOGROUP_KEY, attr); } else { Icon summaryIcon = sbn.getNotification().getSmallIcon(); int summaryIconColor = sbn.getNotification().color; int summaryVisibility = VISIBILITY_PRIVATE; + String summaryChannelId = sbn.getNotification().getChannelId(); if (Flags.autogroupSummaryIconUpdate()) { // Calculate the initial summary icon, icon color and visibility NotificationAttributes iconAttr = getAutobundledSummaryAttributes( @@ -191,12 +265,14 @@ public class GroupHelper { summaryIcon = iconAttr.icon; summaryIconColor = iconAttr.iconColor; summaryVisibility = iconAttr.visibility; + summaryChannelId = iconAttr.channelId; } NotificationAttributes attr = new NotificationAttributes(flags, summaryIcon, - summaryIconColor, summaryVisibility); + summaryIconColor, summaryVisibility, Notification.GROUP_ALERT_CHILDREN, + summaryChannelId); mCallback.addAutoGroupSummary(sbn.getUserId(), sbn.getPackageName(), sbn.getKey(), - attr); + AUTOGROUP_KEY, Integer.MAX_VALUE, attr); } for (String keyToGroup : notificationsToGroup) { if (android.app.Flags.checkAutogroupBeforePost()) { @@ -204,10 +280,10 @@ public class GroupHelper { // Autogrouping for the provided notification is to be done synchronously. sbnToBeAutogrouped = true; } else { - mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true); + mCallback.addAutoGroup(keyToGroup, AUTOGROUP_KEY, /*requestSort=*/true); } } else { - mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true); + mCallback.addAutoGroup(keyToGroup, AUTOGROUP_KEY, /*requestSort=*/true); } } } @@ -263,11 +339,12 @@ public class GroupHelper { } if (removeSummary) { - mCallback.removeAutoGroupSummary(userId, sbn.getPackageName()); + mCallback.removeAutoGroupSummary(userId, sbn.getPackageName(), AUTOGROUP_KEY); } else { NotificationAttributes attr = new NotificationAttributes(summaryFlags, sbn.getNotification().getSmallIcon(), sbn.getNotification().color, - VISIBILITY_PRIVATE); + VISIBILITY_PRIVATE, Notification.GROUP_ALERT_CHILDREN, + sbn.getNotification().getChannelId()); boolean attributesUpdated = false; if (Flags.autogroupSummaryIconUpdate()) { NotificationAttributes newAttr = updateAutobundledSummaryAttributes( @@ -279,7 +356,7 @@ public class GroupHelper { } if (updateSummaryFlags || attributesUpdated) { - mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), attr); + mCallback.updateAutogroupSummary(userId, sbn.getPackageName(), AUTOGROUP_KEY, attr); } } if (removeAutogroupOverlay) { @@ -287,16 +364,6 @@ public class GroupHelper { } } - @VisibleForTesting - int getNotGroupedByAppCount(int userId, String pkg) { - synchronized (mUngroupedNotifications) { - String key = generatePackageKey(userId, pkg); - final ArrayMap<String, NotificationAttributes> children = - mUngroupedNotifications.getOrDefault(key, new ArrayMap<>()); - return children.size(); - } - } - NotificationAttributes getAutobundledSummaryAttributes(@NonNull String packageName, @NonNull List<NotificationAttributes> childrenAttr) { Icon newIcon = null; @@ -338,7 +405,20 @@ public class GroupHelper { newColor = COLOR_DEFAULT; } - return new NotificationAttributes(0, newIcon, newColor, newVisibility); + // Use GROUP_ALERT_CHILDREN + // Unless all children have GROUP_ALERT_SUMMARY => avoid muting all notifications in group + int newGroupAlertBehavior = Notification.GROUP_ALERT_SUMMARY; + for (NotificationAttributes attr: childrenAttr) { + if (attr.groupAlertBehavior != Notification.GROUP_ALERT_SUMMARY) { + newGroupAlertBehavior = Notification.GROUP_ALERT_CHILDREN; + break; + } + } + + String channelId = !childrenAttr.isEmpty() ? childrenAttr.get(0).channelId : null; + + return new NotificationAttributes(0, newIcon, newColor, newVisibility, + newGroupAlertBehavior, channelId); } NotificationAttributes updateAutobundledSummaryAttributes(@NonNull String packageName, @@ -348,14 +428,28 @@ public class GroupHelper { childrenAttr); Icon newIcon = newAttr.icon; int newColor = newAttr.iconColor; + String newChannelId = newAttr.channelId; if (newAttr.icon == null) { newIcon = oldAttr.icon; } if (newAttr.iconColor == Notification.COLOR_INVALID) { newColor = oldAttr.iconColor; } + if (newAttr.channelId == null) { + newChannelId = oldAttr.channelId; + } - return new NotificationAttributes(oldAttr.flags, newIcon, newColor, newAttr.visibility); + return new NotificationAttributes(oldAttr.flags, newIcon, newColor, newAttr.visibility, + oldAttr.groupAlertBehavior, newChannelId); + } + + private NotificationAttributes getSummaryAttributes(String pkgName, + ArrayMap<String, NotificationAttributes> childrenMap) { + int flags = getAutogroupSummaryFlags(childrenMap); + NotificationAttributes attr = getAutobundledSummaryAttributes(pkgName, + childrenMap.values().stream().toList()); + return new NotificationAttributes(flags, attr.icon, attr.iconColor, attr.visibility, + attr.groupAlertBehavior, attr.channelId); } /** @@ -388,17 +482,865 @@ public class GroupHelper { } } + /** + * A non-app grouped notification has been added or updated + * Evaluate if: + * (a) an existing autogroup summary needs updated attributes + * (b) a new autogroup summary needs to be added with correct attributes + * (c) other non-app grouped children need to be moved to the autogroup + * + * This method implements autogrouping with sections support. + * + * And stores the list of upgrouped notifications & their flags + */ + private boolean maybeGroupWithSections(NotificationRecord record, + boolean autogroupSummaryExists) { + final StatusBarNotification sbn = record.getSbn(); + boolean sbnToBeAutogrouped = false; + + final NotificationSectioner sectioner = getSection(record); + if (sectioner == null) { + if (DEBUG) { + Log.i(TAG, "Skipping autogrouping for " + record + " no valid section found."); + } + return false; + } + + final String pkgName = sbn.getPackageName(); + final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( + record.getUserId(), pkgName, sectioner); + + // This notification is already aggregated + if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) { + return false; + } + + synchronized (mAggregatedNotifications) { + ArrayMap<String, NotificationAttributes> ungrouped = + mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + ungrouped.put(record.getKey(), new NotificationAttributes( + record.getFlags(), + record.getNotification().getSmallIcon(), + record.getNotification().color, + record.getNotification().visibility, + record.getNotification().getGroupAlertBehavior(), + record.getChannel().getId())); + mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped); + + // scenario 0: ungrouped notifications + if (ungrouped.size() >= mAutoGroupAtCount || autogroupSummaryExists) { + if (DEBUG) { + if (ungrouped.size() >= mAutoGroupAtCount) { + Log.i(TAG, + "Found >=" + mAutoGroupAtCount + + " ungrouped notifications => force grouping"); + } else { + Log.i(TAG, "Found aggregate summary => force grouping"); + } + } + + final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + aggregatedNotificationsAttrs.putAll(ungrouped); + mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); + + // add/update aggregate summary + updateAggregateAppGroup(fullAggregateGroupKey, record.getKey(), + autogroupSummaryExists, sectioner.mSummaryId); + + // add notification to aggregate group + for (String keyToGroup : ungrouped.keySet()) { + if (android.app.Flags.checkAutogroupBeforePost()) { + if (keyToGroup.equals(record.getKey())) { + // Autogrouping for the posted notification is to be done synchronously. + sbnToBeAutogrouped = true; + } else { + mCallback.addAutoGroup(keyToGroup, fullAggregateGroupKey.toString(), + true); + } + } else { + mCallback.addAutoGroup(keyToGroup, fullAggregateGroupKey.toString(), true); + } + } + + //cleanup mUngroupedAbuseNotifications + mUngroupedAbuseNotifications.remove(fullAggregateGroupKey); + } + } + + return sbnToBeAutogrouped; + } + + /** + * A notification was added that's app grouped. + * Evaluate whether: + * (a) an existing autogroup summary needs updated attributes + * (b) if we need to remove our autogroup overlay for this notification + * (c) we need to remove the autogroup summary + * + * This method implements autogrouping with sections support. + * + * And updates the internal state of un-app-grouped notifications and their flags. + */ + private void maybeUngroupWithSections(NotificationRecord record) { + final StatusBarNotification sbn = record.getSbn(); + final String pkgName = sbn.getPackageName(); + final int userId = record.getUserId(); + final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(userId, + pkgName, getSection(record)); + + synchronized (mAggregatedNotifications) { + // if this notification still exists and has an autogroup overlay, but is now + // grouped by the app, clear the overlay + ArrayMap<String, NotificationAttributes> ungrouped = + mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + ungrouped.remove(sbn.getKey()); + mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped); + + final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + // check if the removed notification was part of the aggregate group + if (aggregatedNotificationsAttrs.containsKey(record.getKey())) { + aggregatedNotificationsAttrs.remove(sbn.getKey()); + mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); + + if (DEBUG) { + Log.i(TAG, "maybeUngroup removeAutoGroup: " + record); + } + + mCallback.removeAutoGroup(sbn.getKey()); + + if (aggregatedNotificationsAttrs.isEmpty()) { + if (DEBUG) { + Log.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey); + } + mCallback.removeAutoGroupSummary(userId, pkgName, + fullAggregateGroupKey.toString()); + mAggregatedNotifications.remove(fullAggregateGroupKey); + } else { + if (DEBUG) { + Log.i(TAG, "Aggregate group not empty, updating: " + fullAggregateGroupKey); + } + updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0); + } + } + } + } + + /** + * Called when a notification is newly posted, after some delay, so that the app + * has a chance to post a group summary or children (complete a group). + * Checks whether that notification and other active notifications should be forced grouped + * because their grouping is incorrect: + * - missing summary + * - only summaries + * - sparse groups == multiple groups with very few notifications + * + * @param record the notification that was posted + * @param notificationList the full notification list from NotificationManagerService + * @param summaryByGroupKey the map of group summaries from NotificationManagerService + */ + @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) + protected void onNotificationPostedWithDelay(final NotificationRecord record, + final List<NotificationRecord> notificationList, + final Map<String, NotificationRecord> summaryByGroupKey) { + // Ungrouped notifications are handled separately in + // {@link #onNotificationPosted(StatusBarNotification, boolean)} + final StatusBarNotification sbn = record.getSbn(); + if (!sbn.isAppGroup()) { + return; + } + + if (record.isCanceled) { + return; + } + + final NotificationSectioner sectioner = getSection(record); + if (sectioner == null) { + if (DEBUG) { + Log.i(TAG, "Skipping autogrouping for " + record + " no valid section found."); + } + return; + } + + final String pkgName = sbn.getPackageName(); + final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( + record.getUserId(), pkgName, sectioner); + + // This notification is already aggregated + if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) { + return; + } + + synchronized (mAggregatedNotifications) { + // scenario 1: group w/o summary + // scenario 2: summary w/o children + if (isGroupChildWithoutSummary(record, summaryByGroupKey) || + isGroupSummaryWithoutChildren(record, notificationList)) { + if (DEBUG) { + Log.i(TAG, "isGroupChildWithoutSummary OR isGroupSummaryWithoutChild" + + record); + } + + ArrayMap<String, NotificationAttributes> ungrouped = + mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, + new ArrayMap<>()); + ungrouped.put(record.getKey(), new NotificationAttributes( + record.getFlags(), + record.getNotification().getSmallIcon(), + record.getNotification().color, + record.getNotification().visibility, + record.getNotification().getGroupAlertBehavior(), + record.getChannel().getId())); + mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped); + // Create/update summary and group if >= mAutoGroupAtCount notifications + // or if aggregate group exists + boolean hasSummary = !mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, + new ArrayMap<>()).isEmpty(); + if (ungrouped.size() >= mAutoGroupAtCount || hasSummary) { + if (DEBUG) { + if (ungrouped.size() >= mAutoGroupAtCount) { + Log.i(TAG, + "Found >=" + mAutoGroupAtCount + + " ungrouped notifications => force grouping"); + } else { + Log.i(TAG, "Found aggregate summary => force grouping"); + } + } + aggregateUngroupedNotifications(fullAggregateGroupKey, sbn.getKey(), + ungrouped, hasSummary, sectioner.mSummaryId); + } + + return; + } + + // scenario 3: sparse/singleton groups + if (Flags.notificationForceGroupSingletons()) { + groupSparseGroups(record, notificationList, summaryByGroupKey, sectioner, + fullAggregateGroupKey); + } + } + } + + /** + * Called when a notification is removed, so that this helper can adjust the aggregate groups: + * - Removes the autogroup summary of the notification's section + * if the record was the last child. + * - Recalculates the autogroup summary "attributes": + * icon, icon color, visibility, groupAlertBehavior, flags - if the removed record was + * part of an autogroup. + * - Removes the saved summary of the original group, if the record was the last remaining + * child of a sparse group that was forced auto-grouped. + * + * see also {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} + * + * @param record the removed notification + * @param notificationList the full notification list from NotificationManagerService + */ + @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) + protected void onNotificationRemoved(final NotificationRecord record, + final List<NotificationRecord> notificationList) { + final StatusBarNotification sbn = record.getSbn(); + final String pkgName = sbn.getPackageName(); + final int userId = record.getUserId(); + final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(userId, + pkgName, getSection(record)); + + synchronized (mAggregatedNotifications) { + ArrayMap<String, NotificationAttributes> ungrouped = + mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + ungrouped.remove(record.getKey()); + mUngroupedAbuseNotifications.put(fullAggregateGroupKey, ungrouped); + + final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + // check if the removed notification was part of the aggregate group + if (record.getGroupKey().equals(fullAggregateGroupKey.toString()) + || aggregatedNotificationsAttrs.containsKey(record.getKey())) { + aggregatedNotificationsAttrs.remove(record.getKey()); + mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); + + if (aggregatedNotificationsAttrs.isEmpty()) { + if (DEBUG) { + Log.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey); + } + mCallback.removeAutoGroupSummary(userId, pkgName, + fullAggregateGroupKey.toString()); + mAggregatedNotifications.remove(fullAggregateGroupKey); + } else { + if (DEBUG) { + Log.i(TAG, "Aggregate group not empty, updating: " + fullAggregateGroupKey); + } + updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0); + } + + // Try to cleanup cached summaries if notification was canceled (not snoozed) + if (record.isCanceled) { + maybeClearCanceledSummariesCache(pkgName, userId, + record.getNotification().getGroup(), notificationList); + } + } + } + } + + private record NotificationMoveOp(NotificationRecord record, FullyQualifiedGroupKey oldGroup, + FullyQualifiedGroupKey newGroup) { } + + /** + * Called when a notification channel is updated, so that this helper can adjust + * the aggregate groups by moving children if their section has changed. + * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} + * @param userId the userId of the channel + * @param pkgName the channel's package + * @param channel the channel that was updated + * @param notificationList the full notification list from NotificationManagerService + */ + @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) + public void onChannelUpdated(final int userId, final String pkgName, + final NotificationChannel channel, final List<NotificationRecord> notificationList) { + synchronized (mAggregatedNotifications) { + ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); + for (NotificationRecord r : notificationList) { + if (r.getChannel().getId().equals(channel.getId()) + && r.getSbn().getPackageName().equals(pkgName) + && r.getUserId() == userId) { + notificationsToCheck.put(r.getKey(), r); + } + } + + final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); + + final Set<FullyQualifiedGroupKey> oldGroups = + new HashSet<>(mAggregatedNotifications.keySet()); + for (FullyQualifiedGroupKey oldFullAggKey : oldGroups) { + // Only check aggregate groups that match the same userId & packageName + if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) { + final ArrayMap<String, NotificationAttributes> notificationsInAggGroup = + mAggregatedNotifications.get(oldFullAggKey); + if (notificationsInAggGroup == null) { + continue; + } + + FullyQualifiedGroupKey newFullAggregateGroupKey = null; + for (String key : notificationsInAggGroup.keySet()) { + if (notificationsToCheck.get(key) != null) { + // check if section changes + NotificationSectioner sectioner = getSection( + notificationsToCheck.get(key)); + if (sectioner == null) { + continue; + } + newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName, + sectioner); + if (!oldFullAggKey.equals(newFullAggregateGroupKey)) { + if (DEBUG) { + Log.i(TAG, "Change section on channel update: " + key); + } + notificationsToMove.add( + new NotificationMoveOp(notificationsToCheck.get(key), + oldFullAggKey, newFullAggregateGroupKey)); + } + } + } + + if (newFullAggregateGroupKey != null) { + // Add any notifications left ungrouped to the new section + ArrayMap<String, NotificationAttributes> ungrouped = + mUngroupedAbuseNotifications.get(newFullAggregateGroupKey); + if (ungrouped != null) { + for (NotificationRecord r : notificationList) { + if (ungrouped.containsKey(r.getKey())) { + if (DEBUG) { + Log.i(TAG, "Add previously ungrouped: " + r); + } + notificationsToMove.add( + new NotificationMoveOp(r, null, newFullAggregateGroupKey)); + } + } + //Cleanup mUngroupedAbuseNotifications + mUngroupedAbuseNotifications.remove(newFullAggregateGroupKey); + } + } + } + } + + // Batch move to new section + if (!notificationsToMove.isEmpty()) { + moveNotificationsToNewSection(userId, pkgName, notificationsToMove); + } + } + } + + @GuardedBy("mAggregatedNotifications") + private void moveNotificationsToNewSection(final int userId, final String pkgName, + final List<NotificationMoveOp> notificationsToMove) { + record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record, + boolean hasSummary) { } + ArrayMap<FullyQualifiedGroupKey, GroupUpdateOp> groupsToUpdate = new ArrayMap<>(); + + for (NotificationMoveOp moveOp: notificationsToMove) { + final NotificationRecord record = moveOp.record; + final FullyQualifiedGroupKey oldFullAggregateGroupKey = moveOp.oldGroup; + final FullyQualifiedGroupKey newFullAggregateGroupKey = moveOp.newGroup; + + if (DEBUG) { + Log.i(TAG, + "moveNotificationToNewSection: " + record + " " + newFullAggregateGroupKey + + " from: " + oldFullAggregateGroupKey); + } + + // Update/remove aggregate summary for old group + if (oldFullAggregateGroupKey != null) { + final ArrayMap<String, NotificationAttributes> oldAggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(oldFullAggregateGroupKey, + new ArrayMap<>()); + oldAggregatedNotificationsAttrs.remove(record.getKey()); + mAggregatedNotifications.put(oldFullAggregateGroupKey, + oldAggregatedNotificationsAttrs); + + // Only add once, for triggering notification + if (!groupsToUpdate.containsKey(oldFullAggregateGroupKey)) { + groupsToUpdate.put(oldFullAggregateGroupKey, + new GroupUpdateOp(oldFullAggregateGroupKey, record, true)); + } + } + + // Add/update aggregate summary for new group + if (newFullAggregateGroupKey != null) { + final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(newFullAggregateGroupKey, + new ArrayMap<>()); + boolean newGroupExists = !newAggregatedNotificationsAttrs.isEmpty(); + newAggregatedNotificationsAttrs.put(record.getKey(), + new NotificationAttributes(record.getFlags(), + record.getNotification().getSmallIcon(), + record.getNotification().color, + record.getNotification().visibility, + record.getNotification().getGroupAlertBehavior(), + record.getChannel().getId())); + mAggregatedNotifications.put(newFullAggregateGroupKey, + newAggregatedNotificationsAttrs); + + // Only add once, for triggering notification + if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) { + groupsToUpdate.put(newFullAggregateGroupKey, + new GroupUpdateOp(newFullAggregateGroupKey, record, newGroupExists)); + } + + // Add notification to new group. do not request resort + record.setOverrideGroupKey(null); + mCallback.addAutoGroup(record.getKey(), newFullAggregateGroupKey.toString(), false); + } + } + + // Update groups (sections) + for (FullyQualifiedGroupKey groupKey : groupsToUpdate.keySet()) { + final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>()); + if (aggregatedNotificationsAttrs.isEmpty()) { + mCallback.removeAutoGroupSummary(userId, pkgName, groupKey.toString()); + mAggregatedNotifications.remove(groupKey); + } else { + NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record; + boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary; + NotificationSectioner sectioner = getSection(triggeringNotification); + if (sectioner == null) { + continue; + } + updateAggregateAppGroup(groupKey, triggeringNotification.getKey(), hasSummary, + sectioner.mSummaryId); + } + } + } + + static String getFullAggregateGroupKey(String pkgName, + String groupName, int userId) { + return new FullyQualifiedGroupKey(userId, pkgName, groupName).toString(); + } + + /** + * Returns the full aggregate group key, which contains the userId and package name + * in addition to the aggregate group key (name). + * Equivalent to {@link StatusBarNotification#groupKey()} + */ + static String getFullAggregateGroupKey(NotificationRecord record) { + return new FullyQualifiedGroupKey(record.getUserId(), record.getSbn().getPackageName(), + getSection(record)).toString(); + } + + protected static boolean isAggregatedGroup(NotificationRecord record) { + return (record.mOriginalFlags & Notification.FLAG_AUTOGROUP_SUMMARY) != 0; + } + + private static int getNumChildrenForGroup(@NonNull final String groupKey, + final List<NotificationRecord> notificationList) { + //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList + int numChildren = 0; + // find children for this summary + for (NotificationRecord r : notificationList) { + if (!r.getNotification().isGroupSummary() + && groupKey.equals(r.getSbn().getGroup())) { + numChildren++; + } + } + + if (DEBUG) { + Log.i(TAG, "getNumChildrenForGroup " + groupKey + " numChild: " + numChildren); + } + return numChildren; + } + + private static boolean isGroupSummaryWithoutChildren(final NotificationRecord record, + final List<NotificationRecord> notificationList) { + final StatusBarNotification sbn = record.getSbn(); + final String groupKey = record.getSbn().getGroup(); + + // ignore non app groups and non summaries + if (!sbn.isAppGroup() || !record.getNotification().isGroupSummary()) { + return false; + } + + return getNumChildrenForGroup(groupKey, notificationList) == 0; + } + + private static boolean isGroupChildWithoutSummary(final NotificationRecord record, + final Map<String, NotificationRecord> summaryByGroupKey) { + final StatusBarNotification sbn = record.getSbn(); + final String groupKey = record.getSbn().getGroupKey(); + + if (!sbn.isAppGroup()) { + return false; + } + + if (record.getNotification().isGroupSummary()) { + return false; + } + + if (summaryByGroupKey.containsKey(groupKey)) { + return false; + } + + return true; + } + + @GuardedBy("mAggregatedNotifications") + private void aggregateUngroupedNotifications(FullyQualifiedGroupKey fullAggregateGroupKey, + String triggeringNotifKey, Map<String, NotificationAttributes> ungrouped, + final boolean hasSummary, int summaryId) { + final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + aggregatedNotificationsAttrs.putAll(ungrouped); + mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); + + // add/update aggregate summary + updateAggregateAppGroup(fullAggregateGroupKey, triggeringNotifKey, hasSummary, summaryId); + + // add notification to aggregate group + for (String key: ungrouped.keySet()) { + mCallback.addAutoGroup(key, fullAggregateGroupKey.toString(), true); + } + + //cleanup mUngroupedAbuseNotifications + mUngroupedAbuseNotifications.remove(fullAggregateGroupKey); + } + + @GuardedBy("mAggregatedNotifications") + private void updateAggregateAppGroup(FullyQualifiedGroupKey fullAggregateGroupKey, + String triggeringNotifKey, boolean hasSummary, int summaryId) { + final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + NotificationAttributes attr = getSummaryAttributes(fullAggregateGroupKey.pkg, + aggregatedNotificationsAttrs); + String channelId = hasSummary ? attr.channelId + : aggregatedNotificationsAttrs.get(triggeringNotifKey).channelId; + NotificationAttributes summaryAttr = new NotificationAttributes(attr.flags, attr.icon, + attr.iconColor, attr.visibility, attr.groupAlertBehavior, channelId); + + if (!hasSummary) { + if (DEBUG) { + Log.i(TAG, "Create aggregate summary: " + fullAggregateGroupKey); + } + mCallback.addAutoGroupSummary(fullAggregateGroupKey.userId, fullAggregateGroupKey.pkg, + triggeringNotifKey, fullAggregateGroupKey.toString(), summaryId, summaryAttr); + } else { + if (DEBUG) { + Log.i(TAG, "Update aggregate summary: " + fullAggregateGroupKey); + } + mCallback.updateAutogroupSummary(fullAggregateGroupKey.userId, + fullAggregateGroupKey.pkg, fullAggregateGroupKey.toString(), summaryAttr); + } + } + + @GuardedBy("mAggregatedNotifications") + private void groupSparseGroups(final NotificationRecord record, + final List<NotificationRecord> notificationList, + final Map<String, NotificationRecord> summaryByGroupKey, + final NotificationSectioner sectioner, + final FullyQualifiedGroupKey fullAggregateGroupKey) { + final ArrayMap<String, NotificationRecord> sparseGroupSummaries = getSparseGroups( + fullAggregateGroupKey, notificationList, summaryByGroupKey, sectioner); + if (sparseGroupSummaries.size() >= mAutogroupSparseGroupsAtCount) { + if (DEBUG) { + Log.i(TAG, + "Aggregate sparse groups for: " + record.getSbn().getPackageName() + + " Section: " + sectioner.mName); + } + + ArrayMap<String, NotificationAttributes> ungrouped = + mUngroupedAbuseNotifications.getOrDefault( + fullAggregateGroupKey, new ArrayMap<>()); + final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = + mAggregatedNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>()); + final boolean hasSummary = !aggregatedNotificationsAttrs.isEmpty(); + for (NotificationRecord r : notificationList) { + // Add notifications for detected sparse groups + if (sparseGroupSummaries.containsKey(r.getGroupKey())) { + // Move child notifications to aggregate group + if (!r.getNotification().isGroupSummary()) { + if (DEBUG) { + Log.i(TAG, "Aggregate notification (sparse group): " + r); + } + mCallback.addAutoGroup(r.getKey(), fullAggregateGroupKey.toString(), true); + aggregatedNotificationsAttrs.put(r.getKey(), + new NotificationAttributes(r.getFlags(), + r.getNotification().getSmallIcon(), r.getNotification().color, + r.getNotification().visibility, + r.getNotification().getGroupAlertBehavior(), + r.getChannel().getId())); + + } else if (r.getNotification().isGroupSummary()) { + // Remove summary notifications + if (DEBUG) { + Log.i(TAG, "Remove app summary (sparse group): " + r); + } + mCallback.removeAppProvidedSummary(r.getKey()); + cacheCanceledSummary(r); + } + } else { + // Add any notifications left ungrouped + if (ungrouped.containsKey(r.getKey())) { + if (DEBUG) { + Log.i(TAG, "Aggregate ungrouped (sparse group): " + r); + } + mCallback.addAutoGroup(r.getKey(), fullAggregateGroupKey.toString(), true); + aggregatedNotificationsAttrs.put(r.getKey(),ungrouped.get(r.getKey())); + } + } + } + + mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs); + // add/update aggregate summary + updateAggregateAppGroup(fullAggregateGroupKey, record.getKey(), hasSummary, + sectioner.mSummaryId); + + //cleanup mUngroupedAbuseNotifications + mUngroupedAbuseNotifications.remove(fullAggregateGroupKey); + } + } + + private ArrayMap<String, NotificationRecord> getSparseGroups( + final FullyQualifiedGroupKey fullAggregateGroupKey, + final List<NotificationRecord> notificationList, + final Map<String, NotificationRecord> summaryByGroupKey, + final NotificationSectioner sectioner) { + ArrayMap<String, NotificationRecord> sparseGroups = new ArrayMap<>(); + for (NotificationRecord summary : summaryByGroupKey.values()) { + if (summary != null && sectioner.isInSection(summary)) { + if (summary.getSbn().getPackageName().equalsIgnoreCase(fullAggregateGroupKey.pkg) + && summary.getUserId() == fullAggregateGroupKey.userId + && summary.getSbn().isAppGroup() + && !summary.getGroupKey().equals(fullAggregateGroupKey.toString())) { + int numChildren = getNumChildrenForGroup(summary.getSbn().getGroup(), + notificationList); + if (numChildren > 0 && numChildren < MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING) { + sparseGroups.put(summary.getGroupKey(), summary); + } + } + } + } + return sparseGroups; + } + + @GuardedBy("mAggregatedNotifications") + private void cacheCanceledSummary(NotificationRecord record) { + final FullyQualifiedGroupKey groupKey = new FullyQualifiedGroupKey(record.getUserId(), + record.getSbn().getPackageName(), record.getNotification().getGroup()); + mCanceledSummaries.put(groupKey, new CachedSummary(record.getSbn().getId(), + record.getSbn().getTag(), record.getNotification().getGroup(), record.getKey())); + } + + @GuardedBy("mAggregatedNotifications") + private void maybeClearCanceledSummariesCache(String pkgName, int userId, + String groupName, List<NotificationRecord> notificationList) { + final FullyQualifiedGroupKey findKey = new FullyQualifiedGroupKey(userId, pkgName, + groupName); + CachedSummary summary = mCanceledSummaries.get(findKey); + // Check if any notifications from original group remain + if (summary != null) { + if (DEBUG) { + Log.i(TAG, "Try removing cached summary: " + summary); + } + boolean stillHasChildren = false; + //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList + for (NotificationRecord r : notificationList) { + if (summary.originalGroupKey.equals(r.getNotification().getGroup()) + && r.getUser().getIdentifier() == userId + && r.getSbn().getPackageName().equals(pkgName)) { + stillHasChildren = true; + break; + } + } + if (!stillHasChildren) { + removeCachedSummary(pkgName, userId, summary); + } + } + } + + @VisibleForTesting + @GuardedBy("mAggregatedNotifications") + protected CachedSummary findCanceledSummary(String pkgName, String tag, int id, int userId) { + for (FullyQualifiedGroupKey key: mCanceledSummaries.keySet()) { + if (pkgName.equals(key.pkg) && userId == key.userId) { + CachedSummary summary = mCanceledSummaries.get(key); + if (summary != null && summary.id == id && TextUtils.equals(tag, summary.tag)) { + return summary; + } + } + } + return null; + } + + @VisibleForTesting + @GuardedBy("mAggregatedNotifications") + protected CachedSummary findCanceledSummary(String pkgName, String tag, int id, int userId, + String groupName) { + final FullyQualifiedGroupKey findKey = new FullyQualifiedGroupKey(userId, pkgName, + groupName); + CachedSummary summary = mCanceledSummaries.get(findKey); + if (summary != null && summary.id == id && TextUtils.equals(tag, summary.tag)) { + return summary; + } else { + return null; + } + } + + @GuardedBy("mAggregatedNotifications") + private void removeCachedSummary(String pkgName, int userId, CachedSummary summary) { + final FullyQualifiedGroupKey key = new FullyQualifiedGroupKey(userId, pkgName, + summary.originalGroupKey); + mCanceledSummaries.remove(key); + } + + protected boolean isUpdateForCanceledSummary(final NotificationRecord record) { + synchronized (mAggregatedNotifications) { + if (record.getSbn().isAppGroup() && record.getNotification().isGroupSummary()) { + CachedSummary cachedSummary = findCanceledSummary(record.getSbn().getPackageName(), + record.getSbn().getTag(), record.getSbn().getId(), record.getUserId(), + record.getNotification().getGroup()); + return cachedSummary != null; + } + return false; + } + } + + /** + * Cancels the original group's children when an app cancels a summary that was 'maybe' + * previously removed due to forced grouping of a "sparse group". + * + * @param pkgName packageName + * @param tag original summary notification tag + * @param id original summary notification id + * @param userId original summary userId + */ + @FlaggedApi(Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS) + public void maybeCancelGroupChildrenForCanceledSummary(String pkgName, String tag, int id, + int userId, int cancelReason) { + synchronized (mAggregatedNotifications) { + final CachedSummary summary = findCanceledSummary(pkgName, tag, id, userId); + if (summary != null) { + if (DEBUG) { + Log.i(TAG, "Found cached summary: " + summary.key); + } + mCallback.removeNotificationFromCanceledGroup(userId, pkgName, + summary.originalGroupKey, cancelReason); + removeCachedSummary(pkgName, userId, summary); + } + } + } + + static NotificationSectioner getSection(final NotificationRecord record) { + for (NotificationSectioner sectioner: NOTIFICATION_SHADE_SECTIONS) { + if (sectioner.isInSection(record)) { + return sectioner; + } + } + return null; + } + + record FullyQualifiedGroupKey(int userId, String pkg, String groupName) { + FullyQualifiedGroupKey(int userId, String pkg, @Nullable NotificationSectioner sectioner) { + this(userId, pkg, AGGREGATE_GROUP_KEY + (sectioner != null ? sectioner.mName : "")); + } + + @Override + public String toString() { + return userId + "|" + pkg + "|" + "g:" + groupName; + } + } + + protected static class NotificationSectioner { + final String mName; + final int mSummaryId; + private final Predicate<NotificationRecord> mSectionChecker; + + public NotificationSectioner(String name, int summaryId, + Predicate<NotificationRecord> sectionChecker) { + mName = name; + mSummaryId = summaryId; + mSectionChecker = sectionChecker; + } + + boolean isInSection(final NotificationRecord record) { + return isNotificationGroupable(record) && mSectionChecker.test(record); + } + + private boolean isNotificationGroupable(final NotificationRecord record) { + if (record.isConversation()) { + return false; + } + + Notification notification = record.getSbn().getNotification(); + boolean isColorizedFGS = notification.isForegroundService() + && notification.isColorized() + && record.getImportance() > NotificationManager.IMPORTANCE_MIN; + boolean isCall = record.getImportance() > NotificationManager.IMPORTANCE_MIN + && notification.isStyle(Notification.CallStyle.class); + if (isColorizedFGS || isCall) { + return false; + } + + return true; + } + } + + record CachedSummary(int id, String tag, String originalGroupKey, String key) {} + protected static class NotificationAttributes { public final int flags; public final int iconColor; public final Icon icon; public final int visibility; + public final int groupAlertBehavior; + public final String channelId; - public NotificationAttributes(int flags, Icon icon, int iconColor, int visibility) { + public NotificationAttributes(int flags, Icon icon, int iconColor, int visibility, + int groupAlertBehavior, String channelId) { this.flags = flags; this.icon = icon; this.iconColor = iconColor; this.visibility = visibility; + this.groupAlertBehavior = groupAlertBehavior; + this.channelId = channelId; } public NotificationAttributes(@NonNull NotificationAttributes attr) { @@ -406,6 +1348,8 @@ public class GroupHelper { this.icon = attr.icon; this.iconColor = attr.iconColor; this.visibility = attr.visibility; + this.groupAlertBehavior = attr.groupAlertBehavior; + this.channelId = attr.channelId; } @Override @@ -417,22 +1361,39 @@ public class GroupHelper { return false; } return flags == that.flags && iconColor == that.iconColor && icon.sameAs(that.icon) - && visibility == that.visibility; + && visibility == that.visibility + && groupAlertBehavior == that.groupAlertBehavior + && channelId.equals(that.channelId); } @Override public int hashCode() { - return Objects.hash(flags, iconColor, icon, visibility); + return Objects.hash(flags, iconColor, icon, visibility, groupAlertBehavior, channelId); + } + + @Override + public String toString() { + return "NotificationAttributes: flags: " + flags + " icon: " + icon + " color: " + + iconColor + " vis: " + visibility + " groupAlertBehavior: " + + groupAlertBehavior + " channelId: " + channelId; } } protected interface Callback { - void addAutoGroup(String key, boolean requestSort); + void addAutoGroup(String key, String groupName, boolean requestSort); void removeAutoGroup(String key); - void addAutoGroupSummary(int userId, String pkg, String triggeringKey, + void addAutoGroupSummary(int userId, String pkg, String triggeringKey, String groupName, + int summaryId, NotificationAttributes summaryAttr); + void removeAutoGroupSummary(int user, String pkg, String groupKey); + + void updateAutogroupSummary(int userId, String pkg, String groupKey, NotificationAttributes summaryAttr); - void removeAutoGroupSummary(int user, String pkg); - void updateAutogroupSummary(int userId, String pkg, NotificationAttributes summaryAttr); + + // New callbacks for API abuse grouping + void removeAppProvidedSummary(String key); + + void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, + int cancelReason); } } diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index a7e14d9baea2..614a0a59c691 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -604,6 +604,13 @@ public final class NotificationAttentionHelper { } } + // Suppressed because notification was explicitly flagged as silent + if (android.service.notification.Flags.notificationSilentFlag()) { + if (notification.isSilent()) { + return true; + } + } + // Suppressed for being too recently noisy final String pkg = record.getSbn().getPackageName(); if (mUsageStats.isAlertRateLimited(pkg)) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index a4f534eeba67..1c40f44b7b78 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -39,6 +39,7 @@ import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED; +import static android.app.Notification.FLAG_GROUP_SUMMARY; import static android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; import static android.app.Notification.FLAG_NO_CLEAR; import static android.app.Notification.FLAG_NO_DISMISS; @@ -108,6 +109,7 @@ import static android.service.notification.Adjustment.TYPE_NEWS; import static android.service.notification.Adjustment.TYPE_PROMOTION; import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA; import static android.service.notification.Flags.callstyleCallbackApi; +import static android.service.notification.Flags.notificationForceGrouping; import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners; import static android.service.notification.Flags.redactSensitiveNotificationsBigTextStyle; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; @@ -371,6 +373,7 @@ import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.BackgroundActivityStartCallback; import com.android.server.wm.WindowManagerInternal; +import java.util.function.BiPredicate; import libcore.io.IoUtils; import org.json.JSONException; @@ -512,6 +515,8 @@ public class NotificationManagerService extends SystemService { private static final long DELAY_FOR_ASSISTANT_TIME = 200; + private static final long DELAY_FORCE_REGROUP_TIME = 3000; + private static final String ACTION_NOTIFICATION_TIMEOUT = NotificationManagerService.class.getSimpleName() + ".TIMEOUT"; private static final int REQUEST_CODE_TIMEOUT = 1; @@ -608,6 +613,9 @@ public class NotificationManagerService extends SystemService { static final long NOTIFICATION_MAX_AGE_AT_POST = Duration.ofDays(14).toMillis(); + // Minium number of sparse groups for a package before autogrouping them + private static final int AUTOGROUP_SPARSE_GROUPS_AT_COUNT = 3; + private IActivityManager mAm; private ActivityTaskManagerInternal mAtm; private ActivityManager mActivityManager; @@ -1001,17 +1009,25 @@ public class NotificationManagerService extends SystemService { * icons are different. * @param userId user id of the autogroup summary * @param pkg package of the autogroup summary + * @param groupKey group key of the autogroup summary * @param summaryAttr the new flags and/or icon & color for this summary * @param isAppForeground true if the app is currently in the foreground. */ @GuardedBy("mNotificationLock") - protected void updateAutobundledSummaryLocked(int userId, String pkg, - NotificationAttributes summaryAttr, boolean isAppForeground) { + protected void updateAutobundledSummaryLocked(int userId, String pkg, String groupKey, + NotificationAttributes summaryAttr, boolean isAppForeground) { ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId); if (summaries == null) { return; } - String summaryKey = summaries.get(pkg); + final String autbundledGroupKey; + if (notificationForceGrouping()) { + autbundledGroupKey = groupKey; + } else { + autbundledGroupKey = pkg; + } + + String summaryKey = summaries.get(autbundledGroupKey); if (summaryKey == null) { return; } @@ -1019,12 +1035,26 @@ public class NotificationManagerService extends SystemService { if (summary == null) { return; } + int oldFlags = summary.getSbn().getNotification().flags; boolean attributesUpdated = !summaryAttr.icon.sameAs(summary.getSbn().getNotification().getSmallIcon()) || summaryAttr.iconColor != summary.getSbn().getNotification().color - || summaryAttr.visibility != summary.getSbn().getNotification().visibility; + || summaryAttr.visibility != summary.getSbn().getNotification().visibility + || summaryAttr.groupAlertBehavior != + summary.getSbn().getNotification().getGroupAlertBehavior(); + + if (notificationForceGrouping()) { + if (!summary.getChannel().getId().equals(summaryAttr.channelId)) { + NotificationChannel newChannel = mPreferencesHelper.getNotificationChannel(pkg, + summary.getUid(), summaryAttr.channelId, false); + if (newChannel != null) { + summary.updateNotificationChannel(newChannel); + attributesUpdated = true; + } + } + } if (oldFlags != summaryAttr.flags || attributesUpdated) { summary.getSbn().getNotification().flags = @@ -1032,6 +1062,8 @@ public class NotificationManagerService extends SystemService { summary.getSbn().getNotification().setSmallIcon(summaryAttr.icon); summary.getSbn().getNotification().color = summaryAttr.iconColor; summary.getSbn().getNotification().visibility = summaryAttr.visibility; + summary.getSbn().getNotification() + .setGroupAlertBehavior(summaryAttr.groupAlertBehavior); mHandler.post(new EnqueueNotificationRunnable(userId, summary, isAppForeground, /* isAppProvided= */ false, mPostNotificationTrackerFactory.newTracker(null))); } @@ -2836,12 +2868,17 @@ public class NotificationManagerService extends SystemService { mAutoGroupAtCount = getContext().getResources().getInteger(R.integer.config_autoGroupAtCount); return new GroupHelper(getContext(), getContext().getPackageManager(), - mAutoGroupAtCount, new GroupHelper.Callback() { + mAutoGroupAtCount, AUTOGROUP_SPARSE_GROUPS_AT_COUNT, new GroupHelper.Callback() { @Override - public void addAutoGroup(String key, boolean requestSort) { - synchronized (mNotificationLock) { - addAutogroupKeyLocked(key, requestSort); - } + public void addAutoGroup(String key, String groupName, boolean requestSort) { + synchronized (mNotificationLock) { + if (notificationForceGrouping()) { + convertSummaryToNotificationLocked(key); + addAutogroupKeyLocked(key, groupName, requestSort); + } else { + addAutogroupKeyLocked(key, groupName, requestSort); + } + } } @Override @@ -2853,10 +2890,9 @@ public class NotificationManagerService extends SystemService { @Override public void addAutoGroupSummary(int userId, String pkg, String triggeringKey, - NotificationAttributes summaryAttr) { + String groupName, int summaryId, NotificationAttributes summaryAttr) { NotificationRecord r = createAutoGroupSummary(userId, pkg, triggeringKey, - summaryAttr.flags, summaryAttr.icon, summaryAttr.iconColor, - summaryAttr.visibility); + groupName, summaryId, summaryAttr); if (r != null) { final boolean isAppForeground = mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND; @@ -2867,19 +2903,56 @@ public class NotificationManagerService extends SystemService { } @Override - public void removeAutoGroupSummary(int userId, String pkg) { + public void removeAutoGroupSummary(int userId, String pkg, String groupKey) { synchronized (mNotificationLock) { - clearAutogroupSummaryLocked(userId, pkg); + clearAutogroupSummaryLocked(userId, pkg, groupKey); } } @Override - public void updateAutogroupSummary(int userId, String pkg, + public void updateAutogroupSummary(int userId, String pkg, String groupKey, NotificationAttributes summaryAttr) { boolean isAppForeground = pkg != null && mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND; synchronized (mNotificationLock) { - updateAutobundledSummaryLocked(userId, pkg, summaryAttr, isAppForeground); + updateAutobundledSummaryLocked(userId, pkg, groupKey, summaryAttr, + isAppForeground); + } + } + + @Override + public void removeAppProvidedSummary(String key) { + synchronized (mNotificationLock) { + removeAppSummaryLocked(key); + } + } + + @Override + public void removeNotificationFromCanceledGroup(int userId, String pkg, + String groupKey, int cancelReason) { + synchronized (mNotificationLock) { + final int mustNotHaveFlags; + if (lifetimeExtensionRefactor()) { + // Also don't allow client apps to cancel lifetime extended notifs. + mustNotHaveFlags = (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB + | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); + } else { + mustNotHaveFlags = (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB); + } + FlagChecker childrenFlagChecker = (flags) -> { + if (cancelReason == REASON_CANCEL + || cancelReason == REASON_CLICK + || cancelReason == REASON_CANCEL_ALL) { + if ((flags & FLAG_BUBBLE) != 0) { + return false; + } + } + return (flags & mustNotHaveFlags) == 0; + }; + cancelGroupChildrenLocked(userId, pkg, Binder.getCallingUid(), + Binder.getCallingPid(), null, + false, childrenFlagChecker, groupKey, + REASON_APP_CANCEL, SystemClock.elapsedRealtime()); } } }); @@ -3107,6 +3180,18 @@ public class NotificationManagerService extends SystemService { modifiedChannel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED); } + if (notificationForceGrouping()) { + final NotificationChannel updatedChannel = mPreferencesHelper.getNotificationChannel( + pkg, uid, channel.getId(), false); + mHandler.postDelayed(() -> { + synchronized (mNotificationLock) { + mGroupHelper.onChannelUpdated( + UserHandle.getUserHandleForUid(uid).getIdentifier(), pkg, + updatedChannel, mNotificationList); + } + }, DELAY_FORCE_REGROUP_TIME); + } + handleSavePolicyFile(); } @@ -6652,18 +6737,33 @@ public class NotificationManagerService extends SystemService { } } + @SuppressWarnings("GuardedBy") @GuardedBy("mNotificationLock") - void addAutogroupKeyLocked(String key, boolean requestSort) { + void addAutogroupKeyLocked(String key, String groupName, boolean requestSort) { NotificationRecord r = mNotificationsByKey.get(key); if (r == null) { return; } if (r.getSbn().getOverrideGroupKey() == null) { - addAutoGroupAdjustment(r, GroupHelper.AUTOGROUP_KEY); + if (notificationForceGrouping()) { + if (r.getSbn().isAppGroup()) { + // Override group key early for forced grouped notifications + r.setOverrideGroupKey(groupName); + } + } + + addAutoGroupAdjustment(r, groupName); EventLogTags.writeNotificationAutogrouped(key); + if (!android.app.Flags.checkAutogroupBeforePost() || requestSort) { mRankingHandler.requestSort(); } + + if (notificationForceGrouping()) { + if (r.getSbn().isAppGroup()) { + mListeners.notifyPostedLocked(r, r); + } + } } } @@ -6692,27 +6792,57 @@ public class NotificationManagerService extends SystemService { // Clears the 'fake' auto-group summary. @VisibleForTesting @GuardedBy("mNotificationLock") - void clearAutogroupSummaryLocked(int userId, String pkg) { + void clearAutogroupSummaryLocked(int userId, String pkg, String groupKey) { + final String autbundledGroupKey; + if (notificationForceGrouping()) { + autbundledGroupKey = groupKey; + } else { + autbundledGroupKey = pkg; + } ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId); - if (summaries != null && summaries.containsKey(pkg)) { - final NotificationRecord removed = findNotificationByKeyLocked(summaries.remove(pkg)); + if (summaries != null && summaries.containsKey(autbundledGroupKey)) { + final NotificationRecord removed = findNotificationByKeyLocked( + summaries.remove(autbundledGroupKey)); if (removed != null) { final StatusBarNotification sbn = removed.getSbn(); cancelNotification(MY_UID, MY_PID, pkg, sbn.getTag(), sbn.getId(), 0, 0, false, - userId, REASON_UNAUTOBUNDLED, null); + userId, REASON_UNAUTOBUNDLED, null); } } } @GuardedBy("mNotificationLock") - private boolean hasAutoGroupSummaryLocked(StatusBarNotification sbn) { - ArrayMap<String, String> summaries = mAutobundledSummaries.get(sbn.getUserId()); - return summaries != null && summaries.containsKey(sbn.getPackageName()); + void removeAppSummaryLocked(String key) { + NotificationRecord r = mNotificationsByKey.get(key); + if (r == null) { + return; + } + if (convertSummaryToNotificationLocked(key)) { + r.isCanceled = true; + cancelNotification(Binder.getCallingUid(), + Binder.getCallingPid(), r.getSbn().getPackageName(), + r.getSbn().getTag(), r.getSbn().getId(), 0, 0, + false, r.getUserId(), + NotificationListenerService.REASON_GROUP_OPTIMIZATION, null); + } + } + + @GuardedBy("mNotificationLock") + private boolean hasAutoGroupSummaryLocked(NotificationRecord record) { + final String autbundledGroupKey; + if (notificationForceGrouping()) { + autbundledGroupKey = GroupHelper.getFullAggregateGroupKey(record); + } else { + autbundledGroupKey = record.getSbn().getPackageName(); + } + + ArrayMap<String, String> summaries = mAutobundledSummaries.get(record.getUserId()); + return summaries != null && summaries.containsKey(autbundledGroupKey); } // Creates a 'fake' summary for a package that has exceeded the solo-notification limit. NotificationRecord createAutoGroupSummary(int userId, String pkg, String triggeringKey, - int flagsToSet, Icon summaryIcon, int summaryIconColor, int summaryVisibilty) { + String groupKey, int summaryId, NotificationAttributes summaryAttr) { NotificationRecord summaryRecord = null; boolean isPermissionFixed = mPermissionHelper.isPermissionFixed(pkg, userId); synchronized (mNotificationLock) { @@ -6730,24 +6860,35 @@ public class NotificationManagerService extends SystemService { summaries = new ArrayMap<>(); } mAutobundledSummaries.put(userId, summaries); - if (!summaries.containsKey(pkg)) { + + boolean hasSummary; + String channelId; + if (notificationForceGrouping()) { + hasSummary = summaries.containsKey(groupKey); + channelId = summaryAttr.channelId; + } else { + hasSummary = summaries.containsKey(pkg); + channelId = notificationRecord.getChannel().getId(); + } + + if (!hasSummary) { // Add summary final ApplicationInfo appInfo = adjustedSbn.getNotification().extras.getParcelable( EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo.class); final Bundle extras = new Bundle(); extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, appInfo); - final String channelId = notificationRecord.getChannel().getId(); + final Notification summaryNotification = new Notification.Builder(getContext(), channelId) - .setSmallIcon(summaryIcon) + .setSmallIcon(summaryAttr.icon) .setGroupSummary(true) - .setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN) - .setGroup(GroupHelper.AUTOGROUP_KEY) - .setFlag(flagsToSet, true) - .setColor(summaryIconColor) - .setVisibility(summaryVisibilty) + .setGroupAlertBehavior(summaryAttr.groupAlertBehavior) + .setGroup(groupKey) + .setFlag(summaryAttr.flags, true) + .setColor(summaryAttr.iconColor) + .setVisibility(summaryAttr.visibility) .build(); summaryNotification.extras.putAll(extras); Intent appIntent = getContext().getPackageManager().getLaunchIntentForPackage(pkg); @@ -6759,17 +6900,22 @@ public class NotificationManagerService extends SystemService { final StatusBarNotification summarySbn = new StatusBarNotification(adjustedSbn.getPackageName(), adjustedSbn.getOpPkg(), - Integer.MAX_VALUE, - GroupHelper.AUTOGROUP_KEY, adjustedSbn.getUid(), + summaryId, + groupKey, adjustedSbn.getUid(), adjustedSbn.getInitialPid(), summaryNotification, - adjustedSbn.getUser(), GroupHelper.AUTOGROUP_KEY, + adjustedSbn.getUser(), groupKey, System.currentTimeMillis()); summaryRecord = new NotificationRecord(getContext(), summarySbn, notificationRecord.getChannel()); summaryRecord.setImportanceFixed(isPermissionFixed); summaryRecord.setIsAppImportanceLocked( notificationRecord.getIsAppImportanceLocked()); - summaries.put(pkg, summarySbn.getKey()); + + if (notificationForceGrouping()) { + summaries.put(summarySbn.getGroupKey(), summarySbn.getKey()); + } else { + summaries.put(pkg, summarySbn.getKey()); + } } if (summaryRecord != null && checkDisqualifyingFeatures(userId, uid, summaryRecord.getSbn().getId(), summaryRecord.getSbn().getTag(), summaryRecord, @@ -6780,6 +6926,27 @@ public class NotificationManagerService extends SystemService { return null; } + @GuardedBy("mNotificationLock") + boolean convertSummaryToNotificationLocked(final String key) { + NotificationRecord r = mNotificationsByKey.get(key); + if (r == null) { + return false; + } + // Convert summary to regular notification + if (r.getSbn().isAppGroup() && r.getNotification().isGroupSummary()) { + String oldGroupKey = r.getGroupKey(); + NotificationRecord groupSummary = mSummaryByGroupKey.get(oldGroupKey); + if (groupSummary != null && groupSummary.getKey().equals(r.getKey())) { + mSummaryByGroupKey.remove(oldGroupKey); + } + // Clear summary flag + StatusBarNotification sbn = r.getSbn(); + sbn.getNotification().flags = (r.mOriginalFlags & ~FLAG_GROUP_SUMMARY); + return true; + } + return false; + } + // Gets packages that have requested notification permission, and whether that has been // allowed/denied, for all users on the device. // Returns a single map containing that info keyed by (uid, package name) for all users. @@ -7794,6 +7961,10 @@ public class NotificationManagerService extends SystemService { notification.setTimeoutAfter(NOTIFICATION_TTL); } } + + if (notificationForceGrouping()) { + notification.fixSilentGroup(); + } } /** @@ -8347,8 +8518,15 @@ public class NotificationManagerService extends SystemService { * They will be recreated as needed when the group children are unsnoozed */ private boolean isSnoozable(NotificationRecord record) { - return !(record.getNotification().isGroupSummary() && GroupHelper.AUTOGROUP_KEY.equals( - record.getNotification().getGroup())); + if (notificationForceGrouping()) { + boolean isExemptedSummary = + ((record.getFlags() & FLAG_AUTOGROUP_SUMMARY) != 0 + || GroupHelper.isAggregatedGroup(record)); + return !(record.getNotification().isGroupSummary() && isExemptedSummary); + } else { + return !(record.getNotification().isGroupSummary() + && GroupHelper.AUTOGROUP_KEY.equals(record.getNotification().getGroup())); + } } } @@ -8471,9 +8649,12 @@ public class NotificationManagerService extends SystemService { cancelNotificationLocked( r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName, mCancellationElapsedTimeMs); - cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName, - mSendDelete, childrenFlagChecker, mReason, - mCancellationElapsedTimeMs); + if (r.getNotification().isGroupSummary()) { + cancelGroupChildrenLocked(mUserId, mPkg, mCallingUid, mCallingPid, + listenerName, mSendDelete, childrenFlagChecker, + r.getNotification().getGroup(), mReason, + mCancellationElapsedTimeMs); + } mAttentionHelper.updateLightsLocked(); if (mShortcutHelper != null) { mShortcutHelper.maybeListenForShortcutChangesForBubbles(r, @@ -8481,6 +8662,14 @@ public class NotificationManagerService extends SystemService { mHandler); } } else { + if (notificationForceGrouping()) { + // No notification was found => maybe it was canceled by forced grouping + if (Flags.notificationForceGroupSingletons()) { + mGroupHelper.maybeCancelGroupChildrenForCanceledSummary(mPkg, mTag, + mId, mUserId, mReason); + } + } + // No notification was found, assume that it is snoozed and cancel it. if (mReason != REASON_SNOOZED) { final boolean wasSnoozed = mSnoozeHelper.cancel(mUserId, mPkg, mTag, mId); @@ -8708,7 +8897,7 @@ public class NotificationManagerService extends SystemService { boolean appBanned = !areNotificationsEnabledForPackageInt(pkg, uid); boolean isCallNotification = isCallNotification(pkg, uid); boolean posted = false; - synchronized (mNotificationLock) { + synchronized (NotificationManagerService.this.mNotificationLock) { try { NotificationRecord r = findNotificationByListLocked(mEnqueuedNotifications, key); @@ -8731,6 +8920,29 @@ public class NotificationManagerService extends SystemService { return false; } + if (notificationForceGrouping()) { + if (Flags.notificationForceGroupSingletons()) { + // Check if this is an updated for a summary for an aggregated sparse + // group and remove it because that summary has been canceled + if (mGroupHelper.isUpdateForCanceledSummary(r)) { + if (DBG) { + Log.w(TAG, + "Suppressing notification because summary was canceled: " + + r); + } + + String groupKey = r.getGroupKey(); + NotificationRecord groupSummary = mSummaryByGroupKey.get(groupKey); + if (groupSummary != null && groupSummary.getKey() + .equals(r.getKey())) { + mSummaryByGroupKey.remove(groupKey); + } + return false; + } + } + } + + final boolean isPackageSuspended = isPackagePausedOrSuspended(r.getSbn().getPackageName(), r.getUid()); r.setHidden(isPackageSuspended); @@ -8788,18 +9000,38 @@ public class NotificationManagerService extends SystemService { if (notification.getSmallIcon() != null && !isCritical(r)) { StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null; if (oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup()) + || !Objects.equals(oldSbn.getNotification().getGroup(), + n.getNotification().getGroup()) || oldSbn.getNotification().flags != n.getNotification().flags) { synchronized (mNotificationLock) { - boolean willBeAutogrouped = mGroupHelper.onNotificationPosted(n, - hasAutoGroupSummaryLocked(n)); + final String autogroupName = + notificationForceGrouping() ? + GroupHelper.getFullAggregateGroupKey(r) + : GroupHelper.AUTOGROUP_KEY; + boolean willBeAutogrouped = + mGroupHelper.onNotificationPosted(r, + hasAutoGroupSummaryLocked(r)); if (willBeAutogrouped) { // The newly posted notification will be autogrouped, but // was not autogrouped onPost, to avoid an unnecessary sort. // We add the autogroup key to the notification without a // sort here, and it'll be sorted below with extractSignals. - addAutogroupKeyLocked(key, /* requestSort= */false); + addAutogroupKeyLocked(key, + autogroupName, /*requestSort=*/false); + } else { + if (notificationForceGrouping()) { + // Wait 3 seconds so that the app has a chance to post + // a group summary or children (complete a group) + mHandler.postDelayed(() -> { + synchronized (mNotificationLock) { + mGroupHelper.onNotificationPostedWithDelay( + r, mNotificationList, mSummaryByGroupKey); + } + }, r.getKey(), DELAY_FORCE_REGROUP_TIME); + } } + } } } @@ -8835,9 +9067,18 @@ public class NotificationManagerService extends SystemService { mHandler.post(() -> { synchronized (mNotificationLock) { mGroupHelper.onNotificationPosted( - n, hasAutoGroupSummaryLocked(n)); + r, hasAutoGroupSummaryLocked(r)); } }); + + if (notificationForceGrouping()) { + mHandler.postDelayed(() -> { + synchronized (mNotificationLock) { + mGroupHelper.onNotificationPostedWithDelay(r, + mNotificationList, mSummaryByGroupKey); + } + }, r.getKey(), DELAY_FORCE_REGROUP_TIME); + } } } } @@ -8846,12 +9087,20 @@ public class NotificationManagerService extends SystemService { if (old != null && !old.isCanceled) { mListeners.notifyRemovedLocked(r, REASON_ERROR, r.getStats()); - mHandler.post(new Runnable() { - @Override - public void run() { - mGroupHelper.onNotificationRemoved(n); - } - }); + if (notificationForceGrouping()) { + mHandler.post(() -> { + synchronized (mNotificationLock) { + mGroupHelper.onNotificationRemoved(r, mNotificationList); + } + }); + } else { + mHandler.post(new Runnable() { + @Override + public void run() { + mGroupHelper.onNotificationRemoved(r); + } + }); + } } if (callstyleCallbackApi()) { @@ -9082,6 +9331,18 @@ public class NotificationManagerService extends SystemService { n.flags &= ~Notification.FLAG_GROUP_SUMMARY; } + if (notificationForceGrouping()) { + if (old != null) { + // If this is an update to a summary that was forced grouped => remove summary flag + boolean wasSummary = (old.mOriginalFlags & FLAG_GROUP_SUMMARY) != 0; + boolean wasForcedGrouped = (old.getFlags() & FLAG_GROUP_SUMMARY) == 0 + && old.getSbn().getOverrideGroupKey() != null; + if (n.isGroupSummary() && wasSummary && wasForcedGrouped) { + n.flags &= ~FLAG_GROUP_SUMMARY; + } + } + } + String group = sbn.getGroupKey(); boolean isSummary = n.isGroupSummary(); @@ -9114,8 +9375,10 @@ public class NotificationManagerService extends SystemService { // notification was a summary and the new one isn't, or when the old // notification was a summary and its group key changed. if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) { - cancelGroupChildrenLocked(old, callingUid, callingPid, null, false /* sendDelete */, - childrenFlagChecker, REASON_APP_CANCEL, SystemClock.elapsedRealtime()); + cancelGroupChildrenLocked(old.getUserId(), old.getSbn().getPackageName(), callingUid, + callingPid, null, false /* sendDelete */, childrenFlagChecker, + old.getNotification().getGroup(), REASON_APP_CANCEL, + SystemClock.elapsedRealtime()); } } @@ -9777,12 +10040,21 @@ public class NotificationManagerService extends SystemService { r.isCanceled = true; } mListeners.notifyRemovedLocked(r, reason, r.getStats()); - mHandler.post(new Runnable() { - @Override - public void run() { - mGroupHelper.onNotificationRemoved(r.getSbn()); - } - }); + if (notificationForceGrouping()) { + mHandler.removeCallbacksAndMessages(r.getKey()); + mHandler.post(() -> { + synchronized (NotificationManagerService.this.mNotificationLock) { + mGroupHelper.onNotificationRemoved(r, mNotificationList); + } + }); + } else { + mHandler.post(new Runnable() { + @Override + public void run() { + mGroupHelper.onNotificationRemoved(r); + } + }); + } if (callstyleCallbackApi()) { notifyCallNotificationEventListenerOnRemoved(r); } @@ -9815,9 +10087,15 @@ public class NotificationManagerService extends SystemService { } final ArrayMap<String, String> summaries = mAutobundledSummaries.get(r.getSbn().getUserId()); + final String autbundledGroupKey; + if (notificationForceGrouping()) { + autbundledGroupKey = groupKey; + } else { + autbundledGroupKey = r.getSbn().getPackageName(); + } if (summaries != null && r.getSbn().getKey().equals( - summaries.get(r.getSbn().getPackageName()))) { - summaries.remove(r.getSbn().getPackageName()); + summaries.get(autbundledGroupKey))) { + summaries.remove(autbundledGroupKey); } // Save it for users of getHistoricalNotifications(), unless the whole channel was deleted @@ -10081,6 +10359,15 @@ public class NotificationManagerService extends SystemService { public boolean apply(int flags); } + private static boolean isChildOfGroup(final NotificationRecord childRecord, int userId, + String pkg, String groupKey) { + return (childRecord.getUser().getIdentifier() == userId + && childRecord.getSbn().getPackageName().equals(pkg) + && childRecord.getSbn().isGroup() + && !childRecord.getNotification().isGroupSummary() + && TextUtils.equals(groupKey, childRecord.getNotification().getGroup())); + } + @GuardedBy("mNotificationLock") private void cancelAllNotificationsByListLocked(ArrayList<NotificationRecord> notificationList, @Nullable String pkg, boolean nullPkgIndicatesUserSwitch, @Nullable String channelId, @@ -10238,43 +10525,34 @@ public class NotificationManagerService extends SystemService { // Warning: The caller is responsible for invoking updateLightsLocked(). @GuardedBy("mNotificationLock") - private void cancelGroupChildrenLocked(NotificationRecord r, int callingUid, int callingPid, - String listenerName, boolean sendDelete, FlagChecker flagChecker, int reason, - @ElapsedRealtimeLong long cancellationElapsedTimeMs) { - Notification n = r.getNotification(); - if (!n.isGroupSummary()) { - return; - } - - String pkg = r.getSbn().getPackageName(); - + private void cancelGroupChildrenLocked(int userId, String pkg, int callingUid, int callingPid, + String listenerName, boolean sendDelete, FlagChecker flagChecker, String groupKey, + int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) { if (pkg == null) { - if (DBG) Slog.e(TAG, "No package for group summary: " + r.getKey()); + if (DBG) Slog.e(TAG, "No package for group summary"); return; } - cancelGroupChildrenByListLocked(mNotificationList, r, callingUid, callingPid, listenerName, - sendDelete, true, flagChecker, reason, cancellationElapsedTimeMs); - cancelGroupChildrenByListLocked(mEnqueuedNotifications, r, callingUid, callingPid, - listenerName, sendDelete, false, flagChecker, reason, cancellationElapsedTimeMs); + cancelGroupChildrenByListLocked(mNotificationList, userId, pkg, callingUid, callingPid, + listenerName, sendDelete, true, flagChecker, groupKey, + reason, cancellationElapsedTimeMs); + cancelGroupChildrenByListLocked(mEnqueuedNotifications, userId, pkg, callingUid, callingPid, + listenerName, sendDelete, false, flagChecker, groupKey, + reason, cancellationElapsedTimeMs); } @GuardedBy("mNotificationLock") private void cancelGroupChildrenByListLocked(ArrayList<NotificationRecord> notificationList, - NotificationRecord parentNotification, int callingUid, int callingPid, + int userId, String pkg, int callingUid, int callingPid, String listenerName, boolean sendDelete, boolean wasPosted, FlagChecker flagChecker, - int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) { - final String pkg = parentNotification.getSbn().getPackageName(); - final int userId = parentNotification.getUserId(); + String groupKey, int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) { final int childReason = REASON_GROUP_SUMMARY_CANCELED; for (int i = notificationList.size() - 1; i >= 0; i--) { final NotificationRecord childR = notificationList.get(i); final StatusBarNotification childSbn = childR.getSbn(); - if ((childSbn.isGroup() && !childSbn.getNotification().isGroupSummary()) && - childR.getGroupKey().equals(parentNotification.getGroupKey()) - && (flagChecker == null || flagChecker.apply(childR.getFlags())) - && (!childR.getChannel().isImportantConversation() - || reason != REASON_CANCEL)) { + if (isChildOfGroup(childR, userId, pkg, groupKey) + && (flagChecker == null || flagChecker.apply(childR.getFlags())) + && (!childR.getChannel().isImportantConversation() || reason != REASON_CANCEL)) { EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(), childSbn.getTag(), userId, 0, 0, childReason, listenerName); notificationList.remove(i); @@ -10354,6 +10632,7 @@ public class NotificationManagerService extends SystemService { != null) { return r; } + return null; } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 0d4bdf663679..bd009010a313 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -449,9 +449,16 @@ public final class NotificationRecord { mRankingTimeMs = calculateRankingTimeMs(previous.getRankingTimeMs()); mCreationTimeMs = previous.mCreationTimeMs; mVisibleSinceMs = previous.mVisibleSinceMs; - if (previous.getSbn().getOverrideGroupKey() != null && !getSbn().isAppGroup()) { - getSbn().setOverrideGroupKey(previous.getSbn().getOverrideGroupKey()); + if (android.service.notification.Flags.notificationForceGrouping()) { + if (previous.getSbn().getOverrideGroupKey() != null) { + getSbn().setOverrideGroupKey(previous.getSbn().getOverrideGroupKey()); + } + } else { + if (previous.getSbn().getOverrideGroupKey() != null && !getSbn().isAppGroup()) { + getSbn().setOverrideGroupKey(previous.getSbn().getOverrideGroupKey()); + } } + // Don't copy importance information or mGlobalSortKey, recompute them. } diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index bf6b6521c19a..7265cff19077 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -141,4 +141,11 @@ flag { namespace: "systemui" description: "This flag does not allow notifications older than 2 weeks old to be posted" bug: "339833083" -}
\ No newline at end of file +} + +flag { + name: "notification_force_group_singletons" + namespace: "systemui" + description: "This flag enables forced auto-grouping singleton groups" + bug: "336488844" +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index 8a7d276dbecd..225c1dc752c1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -20,13 +20,23 @@ import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_CAN_COLORIZE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; +import static android.app.Notification.FLAG_GROUP_SUMMARY; import static android.app.Notification.FLAG_NO_CLEAR; import static android.app.Notification.FLAG_ONGOING_EVENT; +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.Notification.VISIBILITY_PRIVATE; import static android.app.Notification.VISIBILITY_PUBLIC; import static android.app.Notification.VISIBILITY_SECRET; +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; +import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; +import static com.android.server.notification.GroupHelper.AGGREGATE_GROUP_KEY; +import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY; import static com.android.server.notification.GroupHelper.BASE_FLAGS; import static com.google.common.truth.Truth.assertThat; @@ -41,6 +51,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -49,6 +60,7 @@ import static org.mockito.Mockito.when; import android.annotation.SuppressLint; import android.app.Notification; +import android.app.NotificationChannel; import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.drawable.AdaptiveIconDrawable; @@ -66,6 +78,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.server.UiServiceTestCase; +import com.android.server.notification.GroupHelper.CachedSummary; import com.android.server.notification.GroupHelper.NotificationAttributes; import org.junit.Before; @@ -90,11 +103,15 @@ public class GroupHelperTest extends UiServiceTestCase { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); private final int DEFAULT_VISIBILITY = VISIBILITY_PRIVATE; + private final int DEFAULT_GROUP_ALERT = GROUP_ALERT_CHILDREN; + + private final String TEST_CHANNEL_ID = "TEST_CHANNEL_ID"; private @Mock GroupHelper.Callback mCallback; private @Mock PackageManager mPackageManager; private final static int AUTOGROUP_AT_COUNT = 7; + private final static int AUTOGROUP_SINGLETONS_AT_COUNT = 2; private GroupHelper mGroupHelper; private @Mock Icon mSmallIcon; @@ -113,7 +130,7 @@ public class GroupHelperTest extends UiServiceTestCase { MockitoAnnotations.initMocks(this); mGroupHelper = new GroupHelper(getContext(), mPackageManager, AUTOGROUP_AT_COUNT, - mCallback); + AUTOGROUP_SINGLETONS_AT_COUNT, mCallback); NotificationRecord r = mock(NotificationRecord.class); StatusBarNotification sbn = getSbn("package", 0, "0", UserHandle.SYSTEM); @@ -124,7 +141,7 @@ public class GroupHelperTest extends UiServiceTestCase { private StatusBarNotification getSbn(String pkg, int id, String tag, UserHandle user, String groupKey, Icon smallIcon, int iconColor) { - Notification.Builder nb = new Notification.Builder(getContext(), "test_channel_id") + Notification.Builder nb = new Notification.Builder(getContext(), TEST_CHANNEL_ID) .setContentTitle("A") .setWhen(1205) .setSmallIcon(smallIcon) @@ -146,15 +163,54 @@ public class GroupHelperTest extends UiServiceTestCase { return getSbn(pkg, id, tag, user, null); } + private NotificationRecord getNotificationRecord(String pkg, int id, String tag, + UserHandle user) { + return getNotificationRecord(pkg, id, tag, user, null, false); + } + + private NotificationRecord getNotificationRecord(String pkg, int id, String tag, + UserHandle user, String groupKey, boolean isSummary) { + return getNotificationRecord(pkg, id, tag, user, groupKey, isSummary, IMPORTANCE_DEFAULT); + } + + private NotificationRecord getNotificationRecord(String pkg, int id, String tag, + UserHandle user, String groupKey, boolean isSummary, int importance) { + return getNotificationRecord(pkg, id, tag, user, groupKey, isSummary, + new NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_ID, importance)); + } + + private NotificationRecord getNotificationRecord(String pkg, int id, String tag, + UserHandle user, String groupKey, boolean isSummary, NotificationChannel channel) { + StatusBarNotification sbn = getSbn(pkg, id, tag, user, groupKey); + if (isSummary) { + sbn.getNotification().flags |= FLAG_GROUP_SUMMARY; + } + return new NotificationRecord(getContext(), sbn, channel); + } + + private NotificationRecord getNotificationRecord(StatusBarNotification sbn) { + return new NotificationRecord(getContext(), sbn, + new NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT)); + } + private NotificationAttributes getNotificationAttributes(int flags) { - return new NotificationAttributes(flags, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY); + return new NotificationAttributes(flags, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, + DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); + } + + private String getExpectedAutogroupKey(final NotificationRecord record) { + if (android.service.notification.Flags.notificationForceGrouping()) { + return GroupHelper.getFullAggregateGroupKey(record); + } else { + return AUTOGROUP_KEY; + } } @Test public void testGetAutogroupSummaryFlags_noChildren() { ArrayMap<String, NotificationAttributes> children = new ArrayMap<>(); - assertEquals(BASE_FLAGS, mGroupHelper.getAutogroupSummaryFlags(children)); + assertEquals(BASE_FLAGS, GroupHelper.getAutogroupSummaryFlags(children)); } @Test @@ -165,7 +221,7 @@ public class GroupHelperTest extends UiServiceTestCase { children.put("c", getNotificationAttributes(FLAG_BUBBLE)); assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS, - mGroupHelper.getAutogroupSummaryFlags(children)); + GroupHelper.getAutogroupSummaryFlags(children)); } @Test @@ -176,7 +232,7 @@ public class GroupHelperTest extends UiServiceTestCase { children.put("c", getNotificationAttributes(FLAG_BUBBLE)); assertEquals(FLAG_NO_CLEAR | FLAG_ONGOING_EVENT | BASE_FLAGS, - mGroupHelper.getAutogroupSummaryFlags(children)); + GroupHelper.getAutogroupSummaryFlags(children)); } @Test @@ -187,7 +243,7 @@ public class GroupHelperTest extends UiServiceTestCase { children.put("c", getNotificationAttributes(FLAG_BUBBLE)); assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS, - mGroupHelper.getAutogroupSummaryFlags(children)); + GroupHelper.getAutogroupSummaryFlags(children)); } @Test @@ -199,7 +255,7 @@ public class GroupHelperTest extends UiServiceTestCase { children.put("d", getNotificationAttributes(FLAG_ONGOING_EVENT)); assertEquals(FLAG_ONGOING_EVENT | BASE_FLAGS, - mGroupHelper.getAutogroupSummaryFlags(children)); + GroupHelper.getAutogroupSummaryFlags(children)); } @Test @@ -210,7 +266,7 @@ public class GroupHelperTest extends UiServiceTestCase { children.put("c", getNotificationAttributes(FLAG_BUBBLE)); assertEquals(BASE_FLAGS, - mGroupHelper.getAutogroupSummaryFlags(children)); + GroupHelper.getAutogroupSummaryFlags(children)); } @Test @@ -222,7 +278,7 @@ public class GroupHelperTest extends UiServiceTestCase { children.put("d", getNotificationAttributes(FLAG_AUTO_CANCEL | FLAG_FOREGROUND_SERVICE)); assertEquals(FLAG_AUTO_CANCEL | BASE_FLAGS, - mGroupHelper.getAutogroupSummaryFlags(children)); + GroupHelper.getAutogroupSummaryFlags(children)); } @Test @@ -235,15 +291,16 @@ public class GroupHelperTest extends UiServiceTestCase { FLAG_AUTO_CANCEL | FLAG_FOREGROUND_SERVICE | FLAG_ONGOING_EVENT)); assertEquals(FLAG_AUTO_CANCEL| FLAG_ONGOING_EVENT | BASE_FLAGS, - mGroupHelper.getAutogroupSummaryFlags(children)); + GroupHelper.getAutogroupSummaryFlags(children)); } @Test public void testNoGroup_postingUnderLimit() { final String pkg = "package"; for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { - mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), - false); + mGroupHelper.onNotificationPosted( + getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), + false); } verifyZeroInteractions(mCallback); } @@ -253,11 +310,12 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; final String pkg2 = "package2"; for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { - mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), - false); + mGroupHelper.onNotificationPosted( + getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), + false); } mGroupHelper.onNotificationPosted( - getSbn(pkg2, AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM), false); + getNotificationRecord(pkg2, AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM), false); verifyZeroInteractions(mCallback); } @@ -265,11 +323,12 @@ public class GroupHelperTest extends UiServiceTestCase { public void testNoGroup_multiUser() { final String pkg = "package"; for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { - mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), - false); + mGroupHelper.onNotificationPosted( + getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), + false); } mGroupHelper.onNotificationPosted( - getSbn(pkg, AUTOGROUP_AT_COUNT, "four", UserHandle.of(7)), false); + getNotificationRecord(pkg, AUTOGROUP_AT_COUNT, "four", UserHandle.of(7)), false); verifyZeroInteractions(mCallback); } @@ -278,10 +337,11 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { mGroupHelper.onNotificationPosted( - getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false); + getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false); } mGroupHelper.onNotificationPosted( - getSbn(pkg, AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM, "a"), false); + getNotificationRecord(pkg, AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM, "a", false), + false); verifyZeroInteractions(mCallback); } @@ -289,185 +349,241 @@ public class GroupHelperTest extends UiServiceTestCase { @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_alwaysAutogroup() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { mGroupHelper.onNotificationPosted( - getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false); + getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false); } verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + anyInt(), eq(pkg), anyString(), eq(autogroupKey), + anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(autogroupKey), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { assertThat(mGroupHelper.onNotificationPosted( - getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false)).isFalse(); + getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), + false)).isFalse(); } assertThat(mGroupHelper.onNotificationPosted( - getSbn(pkg, AUTOGROUP_AT_COUNT - 1, String.valueOf(AUTOGROUP_AT_COUNT - 1), + getNotificationRecord(pkg, AUTOGROUP_AT_COUNT - 1, String.valueOf(AUTOGROUP_AT_COUNT - 1), UserHandle.SYSTEM), false)).isTrue(); - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_oneChildOngoing_summaryOngoing_alwaysAutogroup() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); if (i == 0) { - sbn.getNotification().flags |= FLAG_ONGOING_EVENT; + r.getNotification().flags |= FLAG_ONGOING_EVENT; } - mGroupHelper.onNotificationPosted(sbn, false); + mGroupHelper.onNotificationPosted(r, false); } verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT))); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_oneChildOngoing_summaryOngoing() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); if (i == 0) { - sbn.getNotification().flags |= FLAG_ONGOING_EVENT; + r.getNotification().flags |= FLAG_ONGOING_EVENT; } - mGroupHelper.onNotificationPosted(sbn, false); + mGroupHelper.onNotificationPosted(r, false); } verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT))); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_oneChildAutoCancel_summaryNotAutoCancel_alwaysAutogroup() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); if (i == 0) { - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; + r.getNotification().flags |= FLAG_AUTO_CANCEL; } - mGroupHelper.onNotificationPosted(sbn, false); + mGroupHelper.onNotificationPosted(r, false); } - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_oneChildAutoCancel_summaryNotAutoCancel() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); if (i == 0) { - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; + r.getNotification().flags |= FLAG_AUTO_CANCEL; } - mGroupHelper.onNotificationPosted(sbn, false); + mGroupHelper.onNotificationPosted(r, false); } - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(autogroupKey), anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_allChildrenAutoCancel_summaryAutoCancel_alwaysAutogroup() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; + mGroupHelper.onNotificationPosted(r, false); } verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL))); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(autogroupKey), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_allChildrenAutoCancel_summaryAutoCancel() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; + mGroupHelper.onNotificationPosted(r, false); } verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL))); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(autogroupKey), anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_summaryAutoCancelNoClear_alwaysAutogroup() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; if (i == 0) { - sbn.getNotification().flags |= FLAG_NO_CLEAR; + r.getNotification().flags |= FLAG_NO_CLEAR; } - mGroupHelper.onNotificationPosted(sbn, false); + mGroupHelper.onNotificationPosted(r, false); } verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR))); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(autogroupKey), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAddSummary_summaryAutoCancelNoClear() { final String pkg = "package"; + final String autogroupKey = getExpectedAutogroupKey( + getNotificationRecord(pkg, 0, String.valueOf(0), UserHandle.SYSTEM)); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; if (i == 0) { - sbn.getNotification().flags |= FLAG_NO_CLEAR; + r.getNotification().flags |= FLAG_NO_CLEAR; } - mGroupHelper.onNotificationPosted(sbn, false); + mGroupHelper.onNotificationPosted(r, false); } verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(autogroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR))); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(autogroupKey), anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @@ -475,15 +591,16 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_ONGOING_EVENT; - notifications.add(sbn); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_ONGOING_EVENT; + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // One notification is no longer ongoing @@ -491,7 +608,7 @@ public class GroupHelperTest extends UiServiceTestCase { mGroupHelper.onNotificationPosted(notifications.get(0), true); // Summary should keep FLAG_ONGOING_EVENT if any child has it - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT))); } @@ -500,24 +617,25 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); if (i == 0) { - sbn.getNotification().flags |= FLAG_ONGOING_EVENT; + r.getNotification().flags |= FLAG_ONGOING_EVENT; } - notifications.add(sbn); + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // remove ongoing mGroupHelper.onNotificationRemoved(notifications.get(0)); // Summary is no longer ongoing - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); } @@ -526,14 +644,15 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - notifications.add(sbn); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // update to ongoing @@ -541,7 +660,7 @@ public class GroupHelperTest extends UiServiceTestCase { mGroupHelper.onNotificationPosted(notifications.get(0), true); // Summary is now ongoing - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT))); } @@ -550,23 +669,25 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - notifications.add(sbn); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // add ongoing - StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT + 1, null, UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_ONGOING_EVENT; - mGroupHelper.onNotificationPosted(sbn, true); + NotificationRecord r = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT + 1, null, + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_ONGOING_EVENT; + mGroupHelper.onNotificationPosted(r, true); // Summary is now ongoing - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT))); } @@ -575,51 +696,84 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); if (i == 0) { - sbn.getNotification().flags |= FLAG_ONGOING_EVENT; + r.getNotification().flags |= FLAG_ONGOING_EVENT; } - notifications.add(sbn); + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // app group the ongoing child - StatusBarNotification sbn = getSbn(pkg, 0, "0", UserHandle.SYSTEM, "app group now"); - mGroupHelper.onNotificationPosted(sbn, true); + NotificationRecord r = getNotificationRecord(pkg, 0, "0", UserHandle.SYSTEM, + "app group now", false); + mGroupHelper.onNotificationPosted(r, true); // Summary is no longer ongoing - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); } @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testAutoGrouped_singleOngoing_removeNonOngoingChild() { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); if (i == 0) { - sbn.getNotification().flags |= FLAG_ONGOING_EVENT; + r.getNotification().flags |= FLAG_ONGOING_EVENT; } - notifications.add(sbn); + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // remove ongoing mGroupHelper.onNotificationRemoved(notifications.get(1)); // Summary is still ongoing - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAutoGrouped_singleOngoing_removeNonOngoingChild_forceGrouping() { + final String pkg = "package"; + + // Post AUTOGROUP_AT_COUNT ongoing notifications + ArrayList<NotificationRecord> notifications = new ArrayList<>(); + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + if (i == 0) { + r.getNotification().flags |= FLAG_ONGOING_EVENT; + } + notifications.add(r); + } + + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); + } + + // remove ongoing + mGroupHelper.onNotificationRemoved(notifications.get(1)); + + // Summary is still ongoing + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @@ -627,15 +781,16 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; - notifications.add(sbn); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // One notification is no longer autocancelable @@ -643,7 +798,7 @@ public class GroupHelperTest extends UiServiceTestCase { mGroupHelper.onNotificationPosted(notifications.get(0), true); // Summary should no longer be autocancelable - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); } @@ -652,17 +807,18 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); if (i != 0) { - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; + r.getNotification().flags |= FLAG_AUTO_CANCEL; } - notifications.add(sbn); + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // Missing notification is now autocancelable @@ -670,254 +826,327 @@ public class GroupHelperTest extends UiServiceTestCase { mGroupHelper.onNotificationPosted(notifications.get(0), true); // Summary should now autocancelable - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL))); } @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testAutoGrouped_allAutoCancel_updateChildAppGrouped() { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; + notifications.add(r); + } + + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); + } + + // One notification is now grouped by app + NotificationRecord r = getNotificationRecord(pkg, 0, "0", UserHandle.SYSTEM, + "app group now", false); + mGroupHelper.onNotificationPosted(r, true); + + // Summary should be still be autocancelable + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAutoGrouped_allAutoCancel_updateChildAppGrouped_forceGrouping() { + final String pkg = "package"; + + // Post AUTOGROUP_AT_COUNT ongoing notifications + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; - notifications.add(sbn); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // One notification is now grouped by app - StatusBarNotification sbn = getSbn(pkg, 0, "0", UserHandle.SYSTEM, "app group now"); - mGroupHelper.onNotificationPosted(sbn, true); + NotificationRecord r = getNotificationRecord(pkg, 0, "0", UserHandle.SYSTEM, + "app group now", false); + mGroupHelper.onNotificationPosted(r, true); // Summary should be still be autocancelable - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testAutoGrouped_allAutoCancel_removeChild() { final String pkg = "package"; // Post AUTOGROUP_AT_COUNT ongoing notifications - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - sbn.getNotification().flags |= FLAG_AUTO_CANCEL; - notifications.add(sbn); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; + notifications.add(r); } - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } mGroupHelper.onNotificationRemoved(notifications.get(0)); // Summary should still be autocancelable - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAutoGrouped_allAutoCancel_removeChild_forceGrouping() { + final String pkg = "package"; + + // Post AUTOGROUP_AT_COUNT ongoing notifications + ArrayList<NotificationRecord> notifications = new ArrayList<>(); + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + r.getNotification().flags |= FLAG_AUTO_CANCEL; + notifications.add(r); + } + + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); + } + + mGroupHelper.onNotificationRemoved(notifications.get(0)); + + // Summary should still be autocancelable + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); } @Test @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testDropToZeroRemoveGroup_disableFlag() { final String pkg = "package"; - List<StatusBarNotification> posted = new ArrayList<>(); + ArrayList<NotificationRecord> posted = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - posted.add(sbn); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + posted.add(r); + mGroupHelper.onNotificationPosted(r, false); } - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), anyString(), + anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { mGroupHelper.onNotificationRemoved(posted.remove(0)); } verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); mGroupHelper.onNotificationRemoved(posted.remove(0)); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testDropToZeroRemoveGroup() { final String pkg = "package"; - List<StatusBarNotification> posted = new ArrayList<>(); + ArrayList<NotificationRecord> posted = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - posted.add(sbn); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + posted.add(r); + mGroupHelper.onNotificationPosted(r, false); } - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), anyString(), + anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { mGroupHelper.onNotificationRemoved(posted.remove(0)); } verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); mGroupHelper.onNotificationRemoved(posted.remove(0)); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString()); } @Test @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAppStartsGrouping_disableFlag() { final String pkg = "package"; - List<StatusBarNotification> posted = new ArrayList<>(); + ArrayList<NotificationRecord> posted = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - posted.add(sbn); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + posted.add(r); + mGroupHelper.onNotificationPosted(r, false); } - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + anyString(), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - final StatusBarNotification sbn = - getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "app group"); - sbn.setOverrideGroupKey("autogrouped"); - mGroupHelper.onNotificationPosted(sbn, true); - verify(mCallback, times(1)).removeAutoGroup(sbn.getKey()); + final NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "app group", false); + r.getSbn().setOverrideGroupKey("autogrouped"); + mGroupHelper.onNotificationPosted(r, true); + verify(mCallback, times(1)).removeAutoGroup(r.getKey()); if (i < AUTOGROUP_AT_COUNT - 1) { - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), + anyString()); } } - verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testAppStartsGrouping() { final String pkg = "package"; - List<StatusBarNotification> posted = new ArrayList<>(); + ArrayList<NotificationRecord> posted = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - posted.add(sbn); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + posted.add(r); + mGroupHelper.onNotificationPosted(r, false); } - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + anyString(), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - final StatusBarNotification sbn = - getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "app group"); - sbn.setOverrideGroupKey("autogrouped"); - mGroupHelper.onNotificationPosted(sbn, true); - verify(mCallback, times(1)).removeAutoGroup(sbn.getKey()); + final NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "app group", false); + r.getSbn().setOverrideGroupKey("autogrouped"); + mGroupHelper.onNotificationPosted(r, true); + verify(mCallback, times(1)).removeAutoGroup(r.getKey()); if (i < AUTOGROUP_AT_COUNT - 1) { - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), + anyString()); } } - verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString()); } @Test @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled_alwaysGroup() { final String pkg = "package"; - List<StatusBarNotification> posted = new ArrayList<>(); + ArrayList<NotificationRecord> posted = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - posted.add(sbn); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + posted.add(r); + mGroupHelper.onNotificationPosted(r, false); } - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + anyString(), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); for (int i = posted.size() - 2; i >= 0; i--) { mGroupHelper.onNotificationRemoved(posted.remove(i)); } verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); - // only one child remains - assertEquals(1, mGroupHelper.getNotGroupedByAppCount(UserHandle.USER_SYSTEM, pkg)); - // Add new notification; it should be autogrouped even though the total count is // < AUTOGROUP_AT_COUNT - final StatusBarNotification sbn = getSbn(pkg, 5, String.valueOf(5), UserHandle.SYSTEM); - posted.add(sbn); - assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isFalse(); - verify(mCallback, times(1)).addAutoGroup(sbn.getKey(), true); + final NotificationRecord r = getNotificationRecord(pkg, 5, String.valueOf(5), + UserHandle.SYSTEM); + final String autogroupKey = getExpectedAutogroupKey(r); + posted.add(r); + assertThat(mGroupHelper.onNotificationPosted(r, true)).isFalse(); + verify(mCallback, times(1)).addAutoGroup(r.getKey(), autogroupKey, true); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), any()); + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled() { final String pkg = "package"; - List<StatusBarNotification> posted = new ArrayList<>(); + ArrayList<NotificationRecord> posted = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM); - posted.add(sbn); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM); + posted.add(r); + mGroupHelper.onNotificationPosted(r, false); } - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + anyString(), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); for (int i = posted.size() - 2; i >= 0; i--) { mGroupHelper.onNotificationRemoved(posted.remove(i)); } verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); - // only one child remains - assertEquals(1, mGroupHelper.getNotGroupedByAppCount(UserHandle.USER_SYSTEM, pkg)); - // Add new notification; it should be autogrouped even though the total count is // < AUTOGROUP_AT_COUNT - final StatusBarNotification sbn = getSbn(pkg, 5, String.valueOf(5), UserHandle.SYSTEM); - posted.add(sbn); - assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isTrue(); + final NotificationRecord r = getNotificationRecord(pkg, 5, String.valueOf(5), + UserHandle.SYSTEM); + posted.add(r); + assertThat(mGroupHelper.onNotificationPosted(r, true)).isTrue(); // addAutoGroup not called on sbn, because the autogrouping is expected to be done // synchronously. verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); - verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), any()); + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); } @Test @@ -929,29 +1158,32 @@ public class GroupHelperTest extends UiServiceTestCase { when(icon.sameAs(icon)).thenReturn(true); final int iconColor = Color.BLUE; final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor, - DEFAULT_VISIBILITY); + DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); // Add notifications with same icon and color for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, - icon, iconColor); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord( + getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, icon, iconColor)); + mGroupHelper.onNotificationPosted(r, false); } // Check that the summary would have the same icon and color verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(attr)); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + anyInt(), eq(pkg), anyString(), anyString(), anyInt(), eq(attr)); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); // After auto-grouping, add new notification with the same color - StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT, - String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor); - mGroupHelper.onNotificationPosted(sbn, true); + NotificationRecord r = getNotificationRecord( + getSbn(pkg, AUTOGROUP_AT_COUNT, String.valueOf(AUTOGROUP_AT_COUNT), + UserHandle.SYSTEM,null, icon, iconColor)); + mGroupHelper.onNotificationPosted(r, true); // Check that the summary was updated //NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor); - verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(attr)); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + eq(attr)); } @Test @@ -963,29 +1195,31 @@ public class GroupHelperTest extends UiServiceTestCase { when(icon.sameAs(icon)).thenReturn(true); final int iconColor = Color.BLUE; final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor, - DEFAULT_VISIBILITY); + DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); // Add notifications with same icon and color for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, - icon, iconColor); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord( + getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, icon, iconColor)); + mGroupHelper.onNotificationPosted(r, false); } // Check that the summary would have the same icon and color - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(attr)); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + anyString(), anyInt(), eq(attr)); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); // After auto-grouping, add new notification with the same color - StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT, - String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor); - mGroupHelper.onNotificationPosted(sbn, true); + NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT, + String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor)); + mGroupHelper.onNotificationPosted(r, true); // Check that the summary was updated //NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor); - verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(attr)); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + eq(attr)); } @Test @@ -1004,33 +1238,37 @@ public class GroupHelperTest extends UiServiceTestCase { doReturn(monochromeIcon).when(groupHelper).getMonochromeAppIcon(eq(pkg)); final NotificationAttributes initialAttr = new NotificationAttributes(BASE_FLAGS, - initialIcon, initialIconColor, DEFAULT_VISIBILITY); + initialIcon, initialIconColor, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + TEST_CHANNEL_ID); // Add notifications with same icon and color for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, - initialIcon, initialIconColor); - groupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord( + getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, + initialIcon, initialIconColor)); + groupHelper.onNotificationPosted(r, false); } // Check that the summary would have the same icon and color - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(initialAttr)); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + anyString(), anyInt(), eq(initialAttr)); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); // After auto-grouping, add new notification with a different color final Icon newIcon = mock(Icon.class); final int newIconColor = Color.YELLOW; - StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT, + NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT, String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, newIcon, - newIconColor); - groupHelper.onNotificationPosted(sbn, true); + newIconColor)); + groupHelper.onNotificationPosted(r, true); // Summary should be updated to the default color and the icon to the monochrome icon NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, monochromeIcon, - COLOR_DEFAULT, DEFAULT_VISIBILITY); - verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr)); + COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + eq(newAttr)); } @Test @@ -1049,33 +1287,37 @@ public class GroupHelperTest extends UiServiceTestCase { doReturn(monochromeIcon).when(groupHelper).getMonochromeAppIcon(eq(pkg)); final NotificationAttributes initialAttr = new NotificationAttributes(BASE_FLAGS, - initialIcon, initialIconColor, DEFAULT_VISIBILITY); + initialIcon, initialIconColor, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + TEST_CHANNEL_ID); // Add notifications with same icon and color for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, - initialIcon, initialIconColor); - groupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord( + getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, + initialIcon, initialIconColor)); + groupHelper.onNotificationPosted(r, false); } // Check that the summary would have the same icon and color - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(initialAttr)); - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + anyString(), anyInt(), eq(initialAttr)); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); // After auto-grouping, add new notification with a different color final Icon newIcon = mock(Icon.class); final int newIconColor = Color.YELLOW; - StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT, - String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, newIcon, - newIconColor); - groupHelper.onNotificationPosted(sbn, true); + NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT, + String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, newIcon, + newIconColor)); + groupHelper.onNotificationPosted(r, true); // Summary should be updated to the default color and the icon to the monochrome icon NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, monochromeIcon, - COLOR_DEFAULT, DEFAULT_VISIBILITY); - verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr)); + COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + eq(newAttr)); } @Test @@ -1087,32 +1329,35 @@ public class GroupHelperTest extends UiServiceTestCase { when(icon.sameAs(icon)).thenReturn(true); final int iconColor = Color.BLUE; final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor, - VISIBILITY_PRIVATE); + VISIBILITY_PRIVATE, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); // Add notifications with same icon and color and default visibility (private) for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, - icon, iconColor); - mGroupHelper.onNotificationPosted(sbn, false); + NotificationRecord r = getNotificationRecord( + getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, + icon, iconColor)); + mGroupHelper.onNotificationPosted(r, false); } // Check that the summary has private visibility verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(attr)); + anyInt(), eq(pkg), anyString(), anyString(), anyInt(), eq(attr)); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); // After auto-grouping, add new notification with public visibility - StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT, - String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor); - sbn.getNotification().visibility = VISIBILITY_PUBLIC; - mGroupHelper.onNotificationPosted(sbn, true); + NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT, + String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor)); + r.getNotification().visibility = VISIBILITY_PUBLIC; + mGroupHelper.onNotificationPosted(r, true); // Check that the summary visibility was updated NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor, - VISIBILITY_PUBLIC); - verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr)); + VISIBILITY_PUBLIC, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + eq(newAttr)); } @Test @@ -1124,71 +1369,116 @@ public class GroupHelperTest extends UiServiceTestCase { when(icon.sameAs(icon)).thenReturn(true); final int iconColor = Color.BLUE; final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor, - VISIBILITY_PRIVATE); + VISIBILITY_PRIVATE, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); // Add notifications with same icon and color and default visibility (private) for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, - icon, iconColor); - assertThat(mGroupHelper.onNotificationPosted(sbn, false)).isFalse(); + NotificationRecord r = getNotificationRecord( + getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, + icon, iconColor)); + assertThat(mGroupHelper.onNotificationPosted(r, false)).isFalse(); } // The last notification added will reach the autogroup threshold. - StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT - 1, - String.valueOf(AUTOGROUP_AT_COUNT - 1), UserHandle.SYSTEM, null, icon, iconColor); - assertThat(mGroupHelper.onNotificationPosted(sbn, false)).isTrue(); + NotificationRecord r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT - 1, + String.valueOf(AUTOGROUP_AT_COUNT - 1), UserHandle.SYSTEM, null, icon, iconColor)); + assertThat(mGroupHelper.onNotificationPosted(r, false)).isTrue(); // Check that the summary has private visibility - verify(mCallback, times(1)).addAutoGroupSummary( - anyInt(), eq(pkg), anyString(), eq(attr)); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), anyString(), + anyInt(), eq(attr)); // The last sbn is expected to be added to autogroup synchronously. - verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyString(), + anyBoolean()); verify(mCallback, never()).removeAutoGroup(anyString()); - verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); // After auto-grouping, add new notification with public visibility - sbn = getSbn(pkg, AUTOGROUP_AT_COUNT, - String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor); - sbn.getNotification().visibility = VISIBILITY_PUBLIC; - assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isTrue(); + r = getNotificationRecord(getSbn(pkg, AUTOGROUP_AT_COUNT, + String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor)); + r.getNotification().visibility = VISIBILITY_PUBLIC; + assertThat(mGroupHelper.onNotificationPosted(r, true)).isTrue(); // Check that the summary visibility was updated NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor, - VISIBILITY_PUBLIC); - verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr)); + VISIBILITY_PUBLIC, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + eq(newAttr)); } @Test @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE) + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testAutoGrouped_diffIcon_diffColor_removeChild_updateTo_sameIcon_sameColor() { final String pkg = "package"; final Icon initialIcon = mock(Icon.class); when(initialIcon.sameAs(initialIcon)).thenReturn(true); final int initialIconColor = Color.BLUE; final NotificationAttributes initialAttr = new NotificationAttributes( - GroupHelper.FLAG_INVALID, initialIcon, initialIconColor, DEFAULT_VISIBILITY); + GroupHelper.FLAG_INVALID, initialIcon, initialIconColor, DEFAULT_VISIBILITY, + DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); // Add AUTOGROUP_AT_COUNT-1 notifications with same icon and color - ArrayList<StatusBarNotification> notifications = new ArrayList<>(); + ArrayList<NotificationRecord> notifications = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { - StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, - initialIcon, initialIconColor); - notifications.add(sbn); + NotificationRecord r = getNotificationRecord( + getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, + initialIcon, initialIconColor)); + notifications.add(r); } // And an additional notification with different icon and color final int lastIdx = AUTOGROUP_AT_COUNT - 1; - StatusBarNotification newSbn = getSbn(pkg, lastIdx, + NotificationRecord newRec = getNotificationRecord(getSbn(pkg, lastIdx, String.valueOf(lastIdx), UserHandle.SYSTEM, null, mock(Icon.class), - Color.YELLOW); - notifications.add(newSbn); - for (StatusBarNotification sbn: notifications) { - mGroupHelper.onNotificationPosted(sbn, false); + Color.YELLOW)); + notifications.add(newRec); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); } // Remove last notification (the only one with different icon and color) mGroupHelper.onNotificationRemoved(notifications.get(lastIdx)); // Summary should be updated to the common icon and color - verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(initialAttr)); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + eq(initialAttr)); + } + + @Test + @EnableFlags({Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE, + FLAG_NOTIFICATION_FORCE_GROUPING}) + public void testAutoGrouped_diffIcon_diffColor_removeChild_updateTo_sameIcon_sameColor_forceGrouping() { + final String pkg = "package"; + final Icon initialIcon = mock(Icon.class); + when(initialIcon.sameAs(initialIcon)).thenReturn(true); + final int initialIconColor = Color.BLUE; + final NotificationAttributes initialAttr = new NotificationAttributes( + BASE_FLAGS, initialIcon, initialIconColor, DEFAULT_VISIBILITY, + DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID); + + // Add AUTOGROUP_AT_COUNT-1 notifications with same icon and color + ArrayList<NotificationRecord> notifications = new ArrayList<>(); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord r = getNotificationRecord( + getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null, + initialIcon, initialIconColor)); + notifications.add(r); + } + // And an additional notification with different icon and color + final int lastIdx = AUTOGROUP_AT_COUNT - 1; + NotificationRecord newRec = getNotificationRecord(getSbn(pkg, lastIdx, + String.valueOf(lastIdx), UserHandle.SYSTEM, null, mock(Icon.class), + Color.YELLOW)); + notifications.add(newRec); + for (NotificationRecord r: notifications) { + mGroupHelper.onNotificationPosted(r, false); + } + + // Remove last notification (the only one with different icon and color) + mGroupHelper.onNotificationRemoved(notifications.get(lastIdx)); + + // Summary should be updated to the common icon and color + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + eq(initialAttr)); } @Test @@ -1202,7 +1492,7 @@ public class GroupHelperTest extends UiServiceTestCase { List<NotificationAttributes> childrenAttr = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { childrenAttr.add(new NotificationAttributes(0, icon, COLOR_DEFAULT, - DEFAULT_VISIBILITY)); + DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID)); } //Check that the generated summary icon is the same as the child notifications' @@ -1223,7 +1513,7 @@ public class GroupHelperTest extends UiServiceTestCase { List<NotificationAttributes> childrenAttr = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), COLOR_DEFAULT, - DEFAULT_VISIBILITY)); + DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID)); } // Check that the generated summary icon is the monochrome icon @@ -1240,7 +1530,7 @@ public class GroupHelperTest extends UiServiceTestCase { List<NotificationAttributes> childrenAttr = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, - DEFAULT_VISIBILITY)); + DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID)); } // Check that the generated summary icon color is the same as the child notifications' @@ -1257,7 +1547,7 @@ public class GroupHelperTest extends UiServiceTestCase { List<NotificationAttributes> childrenAttr = new ArrayList<>(); for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), i, - DEFAULT_VISIBILITY)); + DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID)); } // Check that the generated summary icon color is the default color @@ -1274,10 +1564,10 @@ public class GroupHelperTest extends UiServiceTestCase { // Create notifications with private and public visibility List<NotificationAttributes> childrenAttr = new ArrayList<>(); childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, - VISIBILITY_PUBLIC)); + VISIBILITY_PUBLIC, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID)); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, - VISIBILITY_PRIVATE)); + VISIBILITY_PRIVATE, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID)); } // Check that the generated summary visibility is public @@ -1301,7 +1591,7 @@ public class GroupHelperTest extends UiServiceTestCase { visibility = VISIBILITY_SECRET; } childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, - visibility)); + visibility, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID)); } // Check that the generated summary visibility is private @@ -1311,6 +1601,90 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test + public void testAutobundledSummaryAlertBehavior_oneChildAlertChildren() { + final String pkg = "package"; + final int iconColor = Color.BLUE; + // Create notifications with GROUP_ALERT_SUMMARY + one with GROUP_ALERT_CHILDREN + List<NotificationAttributes> childrenAttr = new ArrayList<>(); + childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, + VISIBILITY_PUBLIC, GROUP_ALERT_CHILDREN, TEST_CHANNEL_ID)); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, + VISIBILITY_PRIVATE, GROUP_ALERT_SUMMARY, TEST_CHANNEL_ID)); + } + // Check that the generated summary alert behavior is GROUP_ALERT_CHILDREN + int groupAlertBehavior = mGroupHelper.getAutobundledSummaryAttributes(pkg, + childrenAttr).groupAlertBehavior; + assertThat(groupAlertBehavior).isEqualTo(GROUP_ALERT_CHILDREN); + } + + @Test + public void testAutobundledSummaryAlertBehavior_oneChildAlertAll() { + final String pkg = "package"; + final int iconColor = Color.BLUE; + // Create notifications with GROUP_ALERT_SUMMARY + one with GROUP_ALERT_ALL + List<NotificationAttributes> childrenAttr = new ArrayList<>(); + childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, + VISIBILITY_PUBLIC, GROUP_ALERT_ALL, TEST_CHANNEL_ID)); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, + VISIBILITY_PRIVATE, GROUP_ALERT_SUMMARY, TEST_CHANNEL_ID)); + } + // Check that the generated summary alert behavior is GROUP_ALERT_CHILDREN + int groupAlertBehavior = mGroupHelper.getAutobundledSummaryAttributes(pkg, + childrenAttr).groupAlertBehavior; + assertThat(groupAlertBehavior).isEqualTo(GROUP_ALERT_CHILDREN); + } + + @Test + public void testAutobundledSummaryAlertBehavior_allChildAlertSummary() { + final String pkg = "package"; + final int iconColor = Color.BLUE; + // Create notifications with GROUP_ALERT_SUMMARY + List<NotificationAttributes> childrenAttr = new ArrayList<>(); + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, + VISIBILITY_PRIVATE, GROUP_ALERT_SUMMARY, TEST_CHANNEL_ID)); + } + + // Check that the generated summary alert behavior is GROUP_ALERT_SUMMARY + int groupAlertBehavior = mGroupHelper.getAutobundledSummaryAttributes(pkg, + childrenAttr).groupAlertBehavior; + assertThat(groupAlertBehavior).isEqualTo(GROUP_ALERT_SUMMARY); + } + + @Test + @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE) + public void testAutobundledSummaryChannelId() { + final String pkg = "package"; + final int iconColor = Color.BLUE; + final String expectedChannelId = TEST_CHANNEL_ID + "0"; + // Create notifications with different channelIds + List<NotificationAttributes> childrenAttr = new ArrayList<>(); + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + childrenAttr.add(new NotificationAttributes(0, mock(Icon.class), iconColor, + DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, TEST_CHANNEL_ID+i)); + } + + // Check that the generated summary channelId is the first child in the list + String summaryChannelId = mGroupHelper.getAutobundledSummaryAttributes(pkg, + childrenAttr).channelId; + assertThat(summaryChannelId).isEqualTo(expectedChannelId); + } + + @Test + @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE) + public void testAutobundledSummaryChannelId_noChildren() { + final String pkg = "package"; + // No child notifications + List<NotificationAttributes> childrenAttr = new ArrayList<>(); + // Check that the generated summary channelId is null + String summaryChannelId = mGroupHelper.getAutobundledSummaryAttributes(pkg, + childrenAttr).channelId; + assertThat(summaryChannelId).isNull(); + } + + @Test @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE) public void testMonochromeAppIcon_adaptiveIconExists() throws Exception { final String pkg = "testPackage"; @@ -1333,4 +1707,855 @@ public class GroupHelperTest extends UiServiceTestCase { assertThat(mGroupHelper.getMonochromeAppIcon(pkg).getResId()) .isEqualTo(fallbackIconResId); } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testGetAggregateGroupKey() { + final String fullAggregateGroupKey = GroupHelper.getFullAggregateGroupKey("pkg", + "groupKey", 1234); + assertThat(fullAggregateGroupKey).isEqualTo("1234|pkg|g:groupKey"); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testNoGroup_postingUnderLimit_forcedGrouping() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testNoGroup_AutobundledAlready_forcedGrouping() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, null, true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testNoGroup_isCanceled_forcedGrouping() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp" + i, true); + r.isCanceled = true; + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testNoGroup_isAggregated_forcedGrouping() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + String aggregateGroupKey = AGGREGATE_GROUP_KEY + "AlertingSection"; + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, aggregateGroupKey, true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testNoGroup_multiPackage_forcedGrouping() { + final String pkg = "package"; + final String pkg2 = "package2"; + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + NotificationRecord r = getNotificationRecord(pkg2, AUTOGROUP_AT_COUNT, + String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testNoGroup_multiUser_forcedGrouping() { + final String pkg = "package"; + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + NotificationRecord r = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT, + String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.of(7), "testGrp", true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testNoGroup_summaryWithChildren_forcedGrouping() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + // Next posted summary has 1 child => no forced grouping + NotificationRecord summary = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT, + String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(summary); + NotificationRecord child = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT + 1, + String.valueOf(AUTOGROUP_AT_COUNT + 1), UserHandle.SYSTEM, "testGrp", false); + notificationList.add(child); + mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup); + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testNoGroup_groupWithSummary_forcedGrouping() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + // Next posted notification has summary => no forced grouping + NotificationRecord summary = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT, + String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(summary); + NotificationRecord child = getNotificationRecord(pkg, AUTOGROUP_AT_COUNT + 1, + String.valueOf(AUTOGROUP_AT_COUNT + 1), UserHandle.SYSTEM, "testGrp", false); + notificationList.add(child); + summaryByGroup.put(summary.getGroupKey(), summary); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAddAggregateSummary_summaryNoChildren() { + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post group summaries without children => force autogroup + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, true); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAddAggregateSummary_childrenNoSummary() { + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post group notifications without summaries => force autogroup + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAddAggregateSummary_multipleSections() { + final String pkg = "package"; + final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post notifications with different importance values => force group into separate sections + NotificationRecord r; + for (int i = 0; i < 2 * AUTOGROUP_AT_COUNT; i++) { + if (i % 2 == 0) { + r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, + "testGrp " + i, true, IMPORTANCE_DEFAULT); + } else { + r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, + "testGrp " + i, false, IMPORTANCE_LOW); + } + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_alerting), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_alerting), eq(true)); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_silent), eq(true)); + + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + } + + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) + public void testAddAggregateSummary_mixUngroupedAndAbusive_alwaysAutogroup() { + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + // Post ungrouped notifications => create autogroup + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + mGroupHelper.onNotificationPosted( + getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false); + } + verify(mCallback, times(1)).addAutoGroupSummary( + anyInt(), eq(pkg), anyString(), eq(expectedGroupKey), + anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(expectedGroupKey), + anyBoolean()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + + reset(mCallback); + + // Post group notifications without summaries => add to autogroup + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final int id = AUTOGROUP_AT_COUNT; + NotificationRecord r = getNotificationRecord(pkg, id, String.valueOf(id), + UserHandle.SYSTEM, "testGrp " + id, false); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + + // Check that the new notification was added + verify(mCallback, times(1)).addAutoGroup(eq(r.getKey()), + eq(expectedGroupKey), eq(true)); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey), any()); + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) + public void testUpdateAggregateSummary_postUngroupedAfterForcedGrouping_alwaysAutogroup() { + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post group notifications without summaries => force autogroup + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + + reset(mCallback); + + // Post ungrouped notification => update autogroup + final int id = AUTOGROUP_AT_COUNT; + NotificationRecord r = getNotificationRecord(pkg, id, String.valueOf(id), + UserHandle.SYSTEM); + mGroupHelper.onNotificationPosted(r, true); + + verify(mCallback, times(1)).addAutoGroup(eq(r.getKey()), + eq(expectedGroupKey), eq(true)); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) + public void testUpdateAggregateSummary_postUngroupedAfterForcedGrouping() { + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post group notifications without summaries => force autogroup + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + + reset(mCallback); + + // Post ungrouped notification => update autogroup + final int id = AUTOGROUP_AT_COUNT; + NotificationRecord r = getNotificationRecord(pkg, id, String.valueOf(id), + UserHandle.SYSTEM); + mGroupHelper.onNotificationPosted(r, true); + + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testUpdateAggregateSummary_postAfterForcedGrouping() { + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post group notifications w/o summaries and summaries w/o children => force autogrouping + NotificationRecord r; + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + if (i % 2 == 0) { + r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, + "testGrp " + i, true); + } else { + r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, + "testGrp " + i, false); + } + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + + // Post another notification after forced grouping + final Icon icon = mock(Icon.class); + when(icon.sameAs(icon)).thenReturn(true); + final int iconColor = Color.BLUE; + r = getNotificationRecord( + getSbn(pkg, AUTOGROUP_AT_COUNT, String.valueOf(AUTOGROUP_AT_COUNT), + UserHandle.SYSTEM, "testGrp " + AUTOGROUP_AT_COUNT, icon, iconColor)); + + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT + 1)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey), any()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testRemoveAggregateSummary_removeAllNotifications() { + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post group notifications without summaries => force autogroup + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + Mockito.reset(mCallback); + + // Remove all posted notifications + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false); + r.setOverrideGroupKey(expectedGroupKey); + mGroupHelper.onNotificationRemoved(r, notificationList); + } + // Check that the autogroup summary is removed + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey)); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testMoveAggregateGroups_updateChannel() { + final String pkg = "package"; + final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final NotificationChannel channel = new NotificationChannel(TEST_CHANNEL_ID, + TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post group notifications without summaries => force autogroup + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false, channel); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_alerting), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_alerting), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + Mockito.reset(mCallback); + + // Update the channel importance for all posted notifications + final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + channel.setImportance(IMPORTANCE_LOW); + for (NotificationRecord r: notificationList) { + r.updateNotificationChannel(channel); + } + mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel, + notificationList); + + // Check that all notifications are moved to the silent section group + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_silent), eq(false)); + + // Check that the alerting section group is removed + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey_alerting)); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testMoveAggregateGroups_updateChannel_multipleChannels() { + final String pkg = "package"; + final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final NotificationChannel channel1 = new NotificationChannel("TEST_CHANNEL_ID1", + "TEST_CHANNEL_ID1", IMPORTANCE_DEFAULT); + final NotificationChannel channel2 = new NotificationChannel("TEST_CHANNEL_ID2", + "TEST_CHANNEL_ID2", IMPORTANCE_DEFAULT); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post notifications with different channels that autogroup within the same section + NotificationRecord r; + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + if (i % 2 == 0) { + r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false, channel1); + } else { + r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false, channel2); + } + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + NotificationAttributes expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS, + mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + "TEST_CHANNEL_ID1"); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_alerting), anyInt(), eq(expectedSummaryAttr)); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_alerting), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + Mockito.reset(mCallback); + + // Update channel1's importance + final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + channel1.setImportance(IMPORTANCE_LOW); + for (NotificationRecord record: notificationList) { + if (record.getChannel().getId().equals(channel1.getId())) { + record.updateNotificationChannel(channel1); + } + } + mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1, + notificationList); + + // Check that channel1's notifications are moved to the silent section group + expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS, + mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + "TEST_CHANNEL_ID1"); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_silent), anyInt(), eq(expectedSummaryAttr)); + verify(mCallback, times(AUTOGROUP_AT_COUNT/2 + 1)).addAutoGroup(anyString(), + eq(expectedGroupKey_silent), eq(false)); + + // Check that the alerting section group is not removed, only updated + expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS, + mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + "TEST_CHANNEL_ID2"); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey_alerting)); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey_alerting), eq(expectedSummaryAttr)); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testMoveAggregateGroups_updateChannel_groupsUngrouped() { + final String pkg = "package"; + final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + + // Post too few group notifications without summaries => do not autogroup + final NotificationChannel lowPrioChannel = new NotificationChannel("TEST_CHANNEL_LOW_ID", + "TEST_CHANNEL_LOW_ID", IMPORTANCE_LOW); + final int numUngrouped = AUTOGROUP_AT_COUNT - 1; + int startIdx = 42; + for (int i = startIdx; i < startIdx + numUngrouped; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false, lowPrioChannel); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + + reset(mCallback); + + final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final NotificationChannel channel = new NotificationChannel(TEST_CHANNEL_ID, + TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + + // Post group notifications without summaries => force autogroup + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, "testGrp " + i, false, channel); + notificationList.add(r); + mGroupHelper.onNotificationPostedWithDelay(r, notificationList, summaryByGroup); + } + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_alerting), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_alerting), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + Mockito.reset(mCallback); + + // Update the channel importance for all posted notifications + final int numSilentGroupNotifications = AUTOGROUP_AT_COUNT + numUngrouped; + channel.setImportance(IMPORTANCE_LOW); + for (NotificationRecord r: notificationList) { + r.updateNotificationChannel(channel); + } + mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel, + notificationList); + + // Check that all notifications are moved to the silent section group + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(numSilentGroupNotifications)).addAutoGroup(anyString(), + eq(expectedGroupKey_silent), eq(false)); + + // Check that the alerting section group is removed + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey_alerting)); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) + public void testNoGroup_singletonGroup_underLimit() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + // Post singleton groups, under forced group limit + for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT - 1; i++) { + NotificationRecord summary = getNotificationRecord(pkg, i, + String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true); + notificationList.add(summary); + NotificationRecord child = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp "+i, false); + notificationList.add(child); + summaryByGroup.put(summary.getGroupKey(), summary); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup); + } + verifyZeroInteractions(mCallback); + } + + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + @DisableFlags(Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS) + public void testAddAggregateSummary_singletonGroup_disableFlag() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + // Post singleton groups, above forced group limit + for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) { + NotificationRecord summary = getNotificationRecord(pkg, i, + String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true); + notificationList.add(summary); + NotificationRecord child = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp "+i, false); + notificationList.add(child); + summaryByGroup.put(summary.getGroupKey(), summary); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup); + } + // FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS is disabled => don't force group + verifyZeroInteractions(mCallback); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) + public void testAddAggregateSummary_singletonGroups() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + // Post singleton groups, above forced group limit + for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) { + NotificationRecord summary = getNotificationRecord(pkg, i, + String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true); + notificationList.add(summary); + NotificationRecord child = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp "+i, false); + notificationList.add(child); + summaryByGroup.put(summary.getGroupKey(), summary); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + summary.isCanceled = true; // simulate removing the app summary + mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup); + + } + // Check that notifications are forced grouped + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); + verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + + // Check that summaries are canceled + verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).removeAppProvidedSummary(anyString()); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) + public void testCancelCachedSummary_singletonGroups() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + final int id = 0; + // Post singleton groups, above forced group limit + for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) { + NotificationRecord summary = getNotificationRecord(pkg, i, + String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true); + notificationList.add(summary); + NotificationRecord child = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp "+i, false); + notificationList.add(child); + summaryByGroup.put(summary.getGroupKey(), summary); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + summary.isCanceled = true; // simulate removing the app summary + mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup); + } + Mockito.reset(mCallback); + + // App cancels the summary of an aggregated group + mGroupHelper.maybeCancelGroupChildrenForCanceledSummary(pkg, String.valueOf(id), id, + UserHandle.SYSTEM.getIdentifier(), REASON_APP_CANCEL); + + verify(mCallback, times(1)).removeNotificationFromCanceledGroup( + eq(UserHandle.SYSTEM.getIdentifier()), eq(pkg), eq("testGrp " + id), + eq(REASON_APP_CANCEL)); + CachedSummary cachedSummary = mGroupHelper.findCanceledSummary(pkg, String.valueOf(id), id, + UserHandle.SYSTEM.getIdentifier()); + assertThat(cachedSummary).isNull(); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) + public void testRemoveCachedSummary_singletonGroups_removeChildren() { + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final int id = 0; + NotificationRecord childToRemove = null; + // Post singleton groups, above forced group limit + for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) { + NotificationRecord summary = getNotificationRecord(pkg, i, + String.valueOf(i), UserHandle.SYSTEM, "testGrp "+i, true); + notificationList.add(summary); + NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), + UserHandle.SYSTEM, "testGrp " + i, false); + if (i == id) { + childToRemove = child; + } + notificationList.add(child); + summaryByGroup.put(summary.getGroupKey(), summary); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + summary.isCanceled = true; // simulate removing the app summary + mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup); + } + // override group key for child notifications + List<NotificationRecord> notificationListAfterGrouping = new ArrayList<>( + notificationList.stream().filter(r -> { + if (r.getSbn().getNotification().isGroupChild()) { + r.setOverrideGroupKey(expectedGroupKey); + return true; + } else { + return false; + } + }).toList()); + summaryByGroup.clear(); + Mockito.reset(mCallback); + + //Cancel child 0 => remove cached summary + childToRemove.isCanceled = true; + notificationListAfterGrouping.remove(childToRemove); + mGroupHelper.onNotificationRemoved(childToRemove, notificationListAfterGrouping); + CachedSummary cachedSummary = mGroupHelper.findCanceledSummary(pkg, String.valueOf(id), id, + UserHandle.SYSTEM.getIdentifier()); + assertThat(cachedSummary).isNull(); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testGroupSectioners() { + final NotificationRecord notification_alerting = getNotificationRecord(mPkg, 0, "", mUser, + "", false, IMPORTANCE_DEFAULT); + assertThat(GroupHelper.getSection(notification_alerting).mName).isEqualTo("AlertingSection"); + + final NotificationRecord notification_silent = getNotificationRecord(mPkg, 0, "", mUser, + "", false, IMPORTANCE_LOW); + assertThat(GroupHelper.getSection(notification_silent).mName).isEqualTo("SilentSection"); + + NotificationRecord notification_conversation = mock(NotificationRecord.class); + when(notification_conversation.isConversation()).thenReturn(true); + assertThat(GroupHelper.getSection(notification_conversation)).isNull(); + + NotificationRecord notification_call = spy(getNotificationRecord(mPkg, 0, "", mUser, + "", false, IMPORTANCE_LOW)); + Notification n = mock(Notification.class); + StatusBarNotification sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM)); + when(notification_call.isConversation()).thenReturn(false); + when(notification_call.getNotification()).thenReturn(n); + when(notification_call.getSbn()).thenReturn(sbn); + when(sbn.getNotification()).thenReturn(n); + when(n.isStyle(Notification.CallStyle.class)).thenReturn(true); + assertThat(GroupHelper.getSection(notification_call)).isNull(); + + NotificationRecord notification_colorFg = spy(getNotificationRecord(mPkg, 0, "", mUser, + "", false, IMPORTANCE_LOW)); + sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM)); + n = mock(Notification.class); + when(notification_colorFg.isConversation()).thenReturn(false); + when(notification_colorFg.getNotification()).thenReturn(n); + when(notification_colorFg.getSbn()).thenReturn(sbn); + when(sbn.getNotification()).thenReturn(n); + when(n.isForegroundService()).thenReturn(true); + when(n.isColorized()).thenReturn(true); + when(n.isStyle(Notification.CallStyle.class)).thenReturn(false); + assertThat(GroupHelper.getSection(notification_colorFg)).isNull(); + } + } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index 3da80314e6d4..2233aa2eed0f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -84,6 +84,8 @@ import android.os.UserManager; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.service.notification.NotificationListenerService; @@ -1221,6 +1223,44 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + public void testSilentNotification_flagSilent() throws Exception { + final Notification n = new Builder(getContext(), "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setSilent(true) + .build(); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid, + mPid, n, mUser, null, System.currentTimeMillis()); + NotificationRecord r = new NotificationRecord(getContext(), sbn, + new NotificationChannel("test", "test", IMPORTANCE_HIGH)); + + mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(r.isInterruptive()); + assertEquals(-1, r.getLastAudiblyAlertedMs()); + assertTrue(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS)); + } + + @Test + @DisableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_SILENT_FLAG) + public void testSilentNotification_groupKeySilent() throws Exception { + final Notification n = new Builder(getContext(), "test") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setSilent(true) + .build(); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid, + mPid, n, mUser, null, System.currentTimeMillis()); + NotificationRecord r = new NotificationRecord(getContext(), sbn, + new NotificationChannel("test", "test", IMPORTANCE_HIGH)); + + mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(r.isInterruptive()); + assertEquals(-1, r.getLastAudiblyAlertedMs()); + assertTrue(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS)); + } + + @Test public void testHonorAlertOnlyOnceForBuzz() throws Exception { NotificationRecord r = getBuzzyNotification(); NotificationRecord s = getBuzzyOnceNotification(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index c48d745b1dc0..5d306e152ad7 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -39,8 +39,10 @@ import static android.app.Notification.FLAG_NO_DISMISS; import static android.app.Notification.FLAG_ONGOING_EVENT; import static android.app.Notification.FLAG_ONLY_ALERT_ONCE; import static android.app.Notification.FLAG_USER_INITIATED_JOB; +import static android.app.Notification.GROUP_ALERT_CHILDREN; import static android.app.Notification.VISIBILITY_PRIVATE; import static android.app.NotificationChannel.NEWS_ID; +import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE; import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED; import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; @@ -98,11 +100,13 @@ import static android.service.notification.Adjustment.TYPE_NEWS; import static android.service.notification.Condition.SOURCE_CONTEXT; import static android.service.notification.Condition.SOURCE_USER_ACTION; import static android.service.notification.Condition.STATE_TRUE; +import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING; import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS; +import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_LOCKDOWN; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; @@ -117,6 +121,7 @@ import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; import static com.android.server.notification.Flags.FLAG_REJECT_OLD_NOTIFICATIONS; +import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY; import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION; import static com.android.server.notification.NotificationManagerService.DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; import static com.android.server.notification.NotificationManagerService.NOTIFICATION_TTL; @@ -369,6 +374,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private static final int TOAST_DURATION = 2_000; private static final int SECONDARY_DISPLAY_ID = 42; private static final int TEST_PROFILE_USERHANDLE = 12; + private static final long DELAY_FORCE_REGROUP_TIME = 3000; private static final String ACTION_NOTIFICATION_TIMEOUT = NotificationManagerService.class.getSimpleName() + ".TIMEOUT"; @@ -2487,43 +2493,372 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testAutobundledSummary_notificationAdded() { NotificationRecord summary = - generateNotificationRecord(mTestNotificationChannel, 0, "pkg", true); + generateNotificationRecord(mTestNotificationChannel, 0, AUTOGROUP_KEY, true); summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY; mService.addNotification(summary); mService.mSummaryByGroupKey.put("pkg", summary); mService.mAutobundledSummaries.put(0, new ArrayMap<>()); mService.mAutobundledSummaries.get(0).put("pkg", summary.getKey()); - mService.updateAutobundledSummaryLocked(0, "pkg", + mService.updateAutobundledSummaryLocked(0, "pkg", AUTOGROUP_KEY, new NotificationAttributes(GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, - mock(Icon.class), 0, VISIBILITY_PRIVATE), false); + mock(Icon.class), 0, + VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID), false); waitForIdle(); assertTrue(summary.getSbn().isOngoing()); } @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAutobundledSummary_notificationAdded_forcedGrouping() { + NotificationRecord summary = + generateNotificationRecord(mTestNotificationChannel, 0, AUTOGROUP_KEY, true); + summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY; + mService.addNotification(summary); + mService.mSummaryByGroupKey.put("pkg", summary); + mService.mAutobundledSummaries.put(0, new ArrayMap<>()); + mService.mAutobundledSummaries.get(0).put(summary.getGroupKey(), summary.getKey()); + + mService.updateAutobundledSummaryLocked(0, "pkg", summary.getGroupKey(), + new NotificationAttributes(GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, + mock(Icon.class), 0, + VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID), false); + waitForIdle(); + + assertTrue(summary.getSbn().isOngoing()); + } + + @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testAutobundledSummary_notificationRemoved() { NotificationRecord summary = - generateNotificationRecord(mTestNotificationChannel, 0, "pkg", true); + generateNotificationRecord(mTestNotificationChannel, 0, AUTOGROUP_KEY, true); summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY; summary.getNotification().flags |= Notification.FLAG_ONGOING_EVENT; mService.addNotification(summary); mService.mAutobundledSummaries.put(0, new ArrayMap<>()); mService.mAutobundledSummaries.get(0).put("pkg", summary.getKey()); - mService.mSummaryByGroupKey.put("pkg", summary); + mService.mSummaryByGroupKey.put(summary.getGroupKey(), summary); - mService.updateAutobundledSummaryLocked(0, "pkg", + mService.updateAutobundledSummaryLocked(0, "pkg", AUTOGROUP_KEY, new NotificationAttributes(GroupHelper.BASE_FLAGS, - mock(Icon.class), 0, VISIBILITY_PRIVATE), false); + mock(Icon.class), 0, + VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID), false); waitForIdle(); assertFalse(summary.getSbn().isOngoing()); } @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAutobundledSummary_notificationRemoved_forceGrouping() { + NotificationRecord summary = + generateNotificationRecord(mTestNotificationChannel, 0, AUTOGROUP_KEY, true); + summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY; + summary.getNotification().flags |= Notification.FLAG_ONGOING_EVENT; + mService.addNotification(summary); + mService.mAutobundledSummaries.put(0, new ArrayMap<>()); + mService.mAutobundledSummaries.get(0).put(summary.getGroupKey(), summary.getKey()); + + mService.updateAutobundledSummaryLocked(0, "pkg", summary.getGroupKey(), + new NotificationAttributes(GroupHelper.BASE_FLAGS, + mock(Icon.class), 0, + VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID), false); + waitForIdle(); + + assertFalse(summary.getSbn().isOngoing()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAggregatedSummary_updateSummaryAttributes() { + final String aggregateGroupName = "Aggregate_Test"; + final String newChannelId = "newChannelId"; + final NotificationChannel newChannel = new NotificationChannel( + newChannelId, newChannelId, IMPORTANCE_DEFAULT); + mService.setPreferencesHelper(mPreferencesHelper); + final NotificationRecord summary = + generateNotificationRecord(mTestNotificationChannel, 0, aggregateGroupName, true); + final String groupKey = summary.getGroupKey(); + summary.getNotification().flags |= Notification.FLAG_AUTOGROUP_SUMMARY; + mService.addNotification(summary); + mService.mAutobundledSummaries.put(0, new ArrayMap<>()); + mService.mAutobundledSummaries.get(0).put(groupKey, summary.getKey()); + when(mPreferencesHelper.getNotificationChannel(eq("pkg"), anyInt(), + eq(newChannelId), anyBoolean())).thenReturn(newChannel); + + mService.updateAutobundledSummaryLocked(0, "pkg", groupKey, + new NotificationAttributes(GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, + mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, newChannelId), + false); + waitForIdle(); + + assertTrue(summary.getSbn().isOngoing()); + assertThat(summary.getNotification().getGroupAlertBehavior()).isEqualTo( + GROUP_ALERT_CHILDREN); + + assertThat(summary.getChannel().getId()).isEqualTo(newChannelId); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAddAggregateNotification_notifyPostedLocked() throws Exception { + final String originalGroupName = "originalGroup"; + final NotificationRecord r = + generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false); + mService.addNotification(r); + mService.addAutogroupKeyLocked(r.getKey(), "grpKey", true); + + assertThat(r.getSbn().getOverrideGroupKey()).isEqualTo("grpKey"); + verify(mRankingHandler, times(1)).requestSort(); + verify(mListeners, times(1)).notifyPostedLocked(eq(r), eq(r)); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAddAggregateSummaryNotification_convertSummary() throws Exception { + final String originalGroupName = "originalGroup"; + final NotificationRecord r = + generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, true); + final String groupKey = r.getGroupKey(); + mService.addNotification(r); + assertThat(mService.mSummaryByGroupKey.containsKey(groupKey)).isTrue(); + boolean isConverted = mService.convertSummaryToNotificationLocked(r.getKey()); + + assertThat(isConverted).isTrue(); + assertThat(r.getSbn().isGroup()).isTrue(); + assertThat(r.getNotification().isGroupSummary()).isFalse(); + assertThat(mService.mSummaryByGroupKey.containsKey(groupKey)).isFalse(); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) + public void testAggregateGroups_RemoveAppSummary() throws Exception { + final String originalGroupName = "originalGroup"; + final NotificationRecord r = + generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, true); + mService.addNotification(r); + mService.removeAppSummaryLocked(r.getKey()); + + assertThat(r.isCanceled).isTrue(); + waitForIdle(); + verify(mWorkerHandler, times(1)).scheduleCancelNotification(any(), eq(0)); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testUngroupingAggregateSummary() throws Exception { + final String originalGroupName = "originalGroup"; + final String aggregateGroupName = "Aggregate_Test"; + final int summaryId = Integer.MAX_VALUE; + // Add 2 group notifications without a summary + NotificationRecord nr0 = + generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false); + NotificationRecord nr1 = + generateNotificationRecord(mTestNotificationChannel, 1, originalGroupName, false); + mService.addNotification(nr0); + mService.addNotification(nr1); + mService.mSummaryByGroupKey.remove(nr0.getGroupKey()); + + // GroupHelper is a mock, so make the calls it would make + // Add aggregate group summary + NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS, + mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, + nr0.getChannel().getId()); + NotificationRecord aggregateSummary = mService.createAutoGroupSummary(nr0.getUserId(), + nr0.getSbn().getPackageName(), nr0.getKey(), aggregateGroupName, summaryId, attr); + mService.addNotification(aggregateSummary); + nr0.setOverrideGroupKey(aggregateGroupName); + nr1.setOverrideGroupKey(aggregateGroupName); + final String fullAggregateGroupKey = nr0.getGroupKey(); + + // Check that the aggregate group summary was created + assertThat(aggregateSummary.getNotification().getGroup()).isEqualTo(aggregateGroupName); + assertThat(aggregateSummary.getNotification().getChannelId()).isEqualTo( + nr0.getChannel().getId()); + assertThat(mService.mSummaryByGroupKey.containsKey(fullAggregateGroupKey)).isTrue(); + + // Cancel both children + mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0.getSbn().getTag(), + nr0.getSbn().getId(), nr0.getSbn().getUserId()); + mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr1.getSbn().getTag(), + nr1.getSbn().getId(), nr1.getSbn().getUserId()); + waitForIdle(); + + verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any()); + verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any()); + + // GroupHelper would send 'remove summary' event + mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName(), + fullAggregateGroupKey); + waitForIdle(); + + // Make sure the summary was removed and not re-posted + assertThat(mService.getNotificationRecordCount()).isEqualTo(0); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) + public void testCancelGroupChildrenForCanceledSummary_singletonGroup() throws Exception { + final String originalGroupName = "originalGroup"; + final String aggregateGroupName = "Aggregate_Test"; + final int summaryId = Integer.MAX_VALUE; + // Add a "singleton group" + NotificationRecord nr0 = + generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false); + NotificationRecord nr1 = + generateNotificationRecord(mTestNotificationChannel, 1, originalGroupName, false); + final NotificationRecord summary = + generateNotificationRecord(mTestNotificationChannel, 2, originalGroupName, true); + final String originalGroupKey = summary.getGroupKey(); + mService.addNotification(nr0); + mService.addNotification(nr1); + mService.addNotification(summary); + + // GroupHelper is a mock, so make the calls it would make + // Remove the app's summary notification + mService.removeAppSummaryLocked(summary.getKey()); + waitForIdle(); + + // Add aggregate group summary + NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS, + mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, + nr0.getChannel().getId()); + NotificationRecord aggregateSummary = mService.createAutoGroupSummary(nr0.getUserId(), + nr0.getSbn().getPackageName(), nr0.getKey(), aggregateGroupName, summaryId, attr); + mService.addNotification(aggregateSummary); + + nr0.setOverrideGroupKey(aggregateGroupName); + nr1.setOverrideGroupKey(aggregateGroupName); + final String fullAggregateGroupKey = nr0.getGroupKey(); + + assertThat(aggregateSummary.getNotification().getGroup()).isEqualTo(aggregateGroupName); + assertThat(aggregateSummary.getNotification().getChannelId()).isEqualTo( + nr0.getChannel().getId()); + assertThat(mService.mSummaryByGroupKey.containsKey(fullAggregateGroupKey)).isTrue(); + assertThat(mService.mSummaryByGroupKey.containsKey(originalGroupKey)).isFalse(); + + // Cancel the original app summary (is already removed) + mBinderService.cancelNotificationWithTag(summary.getSbn().getPackageName(), + summary.getSbn().getPackageName(), summary.getSbn().getTag(), + summary.getSbn().getId(), summary.getSbn().getUserId()); + waitForIdle(); + + // Check if NMS.CancelNotificationRunnable calls maybeCancelGroupChildrenForCanceledSummary + verify(mGroupHelper, times(1)).maybeCancelGroupChildrenForCanceledSummary( + eq(summary.getSbn().getPackageName()), eq(summary.getSbn().getTag()), + eq(summary.getSbn().getId()), eq(summary.getSbn().getUserId()), + eq(REASON_APP_CANCEL)); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testUpdateChannel_notifyGroupHelper() throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + mTestNotificationChannel.setLightColor(Color.CYAN); + when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(), + eq(mTestNotificationChannel.getId()), anyBoolean())) + .thenReturn(mTestNotificationChannel); + + mBinderService.updateNotificationChannelForPackage(mPkg, mUid, mTestNotificationChannel); + mTestableLooper.moveTimeForward(DELAY_FORCE_REGROUP_TIME); + waitForIdle(); + + verify(mGroupHelper, times(1)).onChannelUpdated(eq(Process.myUserHandle().getIdentifier()), + eq(mPkg), eq(mTestNotificationChannel), any()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testSnoozeRunnable_snoozeAggregateGroupChild_summaryNotSnoozed() throws Exception { + final String aggregateGroupName = "Aggregate_Test"; + + // build autogroup summary notification + Notification.Builder nb = new Notification.Builder(mContext, + mTestNotificationChannel.getId()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setGroup(aggregateGroupName) + .setGroupSummary(true) + .setFlag(Notification.FLAG_AUTOGROUP_SUMMARY, true); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, + "tag" + System.currentTimeMillis(), mUid, 0, nb.build(), + UserHandle.getUserHandleForUid(mUid), null, 0); + final NotificationRecord summary = new NotificationRecord(mContext, sbn, + mTestNotificationChannel); + + final NotificationRecord child = generateNotificationRecord( + mTestNotificationChannel, 2, aggregateGroupName, false); + mService.addNotification(summary); + mService.addNotification(child); + when(mSnoozeHelper.canSnooze(anyInt())).thenReturn(true); + + // snooze child only + NotificationManagerService.SnoozeNotificationRunnable snoozeNotificationRunnable = + mService.new SnoozeNotificationRunnable( + child.getKey(), 100, null); + snoozeNotificationRunnable.run(); + + // only child should be snoozed + verify(mSnoozeHelper, times(1)).snooze(any(NotificationRecord.class), anyLong()); + + // both group summary and child should be cancelled + assertNull(mService.getNotificationRecord(summary.getKey())); + assertNull(mService.getNotificationRecord(child.getKey())); + + assertEquals(4, mNotificationRecordLogger.numCalls()); + assertEquals(NotificationRecordLogger.NotificationEvent.NOTIFICATION_SNOOZED, + mNotificationRecordLogger.event(0)); + assertEquals( + NotificationRecordLogger.NotificationCancelledEvent.NOTIFICATION_CANCEL_SNOOZED, + mNotificationRecordLogger.event(1)); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testOnlyForceGroupIfNeeded_newNotification_notAutogrouped() { + NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null, false); + when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(false); + mService.addEnqueuedNotification(r); + NotificationManagerService.PostNotificationRunnable runnable = + mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); + runnable.run(); + waitForIdle(); + + mTestableLooper.moveTimeForward(DELAY_FORCE_REGROUP_TIME); + waitForIdle(); + + verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean()); + verify(mGroupHelper, times(1)).onNotificationPostedWithDelay(eq(r), any(), any()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testOnlyForceGroupIfNeeded_newNotification_wasAutogrouped() { + NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null, false); + when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(true); + mService.addEnqueuedNotification(r); + NotificationManagerService.PostNotificationRunnable runnable = + mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); + runnable.run(); + waitForIdle(); + + mTestableLooper.moveTimeForward(DELAY_FORCE_REGROUP_TIME); + waitForIdle(); + + verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean()); + verify(mGroupHelper, never()).onNotificationPostedWithDelay(eq(r), any(), any()); + } + + @Test public void testCancelAllNotifications_IgnoreForegroundService() throws Exception { when(mAmi.applyForegroundServiceNotification( any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY); @@ -3653,9 +3988,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mPermissionHelper.hasPermission(mUid)).thenReturn(true); when(mPermissionHelper.isPermissionFixed(mPkg, temp.getUserId())).thenReturn(true); + NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS, + mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID); + NotificationRecord r = mService.createAutoGroupSummary(temp.getUserId(), - temp.getSbn().getPackageName(), temp.getKey(), 0, mock(Icon.class), 0, - VISIBILITY_PRIVATE); + temp.getSbn().getPackageName(), temp.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr); assertThat(r.isImportanceFixed()).isTrue(); } @@ -4796,6 +5133,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testSnoozeRunnable_snoozeAutoGroupChild_summaryNotSnoozed() throws Exception { final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, GroupHelper.AUTOGROUP_KEY, true); @@ -5659,7 +5997,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testAddAutogroup_requestsSort() throws Exception { final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); mService.addNotification(r); - mService.addAutogroupKeyLocked(r.getKey(), true); + mService.addAutogroupKeyLocked(r.getKey(), "grpKey", true); verify(mRankingHandler, times(1)).requestSort(); } @@ -5679,7 +6017,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); r.setOverrideGroupKey("TEST"); mService.addNotification(r); - mService.addAutogroupKeyLocked(r.getKey(), true); + mService.addAutogroupKeyLocked(r.getKey(), "grpName", true); verify(mRankingHandler, never()).requestSort(); } @@ -5689,7 +6027,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testAutogroupSuppressSort_noSort() throws Exception { final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); mService.addNotification(r); - mService.addAutogroupKeyLocked(r.getKey(), false); + mService.addAutogroupKeyLocked(r.getKey(), "grpName", false); verify(mRankingHandler, never()).requestSort(); } @@ -12688,6 +13026,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testUngroupingOngoingAutoSummary() throws Exception { NotificationRecord nr0 = generateNotificationRecord(mTestNotificationChannel, 0); @@ -12701,10 +13040,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // grouphelper is a mock here, so make the calls it would make // add summary + NotificationAttributes attr = new NotificationAttributes( + GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0, + VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID); mService.addNotification( mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(), - nr1.getKey(), GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0, - VISIBILITY_PRIVATE)); + nr1.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr)); // cancel both children mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0.getSbn().getTag(), @@ -12714,7 +13055,46 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); // group helper would send 'remove summary' event - mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName()); + mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName(), + AUTOGROUP_KEY); + waitForIdle(); + + // make sure the summary was removed and not re-posted + assertThat(mService.getNotificationRecordCount()).isEqualTo(0); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testUngroupingOngoingAutoSummary_forceGrouping() throws Exception { + NotificationRecord nr0 = + generateNotificationRecord(mTestNotificationChannel, 0); + NotificationRecord nr1 = + generateNotificationRecord(mTestNotificationChannel, 0); + nr1.getSbn().getNotification().flags |= FLAG_ONGOING_EVENT; + + mService.addNotification(nr0); + mService.addNotification(nr1); + + // grouphelper is a mock here, so make the calls it would make + + // add summary + NotificationAttributes attr = new NotificationAttributes( + GroupHelper.BASE_FLAGS | FLAG_ONGOING_EVENT, mock(Icon.class), 0, + VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID); + mService.addNotification( + mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(), + nr1.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr)); + + // cancel both children + mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0.getSbn().getTag(), + nr0.getSbn().getId(), nr0.getSbn().getUserId()); + mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr1.getSbn().getTag(), + nr1.getSbn().getId(), nr1.getSbn().getUserId()); + waitForIdle(); + + // group helper would send 'remove summary' event + mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName(), + AUTOGROUP_KEY); waitForIdle(); // make sure the summary was removed and not re-posted @@ -12722,6 +13102,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testUngroupingAutoSummary_differentUsers() throws Exception { NotificationRecord nr0 = generateNotificationRecord(mTestNotificationChannel, 0, USER_SYSTEM); @@ -12729,11 +13110,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { generateNotificationRecord(mTestNotificationChannel, 1, USER_SYSTEM); // add notifications + summary for USER_SYSTEM + NotificationAttributes attr = new NotificationAttributes( + GroupHelper.BASE_FLAGS, mock(Icon.class), 0, + VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID); mService.addNotification(nr0); mService.addNotification(nr1); mService.addNotification( mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(), - nr1.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0, VISIBILITY_PRIVATE)); + nr1.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr)); // add notifications + summary for USER_ALL NotificationRecord nr0_all = @@ -12746,7 +13130,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification( mService.createAutoGroupSummary(nr0_all.getUserId(), nr0_all.getSbn().getPackageName(), - nr0_all.getKey(), GroupHelper.BASE_FLAGS, mock(Icon.class), 0, VISIBILITY_PRIVATE)); + nr0_all.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr)); // cancel both children for USER_ALL mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0_all.getSbn().getTag(), @@ -12757,7 +13141,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // group helper would send 'remove summary' event mService.clearAutogroupSummaryLocked(UserHandle.USER_ALL, - nr0_all.getSbn().getPackageName()); + nr0_all.getSbn().getPackageName(), AUTOGROUP_KEY); waitForIdle(); // make sure the right summary was removed @@ -12770,6 +13154,58 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testUngroupingAutoSummary_differentUsers_forceGrouping() throws Exception { + NotificationRecord nr0 = + generateNotificationRecord(mTestNotificationChannel, 0, USER_SYSTEM); + NotificationRecord nr1 = + generateNotificationRecord(mTestNotificationChannel, 1, USER_SYSTEM); + + // add notifications + summary for USER_SYSTEM + NotificationAttributes attr = new NotificationAttributes( + GroupHelper.BASE_FLAGS, mock(Icon.class), 0, + VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, DEFAULT_CHANNEL_ID); + mService.addNotification(nr0); + mService.addNotification(nr1); + mService.addNotification( + mService.createAutoGroupSummary(nr1.getUserId(), nr1.getSbn().getPackageName(), + nr1.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr)); + + // add notifications + summary for USER_ALL + NotificationRecord nr0_all = + generateNotificationRecord(mTestNotificationChannel, 2, UserHandle.USER_ALL); + NotificationRecord nr1_all = + generateNotificationRecord(mTestNotificationChannel, 3, UserHandle.USER_ALL); + + mService.addNotification(nr0_all); + mService.addNotification(nr1_all); + mService.addNotification( + mService.createAutoGroupSummary(nr0_all.getUserId(), + nr0_all.getSbn().getPackageName(), + nr0_all.getKey(), AUTOGROUP_KEY, Integer.MAX_VALUE, attr)); + + // cancel both children for USER_ALL + mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr0_all.getSbn().getTag(), + nr0_all.getSbn().getId(), UserHandle.USER_ALL); + mBinderService.cancelNotificationWithTag(mPkg, mPkg, nr1_all.getSbn().getTag(), + nr1_all.getSbn().getId(), UserHandle.USER_ALL); + waitForIdle(); + + // group helper would send 'remove summary' event + mService.clearAutogroupSummaryLocked(UserHandle.USER_ALL, + nr0_all.getSbn().getPackageName(), AUTOGROUP_KEY); + waitForIdle(); + + // make sure the right summary was removed + assertThat(mService.getNotificationCount(nr0_all.getSbn().getPackageName(), + UserHandle.USER_ALL, 0, null)).isEqualTo(0); + + // the USER_SYSTEM notifications + summary were not removed + assertThat(mService.getNotificationCount(nr0.getSbn().getPackageName(), + USER_SYSTEM, 0, null)).isEqualTo(3); + } + + @Test public void testStrongAuthTracker_isInLockDownMode() { mStrongAuthTracker.setGetStrongAuthForUserReturnValue( STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); |