Merge "Make sure PendingIntent cancel listener is unregistered" into rvc-dev am: 0e3d9d31a2

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/12037679

Change-Id: I5887b805f035a9986f9835924d50ea5ef73ef264
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 6dc8322..c6d1286 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -124,8 +124,26 @@
     private int mNotificationId;
     private int mAppUid = -1;
 
+    /**
+     * A bubble is created and can be updated. This intent is updated until the user first
+     * expands the bubble. Once the user has expanded the contents, we ignore the intent updates
+     * to prevent restarting the intent & possibly altering UI state in the activity in front of
+     * the user.
+     *
+     * Once the bubble is overflowed, the activity is finished and updates to the
+     * notification are respected. Typically an update to an overflowed bubble would result in
+     * that bubble being added back to the stack anyways.
+     */
     @Nullable
     private PendingIntent mIntent;
+    private boolean mIntentActive;
+    @Nullable
+    private PendingIntent.CancelListener mIntentCancelListener;
+
+    /**
+     * Sent when the bubble & notification are no longer visible to the user (i.e. no
+     * notification in the shade, no bubble in the stack or overflow).
+     */
     @Nullable
     private PendingIntent mDeleteIntent;
 
@@ -150,13 +168,19 @@
         mShowBubbleUpdateDot = false;
     }
 
-    /** Used in tests when no UI is required. */
     @VisibleForTesting(visibility = PRIVATE)
     Bubble(@NonNull final NotificationEntry e,
-            @Nullable final BubbleController.NotificationSuppressionChangedListener listener) {
+            @Nullable final BubbleController.NotificationSuppressionChangedListener listener,
+            final BubbleController.PendingIntentCanceledListener intentCancelListener) {
         Objects.requireNonNull(e);
         mKey = e.getKey();
         mSuppressionListener = listener;
+        mIntentCancelListener = intent -> {
+            if (mIntent != null) {
+                mIntent.unregisterCancelListener(mIntentCancelListener);
+            }
+            intentCancelListener.onPendingIntentCanceled(this);
+        };
         setEntry(e);
     }
 
@@ -238,6 +262,10 @@
             mExpandedView = null;
         }
         mIconView = null;
+        if (mIntent != null) {
+            mIntent.unregisterCancelListener(mIntentCancelListener);
+        }
+        mIntentActive = false;
     }
 
     void setPendingIntentCanceled() {
@@ -371,11 +399,24 @@
             mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight();
             mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId();
             mIcon = entry.getBubbleMetadata().getIcon();
-            mIntent = entry.getBubbleMetadata().getIntent();
+
+            if (!mIntentActive || mIntent == null) {
+                if (mIntent != null) {
+                    mIntent.unregisterCancelListener(mIntentCancelListener);
+                }
+                mIntent = entry.getBubbleMetadata().getIntent();
+                if (mIntent != null) {
+                    mIntent.registerCancelListener(mIntentCancelListener);
+                }
+            } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) {
+                // Was an intent bubble now it's a shortcut bubble... still unregister the listener
+                mIntent.unregisterCancelListener(mIntentCancelListener);
+                mIntent = null;
+            }
             mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent();
         }
         mIsImportantConversation =
-                entry.getChannel() == null ? false : entry.getChannel().isImportantConversation();
+                entry.getChannel() != null && entry.getChannel().isImportantConversation();
     }
 
     @Nullable
@@ -395,10 +436,15 @@
     }
 
     /**
-     * @return if the bubble was ever expanded
+     * Sets if the intent used for this bubble is currently active (i.e. populating an
+     * expanded view, expanded or not).
      */
-    boolean getWasAccessed() {
-        return mLastAccessed != 0L;
+    void setIntentActive() {
+        mIntentActive = true;
+    }
+
+    boolean isIntentActive() {
+        return mIntentActive;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 6ea0cde..6f103b0 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -263,6 +263,16 @@
     }
 
     /**
+     * Listener to be notified when a pending intent has been canceled for a bubble.
+     */
+    public interface PendingIntentCanceledListener {
+        /**
+         * Called when the pending intent for a bubble has been canceled.
+         */
+        void onPendingIntentCanceled(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
@@ -390,6 +400,18 @@
                 }
             }
         });
+        mBubbleData.setPendingIntentCancelledListener(bubble -> {
+            if (bubble.getBubbleIntent() == null) {
+                return;
+            }
+            if (bubble.isIntentActive()) {
+                bubble.setPendingIntentCanceled();
+                return;
+            }
+            mHandler.post(
+                    () -> removeBubble(bubble.getKey(),
+                            BubbleController.DISMISS_INVALID_INTENT));
+        });
 
         mNotificationEntryManager = entryManager;
         mNotificationGroupManager = groupManager;
