Bubble API: post update when bubble notification suppression flag changes

When bubbles are expanded or if the user dismisses the notification, the
notification associated with the bubble is suppressed from the shade, but
still technically active / in active notification list because of the
bubble.

We already have a concept of this with isNotificationSuppressed on
BubbleMetadata, this just ensures that the flag is properly updated when
the state changes in System UI. This allows other things showing
notifications (e.g. long press on launcher app icon -> see notifs) to
hide or show notifications consistently with the rest of system ui.

I'm re-using the flag that apps may set to suppress notifications when
they send a bubble that is foreground, since its the same concept.

When this flag state changes a new update is sent with the flag
adjusted.

Added a notification delegate method to communicate the suppression changes
back to NoMan. This is piped through a listener set on Bubble objects.

Test: atest NotificationManagerTest NotificationTest NotificationManagerServiceTest
Bug: 131926652
Bug: 144352570
Change-Id: I25e86849858cdfe2fb81ab51b1ff09980bf70944
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 1af275f..16898a0 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -8634,17 +8634,23 @@
         public static final int FLAG_AUTO_EXPAND_BUBBLE = 0x00000001;
 
         /**
-         * If set and the app posting the bubble is in the foreground, the bubble will
-         * be posted <b>without</b> the associated notification in the notification shade.
+         * Indicates whether the notification associated with the bubble is being visually
+         * suppressed from the notification shade. When <code>true</code> the notification is
+         * hidden, when <code>false</code> the notification shows as normal.
          *
-         * <p>This flag has no effect if the app posting the bubble is not in the foreground.
-         * The app is considered foreground if it is visible and on the screen, note that
-         * a foreground service does not qualify.
-         * </p>
+         * <p>Apps sending bubbles may set this flag so that the bubble is posted <b>without</b>
+         * the associated notification in the notification shade.</p>
          *
-         * <p>Generally this flag should only be set if the user has performed an action to request
-         * or create a bubble, or if the user has seen the content in the notification and the
-         * notification is no longer relevant.</p>
+         * <p>Apps sending bubbles can only apply this flag when the app is in the foreground,
+         * otherwise the flag is not respected. The app is considered foreground if it is visible
+         * and on the screen, note that a foreground service does not qualify.</p>
+         *
+         * <p>Generally this flag should only be set by the app if the user has performed an
+         * action to request or create a bubble, or if the user has seen the content in the
+         * notification and the notification is no longer relevant. </p>
+         *
+         * <p>The system will also update this flag with <code>true</code> to hide the notification
+         * from the user once the bubble has been expanded. </p>
          *
          * @hide
          */
@@ -8762,6 +8768,24 @@
         }
 
         /**
+         * Indicates whether the notification associated with the bubble is being visually
+         * suppressed from the notification shade. When <code>true</code> the notification is
+         * hidden, when <code>false</code> the notification shows as normal.
+         *
+         * <p>Apps sending bubbles may set this flag so that the bubble is posted <b>without</b>
+         * the associated notification in the notification shade.</p>
+         *
+         * <p>Apps sending bubbles can only apply this flag when the app is in the foreground,
+         * otherwise the flag is not respected. The app is considered foreground if it is visible
+         * and on the screen, note that a foreground service does not qualify.</p>
+         *
+         * <p>Generally the app should only set this flag if the user has performed an
+         * action to request or create a bubble, or if the user has seen the content in the
+         * notification and the notification is no longer relevant. </p>
+         *
+         * <p>The system will update this flag with <code>true</code> to hide the notification
+         * from the user once the bubble has been expanded.</p>
+         *
          * @return whether this bubble should suppress the notification when it is posted.
          *
          * @see BubbleMetadata.Builder#setSuppressNotification(boolean)
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index a9f7b84..7622883 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -78,6 +78,7 @@
             in int notificationLocation, boolean modifiedBeforeSending);
     void onNotificationSettingsViewed(String key);
     void onNotificationBubbleChanged(String key, boolean isBubble);
+    void onBubbleNotificationSuppressionChanged(String key, boolean isSuppressed);
     void grantInlineReplyUriPermission(String key, in Uri uri, in UserHandle user, String packageName);
     void clearInlineReplyUriPermissions(String key);
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index ccce85c..45705b7 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -60,6 +60,14 @@
     private long mLastUpdated;
     private long mLastAccessed;
 
+    private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
+
+    /** Whether the bubble should show a dot for the notification indicating updated content. */
+    private boolean mShowBubbleUpdateDot = true;
+
+    /** Whether flyout text should be suppressed, regardless of any other flags or state. */
+    private boolean mSuppressFlyout;
+
     // Items that are typically loaded later
     private String mAppName;
     private ShortcutInfo mShortcutInfo;
