Refactor parts of BubbleController

So it'll be easier to switch out NEM for the new notification pipeline

Test: atest SystemUITests
Change-Id: I97b1949695fde1e67f6a8782ab21bb726a5bdde1
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 894ecf6..ac06f95 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -190,6 +190,9 @@
 
     private boolean mInflateSynchronously;
 
+    // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline
+    private final List<NotifCallback> mCallbacks = new ArrayList<>();
+
     /**
      * Listener to be notified when some states of the bubbles change.
      */
@@ -231,7 +234,35 @@
          * Called when the notification suppression state of a bubble changes.
          */
         void onBubbleNotificationSuppressionChange(Bubble bubble);
+    }
 
+    /**
+     * Callback for when the BubbleController wants to interact with the notification pipeline to:
+     * - Remove a previously bubbled notification
+     * - Update the notification shade since bubbled notification should/shouldn't be showing
+     */
+    public interface NotifCallback {
+        /**
+         * Called when the BubbleController wants to remove an entry that it was previously hiding
+         * from the shade. See {@link BubbleController#isBubbleNotificationSuppressedFromShade}.
+         */
+        void removeNotification(NotificationEntry entry);
+
+        /**
+         * Called when a bubbled notification has changed whether it should be
+         * filtered from the shade.
+         */
+        void invalidateNotificationFilter(String reason);
+
+        /**
+         * Called on a bubbled entry that has been removed when there are no longer
+         * bubbled entries in its group.
+         *
+         * Checks whether its group has any other (non-bubbled) children. If it doesn't,
+         * removes all remnants of the group's summary from the notification pipeline.
+         * TODO: (b/145659174) Only old pipeline needs this - delete post-migration.
+         */
+        void maybeCancelSummary(NotificationEntry entry);
     }
 
     /**
@@ -332,26 +363,8 @@
         });
 
         mNotificationEntryManager = entryManager;
-        mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
-        mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
         mNotificationGroupManager = groupManager;
-        mNotificationGroupManager.addOnGroupChangeListener(
-                new NotificationGroupManager.OnGroupChangeListener() {
-                    @Override
-                    public void onGroupSuppressionChanged(
-                            NotificationGroupManager.NotificationGroup group,
-                            boolean suppressed) {
-                        // More notifications could be added causing summary to no longer
-                        // be suppressed -- in this case need to remove the key.
-                        final String groupKey = group.summary != null
-                                ? group.summary.getSbn().getGroupKey()
-                                : null;
-                        if (!suppressed && groupKey != null
-                                && mBubbleData.isSummarySuppressed(groupKey)) {
-                            mBubbleData.removeSuppressedSummary(groupKey);
-                        }
-                    }
-                });
+        setupNEM();
 
         mNotificationShadeWindowController = notificationShadeWindowController;
         mStatusBarStateListener = new StatusBarStateListener();
@@ -391,6 +404,104 @@
     }
 
     /**
+     * See {@link NotifCallback}.
+     */
+    public void addNotifCallback(NotifCallback callback) {
+        mCallbacks.add(callback);
+    }
+
+    private void setupNEM() {
+        mNotificationEntryManager.addNotificationEntryListener(
+                new NotificationEntryListener() {
+                    @Override
+                    public void onNotificationAdded(NotificationEntry entry) {
+                        onEntryAdded(entry);
+                    }
+
+                    @Override
+                    public void onPreEntryUpdated(NotificationEntry entry) {
+                        onEntryUpdated(entry);
+                    }
+
+                    @Override
+                    public void onNotificationRankingUpdated(RankingMap rankingMap) {
+                        onRankingUpdated(rankingMap);
+                    }
+                });
+
+        mNotificationEntryManager.setNotificationRemoveInterceptor(
+                new NotificationRemoveInterceptor() {
+                    @Override
+                    public boolean onNotificationRemoveRequested(String key, int reason) {
+                        NotificationEntry entry =
+                                mNotificationEntryManager.getActiveNotificationUnfiltered(key);
+                        return shouldInterceptDismissal(entry, reason);
+                    }
+                });
+
+        mNotificationGroupManager.addOnGroupChangeListener(
+                new NotificationGroupManager.OnGroupChangeListener() {
+                    @Override
+                    public void onGroupSuppressionChanged(
+                            NotificationGroupManager.NotificationGroup group,
+                            boolean suppressed) {
+                        // More notifications could be added causing summary to no longer
+                        // be suppressed -- in this case need to remove the key.
+                        final String groupKey = group.summary != null
+                                ? group.summary.getSbn().getGroupKey()
+                                : null;
+                        if (!suppressed && groupKey != null
+                                && mBubbleData.isSummarySuppressed(groupKey)) {
+                            mBubbleData.removeSuppressedSummary(groupKey);
+                        }
+                    }
+                });
+
+        addNotifCallback(new NotifCallback() {
+            @Override
+            public void removeNotification(NotificationEntry entry) {
+                mNotificationEntryManager.performRemoveNotification(entry.getSbn(),
+                        UNDEFINED_DISMISS_REASON);
+            }
+
+            @Override
+            public void invalidateNotificationFilter(String reason) {
+                mNotificationEntryManager.updateNotifications(reason);
+            }
+
+            @Override
+            public void maybeCancelSummary(NotificationEntry entry) {
+                // Check if removed bubble has an associated suppressed group summary that needs
+                // to be removed now.
+                final String groupKey = entry.getSbn().getGroup();
+                if (mBubbleData.isSummarySuppressed(groupKey)) {
+                    mBubbleData.removeSuppressedSummary(entry.getSbn().getGroupKey());
+
+                    final NotificationEntry summary =
+                            mNotificationEntryManager.getActiveNotificationUnfiltered(
+                                    mBubbleData.getSummaryKey(groupKey));
+                    mNotificationEntryManager.performRemoveNotification(summary.getSbn(),
+                            UNDEFINED_DISMISS_REASON);
+                }
+
+                // Check if summary should be removed from NoManGroup
+                NotificationEntry summary =
+                        mNotificationGroupManager.getLogicalGroupSummary(entry.getSbn());
+                if (summary != null) {
+                    ArrayList<NotificationEntry> summaryChildren =
+                            mNotificationGroupManager.getLogicalChildren(summary.getSbn());
+                    boolean isSummaryThisNotif = summary.getKey().equals(entry.getKey());
+                    if (!isSummaryThisNotif && (summaryChildren == null
+                            || summaryChildren.isEmpty())) {
+                        mNotificationEntryManager.performRemoveNotification(summary.getSbn(),
+                                UNDEFINED_DISMISS_REASON);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
      * Sets whether to perform inflation on the same thread as the caller. This method should only
      * be used in tests, not in production.
      */
@@ -562,13 +673,15 @@
      *
      * False otherwise.
      */
-    public boolean isBubbleNotificationSuppressedFromShade(String key) {
+    public boolean isBubbleNotificationSuppressedFromShade(NotificationEntry entry) {
+        String key = entry.getKey();
         boolean isBubbleAndSuppressed = mBubbleData.hasBubbleWithKey(key)
                 && !mBubbleData.getBubbleWithKey(key).showInShade();
-        NotificationEntry entry = mNotificationEntryManager.getActiveNotificationUnfiltered(key);
-        String groupKey = entry != null ? entry.getSbn().getGroupKey() : null;
+
+        String groupKey = entry.getSbn().getGroupKey();
         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
         boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey));
+
         return (isSummary && isSuppressedSummary) || isBubbleAndSuppressed;
     }
 
@@ -702,160 +815,46 @@
         }
     }
 
-    @SuppressWarnings("FieldCanBeLocal")
-    private final NotificationRemoveInterceptor mRemoveInterceptor =
-            new NotificationRemoveInterceptor() {
-            @Override
-            public boolean onNotificationRemoveRequested(String key, int reason) {
-                NotificationEntry entry =
-                        mNotificationEntryManager.getActiveNotificationUnfiltered(key);
-                String groupKey = entry != null ? entry.getSbn().getGroupKey() : null;
-                ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey);
+    private void onEntryAdded(NotificationEntry entry) {
+        boolean previouslyUserCreated = mUserCreatedBubbles.contains(entry.getKey());
+        boolean userBlocked = mUserBlockedBubbles.contains(entry.getKey());
+        boolean wasAdjusted = BubbleExperimentConfig.adjustForExperiments(
+                mContext, entry, previouslyUserCreated, userBlocked);
 
-                boolean inBubbleData = mBubbleData.hasBubbleWithKey(key);
-                boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey)
-                        && mBubbleData.getSummaryKey(groupKey).equals(key));
-                boolean isSummary = entry != null
-                        && entry.getSbn().getNotification().isGroupSummary();
-                boolean isSummaryOfBubbles = (isSuppressedSummary || isSummary)
-                        && bubbleChildren != null && !bubbleChildren.isEmpty();
-
-                if (!inBubbleData && !isSummaryOfBubbles) {
-                    return false;
-                }
-
-                final boolean isClearAll = reason == REASON_CANCEL_ALL;
-                final boolean isUserDimiss = reason == REASON_CANCEL || reason == REASON_CLICK;
-                final boolean isAppCancel = reason == REASON_APP_CANCEL
-                        || reason == REASON_APP_CANCEL_ALL;
-                final boolean isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED;
-
-                // Need to check for !appCancel here because the notification may have
-                // previously been dismissed & entry.isRowDismissed would still be true
-                boolean userRemovedNotif = (entry != null && entry.isRowDismissed() && !isAppCancel)
-                        || isClearAll || isUserDimiss || isSummaryCancel;
-
-                if (isSummaryOfBubbles) {
-                    return handleSummaryRemovalInterception(entry, userRemovedNotif);
-                }
-
-                // The bubble notification sticks around in the data as long as the bubble is
-                // not dismissed and the app hasn't cancelled the notification.
-                Bubble bubble = mBubbleData.getBubbleWithKey(key);
-                boolean bubbleExtended = entry != null && entry.isBubble() && userRemovedNotif;
-                if (bubbleExtended) {
-                    bubble.setSuppressNotification(true);
-                    bubble.setShowDot(false /* show */, true /* animate */);
-                    mNotificationEntryManager.updateNotifications(
-                            "BubbleController.onNotificationRemoveRequested");
-                    return true;
-                } else if (!userRemovedNotif && entry != null
-                        && !isUserCreatedBubble(bubble.getKey())) {
-                    // This wasn't a user removal so we should remove the bubble as well
-                    mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
-                    return false;
-                }
-                return false;
+        if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
+                && (canLaunchInActivityView(mContext, entry) || wasAdjusted)) {
+            if (wasAdjusted && !previouslyUserCreated) {
+                // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated
+                mUserCreatedBubbles.add(entry.getKey());
             }
-        };
-
-    private boolean handleSummaryRemovalInterception(NotificationEntry summary,
-            boolean userRemovedNotif) {
-        String groupKey = summary.getSbn().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.setSuppressNotification(true);
-                bubbleChild.setShowDot(false /* show */, true /* animate */);
-            }
-            // 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.getSbn().getNotification().flags
-                    & FLAG_AUTOGROUP_SUMMARY) != 0;
-            if (!isAutogroupSummary) {
-                mBubbleData.addSummaryToSuppress(summary.getSbn().getGroupKey(),
-                        summary.getKey());
-                // Tell shade to update for the suppression
-                mNotificationEntryManager.updateNotifications(
-                        "BubbleController.handleSummaryRemovalInterception");
-            }
-            return !isAutogroupSummary;
-        } else {
-            // If it's not a user dismiss it's a cancel.
-            for (int i = 0; i < bubbleChildren.size(); i++) {
-                // First check if any of these are user-created (i.e. experimental bubbles)
-                if (mUserCreatedBubbles.contains(bubbleChildren.get(i).getKey())) {
-                    // Experimental bubble! Intercept the removal.
-                    return true;
-                }
-            }
-            // Not an experimental bubble, safe to remove.
-            mBubbleData.removeSuppressedSummary(groupKey);
-            // Remove any associated bubble children with the summary.
-            for (int i = 0; i < bubbleChildren.size(); i++) {
-                Bubble bubbleChild = bubbleChildren.get(i);
-                mBubbleData.notificationEntryRemoved(bubbleChild.getEntry(),
-                        DISMISS_GROUP_CANCELLED);
-            }
-            return false;
+            updateBubble(entry);
         }
     }
 
-    @SuppressWarnings("FieldCanBeLocal")
-    private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
-        @Override
-        public void onNotificationAdded(NotificationEntry entry) {
-            boolean previouslyUserCreated = mUserCreatedBubbles.contains(entry.getKey());
-            boolean userBlocked = mUserBlockedBubbles.contains(entry.getKey());
-            boolean wasAdjusted = BubbleExperimentConfig.adjustForExperiments(
-                    mContext, entry, previouslyUserCreated, userBlocked);
+    private void onEntryUpdated(NotificationEntry entry) {
+        boolean previouslyUserCreated = mUserCreatedBubbles.contains(entry.getKey());
+        boolean userBlocked = mUserBlockedBubbles.contains(entry.getKey());
+        boolean wasAdjusted = BubbleExperimentConfig.adjustForExperiments(
+                mContext, entry, previouslyUserCreated, userBlocked);
 
-            if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
-                    && (canLaunchInActivityView(mContext, entry) || wasAdjusted)) {
-                if (wasAdjusted && !previouslyUserCreated) {
-                    // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated
-                    mUserCreatedBubbles.add(entry.getKey());
-                }
-                updateBubble(entry);
+        boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
+                && (canLaunchInActivityView(mContext, entry) || wasAdjusted);
+        if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.getKey())) {
+            // It was previously a bubble but no longer a bubble -- lets remove it
+            removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
+        } else if (shouldBubble) {
+            if (wasAdjusted && !previouslyUserCreated) {
+                // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated
+                mUserCreatedBubbles.add(entry.getKey());
             }
+            updateBubble(entry);
         }
