Loading packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +85 −10 Original line number Original line Diff line number Diff line Loading @@ -16,12 +16,15 @@ package com.android.systemui.bubbles; package com.android.systemui.bubbles; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_BUBBLE; import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS; import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CLICK; import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE; Loading Loading @@ -82,6 +85,7 @@ import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.ZenModeController; Loading @@ -90,6 +94,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; import java.util.List; import javax.inject.Inject; import javax.inject.Inject; Loading @@ -109,7 +114,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Retention(SOURCE) @Retention(SOURCE) @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED}) DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @interface DismissReason {} Loading @@ -121,6 +126,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi static final int DISMISS_ACCESSIBILITY_ACTION = 6; static final int DISMISS_ACCESSIBILITY_ACTION = 6; static final int DISMISS_NO_LONGER_BUBBLE = 7; static final int DISMISS_NO_LONGER_BUBBLE = 7; static final int DISMISS_USER_CHANGED = 8; static final int DISMISS_USER_CHANGED = 8; static final int DISMISS_GROUP_CANCELLED = 9; public static final int MAX_BUBBLES = 5; // TODO: actually enforce this public static final int MAX_BUBBLES = 5; // TODO: actually enforce this Loading @@ -133,6 +139,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi private BubbleStateChangeListener mStateChangeListener; private BubbleStateChangeListener mStateChangeListener; private BubbleExpandListener mExpandListener; private BubbleExpandListener mExpandListener; @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; private final NotificationGroupManager mNotificationGroupManager; private BubbleData mBubbleData; private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; @Nullable private BubbleStackView mStackView; Loading Loading @@ -211,10 +218,11 @@ public class BubbleController implements ConfigurationController.ConfigurationLi BubbleData data, ConfigurationController configurationController, BubbleData data, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager) { NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager) { this(context, statusBarWindowController, data, null /* synchronizer */, this(context, statusBarWindowController, data, null /* synchronizer */, configurationController, interruptionStateProvider, zenModeController, configurationController, interruptionStateProvider, zenModeController, notifUserManager); notifUserManager, groupManager); } } public BubbleController(Context context, StatusBarWindowController statusBarWindowController, public BubbleController(Context context, StatusBarWindowController statusBarWindowController, Loading @@ -222,7 +230,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi ConfigurationController configurationController, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager) { NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager) { mContext = context; mContext = context; mNotificationInterruptionStateProvider = interruptionStateProvider; mNotificationInterruptionStateProvider = interruptionStateProvider; mNotifUserManager = notifUserManager; mNotifUserManager = notifUserManager; Loading Loading @@ -251,6 +260,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); mNotificationEntryManager.addNotificationEntryListener(mEntryListener); mNotificationEntryManager.addNotificationEntryListener(mEntryListener); mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor); mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor); mNotificationGroupManager = groupManager; mStatusBarWindowController = statusBarWindowController; mStatusBarWindowController = statusBarWindowController; mStatusBarStateListener = new StatusBarStateListener(); mStatusBarStateListener = new StatusBarStateListener(); Loading Loading @@ -500,24 +510,38 @@ public class BubbleController implements ConfigurationController.ConfigurationLi new NotificationRemoveInterceptor() { new NotificationRemoveInterceptor() { @Override @Override public boolean onNotificationRemoveRequested(String key, int reason) { public boolean onNotificationRemoveRequested(String key, int reason) { if (!mBubbleData.hasBubbleWithKey(key)) { NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key); String groupKey = entry != null ? entry.notification.getGroupKey() : null; ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey); boolean inBubbleData = mBubbleData.hasBubbleWithKey(key); boolean isSummary = entry != null && entry.notification.getNotification().isGroupSummary(); boolean isSummaryOfBubbles = isSummary && bubbleChildren != null && !bubbleChildren.isEmpty(); if (!inBubbleData && !isSummaryOfBubbles) { return false; return false; } } Bubble bubble = mBubbleData.getBubbleWithKey(key); NotificationEntry entry = bubble.getEntry(); final boolean isClearAll = reason == REASON_CANCEL_ALL; final boolean isClearAll = reason == REASON_CANCEL_ALL; final boolean isUserDimiss = reason == REASON_CANCEL; final boolean isUserDimiss = reason == REASON_CANCEL || reason == REASON_CLICK; final boolean isAppCancel = reason == REASON_APP_CANCEL final boolean isAppCancel = reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL; || reason == REASON_APP_CANCEL_ALL; final boolean isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED; // Need to check for !appCancel here because the notification may have // Need to check for !appCancel here because the notification may have // previously been dismissed & entry.isRowDismissed would still be true // previously been dismissed & entry.isRowDismissed would still be true boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel) boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel) || isClearAll || isUserDimiss; || isClearAll || isUserDimiss || isSummaryCancel; if (isSummaryOfBubbles) { return handleSummaryRemovalInterception(entry, userRemovedNotif); } // The bubble notification sticks around in the data as long as the bubble is // The bubble notification sticks around in the data as long as the bubble is // not dismissed and the app hasn't cancelled the notification. // not dismissed and the app hasn't cancelled the notification. Bubble bubble = mBubbleData.getBubbleWithKey(key); boolean bubbleExtended = entry.isBubble() && userRemovedNotif; boolean bubbleExtended = entry.isBubble() && userRemovedNotif; if (bubbleExtended) { if (bubbleExtended) { bubble.setShowInShadeWhenBubble(false); bubble.setShowInShadeWhenBubble(false); Loading @@ -536,6 +560,43 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } }; }; private boolean handleSummaryRemovalInterception(NotificationEntry summary, boolean userRemovedNotif) { String groupKey = summary.notification.getGroupKey(); ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey); if (userRemovedNotif) { // If it's a user dismiss we mark the children to be hidden from the shade. for (int i = 0; i < bubbleChildren.size(); i++) { Bubble bubbleChild = bubbleChildren.get(i); // As far as group manager is concerned, once a child is no longer shown // in the shade, it is essentially removed. mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry()); bubbleChild.setShowInShadeWhenBubble(false); bubbleChild.setShowBubbleDot(false); if (mStackView != null) { mStackView.updateDotVisibility(bubbleChild.getKey()); } } // And since all children are removed, remove the summary. mNotificationGroupManager.onEntryRemoved(summary); // If the summary was auto-generated we don't need to keep that notification around // because apps can't cancel it; so we only intercept & suppress real summaries. boolean isAutogroupSummary = (summary.notification.getNotification().flags & FLAG_AUTOGROUP_SUMMARY) != 0; return !isAutogroupSummary; } else { // Remove any associated bubble children. for (int i = 0; i < bubbleChildren.size(); i++) { Bubble bubbleChild = bubbleChildren.get(i); mBubbleData.notificationEntryRemoved(bubbleChild.getEntry(), DISMISS_GROUP_CANCELLED); } return false; } } @SuppressWarnings("FieldCanBeLocal") @SuppressWarnings("FieldCanBeLocal") private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { @Override @Override Loading Loading @@ -597,7 +658,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } // Do removals, if any. // Do removals, if any. for (Pair<Bubble, Integer> removed : update.removedBubbles) { ArrayList<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(update.removedBubbles); for (Pair<Bubble, Integer> removed : removedBubbles) { final Bubble bubble = removed.first; final Bubble bubble = removed.first; @DismissReason final int reason = removed.second; @DismissReason final int reason = removed.second; mStackView.removeBubble(bubble); mStackView.removeBubble(bubble); Loading @@ -622,6 +685,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // Bad things have happened // Bad things have happened } } } } // Check if summary should be removed from NoManGroup NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary( bubble.getEntry().notification); if (summary != null) { ArrayList<NotificationEntry> summaryChildren = mNotificationGroupManager.getLogicalChildren(summary.notification); if (summaryChildren == null || summaryChildren.isEmpty()) { mNotificationEntryManager.performRemoveNotification( summary.notification, UNDEFINED_DISMISS_REASON); } } } } } } Loading packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +17 −0 Original line number Original line Diff line number Diff line Loading @@ -229,6 +229,23 @@ public class BubbleData { dispatchPendingChanges(); dispatchPendingChanges(); } } /** * Retrieves any bubbles that are part of the notification group represented by the provided * group key. */ ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) { ArrayList<Bubble> bubbleChildren = new ArrayList<>(); if (groupKey == null) { return bubbleChildren; } for (Bubble b : mBubbles) { if (groupKey.equals(b.getEntry().notification.getGroupKey())) { bubbleChildren.add(b); } } return bubbleChildren; } private void doAdd(Bubble bubble) { private void doAdd(Bubble bubble) { if (DEBUG_BUBBLE_DATA) { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "doAdd: " + bubble); Log.d(TAG, "doAdd: " + bubble); Loading packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +15 −5 Original line number Original line Diff line number Diff line Loading @@ -70,6 +70,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.HeadsUpManager; Loading @@ -91,6 +92,8 @@ public class BubbleControllerTest extends SysuiTestCase { @Mock @Mock private NotificationEntryManager mNotificationEntryManager; private NotificationEntryManager mNotificationEntryManager; @Mock @Mock private NotificationGroupManager mNotificationGroupManager; @Mock private WindowManager mWindowManager; private WindowManager mWindowManager; @Mock @Mock private IActivityManager mActivityManager; private IActivityManager mActivityManager; Loading Loading @@ -154,6 +157,7 @@ public class BubbleControllerTest extends SysuiTestCase { // Return non-null notification data from the NEM // Return non-null notification data from the NEM when(mNotificationEntryManager.getNotificationData()).thenReturn(mNotificationData); when(mNotificationEntryManager.getNotificationData()).thenReturn(mNotificationData); when(mNotificationData.get(mRow.getEntry().key)).thenReturn(mRow.getEntry()); when(mNotificationData.getChannel(mRow.getEntry().key)).thenReturn(mRow.getEntry().channel); when(mNotificationData.getChannel(mRow.getEntry().key)).thenReturn(mRow.getEntry().channel); mZenModeConfig.suppressedVisualEffects = 0; mZenModeConfig.suppressedVisualEffects = 0; Loading @@ -168,9 +172,14 @@ public class BubbleControllerTest extends SysuiTestCase { mock(HeadsUpManager.class), mock(HeadsUpManager.class), mock(NotificationInterruptionStateProvider.HeadsUpSuppressor.class)); mock(NotificationInterruptionStateProvider.HeadsUpSuppressor.class)); mBubbleData = new BubbleData(mContext); mBubbleData = new BubbleData(mContext); mBubbleController = new TestableBubbleController(mContext, mStatusBarWindowController, mBubbleController = new TestableBubbleController(mContext, mBubbleData, mConfigurationController, interruptionStateProvider, mStatusBarWindowController, mZenModeController, mLockscreenUserManager); mBubbleData, mConfigurationController, interruptionStateProvider, mZenModeController, mLockscreenUserManager, mNotificationGroupManager); mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener); mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener); mBubbleController.setExpandListener(mBubbleExpandListener); mBubbleController.setExpandListener(mBubbleExpandListener); Loading Loading @@ -631,10 +640,11 @@ public class BubbleControllerTest extends SysuiTestCase { ConfigurationController configurationController, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, ZenModeController zenModeController, NotificationLockscreenUserManager lockscreenUserManager) { NotificationLockscreenUserManager lockscreenUserManager, NotificationGroupManager groupManager) { super(context, statusBarWindowController, data, Runnable::run, super(context, statusBarWindowController, data, Runnable::run, configurationController, interruptionStateProvider, zenModeController, configurationController, interruptionStateProvider, zenModeController, lockscreenUserManager); lockscreenUserManager, groupManager); } } } } Loading services/core/java/com/android/server/notification/NotificationManagerService.java +15 −1 Original line number Original line Diff line number Diff line Loading @@ -5249,12 +5249,26 @@ public class NotificationManagerService extends SystemService { return; return; } } // Bubbled children get to stick around if the summary was manually cancelled // (user removed) from systemui. FlagChecker childrenFlagChecker = null; if (mReason == REASON_CANCEL || mReason == REASON_CLICK || mReason == REASON_CANCEL_ALL) { childrenFlagChecker = (flags) -> { if ((flags & FLAG_BUBBLE) != 0) { return false; } return true; }; } // Cancel the notification. // Cancel the notification. boolean wasPosted = removeFromNotificationListsLocked(r); boolean wasPosted = removeFromNotificationListsLocked(r); cancelNotificationLocked( cancelNotificationLocked( r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName); r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName); cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName, cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName, mSendDelete, null); mSendDelete, childrenFlagChecker); updateLightsLocked(); updateLightsLocked(); } else { } else { // No notification was found, assume that it is snoozed and cancel it. // No notification was found, assume that it is snoozed and cancel it. Loading services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +100 −1 Original line number Original line Diff line number Diff line Loading @@ -20,6 +20,7 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREG import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.app.Notification.CATEGORY_CALL; import static android.app.Notification.CATEGORY_CALL; import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.NotificationManager.EXTRA_BLOCKED_STATE; import static android.app.NotificationManager.EXTRA_BLOCKED_STATE; Loading Loading @@ -439,14 +440,22 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { return sbn; return sbn; } } private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, String groupKey, boolean isSummary) { String groupKey, boolean isSummary) { return generateNotificationRecord(channel, id, groupKey, isSummary, false /* isBubble */); } private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, String groupKey, boolean isSummary, boolean isBubble) { Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) .setContentTitle("foo") .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setSmallIcon(android.R.drawable.sym_def_app_icon) .setGroup(groupKey) .setGroup(groupKey) .setGroupSummary(isSummary); .setGroupSummary(isSummary); if (isBubble) { nb.setBubbleMetadata(getBasicBubbleMetadataBuilder().build()); } StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0, StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0, nb.build(), new UserHandle(mUid), null, 0); nb.build(), new UserHandle(mUid), null, 0); return new NotificationRecord(mContext, sbn, channel); return new NotificationRecord(mContext, sbn, channel); Loading Loading @@ -542,6 +551,52 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)); .setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)); } } private NotificationRecord addGroupWithBubblesAndValidateAdded(boolean summaryAutoCancel) throws RemoteException { // Notification that has bubble metadata NotificationRecord nrBubble = generateNotificationRecord(mTestNotificationChannel, 1, "BUBBLE_GROUP", false /* isSummary */, true /* isBubble */); // Make the package foreground so that we're allowed to be a bubble when(mActivityManager.getPackageImportance(nrBubble.sbn.getPackageName())).thenReturn( IMPORTANCE_FOREGROUND); mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", nrBubble.sbn.getId(), nrBubble.sbn.getNotification(), nrBubble.sbn.getUserId()); waitForIdle(); // Make sure we are a bubble StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(1, notifsAfter.length); assertTrue((notifsAfter[0].getNotification().flags & FLAG_BUBBLE) != 0); // Plain notification without bubble metadata NotificationRecord nrPlain = generateNotificationRecord(mTestNotificationChannel, 2, "BUBBLE_GROUP", false /* isSummary */, false /* isBubble */); mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", nrPlain.sbn.getId(), nrPlain.sbn.getNotification(), nrPlain.sbn.getUserId()); waitForIdle(); notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(2, notifsAfter.length); // Summary notification for both of those NotificationRecord nrSummary = generateNotificationRecord(mTestNotificationChannel, 3, "BUBBLE_GROUP", true /* isSummary */, false /* isBubble */); if (summaryAutoCancel) { nrSummary.getNotification().flags |= FLAG_AUTO_CANCEL; } mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", nrSummary.sbn.getId(), nrSummary.sbn.getNotification(), nrSummary.sbn.getUserId()); waitForIdle(); notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(3, notifsAfter.length); return nrSummary; } @Test @Test public void testCreateNotificationChannels_SingleChannel() throws Exception { public void testCreateNotificationChannels_SingleChannel() throws Exception { final NotificationChannel channel = final NotificationChannel channel = Loading Loading @@ -5192,4 +5247,48 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertTrue(notif.getBubbleMetadata().getAutoExpandBubble()); assertTrue(notif.getBubbleMetadata().getAutoExpandBubble()); assertTrue(notif.getBubbleMetadata().isNotificationSuppressed()); assertTrue(notif.getBubbleMetadata().isNotificationSuppressed()); } } @Test public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryDismissed() throws Exception { // Bubbles are allowed! setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */); NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded( true /* summaryAutoCancel */); // Dismiss summary final NotificationVisibility nv = NotificationVisibility.obtain(nrSummary.getKey(), 1, 2, true); mService.mNotificationDelegate.onNotificationClear(mUid, 0, PKG, nrSummary.sbn.getTag(), nrSummary.sbn.getId(), nrSummary.getUserId(), nrSummary.getKey(), NotificationStats.DISMISSAL_SHADE, NotificationStats.DISMISS_SENTIMENT_NEUTRAL, nv); waitForIdle(); // The bubble should still exist StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(1, notifsAfter.length); } @Test public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryClicked() throws Exception { // Bubbles are allowed! setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */); NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded( true /* summaryAutoCancel */); // Click summary final NotificationVisibility nv = NotificationVisibility.obtain(nrSummary.getKey(), 1, 2, true); mService.mNotificationDelegate.onNotificationClick(mUid, Binder.getCallingPid(), nrSummary.getKey(), nv); waitForIdle(); // The bubble should still exist StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(1, notifsAfter.length); } } } Loading
packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +85 −10 Original line number Original line Diff line number Diff line Loading @@ -16,12 +16,15 @@ package com.android.systemui.bubbles; package com.android.systemui.bubbles; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_BUBBLE; import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS; import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CLICK; import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE; Loading Loading @@ -82,6 +85,7 @@ import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.ZenModeController; Loading @@ -90,6 +94,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; import java.util.List; import javax.inject.Inject; import javax.inject.Inject; Loading @@ -109,7 +114,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Retention(SOURCE) @Retention(SOURCE) @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED}) DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @interface DismissReason {} Loading @@ -121,6 +126,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi static final int DISMISS_ACCESSIBILITY_ACTION = 6; static final int DISMISS_ACCESSIBILITY_ACTION = 6; static final int DISMISS_NO_LONGER_BUBBLE = 7; static final int DISMISS_NO_LONGER_BUBBLE = 7; static final int DISMISS_USER_CHANGED = 8; static final int DISMISS_USER_CHANGED = 8; static final int DISMISS_GROUP_CANCELLED = 9; public static final int MAX_BUBBLES = 5; // TODO: actually enforce this public static final int MAX_BUBBLES = 5; // TODO: actually enforce this Loading @@ -133,6 +139,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi private BubbleStateChangeListener mStateChangeListener; private BubbleStateChangeListener mStateChangeListener; private BubbleExpandListener mExpandListener; private BubbleExpandListener mExpandListener; @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; private final NotificationGroupManager mNotificationGroupManager; private BubbleData mBubbleData; private BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; @Nullable private BubbleStackView mStackView; Loading Loading @@ -211,10 +218,11 @@ public class BubbleController implements ConfigurationController.ConfigurationLi BubbleData data, ConfigurationController configurationController, BubbleData data, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager) { NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager) { this(context, statusBarWindowController, data, null /* synchronizer */, this(context, statusBarWindowController, data, null /* synchronizer */, configurationController, interruptionStateProvider, zenModeController, configurationController, interruptionStateProvider, zenModeController, notifUserManager); notifUserManager, groupManager); } } public BubbleController(Context context, StatusBarWindowController statusBarWindowController, public BubbleController(Context context, StatusBarWindowController statusBarWindowController, Loading @@ -222,7 +230,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi ConfigurationController configurationController, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager) { NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager) { mContext = context; mContext = context; mNotificationInterruptionStateProvider = interruptionStateProvider; mNotificationInterruptionStateProvider = interruptionStateProvider; mNotifUserManager = notifUserManager; mNotifUserManager = notifUserManager; Loading Loading @@ -251,6 +260,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); mNotificationEntryManager.addNotificationEntryListener(mEntryListener); mNotificationEntryManager.addNotificationEntryListener(mEntryListener); mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor); mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor); mNotificationGroupManager = groupManager; mStatusBarWindowController = statusBarWindowController; mStatusBarWindowController = statusBarWindowController; mStatusBarStateListener = new StatusBarStateListener(); mStatusBarStateListener = new StatusBarStateListener(); Loading Loading @@ -500,24 +510,38 @@ public class BubbleController implements ConfigurationController.ConfigurationLi new NotificationRemoveInterceptor() { new NotificationRemoveInterceptor() { @Override @Override public boolean onNotificationRemoveRequested(String key, int reason) { public boolean onNotificationRemoveRequested(String key, int reason) { if (!mBubbleData.hasBubbleWithKey(key)) { NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key); String groupKey = entry != null ? entry.notification.getGroupKey() : null; ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey); boolean inBubbleData = mBubbleData.hasBubbleWithKey(key); boolean isSummary = entry != null && entry.notification.getNotification().isGroupSummary(); boolean isSummaryOfBubbles = isSummary && bubbleChildren != null && !bubbleChildren.isEmpty(); if (!inBubbleData && !isSummaryOfBubbles) { return false; return false; } } Bubble bubble = mBubbleData.getBubbleWithKey(key); NotificationEntry entry = bubble.getEntry(); final boolean isClearAll = reason == REASON_CANCEL_ALL; final boolean isClearAll = reason == REASON_CANCEL_ALL; final boolean isUserDimiss = reason == REASON_CANCEL; final boolean isUserDimiss = reason == REASON_CANCEL || reason == REASON_CLICK; final boolean isAppCancel = reason == REASON_APP_CANCEL final boolean isAppCancel = reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL; || reason == REASON_APP_CANCEL_ALL; final boolean isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED; // Need to check for !appCancel here because the notification may have // Need to check for !appCancel here because the notification may have // previously been dismissed & entry.isRowDismissed would still be true // previously been dismissed & entry.isRowDismissed would still be true boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel) boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel) || isClearAll || isUserDimiss; || isClearAll || isUserDimiss || isSummaryCancel; if (isSummaryOfBubbles) { return handleSummaryRemovalInterception(entry, userRemovedNotif); } // The bubble notification sticks around in the data as long as the bubble is // The bubble notification sticks around in the data as long as the bubble is // not dismissed and the app hasn't cancelled the notification. // not dismissed and the app hasn't cancelled the notification. Bubble bubble = mBubbleData.getBubbleWithKey(key); boolean bubbleExtended = entry.isBubble() && userRemovedNotif; boolean bubbleExtended = entry.isBubble() && userRemovedNotif; if (bubbleExtended) { if (bubbleExtended) { bubble.setShowInShadeWhenBubble(false); bubble.setShowInShadeWhenBubble(false); Loading @@ -536,6 +560,43 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } }; }; private boolean handleSummaryRemovalInterception(NotificationEntry summary, boolean userRemovedNotif) { String groupKey = summary.notification.getGroupKey(); ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey); if (userRemovedNotif) { // If it's a user dismiss we mark the children to be hidden from the shade. for (int i = 0; i < bubbleChildren.size(); i++) { Bubble bubbleChild = bubbleChildren.get(i); // As far as group manager is concerned, once a child is no longer shown // in the shade, it is essentially removed. mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry()); bubbleChild.setShowInShadeWhenBubble(false); bubbleChild.setShowBubbleDot(false); if (mStackView != null) { mStackView.updateDotVisibility(bubbleChild.getKey()); } } // And since all children are removed, remove the summary. mNotificationGroupManager.onEntryRemoved(summary); // If the summary was auto-generated we don't need to keep that notification around // because apps can't cancel it; so we only intercept & suppress real summaries. boolean isAutogroupSummary = (summary.notification.getNotification().flags & FLAG_AUTOGROUP_SUMMARY) != 0; return !isAutogroupSummary; } else { // Remove any associated bubble children. for (int i = 0; i < bubbleChildren.size(); i++) { Bubble bubbleChild = bubbleChildren.get(i); mBubbleData.notificationEntryRemoved(bubbleChild.getEntry(), DISMISS_GROUP_CANCELLED); } return false; } } @SuppressWarnings("FieldCanBeLocal") @SuppressWarnings("FieldCanBeLocal") private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { @Override @Override Loading Loading @@ -597,7 +658,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } // Do removals, if any. // Do removals, if any. for (Pair<Bubble, Integer> removed : update.removedBubbles) { ArrayList<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(update.removedBubbles); for (Pair<Bubble, Integer> removed : removedBubbles) { final Bubble bubble = removed.first; final Bubble bubble = removed.first; @DismissReason final int reason = removed.second; @DismissReason final int reason = removed.second; mStackView.removeBubble(bubble); mStackView.removeBubble(bubble); Loading @@ -622,6 +685,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // Bad things have happened // Bad things have happened } } } } // Check if summary should be removed from NoManGroup NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary( bubble.getEntry().notification); if (summary != null) { ArrayList<NotificationEntry> summaryChildren = mNotificationGroupManager.getLogicalChildren(summary.notification); if (summaryChildren == null || summaryChildren.isEmpty()) { mNotificationEntryManager.performRemoveNotification( summary.notification, UNDEFINED_DISMISS_REASON); } } } } } } Loading
packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +17 −0 Original line number Original line Diff line number Diff line Loading @@ -229,6 +229,23 @@ public class BubbleData { dispatchPendingChanges(); dispatchPendingChanges(); } } /** * Retrieves any bubbles that are part of the notification group represented by the provided * group key. */ ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) { ArrayList<Bubble> bubbleChildren = new ArrayList<>(); if (groupKey == null) { return bubbleChildren; } for (Bubble b : mBubbles) { if (groupKey.equals(b.getEntry().notification.getGroupKey())) { bubbleChildren.add(b); } } return bubbleChildren; } private void doAdd(Bubble bubble) { private void doAdd(Bubble bubble) { if (DEBUG_BUBBLE_DATA) { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "doAdd: " + bubble); Log.d(TAG, "doAdd: " + bubble); Loading
packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +15 −5 Original line number Original line Diff line number Diff line Loading @@ -70,6 +70,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationData; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.HeadsUpManager; Loading @@ -91,6 +92,8 @@ public class BubbleControllerTest extends SysuiTestCase { @Mock @Mock private NotificationEntryManager mNotificationEntryManager; private NotificationEntryManager mNotificationEntryManager; @Mock @Mock private NotificationGroupManager mNotificationGroupManager; @Mock private WindowManager mWindowManager; private WindowManager mWindowManager; @Mock @Mock private IActivityManager mActivityManager; private IActivityManager mActivityManager; Loading Loading @@ -154,6 +157,7 @@ public class BubbleControllerTest extends SysuiTestCase { // Return non-null notification data from the NEM // Return non-null notification data from the NEM when(mNotificationEntryManager.getNotificationData()).thenReturn(mNotificationData); when(mNotificationEntryManager.getNotificationData()).thenReturn(mNotificationData); when(mNotificationData.get(mRow.getEntry().key)).thenReturn(mRow.getEntry()); when(mNotificationData.getChannel(mRow.getEntry().key)).thenReturn(mRow.getEntry().channel); when(mNotificationData.getChannel(mRow.getEntry().key)).thenReturn(mRow.getEntry().channel); mZenModeConfig.suppressedVisualEffects = 0; mZenModeConfig.suppressedVisualEffects = 0; Loading @@ -168,9 +172,14 @@ public class BubbleControllerTest extends SysuiTestCase { mock(HeadsUpManager.class), mock(HeadsUpManager.class), mock(NotificationInterruptionStateProvider.HeadsUpSuppressor.class)); mock(NotificationInterruptionStateProvider.HeadsUpSuppressor.class)); mBubbleData = new BubbleData(mContext); mBubbleData = new BubbleData(mContext); mBubbleController = new TestableBubbleController(mContext, mStatusBarWindowController, mBubbleController = new TestableBubbleController(mContext, mBubbleData, mConfigurationController, interruptionStateProvider, mStatusBarWindowController, mZenModeController, mLockscreenUserManager); mBubbleData, mConfigurationController, interruptionStateProvider, mZenModeController, mLockscreenUserManager, mNotificationGroupManager); mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener); mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener); mBubbleController.setExpandListener(mBubbleExpandListener); mBubbleController.setExpandListener(mBubbleExpandListener); Loading Loading @@ -631,10 +640,11 @@ public class BubbleControllerTest extends SysuiTestCase { ConfigurationController configurationController, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, ZenModeController zenModeController, NotificationLockscreenUserManager lockscreenUserManager) { NotificationLockscreenUserManager lockscreenUserManager, NotificationGroupManager groupManager) { super(context, statusBarWindowController, data, Runnable::run, super(context, statusBarWindowController, data, Runnable::run, configurationController, interruptionStateProvider, zenModeController, configurationController, interruptionStateProvider, zenModeController, lockscreenUserManager); lockscreenUserManager, groupManager); } } } } Loading
services/core/java/com/android/server/notification/NotificationManagerService.java +15 −1 Original line number Original line Diff line number Diff line Loading @@ -5249,12 +5249,26 @@ public class NotificationManagerService extends SystemService { return; return; } } // Bubbled children get to stick around if the summary was manually cancelled // (user removed) from systemui. FlagChecker childrenFlagChecker = null; if (mReason == REASON_CANCEL || mReason == REASON_CLICK || mReason == REASON_CANCEL_ALL) { childrenFlagChecker = (flags) -> { if ((flags & FLAG_BUBBLE) != 0) { return false; } return true; }; } // Cancel the notification. // Cancel the notification. boolean wasPosted = removeFromNotificationListsLocked(r); boolean wasPosted = removeFromNotificationListsLocked(r); cancelNotificationLocked( cancelNotificationLocked( r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName); r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName); cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName, cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName, mSendDelete, null); mSendDelete, childrenFlagChecker); updateLightsLocked(); updateLightsLocked(); } else { } else { // No notification was found, assume that it is snoozed and cancel it. // No notification was found, assume that it is snoozed and cancel it. Loading
services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +100 −1 Original line number Original line Diff line number Diff line Loading @@ -20,6 +20,7 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREG import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.app.Notification.CATEGORY_CALL; import static android.app.Notification.CATEGORY_CALL; import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.NotificationManager.EXTRA_BLOCKED_STATE; import static android.app.NotificationManager.EXTRA_BLOCKED_STATE; Loading Loading @@ -439,14 +440,22 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { return sbn; return sbn; } } private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, String groupKey, boolean isSummary) { String groupKey, boolean isSummary) { return generateNotificationRecord(channel, id, groupKey, isSummary, false /* isBubble */); } private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, String groupKey, boolean isSummary, boolean isBubble) { Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) .setContentTitle("foo") .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setSmallIcon(android.R.drawable.sym_def_app_icon) .setGroup(groupKey) .setGroup(groupKey) .setGroupSummary(isSummary); .setGroupSummary(isSummary); if (isBubble) { nb.setBubbleMetadata(getBasicBubbleMetadataBuilder().build()); } StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0, StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0, nb.build(), new UserHandle(mUid), null, 0); nb.build(), new UserHandle(mUid), null, 0); return new NotificationRecord(mContext, sbn, channel); return new NotificationRecord(mContext, sbn, channel); Loading Loading @@ -542,6 +551,52 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)); .setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)); } } private NotificationRecord addGroupWithBubblesAndValidateAdded(boolean summaryAutoCancel) throws RemoteException { // Notification that has bubble metadata NotificationRecord nrBubble = generateNotificationRecord(mTestNotificationChannel, 1, "BUBBLE_GROUP", false /* isSummary */, true /* isBubble */); // Make the package foreground so that we're allowed to be a bubble when(mActivityManager.getPackageImportance(nrBubble.sbn.getPackageName())).thenReturn( IMPORTANCE_FOREGROUND); mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", nrBubble.sbn.getId(), nrBubble.sbn.getNotification(), nrBubble.sbn.getUserId()); waitForIdle(); // Make sure we are a bubble StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(1, notifsAfter.length); assertTrue((notifsAfter[0].getNotification().flags & FLAG_BUBBLE) != 0); // Plain notification without bubble metadata NotificationRecord nrPlain = generateNotificationRecord(mTestNotificationChannel, 2, "BUBBLE_GROUP", false /* isSummary */, false /* isBubble */); mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", nrPlain.sbn.getId(), nrPlain.sbn.getNotification(), nrPlain.sbn.getUserId()); waitForIdle(); notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(2, notifsAfter.length); // Summary notification for both of those NotificationRecord nrSummary = generateNotificationRecord(mTestNotificationChannel, 3, "BUBBLE_GROUP", true /* isSummary */, false /* isBubble */); if (summaryAutoCancel) { nrSummary.getNotification().flags |= FLAG_AUTO_CANCEL; } mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", nrSummary.sbn.getId(), nrSummary.sbn.getNotification(), nrSummary.sbn.getUserId()); waitForIdle(); notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(3, notifsAfter.length); return nrSummary; } @Test @Test public void testCreateNotificationChannels_SingleChannel() throws Exception { public void testCreateNotificationChannels_SingleChannel() throws Exception { final NotificationChannel channel = final NotificationChannel channel = Loading Loading @@ -5192,4 +5247,48 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertTrue(notif.getBubbleMetadata().getAutoExpandBubble()); assertTrue(notif.getBubbleMetadata().getAutoExpandBubble()); assertTrue(notif.getBubbleMetadata().isNotificationSuppressed()); assertTrue(notif.getBubbleMetadata().isNotificationSuppressed()); } } @Test public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryDismissed() throws Exception { // Bubbles are allowed! setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */); NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded( true /* summaryAutoCancel */); // Dismiss summary final NotificationVisibility nv = NotificationVisibility.obtain(nrSummary.getKey(), 1, 2, true); mService.mNotificationDelegate.onNotificationClear(mUid, 0, PKG, nrSummary.sbn.getTag(), nrSummary.sbn.getId(), nrSummary.getUserId(), nrSummary.getKey(), NotificationStats.DISMISSAL_SHADE, NotificationStats.DISMISS_SENTIMENT_NEUTRAL, nv); waitForIdle(); // The bubble should still exist StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(1, notifsAfter.length); } @Test public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryClicked() throws Exception { // Bubbles are allowed! setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */); NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded( true /* summaryAutoCancel */); // Click summary final NotificationVisibility nv = NotificationVisibility.obtain(nrSummary.getKey(), 1, 2, true); mService.mNotificationDelegate.onNotificationClick(mUid, Binder.getCallingPid(), nrSummary.getKey(), nv); waitForIdle(); // The bubble should still exist StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); assertEquals(1, notifsAfter.length); } } }