Ensure the notification is removed when bubble is removed & fix cancel all

Bubbles has some particular notification behaviour needs:

1) Whenever there is a bubble, the notif is kept around but may be hidden
   from the shade
2) When the bubble is dismissed, if the notif is no longer in the
   shade => really remove the notification
3) When the bubble is dismissed, if there is still a notification in the
   shade => notif stays around and is normal
4) Clear all should only hide the bubble notification, not remove the
   bubble
5) Apps canceling a notification that has a bubble will cancel the bubble

This CL does this by:

* Including the removal reason removeNotification path
* Adding a NotificationRemoveInterceptor that gets the option of overriding
  the removal
* BubbleController has this new interceptor and uses the removal reason
  to determine what should happen to the bubble & if the notification
  should be allowed to be removed
* When the bubble is dismissed, if the notif is no longer in the shade,
  then actually remove that notification

Test: atest BubbleControllerTest NotificationEntryManagerTest
Bug: 130347307
Bug: 130687293
Change-Id: I4459864a2ee5522076117f84ae37022bdfe4ee5d
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index d071363..9ecc6f4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -16,6 +16,10 @@
 
 package com.android.systemui.bubbles;
 
+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_CANCEL;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.View.INVISIBLE;
@@ -53,13 +57,13 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.statusbar.NotificationVisibility;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.WindowManagerWrapper;
+import com.android.systemui.statusbar.NotificationRemoveInterceptor;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
@@ -210,6 +214,7 @@
 
         mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
         mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
+        mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
 
         mStatusBarWindowController = statusBarWindowController;
         mStatusBarStateListener = new StatusBarStateListener();
@@ -389,6 +394,46 @@
     }
 
     @SuppressWarnings("FieldCanBeLocal")
+    private final NotificationRemoveInterceptor mRemoveInterceptor =
+            new NotificationRemoveInterceptor() {
+            @Override
+            public boolean onNotificationRemoveRequested(String key, int reason) {
+                if (!mBubbleData.hasBubbleWithKey(key)) {
+                    return false;
+                }
+                NotificationEntry entry = mBubbleData.getBubbleWithKey(key).entry;
+
+                final boolean isClearAll = reason == REASON_CANCEL_ALL;
+                final boolean isUserDimiss = reason == REASON_CANCEL;
+                final boolean isAppCancel = reason == REASON_APP_CANCEL
+                        || reason == REASON_APP_CANCEL_ALL;
+
+                // Need to check for !appCancel here because the notification may have
+                // previously been dismissed & entry.isRowDismissed would still be true
+                boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel)
+                        || isClearAll || isUserDimiss;
+
+                // The bubble notification sticks around in the data as long as the bubble is
+                // not dismissed and the app hasn't cancelled the notification.
+                boolean bubbleExtended = entry.isBubble() && !entry.isBubbleDismissed()
+                        && userRemovedNotif;
+                if (bubbleExtended) {
+                    entry.setShowInShadeWhenBubble(false);
+                    if (mStackView != null) {
+                        mStackView.updateDotVisibility(entry.key);
+                    }
+                    mNotificationEntryManager.updateNotifications();
+                    return true;
+                } else if (!userRemovedNotif && !entry.isBubbleDismissed()) {
+                    // This wasn't a user removal so we should remove the bubble as well
+                    mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
+                    return false;
+                }
+                return false;
+            }
+        };
+
+    @SuppressWarnings("FieldCanBeLocal")
     private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
         @Override
         public void onPendingEntryAdded(NotificationEntry entry) {
@@ -396,7 +441,6 @@
                 return;
             }
             if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)) {
-                // TODO: handle group summaries?
                 updateShowInShadeForSuppressNotification(entry);
             }
         }
@@ -426,23 +470,6 @@
                 updateBubble(entry);
             }
         }
-
-        @Override
-        public void onEntryRemoved(NotificationEntry entry,
-                @Nullable NotificationVisibility visibility,
-                boolean removedByUser) {
-            if (!areBubblesEnabled(mContext)) {
-                return;
-            }
-            entry.setShowInShadeWhenBubble(false);
-            if (mStackView != null) {
-                mStackView.updateDotVisibility(entry.key);
-            }
-            if (!removedByUser) {
-                // This was a cancel so we should remove the bubble
-                removeBubble(entry.key, DISMISS_NOTIF_CANCEL);
-            }
-        }
     };
 
     @SuppressWarnings("FieldCanBeLocal")
@@ -455,13 +482,15 @@
         }
 
         @Override
-        public void onBubbleRemoved(Bubble bubble, int reason) {
+        public void onBubbleRemoved(Bubble bubble, @DismissReason int reason) {
             if (mStackView != null) {
                 mStackView.removeBubble(bubble);
             }
-            if (!bubble.entry.showInShadeWhenBubble()) {
-                // The notification is gone & bubble is gone, time to actually remove it
-                mNotificationEntryManager.performRemoveNotification(bubble.entry.notification);
+            if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
+                    && !bubble.entry.showInShadeWhenBubble()) {
+                // The bubble is gone & the notification is gone, time to actually remove it
+                mNotificationEntryManager.performRemoveNotification(bubble.entry.notification,
+                        0 /* reason */);
             } else {
                 // The notification is still in the shade but we've removed the bubble so
                 // lets make sure NoMan knows it's not a bubble anymore