@@ -71,20 +79,6 @@
     private boolean mInflateSynchronously;
 
     /**
-     * Whether this notification should be shown in the shade when it is also displayed as a bubble.
-     *
-     * <p>When a notification is a bubble we don't show it in the shade once the bubble has been
-     * expanded</p>
-     */
-    private boolean mShowInShadeWhenBubble = true;
-
-    /** Whether the bubble should show a dot for the notification indicating updated content. */
-    private boolean mShowBubbleUpdateDot = true;
-
-    /** Whether flyout text should be suppressed, regardless of any other flags or state. */
-    private boolean mSuppressFlyout;
-
-    /**
      * Presentational info about the flyout.
      */
     public static class FlyoutMessage {
@@ -106,11 +100,13 @@
 
     /** Used in tests when no UI is required. */
     @VisibleForTesting(visibility = PRIVATE)
-    Bubble(NotificationEntry e) {
+    Bubble(NotificationEntry e,
+            BubbleController.NotificationSuppressionChangedListener listener) {
         mEntry = e;
         mKey = e.getKey();
         mLastUpdated = e.getSbn().getPostTime();
         mGroupId = groupId(e);
+        mSuppressionListener = listener;
     }
 
     public String getKey() {
@@ -278,7 +274,7 @@
      */
     void markAsAccessedAt(long lastAccessedMillis) {
         mLastAccessed = lastAccessedMillis;
-        setShowInShade(false);
+        setSuppressNotification(true);
         setShowDot(false /* show */, true /* animate */);
     }
 
@@ -290,20 +286,30 @@
     }
 
     /**
-     * Whether this notification should be shown in the shade when it is also displayed as a
-     * bubble.
+     * Whether this notification should be shown in the shade.
      */
     boolean showInShade() {
-        return !mEntry.isRowDismissed() && !shouldSuppressNotification()
-                && (!mEntry.isClearable() || mShowInShadeWhenBubble);
+        return !shouldSuppressNotification() || !mEntry.isClearable();
     }
 
     /**
-     * Sets whether this notification should be shown in the shade when it is also displayed as a
-     * bubble.
+     * Sets whether this notification should be suppressed in the shade.
      */
-    void setShowInShade(boolean showInShade) {
-        mShowInShadeWhenBubble = showInShade;
+    void setSuppressNotification(boolean suppressNotification) {
+        boolean prevShowInShade = showInShade();
+
+        Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
+        int flags = data.getFlags();
+        if (suppressNotification) {
+            flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
+        } else {
+            flags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
+        }
+        data.setFlags(flags);
+
+        if (showInShade() != prevShowInShade && mSuppressionListener != null) {
+            mSuppressionListener.onBubbleNotificationSuppressionChange(this);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 8c9946f..2ed5b10 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -224,6 +224,17 @@
     }
 
     /**
+     * Listener to be notified when a bubbles' notification suppression state changes.
+     */
+    public interface NotificationSuppressionChangedListener {
+        /**
+         * Called when the notification suppression state of a bubble changes.
+         */
+        void onBubbleNotificationSuppressionChange(Bubble bubble);
+
+    }
+
+    /**
      * Listens for the current state of the status bar and updates the visibility state
      * of bubbles as needed.
      */
@@ -303,9 +314,22 @@
 
         configurationController.addCallback(this /* configurationListener */);
 
+        mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
         mBubbleData = data;
         mBubbleData.setListener(mBubbleDataListener);
-        mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
+        mBubbleData.setSuppressionChangedListener(new NotificationSuppressionChangedListener() {
+            @Override
+            public void onBubbleNotificationSuppressionChange(Bubble bubble) {
+                // Make sure NoMan knows it's not showing in the shade anymore so anyone querying it
+                // can tell.
+                try {
+                    mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(),
+                            !bubble.showInShade());
+                } catch (RemoteException e) {
+                    // Bad things have happened
+                }
+            }
+        });
 
         mNotificationEntryManager = entryManager;
         mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
@@ -720,7 +744,7 @@
                 Bubble bubble = mBubbleData.getBubbleWithKey(key);
                 boolean bubbleExtended = entry != null && entry.isBubble() && userRemovedNotif;
                 if (bubbleExtended) {
-                    bubble.setShowInShade(false);
+                    bubble.setSuppressNotification(true);
                     bubble.setShowDot(false /* show */, true /* animate */);
                     mNotificationEntryManager.updateNotifications(
                             "BubbleController.onNotificationRemoveRequested");
@@ -747,7 +771,7 @@
                 // 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.setShowInShade(false);
+                bubbleChild.setSuppressNotification(true);
                 bubbleChild.setShowDot(false /* show */, true /* animate */);
             }
             // And since all children are removed, remove the summary.
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 8b687e7..673121f 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -134,6 +134,9 @@
     @Nullable
     private Listener mListener;
 
+    @Nullable
+    private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
+
     /**
      * We track groups with summaries that aren't visibly displayed but still kept around because
      * the bubble(s) associated with the summary still exist.
@@ -158,6 +161,11 @@
         mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
     }
 
+    public void setSuppressionChangedListener(
+            BubbleController.NotificationSuppressionChangedListener listener) {
+        mSuppressionListener = listener;
+    }
+
     public boolean hasBubbles() {
         return !mBubbles.isEmpty();
     }
@@ -219,7 +227,7 @@
                     return b;
                 }
             }
-            bubble = new Bubble(entry);
+            bubble = new Bubble(entry, mSuppressionListener);
             mPendingBubbles.add(bubble);
         } else {
             bubble.setEntry(entry);
@@ -258,11 +266,13 @@
         } else if (mSelectedBubble == null) {
             setSelectedBubbleInternal(bubble);
         }
-        boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
-        bubble.setShowInShade(!isBubbleExpandedAndSelected && showInShade);
-        bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
-        dispatchPendingChanges();
 
+        boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
+        boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
+        bubble.setSuppressNotification(suppress);
+        bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
+
+        dispatchPendingChanges();
     }
 
     public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 2c9058a..28f01da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -282,7 +282,7 @@
                 mRow.getEntry().getKey()));
 
         // Make it look like dismissed notif
-        mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).setShowInShade(false);
+        mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).setSuppressNotification(true);
 
         // Now remove the bubble
         mBubbleController.removeBubble(
@@ -372,7 +372,8 @@
 
         // Switch which bubble is expanded
         mBubbleController.selectBubble(mRow.getEntry().getKey());
-        mBubbleController.expandStack();
+        mBubbleData.setExpanded(true);
+        assertEquals(mRow.getEntry(), stackView.getExpandedBubble().getEntry());
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry().getKey()));
 
@@ -571,7 +572,6 @@
         verify(mBubbleStateChangeListener).onHasBubblesChanged(true /* hasBubbles */);
     }
 
-
     @Test
     public void testExpandStackAndSelectBubble_removedFirst() {
         final String key = mRow.getEntry().getKey();
@@ -724,6 +724,52 @@
         assertFalse(intercepted);
     }
 
+    @Test
+    public void testNotifyShadeSuppressionChange_notificationDismiss() {
+        BubbleController.NotificationSuppressionChangedListener listener =
+                mock(BubbleController.NotificationSuppressionChangedListener.class);
+        mBubbleData.setSuppressionChangedListener(listener);
+
+        mEntryListener.onNotificationAdded(mRow.getEntry());
+
+        assertTrue(mBubbleController.hasBubbles());
+        assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
+                mRow.getEntry().getKey()));
+
+        mRemoveInterceptor.onNotificationRemoveRequested(mRow.getEntry().getKey(), REASON_CANCEL);
+
+        // Should update show in shade state
+        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
+                mRow.getEntry().getKey()));
+
+        // Should notify delegate that shade state changed
+        verify(listener).onBubbleNotificationSuppressionChange(
+                mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()));
+    }
+
+    @Test
+    public void testNotifyShadeSuppressionChange_bubbleExpanded() {
+        BubbleController.NotificationSuppressionChangedListener listener =
+                mock(BubbleController.NotificationSuppressionChangedListener.class);
+        mBubbleData.setSuppressionChangedListener(listener);
+
+        mEntryListener.onNotificationAdded(mRow.getEntry());
+
+        assertTrue(mBubbleController.hasBubbles());
+        assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
+                mRow.getEntry().getKey()));
+
+        mBubbleData.setExpanded(true);
+
+        // Once a bubble is expanded the notif is suppressed
+        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
+                mRow.getEntry().getKey()));
+
+        // Should notify delegate that shade state changed
+        verify(listener).onBubbleNotificationSuppressionChange(
+                mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()));
+    }
+
     static class TestableBubbleController extends BubbleController {
         // Let's assume surfaces can be synchronized immediately.
         TestableBubbleController(Context context,
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 1a2e237..c9f5b40 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -104,6 +104,9 @@
     @Captor
     private ArgumentCaptor<BubbleData.Update> mUpdateCaptor;
 
+    @Mock
+    private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
+
     @Before
     public void setUp() throws Exception {
         mNotificationTestHelper = new NotificationTestHelper(mContext, mDependency);
@@ -121,20 +124,20 @@
         modifyRanking(mEntryInterruptive)
                 .setVisuallyInterruptive(true)
                 .build();
-        mBubbleInterruptive = new Bubble(mEntryInterruptive);
+        mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener);
 
         ExpandableNotificationRow row = mNotificationTestHelper.createBubble();
         mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d");
         mEntryDismissed.setRow(row);
-        mBubbleDismissed = new Bubble(mEntryDismissed);
+        mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener);
 
-        mBubbleA1 = new Bubble(mEntryA1);
-        mBubbleA2 = new Bubble(mEntryA2);
-        mBubbleA3 = new Bubble(mEntryA3);
-        mBubbleB1 = new Bubble(mEntryB1);
-        mBubbleB2 = new Bubble(mEntryB2);
-        mBubbleB3 = new Bubble(mEntryB3);
-        mBubbleC1 = new Bubble(mEntryC1);
+        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);
 
         mBubbleData = new BubbleData(getContext());
 