@@ -1101,23 +1123,7 @@
         // Lazy init stack view when a bubble is created
         ensureStackViewCreated();
         bubble.setInflateSynchronously(mInflateSynchronously);
-        bubble.inflate(
-                b -> {
-                    mBubbleData.notificationEntryUpdated(b, suppressFlyout,
-                            showInShade);
-                    if (bubble.getBubbleIntent() == null) {
-                        return;
-                    }
-                    bubble.getBubbleIntent().registerCancelListener(pendingIntent -> {
-                        if (bubble.getWasAccessed()) {
-                            bubble.setPendingIntentCanceled();
-                            return;
-                        }
-                        mHandler.post(
-                                () -> removeBubble(bubble.getKey(),
-                                        BubbleController.DISMISS_INVALID_INTENT));
-                    });
-                },
+        bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
                 mContext, mStackView, mBubbleIconFactory, false /* skipInflation */);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index ec4304f..d2dc506 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -59,7 +59,7 @@
 @Singleton
 public class BubbleData {
 
-    BubbleLogger mLogger = new BubbleLoggerImpl();
+    private BubbleLogger mLogger = new BubbleLoggerImpl();
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES;
 
@@ -137,6 +137,7 @@
 
     @Nullable
     private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
+    private BubbleController.PendingIntentCanceledListener mCancelledListener;
 
     /**
      * We track groups with summaries that aren't visibly displayed but still kept around because
@@ -167,6 +168,11 @@
         mSuppressionListener = listener;
     }
 
+    public void setPendingIntentCancelledListener(
+            BubbleController.PendingIntentCanceledListener listener) {
+        mCancelledListener = listener;
+    }
+
     public boolean hasBubbles() {
         return !mBubbles.isEmpty();
     }
@@ -236,7 +242,7 @@
                 bubbleToReturn = mPendingBubbles.get(key);
             } else if (entry != null) {
                 // New bubble
-                bubbleToReturn = new Bubble(entry, mSuppressionListener);
+                bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener);
             } else {
                 // Persisted bubble being promoted
                 bubbleToReturn = persistedBubble;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 1211fb4..3d31712 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -183,6 +183,9 @@
                                 // Apply flags to make behaviour match documentLaunchMode=always.
                                 fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
                                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                                if (mBubble != null) {
+                                    mBubble.setIntentActive();
+                                }
                                 mActivityView.startActivity(mPendingIntent, fillInIntent, options);
                             }
                         } catch (RuntimeException e) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index ed4e686..315caee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -107,6 +107,9 @@
     @Mock
     private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
 
+    @Mock
+    private BubbleController.PendingIntentCanceledListener mPendingIntentCanceledListener;
+
     @Before
     public void setUp() throws Exception {
         mNotificationTestHelper = new NotificationTestHelper(
@@ -127,20 +130,20 @@
         modifyRanking(mEntryInterruptive)
                 .setVisuallyInterruptive(true)
                 .build();
-        mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener);
+        mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null);
 
         ExpandableNotificationRow row = mNotificationTestHelper.createBubble();
         mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d");
         mEntryDismissed.setRow(row);
-        mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener);
+        mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null);
 
-        mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener);
-        mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener);
-        mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener);
-        mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener);
-        mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener);
-        mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener);
-        mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener);
+        mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener);
+        mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener);
+        mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener);
+        mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener);
+        mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener);
+        mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener);
+        mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener);
 
         mBubbleData = new BubbleData(getContext());
 
@@ -847,14 +850,6 @@
         when(entry.getSbn().getPostTime()).thenReturn(postTime);
     }
 
-    private void setOngoing(NotificationEntry entry, boolean ongoing) {
-        if (ongoing) {
-            entry.getSbn().getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
-        } else {
-            entry.getSbn().getNotification().flags &= ~Notification.FLAG_FOREGROUND_SERVICE;
-        }
-    }
-
     /**
      * No ExpandableNotificationRow is required to test BubbleData. This setup is all that is
      * required for BubbleData functionality and verification. NotificationTestHelper is used only
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java
index be03923..2bcc22c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java
@@ -71,7 +71,7 @@
                 .setNotification(mNotif)
                 .build();
 
-        mBubble = new Bubble(mEntry, mSuppressionListener);
+        mBubble = new Bubble(mEntry, mSuppressionListener, null);
 
         Intent target = new Intent(mContext, BubblesTestActivity.class);
         Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(