+    }
 
-        @Override
-        public void onPreEntryUpdated(NotificationEntry entry) {
-            boolean previouslyUserCreated = mUserCreatedBubbles.contains(entry.getKey());
-            boolean userBlocked = mUserBlockedBubbles.contains(entry.getKey());
-            boolean wasAdjusted = BubbleExperimentConfig.adjustForExperiments(
-                    mContext, entry, previouslyUserCreated, userBlocked);
-
-            boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
-                    && (canLaunchInActivityView(mContext, entry) || wasAdjusted);
-            if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.getKey())) {
-                // It was previously a bubble but no longer a bubble -- lets remove it
-                removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
-            } else if (shouldBubble) {
-                if (wasAdjusted && !previouslyUserCreated) {
-                    // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated
-                    mUserCreatedBubbles.add(entry.getKey());
-                }
-                updateBubble(entry);
-            }
-        }
-
-        @Override
-        public void onNotificationRankingUpdated(RankingMap rankingMap) {
-            // Forward to BubbleData to block any bubbles which should no longer be shown
-            mBubbleData.notificationRankingUpdated(rankingMap);
-        }
-    };
+    private void onRankingUpdated(RankingMap rankingMap) {
+        // Forward to BubbleData to block any bubbles which should no longer be shown
+        mBubbleData.notificationRankingUpdated(rankingMap);
+    }
 
     @SuppressWarnings("FieldCanBeLocal")
     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
@@ -888,9 +887,11 @@
                 if (reason != DISMISS_USER_CHANGED) {
                     if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
                             && !bubble.showInShade()) {
-                        // The bubble is gone & the notification is gone, time to actually remove it
-                        mNotificationEntryManager.performRemoveNotification(
-                                bubble.getEntry().getSbn(), UNDEFINED_DISMISS_REASON);
+                        // The bubble is now gone & the notification is hidden from the shade, so
+                        // time to actually remove it
+                        for (NotifCallback cb : mCallbacks) {
+                            cb.removeNotification(bubble.getEntry());
+                        }
                     } else {
                         // Update the flag for SysUI
                         bubble.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
@@ -905,32 +906,11 @@
                         }
                     }
 
-                    // Check if removed bubble has an associated suppressed group summary that needs
-                    // to be removed now.
                     final String groupKey = bubble.getEntry().getSbn().getGroupKey();
-                    if (mBubbleData.isSummarySuppressed(groupKey)
-                            && mBubbleData.getBubblesInGroup(groupKey).isEmpty()) {
-                        // Time to actually remove the summary.
-                        String notifKey = mBubbleData.getSummaryKey(groupKey);
-                        mBubbleData.removeSuppressedSummary(groupKey);
-                        NotificationEntry entry =
-                                mNotificationEntryManager.getActiveNotificationUnfiltered(notifKey);
-                        mNotificationEntryManager.performRemoveNotification(
-                                entry.getSbn(), UNDEFINED_DISMISS_REASON);
-                    }
-
-                    // Check if summary should be removed from NoManGroup
-                    NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary(
-                            bubble.getEntry().getSbn());
-                    if (summary != null) {
-                        ArrayList<NotificationEntry> summaryChildren =
-                                mNotificationGroupManager.getLogicalChildren(summary.getSbn());
-                        boolean isSummaryThisNotif = summary.getKey().equals(
-                                bubble.getEntry().getKey());
-                        if (!isSummaryThisNotif
-                                && (summaryChildren == null || summaryChildren.isEmpty())) {
-                            mNotificationEntryManager.performRemoveNotification(
-                                    summary.getSbn(), UNDEFINED_DISMISS_REASON);
+                    if (mBubbleData.getBubblesInGroup(groupKey).isEmpty()) {
+                        // Time to potentially remove the summary
+                        for (NotifCallback cb : mCallbacks) {
+                            cb.maybeCancelSummary(bubble.getEntry());
                         }
                     }
                 }
@@ -959,8 +939,9 @@
                 mStackView.setExpanded(true);
             }
 
-            mNotificationEntryManager.updateNotifications(
-                    "BubbleData.Listener.applyUpdate");
+            for (NotifCallback cb : mCallbacks) {
+                cb.invalidateNotificationFilter("BubbleData.Listener.applyUpdate");
+            }
             updateStack();
 
             if (DEBUG_BUBBLE_CONTROLLER) {
@@ -981,6 +962,127 @@
     };
 
     /**
+     * We intercept notification entries cancelled by the user (i.e. dismissed) when there is an
+     * active bubble associated with it. We do this so that developers can still cancel it
+     * (and hence the bubbles associated with it). However, these intercepted notifications
+     * should then be hidden from the shade since the user has cancelled them, so we update
+     * {@link Bubble#showInShade}.
+     *
+     * The cancellation of summaries with children associated with bubbles are also handled in this
+     * method. User-cancelled summaries are tracked by {@link BubbleData#addSummaryToSuppress}.
+     *
+     * @return true if we want to intercept the dismissal of the entry, else false
+     */
+    public boolean shouldInterceptDismissal(NotificationEntry entry, int dismissReason) {
+        if (entry == null) {
+            return false;
+        }
+        String key = entry.getKey();
+        String groupKey = entry != null ? entry.getSbn().getGroupKey() : null;
+        ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey);
+
+        boolean inBubbleData = mBubbleData.hasBubbleWithKey(key);
+        boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey)
+                && mBubbleData.getSummaryKey(groupKey).equals(key));
+        boolean isSummary = entry != null
+                && entry.getSbn().getNotification().isGroupSummary();
+        boolean isSummaryOfBubbles = (isSuppressedSummary || isSummary)
+                && bubbleChildren != null && !bubbleChildren.isEmpty();
+
+        if (!inBubbleData && !isSummaryOfBubbles) {
+            return false;
+        }
+
+        final boolean isClearAll = dismissReason == REASON_CANCEL_ALL;
+        final boolean isUserDimiss = dismissReason == REASON_CANCEL
+                || dismissReason == REASON_CLICK;
+        final boolean isAppCancel = dismissReason == REASON_APP_CANCEL
+                || dismissReason == REASON_APP_CANCEL_ALL;
+        final boolean isSummaryCancel = dismissReason == REASON_GROUP_SUMMARY_CANCELED;
+
+        // Need to check for !appCancel here because the notification may have
+        // previously been dismissed & entry.isRowDismissed would still be true
+        boolean userRemovedNotif = (entry != null && entry.isRowDismissed() && !isAppCancel)
+                || isClearAll || isUserDimiss || isSummaryCancel;
+        if (isSummaryOfBubbles) {
+            return handleSummaryRemovalInterception(entry, userRemovedNotif);
+        }
+
+        // The bubble notification sticks around in the data as long as the bubble is
+        // not dismissed and the app hasn't cancelled the notification.
+        Bubble bubble = mBubbleData.getBubbleWithKey(key);
+        boolean bubbleExtended = entry != null && entry.isBubble() && userRemovedNotif;
+        if (bubbleExtended) {
+            bubble.setSuppressNotification(true);
+            bubble.setShowDot(false /* show */, true /* animate */);
+            for (NotifCallback cb : mCallbacks) {
+                cb.invalidateNotificationFilter("BubbleController"
+                        + ".shouldInterceptDismissal");
+            }
+            return true;
+        } else if (!userRemovedNotif && entry != null
+                && !isUserCreatedBubble(bubble.getKey())) {
+            // This wasn't a user removal so we should remove the bubble as well
+            mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
+            return false;
+        }
+        return false;
+    }
+
+    private boolean handleSummaryRemovalInterception(NotificationEntry summary,
+            boolean userRemovedNotif) {
+        String groupKey = summary.getSbn().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.setSuppressNotification(true);
+                bubbleChild.setShowDot(false /* show */, true /* animate */);
+            }
+            // 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.getSbn().getNotification().flags
+                    & FLAG_AUTOGROUP_SUMMARY) != 0;
+            if (!isAutogroupSummary) {
+                // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated
+                mBubbleData.addSummaryToSuppress(summary.getSbn().getGroupKey(),
+                        summary.getKey());
+                // Tell shade to update for the suppression
+                mNotificationEntryManager.updateNotifications("BubbleController"
+                        + ".handleSummaryRemovalInterception");
+            }
+            return !isAutogroupSummary;
+        } else {
+            // If it's not a user dismiss it's a cancel.
+            for (int i = 0; i < bubbleChildren.size(); i++) {
+                // First check if any of these are user-created (i.e. experimental bubbles)
+                if (mUserCreatedBubbles.contains(bubbleChildren.get(i).getKey())) {
+                    // Experimental bubble! Intercept the removal.
+                    return true;
+                }
+            }
+
+            // Not an experimental bubble, safe to remove.
+            mBubbleData.removeSuppressedSummary(groupKey);
+            // Remove any associated bubble children with the summary.
+            for (int i = 0; i < bubbleChildren.size(); i++) {
+                Bubble bubbleChild = bubbleChildren.get(i);
+                mBubbleData.notificationEntryRemoved(bubbleChild.getEntry(),
+                        DISMISS_GROUP_CANCELLED);
+            }
+            return false;
+        }
+    }
+
+    /**
      * Lets any listeners know if bubble state has changed.
      * Updates the visibility of the bubbles based on current state.
      * Does not un-bubble, just hides or un-hides. Notifies any