@@ -237,9 +240,8 @@
                 true /* showInShade */);
         verifyUpdateReceived();
 
-        // Make it look like user swiped away row
-        mEntryDismissed.getRow().dismiss(false /* refocusOnDismiss */);
-        assertThat(mBubbleData.getBubbleWithKey(mBubbleDismissed.getKey()).showInShade()).isFalse();
+        // Suppress the notif / make it look dismissed
+        mBubbleDismissed.setSuppressNotification(true);
 
         mBubbleData.notificationEntryUpdated(mBubbleDismissed, false /* suppressFlyout */,
                 true /* showInShade */);
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 02f721c..7f67657 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java
@@ -16,11 +16,19 @@
 
 package com.android.systemui.bubbles;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 
 import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -30,6 +38,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.tests.R;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,6 +55,10 @@
 
     private NotificationEntry mEntry;
     private Bundle mExtras;
+    private Bubble mBubble;
+
+    @Mock
+    private BubbleController.NotificationSuppressionChangedListener mSuppressionListener;
 
     @Before
     public void setUp() {
@@ -57,6 +70,15 @@
         mEntry = new NotificationEntryBuilder()
                 .setNotification(mNotif)
                 .build();
+
+        mBubble = new Bubble(mEntry, mSuppressionListener);
+
+        Intent target = new Intent(mContext, BubblesTestActivity.class);
+        Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder()
+                .createIntentBubble(PendingIntent.getActivity(mContext, 0, target, 0),
+                        Icon.createWithResource(mContext, R.drawable.android))
+                .build();
+        mEntry.setBubbleMetadata(metadata);
     }
 
     @Test
