diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 31fc6a5be5895..2c02006c8ca5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -452,6 +452,7 @@ public class Bubble implements BubbleViewProvider { */ void setEntry(@NonNull final BubbleEntry entry) { Objects.requireNonNull(entry); + boolean showingDotPreviously = showDot(); mLastUpdated = entry.getStatusBarNotification().getPostTime(); mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification(); mPackageName = entry.getStatusBarNotification().getPackageName(); @@ -498,6 +499,10 @@ public class Bubble implements BubbleViewProvider { mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); mShouldSuppressPeek = entry.shouldSuppressPeek(); + if (showingDotPreviously != showDot()) { + // This will update the UI if needed + setShowDot(showDot()); + } } @Nullable diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 398261600dc7f..8771ceb71d989 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1055,18 +1055,28 @@ public class BubbleController implements ConfigurationChangeListener { public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) { // If this is an interruptive notif, mark that it's interrupted mSysuiProxy.setNotificationInterruption(notif.getKey()); - if (!notif.getRanking().isTextChanged() + boolean isNonInterruptiveNotExpanding = !notif.getRanking().isTextChanged() && (notif.getBubbleMetadata() != null - && !notif.getBubbleMetadata().getAutoExpandBubble()) + && !notif.getBubbleMetadata().getAutoExpandBubble()); + if (isNonInterruptiveNotExpanding && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { // Update the bubble but don't promote it out of overflow Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); - b.setEntry(notif); + if (notif.isBubble()) { + notif.setFlagBubble(false); + } + updateNotNotifyingEntry(b, notif, showInShade); + } else if (mBubbleData.hasAnyBubbleWithKey(notif.getKey()) + && isNonInterruptiveNotExpanding) { + Bubble b = mBubbleData.getAnyBubbleWithkey(notif.getKey()); + if (b != null) { + updateNotNotifyingEntry(b, notif, showInShade); + } } else if (mBubbleData.isSuppressedWithLocusId(notif.getLocusId())) { // Update the bubble but don't promote it out of overflow Bubble b = mBubbleData.getSuppressedBubbleWithKey(notif.getKey()); if (b != null) { - b.setEntry(notif); + updateNotNotifyingEntry(b, notif, showInShade); } } else { Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); @@ -1076,13 +1086,25 @@ public class BubbleController implements ConfigurationChangeListener { if (bubble.shouldAutoExpand()) { bubble.setShouldAutoExpand(false); } + mImpl.mCachedState.updateBubbleSuppressedState(bubble); } else { inflateAndAdd(bubble, suppressFlyout, showInShade); } } } - void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + void updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade) { + boolean isBubbleSelected = Objects.equals(b, mBubbleData.getSelectedBubble()); + boolean isBubbleExpandedAndSelected = isStackExpanded() && isBubbleSelected; + b.setEntry(entry); + boolean suppress = isBubbleExpandedAndSelected || !showInShade || !b.showInShade(); + b.setSuppressNotification(suppress); + b.setShowDot(!isBubbleExpandedAndSelected); + mImpl.mCachedState.updateBubbleSuppressedState(b); + } + + @VisibleForTesting + public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { // Lazy init stack view when a bubble is created ensureStackViewCreated(); bubble.setInflateSynchronously(mInflateSynchronously); @@ -1111,7 +1133,10 @@ public class BubbleController implements ConfigurationChangeListener { } @VisibleForTesting - public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) { + if (!fromSystem) { + return; + } // shouldBubbleUp checks canBubble & for bubble metadata boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry); if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { @@ -1172,9 +1197,9 @@ public class BubbleController implements ConfigurationChangeListener { // notification, so that the bubble will be re-created if shouldBubbleUp returns // true. mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP); - } else if (entry != null && mTmpRanking.isBubble() && !isActive) { + } else if (entry != null && mTmpRanking.isBubble() && !isActiveOrInOverflow) { entry.setFlagBubble(true); - onEntryUpdated(entry, shouldBubbleUp); + onEntryUpdated(entry, shouldBubbleUp, /* fromSystem= */ true); } } } @@ -1773,9 +1798,9 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) { mMainExecutor.execute(() -> { - BubbleController.this.onEntryUpdated(entry, shouldBubbleUp); + BubbleController.this.onEntryUpdated(entry, shouldBubbleUp, fromSystem); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index cf792cda91b5c..37b96ffe5cd1e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -171,8 +171,9 @@ public interface Bubbles { * * @param entry the {@link BubbleEntry} by the notification. * @param shouldBubbleUp {@code true} if this notification should bubble up. + * @param fromSystem {@code true} if this update is from NotificationManagerService. */ - void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp); + void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem); /** * Called when new notification entry removed. diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index e22a896227ef6..4c762702892a0 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -405,8 +405,8 @@ public class BubblesManager implements Dumpable { } @Override - public void onEntryUpdated(NotificationEntry entry) { - BubblesManager.this.onEntryUpdated(entry); + public void onEntryUpdated(NotificationEntry entry, boolean fromSystem) { + BubblesManager.this.onEntryUpdated(entry, fromSystem); } @Override @@ -444,9 +444,10 @@ public class BubblesManager implements Dumpable { } } - void onEntryUpdated(NotificationEntry entry) { + void onEntryUpdated(NotificationEntry entry, boolean fromSystem) { + boolean shouldBubble = mNotificationInterruptStateProvider.shouldBubbleUp(entry); mBubbles.onEntryUpdated(notifToBubbleEntry(entry), - mNotificationInterruptStateProvider.shouldBubbleUp(entry)); + shouldBubble, fromSystem); } void onEntryRemoved(NotificationEntry entry) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index fee17c785ed28..18acf3f6ce531 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -621,7 +622,7 @@ public class BubblesTest extends SysuiTestCase { assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); // Send update - mEntryListener.onEntryUpdated(mRow); + mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); // Nothing should have changed // Notif is suppressed after expansion @@ -789,7 +790,7 @@ public class BubblesTest extends SysuiTestCase { @Test public void testAddNotif_notBubble() { mEntryListener.onEntryAdded(mNonBubbleNotifRow.getEntry()); - mEntryListener.onEntryUpdated(mNonBubbleNotifRow.getEntry()); + mEntryListener.onEntryUpdated(mNonBubbleNotifRow.getEntry(), /* fromSystem= */ true); assertThat(mBubbleController.hasBubbles()).isFalse(); } @@ -827,7 +828,7 @@ public class BubblesTest extends SysuiTestCase { NotificationListenerService.Ranking ranking = new RankingBuilder( mRow.getRanking()).setCanBubble(false).build(); mRow.setRanking(ranking); - mEntryListener.onEntryUpdated(mRow); + mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); assertFalse(mBubbleController.hasBubbles()); verify(mDeleteIntent, never()).send(); @@ -1432,6 +1433,100 @@ public class BubblesTest extends SysuiTestCase { assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse(); } + /** + * Verifies that if a bubble is in the overflow and a non-interruptive notification update + * comes in for it, it stays in the overflow but the entry is updated. + */ + @Test + public void testNonInterruptiveUpdate_doesntBubbleFromOverflow() { + mEntryListener.onEntryAdded(mRow); + mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); + assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); + + // Dismiss the bubble so it's in the overflow + mBubbleController.removeBubble( + mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); + assertThat(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey())).isTrue(); + + // Update the entry to not show in shade + setMetadataFlags(mRow, + Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, /* enableFlag= */ true); + mBubbleController.updateBubble(mBubbleEntry, + /* suppressFlyout= */ false, /* showInShade= */ true); + + // Check that the update was applied - shouldn't be show in shade + assertBubbleNotificationSuppressedFromShade(mBubbleEntry); + // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded) + verify(mBubbleController, times(1)).inflateAndAdd( + any(Bubble.class), anyBoolean(), anyBoolean()); + } + + /** + * Verifies that if a bubble is active, and a non-interruptive notification update comes in for + * it, it doesn't trigger a new inflate and add for that bubble. + */ + @Test + public void testNonInterruptiveUpdate_doesntTriggerInflate() { + mEntryListener.onEntryAdded(mRow); + mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); + assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); + + // Update the entry to not show in shade + setMetadataFlags(mRow, + Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, /* enableFlag= */ true); + mBubbleController.updateBubble(mBubbleEntry, + /* suppressFlyout= */ false, /* showInShade= */ true); + + // Check that the update was applied - shouldn't be show in shade + assertBubbleNotificationSuppressedFromShade(mBubbleEntry); + // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded) + verify(mBubbleController, times(1)).inflateAndAdd( + any(Bubble.class), anyBoolean(), anyBoolean()); + } + + /** + * Verifies that if a bubble is in the overflow and a non-interruptive notification update + * comes in for it with FLAG_BUBBLE that the flag is removed. + */ + @Test + public void testNonInterruptiveUpdate_doesntOverrideOverflowFlagBubble() { + mEntryListener.onEntryAdded(mRow); + mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); + assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); + + // Dismiss the bubble so it's in the overflow + mBubbleController.removeBubble( + mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); + assertThat(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey())).isTrue(); + // Once it's in the overflow it's not actively a bubble (doesn't have FLAG_BUBBLE) + Bubble b = mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey()); + assertThat(b.isBubble()).isFalse(); + + // Send a non-notifying update that has FLAG_BUBBLE + mRow.getSbn().getNotification().flags = FLAG_BUBBLE; + assertThat(mRow.getSbn().getNotification().isBubbleNotification()).isTrue(); + mBubbleController.updateBubble(mBubbleEntry, + /* suppressFlyout= */ false, /* showInShade= */ true); + + // Verify that it still doesn't have FLAG_BUBBLE because it's in the overflow. + b = mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey()); + assertThat(b.isBubble()).isFalse(); + } + + @Test + public void testNonSystemUpdatesIgnored() { + mEntryListener.onEntryAdded(mRow); + assertThat(mBubbleController.hasBubbles()).isTrue(); + + mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ false); + mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ false); + mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ false); + + // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded) + verify(mBubbleController, times(1)).inflateAndAdd( + any(Bubble.class), anyBoolean(), anyBoolean()); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId);