Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 084dbe01 authored by Mady Mellor's avatar Mady Mellor Committed by Android (Google) Code Review
Browse files

Merge "When summary is dismissed, don't remove bubble children" into qt-r1-bubbles-dev

parents 00d0154a 22f2f076
Loading
Loading
Loading
Loading
+85 −10
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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 {}


@@ -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


@@ -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;
@@ -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,
@@ -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;
@@ -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();
@@ -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);
@@ -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
@@ -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);
@@ -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);
                        }
                    }
                }
                }
            }
            }


+17 −0
Original line number Original line Diff line number Diff line
@@ -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);
+15 −5
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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);


@@ -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);
        }
        }
    }
    }


+15 −1
Original line number Original line Diff line number Diff line
@@ -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.
+100 −1
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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);
@@ -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 =
@@ -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);
    }
}
}