@@ -123,4 +145,24 @@
                 BubbleViewInfoTask.extractFlyoutMessage(mContext,
                         mEntry).senderName);
     }
+
+    @Test
+    public void testSuppressionListener_change_notified() {
+        assertThat(mBubble.showInShade()).isTrue();
+
+        mBubble.setSuppressNotification(true);
+
+        assertThat(mBubble.showInShade()).isFalse();
+
+        verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble);
+    }
+
+    @Test
+    public void testSuppressionListener_noChange_doesntNotify() {
+        assertThat(mBubble.showInShade()).isTrue();
+
+        mBubble.setSuppressNotification(false);
+
+        verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any());
+    }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java
index 88fc072..feb4f0e 100644
--- a/services/core/java/com/android/server/notification/NotificationDelegate.java
+++ b/services/core/java/com/android/server/notification/NotificationDelegate.java
@@ -48,7 +48,15 @@
             int notificationLocation);
     void onNotificationDirectReplied(String key);
     void onNotificationSettingsViewed(String key);
+    /**
+     * Called when the state of {@link Notification#FLAG_BUBBLE} is changed.
+     */
     void onNotificationBubbleChanged(String key, boolean isBubble);
+    /**
+     * Called when the state of {@link Notification.BubbleMetadata#FLAG_SUPPRESS_NOTIFICATION}
+     * changes.
+     */
+    void onBubbleNotificationSuppressionChanged(String key, boolean isSuppressed);
 
     /**
      * Grant permission to read the specified URI to the package associated with the
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index eea59ca..38ed677 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1175,6 +1175,34 @@
         }
 
         @Override
+        public void onBubbleNotificationSuppressionChanged(String key, boolean isSuppressed) {
+            synchronized (mNotificationLock) {
+                NotificationRecord r = mNotificationsByKey.get(key);
+                if (r != null) {
+                    Notification.BubbleMetadata data = r.getNotification().getBubbleMetadata();
+                    if (data == null) {
+                        // No data, do nothing
+                        return;
+                    }
+                    boolean currentlySuppressed = data.isNotificationSuppressed();
+                    if (currentlySuppressed == isSuppressed) {
+                        // No changes, do nothing
+                        return;
+                    }
+                    int flags = data.getFlags();
+                    if (isSuppressed) {
+                        flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
+                    } else {
+                        flags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
+                    }
+                    data.setFlags(flags);
+                    mHandler.post(new EnqueueNotificationRunnable(r.getUser().getIdentifier(), r,
+                            true /* isAppForeground */));
+                }
+            }
+        }
+
+        @Override
         /**
          * Grant permission to read the specified URI to the package specified in the
          * NotificationRecord associated with the given key. The callingUid represents the UID of
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 3f7d373..57a6776 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -1364,6 +1364,17 @@
     }
 
     @Override
+    public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed) {
+        enforceStatusBarService();
+        long identity = Binder.clearCallingIdentity();
+        try {
+            mNotificationDelegate.onBubbleNotificationSuppressionChanged(key, isNotifSuppressed);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
     public void grantInlineReplyUriPermission(String key, Uri uri, UserHandle user,
             String packageName) {
         enforceStatusBarService();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 1b92abe..768b472 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -5438,6 +5438,46 @@
     }
 
     @Test
+    public void testOnBubbleNotificationSuppressionChanged() throws Exception {
+        // Bubble notification
+        NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "tag");
+
+        // Bubbles are allowed!
+        setUpPrefsForBubbles(PKG, nr.sbn.getUserId(), true /* global */,
+                true /* app */, true /* channel */);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.sbn.getTag(),
+                nr.sbn.getId(), nr.sbn.getNotification(), nr.sbn.getUserId());
+        waitForIdle();
+
+        // NOT suppressed
+        Notification n =  mBinderService.getActiveNotifications(PKG)[0].getNotification();
+        assertFalse(n.getBubbleMetadata().isNotificationSuppressed());
+
+        // Reset as this is called when the notif is first sent
+        reset(mListeners);
+
+        // Test: update suppression to true
+        mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), true);
+        waitForIdle();
+
+        // Check
+        n =  mBinderService.getActiveNotifications(PKG)[0].getNotification();
+        assertTrue(n.getBubbleMetadata().isNotificationSuppressed());
+
+        // Reset to check again
+        reset(mListeners);
+
+        // Test: update suppression to false
+        mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), false);
+        waitForIdle();
+
+        // Check
+        n = mBinderService.getActiveNotifications(PKG)[0].getNotification();
+        assertFalse(n.getBubbleMetadata().isNotificationSuppressed());
+    }
+
+    @Test
     public void testGrantInlineReplyUriPermission_recordExists() throws Exception {
         NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel, 0);
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag",