Merge "8/ Updating bubbles to run on shell thread" into sc-dev
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 122f917..ffeabd8 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
@@ -49,6 +49,7 @@
 import java.io.PrintWriter;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * Encapsulates the data and UI elements of a bubble.
@@ -58,6 +59,7 @@
     private static final String TAG = "Bubble";
 
     private final String mKey;
+    private final Executor mMainExecutor;
 
     private long mLastUpdated;
     private long mLastAccessed;
@@ -156,7 +158,8 @@
      * Note: Currently this is only being used when the bubble is persisted to disk.
      */
     Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo,
-            final int desiredHeight, final int desiredHeightResId, @Nullable final String title) {
+            final int desiredHeight, final int desiredHeightResId, @Nullable final String title,
+            Executor mainExecutor) {
         Objects.requireNonNull(key);
         Objects.requireNonNull(shortcutInfo);
         mMetadataShortcutId = shortcutInfo.getId();
@@ -170,20 +173,25 @@
         mDesiredHeightResId = desiredHeightResId;
         mTitle = title;
         mShowBubbleUpdateDot = false;
+        mMainExecutor = mainExecutor;
     }
 
     @VisibleForTesting(visibility = PRIVATE)
     Bubble(@NonNull final BubbleEntry entry,
             @Nullable final Bubbles.NotificationSuppressionChangedListener listener,
-            final Bubbles.PendingIntentCanceledListener intentCancelListener) {
+            final Bubbles.PendingIntentCanceledListener intentCancelListener,
+            Executor mainExecutor) {
         mKey = entry.getKey();
         mSuppressionListener = listener;
         mIntentCancelListener = intent -> {
             if (mIntent != null) {
                 mIntent.unregisterCancelListener(mIntentCancelListener);
             }
-            intentCancelListener.onPendingIntentCanceled(this);
+            mainExecutor.execute(() -> {
+                intentCancelListener.onPendingIntentCanceled(this);
+            });
         };
+        mMainExecutor = mainExecutor;
         setEntry(entry);
     }
 
@@ -329,7 +337,8 @@
                 stackView,
                 iconFactory,
                 skipInflation,
-                callback);
+                callback,
+                mMainExecutor);
         if (mInflateSynchronously) {
             mInflationTask.onPostExecute(mInflationTask.doInBackground());
         } else {
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 9419b9c..7538c8b 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
@@ -28,6 +28,16 @@
 import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_LEFT;
 import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_NONE;
 import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_RIGHT;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_AGED;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NOTIF_CANCEL;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_BUBBLE_UP;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
+import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
 
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
@@ -45,6 +55,8 @@
 import android.graphics.PointF;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -53,6 +65,7 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
+import android.util.Slog;
 import android.util.SparseSetArray;
 import android.view.View;
 import android.view.ViewGroup;
@@ -75,6 +88,9 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -83,7 +99,7 @@
  *
  * The controller manages addition, removal, and visible state of bubbles on screen.
  */
-public class BubbleController implements Bubbles {
+public class BubbleController {
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
 
@@ -101,7 +117,8 @@
     public static final String BOTTOM_POSITION = "Bottom";
 
     private final Context mContext;
-    private BubbleExpandListener mExpandListener;
+    private final BubblesImpl mImpl = new BubblesImpl();
+    private Bubbles.BubbleExpandListener mExpandListener;
     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
     private final FloatingContentCoordinator mFloatingContentCoordinator;
     private final BubbleDataRepository mDataRepository;
@@ -111,7 +128,7 @@
     @Nullable private BubbleStackView mStackView;
     private BubbleIconFactory mBubbleIconFactory;
     private BubblePositioner mBubblePositioner;
-    private SysuiProxy mSysuiProxy;
+    private Bubbles.SysuiProxy mSysuiProxy;
 
     // Tracks the id of the current (foreground) user.
     private int mCurrentUserId;
@@ -177,7 +194,7 @@
     /**
      * Injected constructor.
      */
-    public static BubbleController create(Context context,
+    public static Bubbles create(Context context,
             @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
             FloatingContentCoordinator floatingContentCoordinator,
             @Nullable IStatusBarService statusBarService,
@@ -186,14 +203,15 @@
             LauncherApps launcherApps,
             UiEventLogger uiEventLogger,
             ShellTaskOrganizer organizer,
-            ShellExecutor mainExecutor) {
+            ShellExecutor mainExecutor,
+            Handler mainHandler) {
         BubbleLogger logger = new BubbleLogger(uiEventLogger);
         BubblePositioner positioner = new BubblePositioner(context, windowManager);
-        BubbleData data = new BubbleData(context, logger, positioner);
+        BubbleData data = new BubbleData(context, logger, positioner, mainExecutor);
         return new BubbleController(context, data, synchronizer, floatingContentCoordinator,
-                new BubbleDataRepository(context, launcherApps),
+                new BubbleDataRepository(context, launcherApps, mainExecutor),
                 statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
-                logger, organizer, positioner, mainExecutor);
+                logger, organizer, positioner, mainExecutor, mainHandler).mImpl;
     }
 
     /**
@@ -212,7 +230,8 @@
             BubbleLogger bubbleLogger,
             ShellTaskOrganizer organizer,
             BubblePositioner positioner,
-            ShellExecutor mainExecutor) {
+            ShellExecutor mainExecutor,
+            Handler mainHandler) {
         mContext = context;
         mFloatingContentCoordinator = floatingContentCoordinator;
         mDataRepository = dataRepository;
@@ -299,7 +318,12 @@
                 mBubbleData.removeBubblesWithInvalidShortcuts(
                         packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED);
             }
-        });
+        }, mainHandler);
+    }
+
+    @VisibleForTesting
+    public Bubbles getImpl() {
+        return mImpl;
     }
 
     /**
@@ -313,8 +337,7 @@
         }
     }
 
-    @Override
-    public void openBubbleOverflow() {
+    private void openBubbleOverflow() {
         ensureStackViewCreated();
         mBubbleData.setShowingOverflow(true);
         mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
@@ -322,8 +345,7 @@
     }
 
     /** Called when any taskbar state changes (e.g. visibility, position, sizes). */
-    @Override
-    public void onTaskbarChanged(Bundle b) {
+    private void onTaskbarChanged(Bundle b) {
         if (b == null) {
             return;
         }
@@ -371,8 +393,7 @@
      * Called when the status bar has become visible or invisible (either permanently or
      * temporarily).
      */
-    @Override
-    public void onStatusBarVisibilityChanged(boolean visible) {
+    private void onStatusBarVisibilityChanged(boolean visible) {
         if (mStackView != null) {
             // Hide the stack temporarily if the status bar has been made invisible, and the stack
             // is collapsed. An expanded stack should remain visible until collapsed.
@@ -380,15 +401,13 @@
         }
     }
 
-    @Override
-    public void onZenStateChanged() {
+    private void onZenStateChanged() {
         for (Bubble b : mBubbleData.getBubbles()) {
             b.setShowDot(b.showInShade());
         }
     }
 
-    @Override
-    public void onStatusBarStateChanged(boolean isShade) {
+    private void onStatusBarStateChanged(boolean isShade) {
         mIsStatusBarShade = isShade;
         if (!mIsStatusBarShade) {
             collapseStack();
@@ -402,8 +421,7 @@
         updateStack();
     }
 
-    @Override
-    public void onUserChanged(int newUserId) {
+    private void onUserChanged(int newUserId) {
         saveBubbles(mCurrentUserId);
         mBubbleData.dismissAll(DISMISS_USER_CHANGED);
         restoreBubbles(newUserId);
@@ -442,7 +460,7 @@
         return mBubblePositioner;
     }
 
-    SysuiProxy getSysuiProxy() {
+    Bubbles.SysuiProxy getSysuiProxy() {
         return mSysuiProxy;
     }
 
@@ -453,7 +471,8 @@
     private void ensureStackViewCreated() {
         if (mStackView == null) {
             mStackView = new BubbleStackView(
-                    mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator);
+                    mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
+                    mMainExecutor);
             mStackView.onOrientationChanged();
             if (mExpandListener != null) {
                 mStackView.setExpandListener(mExpandListener);
@@ -576,8 +595,7 @@
         mSavedBubbleKeysPerUser.remove(mCurrentUserId);
     }
 
-    @Override
-    public void updateForThemeChanges() {
+    private void updateForThemeChanges() {
         if (mStackView != null) {
             mStackView.onThemeChanged();
         }
@@ -593,8 +611,7 @@
         }
     }
 
-    @Override
-    public void onConfigChanged(Configuration newConfig) {
+    private void onConfigChanged(Configuration newConfig) {
         if (mBubblePositioner != null) {
             // This doesn't trigger any changes, always update it
             mBubblePositioner.update(newConfig.orientation);
@@ -620,18 +637,19 @@
         }
     }
 
-    @Override
-    public void setBubbleScrim(View view) {
+    private void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) {
         mBubbleScrim = view;
+        callback.accept(mMainExecutor, mMainExecutor.executeBlockingForResult(() -> {
+            return Looper.myLooper();
+        }, Looper.class));
     }
 
-    @Override
-    public void setSysuiProxy(SysuiProxy proxy) {
+    private void setSysuiProxy(Bubbles.SysuiProxy proxy) {
         mSysuiProxy = proxy;
     }
 
-    @Override
-    public void setExpandListener(BubbleExpandListener listener) {
+    @VisibleForTesting
+    public void setExpandListener(Bubbles.BubbleExpandListener listener) {
         mExpandListener = ((isExpanding, key) -> {
             if (listener != null) {
                 listener.onBubbleExpandChanged(isExpanding, key);
@@ -654,17 +672,17 @@
         return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
     }
 
-    @Override
+    @VisibleForTesting
     public boolean isStackExpanded() {
         return mBubbleData.isExpanded();
     }
 
-    @Override
+    @VisibleForTesting
     public void collapseStack() {
         mBubbleData.setExpanded(false /* expanded */);
     }
 
-    @Override
+    @VisibleForTesting
     public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
         boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key)
                 && !mBubbleData.getAnyBubbleWithkey(key).showInShade());
@@ -674,23 +692,19 @@
         return (isSummary && isSuppressedSummary) || isSuppressedBubble;
     }
 
-    @Override
-    public boolean isSummarySuppressed(String groupKey) {
-        return mBubbleData.isSummarySuppressed(groupKey);
+    private void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
+            Executor callbackExecutor) {
+        if (mBubbleData.isSummarySuppressed(groupKey)) {
+            mBubbleData.removeSuppressedSummary(groupKey);
+            if (callback != null) {
+                callbackExecutor.execute(() -> {
+                    callback.accept(mBubbleData.getSummaryKey(groupKey));
+                });
+            }
+        }
     }
 
-    @Override
-    public void removeSuppressedSummary(String groupKey) {
-        mBubbleData.removeSuppressedSummary(groupKey);
-    }
-
-    @Override
-    public String getSummaryKey(String groupKey) {
-        return mBubbleData.getSummaryKey(groupKey);
-    }
-
-    @Override
-    public boolean isBubbleExpanded(String key) {
+    private boolean isBubbleExpanded(String key) {
         return isStackExpanded() && mBubbleData != null && mBubbleData.getSelectedBubble() != null
                 && mBubbleData.getSelectedBubble().getKey().equals(key);
     }
@@ -704,7 +718,7 @@
         setIsBubble(bubble, true /* isBubble */);
     }
 
-    @Override
+    @VisibleForTesting
     public void expandStackAndSelectBubble(BubbleEntry entry) {
         if (mIsStatusBarShade) {
             mNotifEntryToExpandOnShadeUnlock = null;
@@ -809,15 +823,13 @@
         }
     }
 
-    @Override
-    public void onEntryAdded(BubbleEntry entry) {
+    private void onEntryAdded(BubbleEntry entry) {
         if (canLaunchInActivityView(mContext, entry)) {
             updateBubble(entry);
         }
     }
 
-    @Override
-    public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
+    private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
         // shouldBubbleUp checks canBubble & for bubble metadata
         boolean shouldBubble = shouldBubbleUp && canLaunchInActivityView(mContext, entry);
         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
@@ -828,8 +840,7 @@
         }
     }
 
-    @Override
-    public void onEntryRemoved(BubbleEntry entry) {
+    private void onEntryRemoved(BubbleEntry entry) {
         if (isSummaryOfBubbles(entry)) {
             final String groupKey = entry.getStatusBarNotification().getGroupKey();
             mBubbleData.removeSuppressedSummary(groupKey);
@@ -844,8 +855,7 @@
         }
     }
 
-    @Override
-    public void onRankingUpdated(RankingMap rankingMap) {
+    private void onRankingUpdated(RankingMap rankingMap) {
         if (mTmpRanking == null) {
             mTmpRanking = new NotificationListenerService.Ranking();
         }
@@ -882,6 +892,8 @@
             return bubbleChildren;
         }
         for (Bubble bubble : mBubbleData.getActiveBubbles()) {
+            // TODO(178620678): Prevent calling into SysUI since this can be a part of a blocking
+            //                  call from SysUI to Shell
             final BubbleEntry entry = mSysuiProxy.getPendingOrActiveEntry(bubble.getKey());
             if (entry != null && groupKey.equals(entry.getStatusBarNotification().getGroupKey())) {
                 bubbleChildren.add(bubble);
@@ -951,7 +963,7 @@
             ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>();
             for (Pair<Bubble, Integer> removed : removedBubbles) {
                 final Bubble bubble = removed.first;
-                @DismissReason final int reason = removed.second;
+                @Bubbles.DismissReason final int reason = removed.second;
 
                 if (mStackView != null) {
                     mStackView.removeBubble(bubble);
@@ -1029,8 +1041,7 @@
         }
     };
 
-    @Override
-    public boolean handleDismissalInterception(BubbleEntry entry,
+    private boolean handleDismissalInterception(BubbleEntry entry,
             @Nullable List<BubbleEntry> children, IntConsumer removeCallback) {
         if (isSummaryOfBubbles(entry)) {
             handleSummaryDismissalInterception(entry, children, removeCallback);
@@ -1137,8 +1148,7 @@
     /**
      * Description of current bubble state.
      */
-    @Override
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+    private void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("BubbleController state:");
         mBubbleData.dump(fd, pw, args);
         pw.println();
@@ -1216,4 +1226,175 @@
             }
         }
     }
+
+    private class BubblesImpl implements Bubbles {
+        @Override
+        public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
+            return mMainExecutor.executeBlockingForResult(() -> {
+                return BubbleController.this.isBubbleNotificationSuppressedFromShade(key, groupKey);
+            }, Boolean.class);
+        }
+
+        @Override
+        public boolean isBubbleExpanded(String key) {
+            return mMainExecutor.executeBlockingForResult(() -> {
+                return BubbleController.this.isBubbleExpanded(key);
+            }, Boolean.class);
+        }
+
+        @Override
+        public boolean isStackExpanded() {
+            return mMainExecutor.executeBlockingForResult(() -> {
+                return BubbleController.this.isStackExpanded();
+            }, Boolean.class);
+        }
+
+        @Override
+        public void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
+                Executor callbackExecutor) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.removeSuppressedSummaryIfNecessary(groupKey, callback,
+                        callbackExecutor);
+            });
+        }
+
+        @Override
+        public void collapseStack() {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.collapseStack();
+            });
+        }
+
+        @Override
+        public void updateForThemeChanges() {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.updateForThemeChanges();
+            });
+        }
+
+        @Override
+        public void expandStackAndSelectBubble(BubbleEntry entry) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.expandStackAndSelectBubble(entry);
+            });
+        }
+
+        @Override
+        public void onTaskbarChanged(Bundle b) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onTaskbarChanged(b);
+            });
+        }
+
+        @Override
+        public void openBubbleOverflow() {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.openBubbleOverflow();
+            });
+        }
+
+        @Override
+        public boolean handleDismissalInterception(BubbleEntry entry,
+                @Nullable List<BubbleEntry> children, IntConsumer removeCallback) {
+            return mMainExecutor.executeBlockingForResult(() -> {
+                return BubbleController.this.handleDismissalInterception(entry, children,
+                        removeCallback);
+            }, Boolean.class);
+        }
+
+        @Override
+        public void setSysuiProxy(SysuiProxy proxy) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.setSysuiProxy(proxy);
+            });
+        }
+
+        @Override
+        public void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.setBubbleScrim(view, callback);
+            });
+        }
+
+        @Override
+        public void setExpandListener(BubbleExpandListener listener) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.setExpandListener(listener);
+            });
+        }
+
+        @Override
+        public void onEntryAdded(BubbleEntry entry) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onEntryAdded(entry);
+            });
+        }
+
+        @Override
+        public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onEntryUpdated(entry, shouldBubbleUp);
+            });
+        }
+
+        @Override
+        public void onEntryRemoved(BubbleEntry entry) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onEntryRemoved(entry);
+            });
+        }
+
+        @Override
+        public void onRankingUpdated(RankingMap rankingMap) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onRankingUpdated(rankingMap);
+            });
+        }
+
+        @Override
+        public void onStatusBarVisibilityChanged(boolean visible) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onStatusBarVisibilityChanged(visible);
+            });
+        }
+
+        @Override
+        public void onZenStateChanged() {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onZenStateChanged();
+            });
+        }
+
+        @Override
+        public void onStatusBarStateChanged(boolean isShade) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onStatusBarStateChanged(isShade);
+            });
+        }
+
+        @Override
+        public void onUserChanged(int newUserId) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onUserChanged(newUserId);
+            });
+        }
+
+        @Override
+        public void onConfigChanged(Configuration newConfig) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onConfigChanged(newConfig);
+            });
+        }
+
+        @Override
+        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            try {
+                mMainExecutor.executeBlocking(() -> {
+                    BubbleController.this.dump(fd, pw, args);
+                });
+            } catch (InterruptedException e) {
+                Slog.e(TAG, "Failed to dump BubbleController in 2s");
+            }
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index e24ff06..9d196ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -46,6 +46,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -117,6 +118,7 @@
 
     private final Context mContext;
     private final BubblePositioner mPositioner;
+    private final Executor mMainExecutor;
     /** Bubbles that are actively in the stack. */
     private final List<Bubble> mBubbles;
     /** Bubbles that aged out to overflow. */
@@ -155,10 +157,12 @@
      */
     private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();
 
-    public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner) {
+    public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner,
+            Executor mainExecutor) {
         mContext = context;
         mLogger = bubbleLogger;
         mPositioner = positioner;
+        mMainExecutor = mainExecutor;
         mOverflow = new BubbleOverflow(context, positioner);
         mBubbles = new ArrayList<>();
         mOverflowBubbles = new ArrayList<>();
@@ -264,7 +268,8 @@
                 bubbleToReturn = mPendingBubbles.get(key);
             } else if (entry != null) {
                 // New bubble
-                bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener);
+                bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener,
+                        mMainExecutor);
             } else {
                 // Persisted bubble being promoted
                 bubbleToReturn = persistedBubble;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
index fc565f1..3108b02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
@@ -27,6 +27,8 @@
 import com.android.wm.shell.bubbles.storage.BubbleEntity
 import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
 import com.android.wm.shell.bubbles.storage.BubbleVolatileRepository
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.common.annotations.ExternalThread
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
@@ -34,12 +36,12 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.yield
 
-internal class BubbleDataRepository(context: Context, private val launcherApps: LauncherApps) {
+internal class BubbleDataRepository(context: Context, private val launcherApps: LauncherApps,
+        private val mainExecutor : ShellExecutor) {
     private val volatileRepository = BubbleVolatileRepository(launcherApps)
     private val persistentRepository = BubblePersistentRepository(context)
 
     private val ioScope = CoroutineScope(Dispatchers.IO)
-    private val uiScope = CoroutineScope(Dispatchers.Main)
     private var job: Job? = null
 
     /**
@@ -109,6 +111,8 @@
 
     /**
      * Load bubbles from disk.
+     * @param cb The callback to be run after the bubbles are loaded.  This callback is always made
+     *           on the main thread of the hosting process.
      */
     @SuppressLint("WrongConstant")
     fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch {
@@ -163,10 +167,11 @@
                             shortcutInfo,
                             entity.desiredHeight,
                             entity.desiredHeightResId,
-                            entity.title
+                            entity.title,
+                            mainExecutor
                     ) }
         }
-        uiScope.launch { cb(bubbles) }
+        mainExecutor.execute { cb(bubbles) }
     }
 }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index fac3686..af421fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -79,6 +79,7 @@
 import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout;
 import com.android.wm.shell.bubbles.animation.StackAnimationController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
 import java.io.FileDescriptor;
@@ -147,7 +148,7 @@
      * Handler to use for all delayed animations - this way, we can easily cancel them before
      * starting a new animation.
      */
-    private final Handler mDelayedAnimationHandler = new Handler();
+    private final ShellExecutor mDelayedAnimationExecutor;
 
     /**
      * Interface to synchronize {@link View} state and the screen.
@@ -311,7 +312,7 @@
         }
     }
 
-    private BubbleController.BubbleExpandListener mExpandListener;
+    private Bubbles.BubbleExpandListener mExpandListener;
 
     /** Callback to run when we want to unbubble the given notification's conversation. */
     private Consumer<String> mUnbubbleConversationCallback;
@@ -734,9 +735,11 @@
     @SuppressLint("ClickableViewAccessibility")
     public BubbleStackView(Context context, BubbleController bubbleController,
             BubbleData data, @Nullable SurfaceSynchronizer synchronizer,
-            FloatingContentCoordinator floatingContentCoordinator) {
+            FloatingContentCoordinator floatingContentCoordinator,
+            ShellExecutor mainExecutor) {
         super(context);
 
+        mDelayedAnimationExecutor = mainExecutor;
         mBubbleController = bubbleController;
         mBubbleData = data;
 
@@ -1366,7 +1369,7 @@
     /**
      * Sets the listener to notify when the bubble stack is expanded.
      */
-    public void setExpandListener(BubbleController.BubbleExpandListener listener) {
+    public void setExpandListener(Bubbles.BubbleExpandListener listener) {
         mExpandListener = listener;
     }
 
@@ -1734,7 +1737,7 @@
             mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
         }
 
-        mDelayedAnimationHandler.postDelayed(() -> {
+        mDelayedAnimationExecutor.executeDelayed(() -> {
             PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
             PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
                     .spring(AnimatableScaleMatrix.SCALE_X,
@@ -1791,10 +1794,12 @@
 
         final long startDelay =
                 (long) (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 0.6f);
-        mDelayedAnimationHandler.postDelayed(() -> mExpandedAnimationController.collapseBackToStack(
-                mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
-                /* collapseTo */,
-                () -> mBubbleContainer.setActiveController(mStackAnimationController)), startDelay);
+        mDelayedAnimationExecutor.executeDelayed(() -> {
+            mExpandedAnimationController.collapseBackToStack(
+                    mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
+                    /* collapseTo */,
+                    () -> mBubbleContainer.setActiveController(mStackAnimationController));
+        }, startDelay);
 
         if (mTaskbarScrim.getVisibility() == VISIBLE) {
             mTaskbarScrim.animate().alpha(0f).start();
@@ -1945,7 +1950,7 @@
 
         mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
 
-        mDelayedAnimationHandler.postDelayed(() -> {
+        mDelayedAnimationExecutor.executeDelayed(() -> {
             if (!mIsExpanded) {
                 mIsBubbleSwitchAnimating = false;
                 return;
@@ -1978,7 +1983,7 @@
      * animating flags for those animations.
      */
     private void cancelDelayedExpandCollapseSwitchAnimations() {
-        mDelayedAnimationHandler.removeCallbacksAndMessages(null);
+        mDelayedAnimationExecutor.removeAllCallbacks();
 
         mIsExpansionAnimating = false;
         mIsBubbleSwitchAnimating = false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index 0e7e92d..c5a712e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -46,6 +46,7 @@
 
 import java.lang.ref.WeakReference;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * Simple task to inflate views & load necessary info to display a bubble.
@@ -71,6 +72,7 @@
     private BubbleIconFactory mIconFactory;
     private boolean mSkipInflation;
     private Callback mCallback;
+    private Executor mMainExecutor;
 
     /**
      * Creates a task to load information for the provided {@link Bubble}. Once all info
@@ -82,7 +84,8 @@
             BubbleStackView stackView,
             BubbleIconFactory factory,
             boolean skipInflation,
-            Callback c) {
+            Callback c,
+            Executor mainExecutor) {
         mBubble = b;
         mContext = new WeakReference<>(context);
         mController = new WeakReference<>(controller);
@@ -90,6 +93,7 @@
         mIconFactory = factory;
         mSkipInflation = skipInflation;
         mCallback = c;
+        mMainExecutor = mainExecutor;
     }
 
     @Override
@@ -103,10 +107,12 @@
         if (isCancelled() || viewInfo == null) {
             return;
         }
-        mBubble.setViewInfo(viewInfo);
-        if (mCallback != null) {
-            mCallback.onBubbleViewsReady(mBubble);
-        }
+        mMainExecutor.execute(() -> {
+            mBubble.setViewInfo(viewInfo);
+            if (mCallback != null) {
+                mCallback.onBubbleViewsReady(mBubble);
+            }
+        });
     }
 
     /**
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 fa5ac44..6102147 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
@@ -23,6 +23,7 @@
 
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.os.Looper;
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.util.ArraySet;
 import android.view.View;
@@ -37,6 +38,9 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -86,13 +90,14 @@
     /** @return {@code true} if stack of bubbles is expanded or not. */
     boolean isStackExpanded();
 
-    /** @return {@code true} if the summary for the provided group key is suppressed. */
-    boolean isSummarySuppressed(String groupKey);
-
     /**
-     * Removes a group key indicating that summary for this group should no longer be suppressed.
+     * Removes a group key indicating that the summary for this group should no longer be
+     * suppressed.
+     *
+     * @param callback If removed, this callback will be called with the summary key of the group
      */
-    void removeSuppressedSummary(String groupKey);
+    void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
+            Executor callbackExecutor);
 
     /** Tell the stack of bubbles to collapse. */
     void collapseStack();
@@ -134,19 +139,16 @@
     boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children,
             IntConsumer removeCallback);
 
-    /**
-     * Retrieves the notif entry key of the summary associated with the provided group key.
-     *
-     * @param groupKey the group to look up
-     * @return the key for the notification that is the summary of this group.
-     */
-    String getSummaryKey(String groupKey);
-
     /** Set the proxy to commnuicate with SysUi side components. */
     void setSysuiProxy(SysuiProxy proxy);
 
-    /** Set the scrim view for bubbles. */
-    void setBubbleScrim(View view);
+    /**
+     * Set the scrim view for bubbles.
+     *
+     * @param callback The callback made with the executor and the executor's looper that the view
+     *                 will be running on.
+     **/
+    void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback);
 
     /** Set a listener to be notified of bubble expand events. */
     void setExpandListener(BubbleExpandListener listener);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt
index a1b0dbe..cf0cefe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt
@@ -91,7 +91,6 @@
     private var touchSlop: Int = -1
     private var movedEnough = false
 
-    private val handler = Handler(Looper.myLooper()!!)
     private var performedLongClick = false
 
     @Suppress("UNCHECKED_CAST")
@@ -115,7 +114,7 @@
                 viewPositionOnTouchDown.set(v.translationX, v.translationY)
 
                 performedLongClick = false
-                handler.postDelayed({
+                v.handler.postDelayed({
                     if (v.isLongClickable) {
                         performedLongClick = v.performLongClick()
                     }
@@ -125,7 +124,7 @@
             MotionEvent.ACTION_MOVE -> {
                 if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) {
                     movedEnough = true
-                    handler.removeCallbacksAndMessages(null)
+                    v.handler.removeCallbacksAndMessages(null)
                 }
 
                 if (movedEnough) {
@@ -141,7 +140,7 @@
                 } else if (!performedLongClick) {
                     v.performClick()
                 } else {
-                    handler.removeCallbacksAndMessages(null)
+                    v.handler.removeCallbacksAndMessages(null)
                 }
 
                 velocityTracker.clear()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
index b736fb0..6abc8f6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
@@ -20,6 +20,7 @@
 import android.os.SystemClock;
 import android.os.Trace;
 
+import java.lang.reflect.Array;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
@@ -68,6 +69,26 @@
     }
 
     /**
+     * Convenience method to execute the blocking call with a default timeout and returns a value.
+     * Waits indefinitely for a typed result from a call.
+     */
+    default <T> T executeBlockingForResult(Supplier<T> runnable, Class clazz) {
+        final T[] result = (T[]) Array.newInstance(clazz, 1);
+        final CountDownLatch latch = new CountDownLatch(1);
+        execute(() -> {
+            result[0] = runnable.get();
+            latch.countDown();
+        });
+        try {
+            latch.await();
+            return result[0];
+        } catch (InterruptedException e) {
+            return null;
+        }
+    }
+
+
+    /**
      * See {@link android.os.Handler#postDelayed(Runnable, long)}.
      */
     void executeDelayed(Runnable runnable, long delayMillis);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 7c9b9c3..d3a736e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -41,6 +41,7 @@
 
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.bubbles.BubbleData.TimeSource;
+import com.android.wm.shell.common.ShellExecutor;
 
 import com.google.common.collect.ImmutableList;
 
@@ -98,6 +99,8 @@
     private PendingIntent mDeleteIntent;
     @Mock
     private BubbleLogger mBubbleLogger;
+    @Mock
+    private ShellExecutor mMainExecutor;
 
     @Captor
     private ArgumentCaptor<BubbleData.Update> mUpdateCaptor;
@@ -124,21 +127,31 @@
                 mock(NotificationListenerService.Ranking.class);
         when(ranking.visuallyInterruptive()).thenReturn(true);
         mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking);
-        mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null);
+        mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null,
+                mMainExecutor);
 
         mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null);
-        mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null);
+        mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null,
+                mMainExecutor);
 
-        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);
+        mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener,
+                mMainExecutor);
+        mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener,
+                mMainExecutor);
+        mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener,
+                mMainExecutor);
+        mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener,
+                mMainExecutor);
+        mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener,
+                mMainExecutor);
+        mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener,
+                mMainExecutor);
+        mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener,
+                mMainExecutor);
         TestableBubblePositioner positioner = new TestableBubblePositioner(mContext,
                 mock(WindowManager.class));
-        mBubbleData = new BubbleData(getContext(), mBubbleLogger, positioner);
+        mBubbleData = new BubbleData(getContext(), mBubbleLogger, positioner,
+                mMainExecutor);
 
         // Used by BubbleData to set lastAccessedTime
         when(mTimeSource.currentTimeMillis()).thenReturn(1000L);
@@ -796,7 +809,7 @@
         assertWithMessage("addedBubble").that(update.addedBubble).isEqualTo(expected);
     }
 
-    private void assertBubbleRemoved(Bubble expected, @BubbleController.DismissReason int reason) {
+    private void assertBubbleRemoved(Bubble expected, @Bubbles.DismissReason int reason) {
         BubbleData.Update update = mUpdateCaptor.getValue();
         assertWithMessage("removedBubbles").that(update.removedBubbles)
                 .isEqualTo(ImmutableList.of(Pair.create(expected, reason)));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
index bde04b6..fc828b3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java
@@ -39,6 +39,7 @@
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -54,6 +55,8 @@
     private Notification mNotif;
     @Mock
     private StatusBarNotification mSbn;
+    @Mock
+    private ShellExecutor mMainExecutor;
 
     private BubbleEntry mBubbleEntry;
     private Bundle mExtras;
@@ -78,7 +81,7 @@
         when(mNotif.getBubbleMetadata()).thenReturn(metadata);
         when(mSbn.getKey()).thenReturn("mock");
         mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false);
-        mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null);
+        mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null, mMainExecutor);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java
index 0d4d126..35656bd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchStateTest.java
@@ -47,17 +47,17 @@
     private PipTouchState mTouchState;
     private CountDownLatch mDoubleTapCallbackTriggeredLatch;
     private CountDownLatch mHoverExitCallbackTriggeredLatch;
-    private TestShellExecutor mShellMainExecutor;
+    private TestShellExecutor mMainExecutor;
 
     @Before
     public void setUp() throws Exception {
-        mShellMainExecutor = new TestShellExecutor();
+        mMainExecutor = new TestShellExecutor();
         mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1);
         mHoverExitCallbackTriggeredLatch = new CountDownLatch(1);
         mTouchState = new PipTouchState(ViewConfiguration.get(getContext()),
                 mDoubleTapCallbackTriggeredLatch::countDown,
                 mHoverExitCallbackTriggeredLatch::countDown,
-                mShellMainExecutor);
+                mMainExecutor);
         assertFalse(mTouchState.isDoubleTap());
         assertFalse(mTouchState.isWaitingForDoubleTap());
     }
@@ -87,7 +87,7 @@
         assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10);
         mTouchState.scheduleDoubleTapTimeoutCallback();
 
-        mShellMainExecutor.flushAll();
+        mMainExecutor.flushAll();
         assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0);
     }
 
@@ -122,7 +122,7 @@
     @Test
     public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception {
         mTouchState.scheduleHoverExitTimeoutCallback();
-        mShellMainExecutor.flushAll();
+        mMainExecutor.flushAll();
         assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0);
     }
 
@@ -137,7 +137,7 @@
         mTouchState.scheduleHoverExitTimeoutCallback();
         mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(),
                 0, 0));
-        mShellMainExecutor.flushAll();
+        mMainExecutor.flushAll();
         assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 4d69700..b0067cd 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -29,6 +29,7 @@
 import com.android.systemui.assist.AssistModule;
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.controls.dagger.ControlsModule;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.dagger.DemoModeModule;
 import com.android.systemui.doze.dagger.DozeComponent;
 import com.android.systemui.dump.DumpManager;
@@ -75,6 +76,7 @@
 import com.android.wm.shell.bubbles.Bubbles;
 
 import java.util.Optional;
+import java.util.concurrent.Executor;
 
 import dagger.Binds;
 import dagger.BindsOptionalOf;
@@ -153,6 +155,7 @@
     @Binds
     abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
 
+    // TODO: This should provided by the WM component
     /** Provides Optional of BubbleManager */
     @SysUISingleton
     @Provides
@@ -166,11 +169,12 @@
             ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager,
             NotificationGroupManagerLegacy groupManager, NotificationEntryManager entryManager,
             NotifPipeline notifPipeline, SysUiState sysUiState, FeatureFlags featureFlags,
-            DumpManager dumpManager) {
+            DumpManager dumpManager, @Main Executor sysuiMainExecutor) {
         return Optional.ofNullable(BubblesManager.create(context, bubblesOptional,
                 notificationShadeWindowController, statusBarStateController, shadeController,
                 configurationController, statusBarService, notificationManager,
                 interruptionStateProvider, zenModeController, notifUserManager,
-                groupManager, entryManager, notifPipeline, sysUiState, featureFlags, dumpManager));
+                groupManager, entryManager, notifPipeline, sysUiState, featureFlags, dumpManager,
+                sysuiMainExecutor));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
index 7f30009..6023b7f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
@@ -26,25 +26,41 @@
 import android.graphics.PorterDuff.Mode;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
 import android.util.AttributeSet;
 import android.view.View;
 
 import androidx.core.graphics.ColorUtils;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.colorextraction.drawable.ScrimDrawable;
 
+import java.util.concurrent.Executor;
+
+
 /**
- * A view which can draw a scrim
+ * A view which can draw a scrim.  This view maybe be used in multiple windows running on different
+ * threads, but is controlled by {@link com.android.systemui.statusbar.phone.ScrimController} so we
+ * need to be careful to synchronize when necessary.
  */
 public class ScrimView extends View {
+    private final Object mColorLock = new Object();
+
+    @GuardedBy("mColorLock")
     private final ColorExtractor.GradientColors mColors;
+    // Used only for returning the colors
+    private final ColorExtractor.GradientColors mTmpColors = new ColorExtractor.GradientColors();
     private float mViewAlpha = 1.0f;
     private Drawable mDrawable;
     private PorterDuffColorFilter mColorFilter;
     private int mTintColor;
     private Runnable mChangeRunnable;
+    private Executor mChangeRunnableExecutor;
+    private Executor mExecutor;
+    private Looper mExecutorLooper;
 
     public ScrimView(Context context) {
         this(context, null);
@@ -64,7 +80,16 @@
         mDrawable = new ScrimDrawable();
         mDrawable.setCallback(this);
         mColors = new ColorExtractor.GradientColors();
-        updateColorWithTint(false);
+        mExecutorLooper = Looper.myLooper();
+        mExecutor = Runnable::run;
+        executeOnExecutor(() -> {
+            updateColorWithTint(false);
+        });
+    }
+
+    public void setExecutor(Executor executor, Looper looper) {
+        mExecutor = executor;
+        mExecutorLooper = looper;
     }
 
     @Override
@@ -75,11 +100,13 @@
     }
 
     public void setDrawable(Drawable drawable) {
-        mDrawable = drawable;
-        mDrawable.setCallback(this);
-        mDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom());
-        mDrawable.setAlpha((int) (255 * mViewAlpha));
-        invalidate();
+        executeOnExecutor(() -> {
+            mDrawable = drawable;
+            mDrawable.setCallback(this);
+            mDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom());
+            mDrawable.setAlpha((int) (255 * mViewAlpha));
+            invalidate();
+        });
     }
 
     @Override
@@ -99,6 +126,13 @@
         }
     }
 
+    @Override
+    public void setClickable(boolean clickable) {
+        executeOnExecutor(() -> {
+            super.setClickable(clickable);
+        });
+    }
+
     public void setColors(@NonNull ColorExtractor.GradientColors colors) {
         setColors(colors, false);
     }
@@ -107,11 +141,15 @@
         if (colors == null) {
             throw new IllegalArgumentException("Colors cannot be null");
         }
-        if (mColors.equals(colors)) {
-            return;
-        }
-        mColors.set(colors);
-        updateColorWithTint(animated);
+        executeOnExecutor(() -> {
+            synchronized(mColorLock) {
+                if (mColors.equals(colors)) {
+                    return;
+                }
+                mColors.set(colors);
+            }
+            updateColorWithTint(animated);
+        });
     }
 
     @VisibleForTesting
@@ -120,7 +158,10 @@
     }
 
     public ColorExtractor.GradientColors getColors() {
-        return mColors;
+        synchronized(mColorLock) {
+            mTmpColors.set(mColors);
+        }
+        return mTmpColors;
     }
 
     public void setTint(int color) {
@@ -128,11 +169,13 @@
     }
 
     public void setTint(int color, boolean animated) {
-        if (mTintColor == color) {
-            return;
-        }
-        mTintColor = color;
-        updateColorWithTint(animated);
+        executeOnExecutor(() -> {
+            if (mTintColor == color) {
+                return;
+            }
+            mTintColor = color;
+            updateColorWithTint(animated);
+        });
     }
 
     private void updateColorWithTint(boolean animated) {
@@ -160,7 +203,7 @@
         }
 
         if (mChangeRunnable != null) {
-            mChangeRunnable.run();
+            mChangeRunnableExecutor.execute(mChangeRunnable);
         }
     }
 
@@ -184,26 +227,37 @@
         if (isNaN(alpha)) {
             throw new IllegalArgumentException("alpha cannot be NaN: " + alpha);
         }
-        if (alpha != mViewAlpha) {
-            mViewAlpha = alpha;
+        executeOnExecutor(() -> {
+            if (alpha != mViewAlpha) {
+                mViewAlpha = alpha;
 
-            mDrawable.setAlpha((int) (255 * alpha));
-            if (mChangeRunnable != null) {
-                mChangeRunnable.run();
+                mDrawable.setAlpha((int) (255 * alpha));
+                if (mChangeRunnable != null) {
+                    mChangeRunnableExecutor.execute(mChangeRunnable);
+                }
             }
-        }
+        });
     }
 
     public float getViewAlpha() {
         return mViewAlpha;
     }
 
-    public void setChangeRunnable(Runnable changeRunnable) {
+    public void setChangeRunnable(Runnable changeRunnable, Executor changeRunnableExecutor) {
         mChangeRunnable = changeRunnable;
+        mChangeRunnableExecutor = changeRunnableExecutor;
     }
 
     @Override
     protected boolean canReceivePointerEvents() {
         return false;
     }
+
+    private void executeOnExecutor(Runnable r) {
+        if (mExecutor == null || Looper.myLooper() == mExecutorLooper) {
+            r.run();
+        } else {
+            mExecutor.execute(r);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 19c0b6d..e39065b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -48,6 +48,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.statusbar.BlurUtils;
 import com.android.systemui.statusbar.FeatureFlags;
@@ -65,6 +66,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
@@ -150,6 +152,7 @@
     private final AlarmTimeout mTimeTicker;
     private final KeyguardVisibilityCallback mKeyguardVisibilityCallback;
     private final Handler mHandler;
+    private final Executor mMainExecutor;
     private final BlurUtils mBlurUtils;
 
     private GradientColors mColors;
@@ -214,8 +217,7 @@
             DelayedWakeLock.Builder delayedWakeLockBuilder, Handler handler,
             KeyguardUpdateMonitor keyguardUpdateMonitor, DockManager dockManager,
             BlurUtils blurUtils, ConfigurationController configurationController,
-            FeatureFlags featureFlags) {
-
+            FeatureFlags featureFlags, @Main Executor mainExecutor) {
         mScrimStateListener = lightBarController::setScrimState;
         mDefaultScrimAlpha = featureFlags.isShadeOpaque() ? BUSY_SCRIM_ALPHA : GAR_SCRIM_ALPHA;
         ScrimState.BUBBLE_EXPANDED.setBubbleAlpha(featureFlags.isShadeOpaque()
@@ -228,6 +230,7 @@
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mKeyguardVisibilityCallback = new KeyguardVisibilityCallback();
         mHandler = handler;
+        mMainExecutor = mainExecutor;
         mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout,
                 "hide_aod_wallpaper", mHandler);
         mWakeLock = delayedWakeLockBuilder.setHandler(mHandler).setTag("Scrims").build();
@@ -273,7 +276,7 @@
         updateThemeColors();
 
         if (mScrimBehindChangeRunnable != null) {
-            mScrimBehind.setChangeRunnable(mScrimBehindChangeRunnable);
+            mScrimBehind.setChangeRunnable(mScrimBehindChangeRunnable, mMainExecutor);
             mScrimBehindChangeRunnable = null;
         }
 
@@ -1022,7 +1025,7 @@
         if (mScrimBehind == null) {
             mScrimBehindChangeRunnable = changeRunnable;
         } else {
-            mScrimBehind.setChangeRunnable(changeRunnable);
+            mScrimBehind.setChangeRunnable(changeRunnable, mMainExecutor);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index b20c457..e08224c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -841,8 +841,10 @@
 
         mBubbleExpandListener =
                 (isExpanding, key) -> {
-                    mNotificationsController.requestNotificationUpdate("onBubbleExpandChanged");
-                    updateScrimController();
+                    mContext.getMainExecutor().execute(() -> {
+                        mNotificationsController.requestNotificationUpdate("onBubbleExpandChanged");
+                        updateScrimController();
+                    });
                 };
 
         mActivityIntentHelper = new ActivityIntentHelper(mContext);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index bf823b4..6e7aed0 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -38,6 +38,7 @@
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.service.notification.NotificationListenerService.RankingMap;
@@ -83,10 +84,14 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.function.IntConsumer;
+import java.util.function.Supplier;
 
 /**
  * The SysUi side bubbles manager which communicate with other SysUi components.
@@ -106,8 +111,9 @@
     private final NotificationGroupManagerLegacy mNotificationGroupManager;
     private final NotificationEntryManager mNotificationEntryManager;
     private final NotifPipeline mNotifPipeline;
+    private final Executor mSysuiMainExecutor;
 
-    private final ScrimView mBubbleScrim;
+    private ScrimView mBubbleScrim;
     private final Bubbles.SysuiProxy mSysuiProxy;
     // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline
     private final List<NotifCallback> mCallbacks = new ArrayList<>();
@@ -133,14 +139,15 @@
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
             FeatureFlags featureFlags,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            Executor sysuiMainExecutor) {
         if (bubblesOptional.isPresent()) {
             return new BubblesManager(context, bubblesOptional.get(),
                     notificationShadeWindowController, statusBarStateController, shadeController,
                     configurationController, statusBarService, notificationManager,
                     interruptionStateProvider, zenModeController, notifUserManager,
                     groupManager, entryManager, notifPipeline, sysUiState, featureFlags,
-                    dumpManager);
+                    dumpManager, sysuiMainExecutor);
         } else {
             return null;
         }
@@ -163,7 +170,8 @@
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
             FeatureFlags featureFlags,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            Executor sysuiMainExecutor) {
         mContext = context;
         mBubbles = bubbles;
         mNotificationShadeWindowController = notificationShadeWindowController;
@@ -173,6 +181,7 @@
         mNotificationGroupManager = groupManager;
         mNotificationEntryManager = entryManager;
         mNotifPipeline = notifPipeline;
+        mSysuiMainExecutor = sysuiMainExecutor;
 
         mBarService = statusBarService == null
                 ? IStatusBarService.Stub.asInterface(
@@ -181,7 +190,9 @@
 
         mBubbleScrim = new ScrimView(mContext);
         mBubbleScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-        mBubbles.setBubbleScrim(mBubbleScrim);
+        mBubbles.setBubbleScrim(mBubbleScrim, (executor, looper) -> {
+            mBubbleScrim.setExecutor(executor, looper);
+        });
 
         if (featureFlags.isNewNotifPipelineRenderingEnabled()) {
             setupNotifPipeline();
@@ -237,128 +248,177 @@
                 });
 
         mSysuiProxy = new Bubbles.SysuiProxy() {
+            private <T> T executeBlockingForResult(Supplier<T> runnable, Executor executor,
+                    Class clazz) {
+                if (Looper.myLooper() == Looper.getMainLooper()) {
+                    return runnable.get();
+                }
+                final T[] result = (T[]) Array.newInstance(clazz, 1);
+                final CountDownLatch latch = new CountDownLatch(1);
+                executor.execute(() -> {
+                    result[0] = runnable.get();
+                    latch.countDown();
+                });
+                try {
+                    latch.await();
+                    return result[0];
+                } catch (InterruptedException e) {
+                    return null;
+                }
+            }
+
             @Override
             @Nullable
             public BubbleEntry getPendingOrActiveEntry(String key) {
-                NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(key);
-                return entry == null ? null : notifToBubbleEntry(entry);
+                return executeBlockingForResult(() -> {
+                    NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    return entry == null ? null : notifToBubbleEntry(entry);
+                }, sysuiMainExecutor, BubbleEntry.class);
             }
 
             @Override
             public List<BubbleEntry> getShouldRestoredEntries(ArraySet<String> savedBubbleKeys) {
-                List<BubbleEntry> result = new ArrayList<>();
-                List<NotificationEntry> activeEntries =
-                        mNotificationEntryManager.getActiveNotificationsForCurrentUser();
-                for (int i = 0; i < activeEntries.size(); i++) {
-                    NotificationEntry entry = activeEntries.get(i);
-                    if (savedBubbleKeys.contains(entry.getKey())
-                            && mNotificationInterruptStateProvider.shouldBubbleUp(entry)
-                            && entry.isBubble()) {
-                        result.add(notifToBubbleEntry(entry));
+                return executeBlockingForResult(() -> {
+                    List<BubbleEntry> result = new ArrayList<>();
+                    List<NotificationEntry> activeEntries =
+                            mNotificationEntryManager.getActiveNotificationsForCurrentUser();
+                    for (int i = 0; i < activeEntries.size(); i++) {
+                        NotificationEntry entry = activeEntries.get(i);
+                        if (savedBubbleKeys.contains(entry.getKey())
+                                && mNotificationInterruptStateProvider.shouldBubbleUp(entry)
+                                && entry.isBubble()) {
+                            result.add(notifToBubbleEntry(entry));
+                        }
                     }
-                }
-                return result;
+                    return result;
+                }, sysuiMainExecutor, List.class);
             }
 
             @Override
             public boolean isNotificationShadeExpand() {
-                return mNotificationShadeWindowController.getPanelExpanded();
+                return executeBlockingForResult(() -> {
+                    return mNotificationShadeWindowController.getPanelExpanded();
+                }, sysuiMainExecutor, Boolean.class);
             }
 
             @Override
             public boolean shouldBubbleUp(String key) {
-                final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(
-                        key);
-                if (entry != null) {
-                    return mNotificationInterruptStateProvider.shouldBubbleUp(entry);
-                }
-                return false;
+                return executeBlockingForResult(() -> {
+                    final NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    if (entry != null) {
+                        return mNotificationInterruptStateProvider.shouldBubbleUp(entry);
+                    }
+                    return false;
+                }, sysuiMainExecutor, Boolean.class);
             }
 
             @Override
             public void setNotificationInterruption(String key) {
-                final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(
-                        key);
-                if (entry != null && entry.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
-                    entry.setInterruption();
-                }
+                sysuiMainExecutor.execute(() -> {
+                    final NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    if (entry != null
+                            && entry.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
+                        entry.setInterruption();
+                    }
+                });
             }
 
             @Override
             public void requestNotificationShadeTopUi(boolean requestTopUi, String componentTag) {
-                mNotificationShadeWindowController.setRequestTopUi(requestTopUi, componentTag);
+                sysuiMainExecutor.execute(() -> {
+                    mNotificationShadeWindowController.setRequestTopUi(requestTopUi, componentTag);
+                });
             }
 
             @Override
             public void notifyRemoveNotification(String key, int reason) {
-                final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(
-                        key);
-                if (entry != null) {
-                    for (NotifCallback cb : mCallbacks) {
-                        cb.removeNotification(entry, getDismissedByUserStats(entry, true), reason);
+                sysuiMainExecutor.execute(() -> {
+                    final NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    if (entry != null) {
+                        for (NotifCallback cb : mCallbacks) {
+                            cb.removeNotification(entry, getDismissedByUserStats(entry, true),
+                                    reason);
+                        }
                     }
-                }
+                });
             }
 
             @Override
             public void notifyInvalidateNotifications(String reason) {
-                for (NotifCallback cb : mCallbacks) {
-                    cb.invalidateNotifications(reason);
-                }
+                sysuiMainExecutor.execute(() -> {
+                    for (NotifCallback cb : mCallbacks) {
+                        cb.invalidateNotifications(reason);
+                    }
+                });
             }
 
             @Override
             public void notifyMaybeCancelSummary(String key) {
-                final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(
-                        key);
-                if (entry != null) {
-                    for (NotifCallback cb : mCallbacks) {
-                        cb.maybeCancelSummary(entry);
+                sysuiMainExecutor.execute(() -> {
+                    final NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    if (entry != null) {
+                        for (NotifCallback cb : mCallbacks) {
+                            cb.maybeCancelSummary(entry);
+                        }
                     }
-                }
+                });
             }
 
             @Override
             public void removeNotificationEntry(String key) {
-                final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(
-                        key);
-                if (entry != null) {
-                    mNotificationGroupManager.onEntryRemoved(entry);
-                }
+                sysuiMainExecutor.execute(() -> {
+                    final NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    if (entry != null) {
+                        mNotificationGroupManager.onEntryRemoved(entry);
+                    }
+                });
             }
 
             @Override
             public void updateNotificationBubbleButton(String key) {
-                final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(
-                        key);
-                if (entry != null && entry.getRow() != null) {
-                    entry.getRow().updateBubbleButton();
-                }
+                sysuiMainExecutor.execute(() -> {
+                    final NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    if (entry != null && entry.getRow() != null) {
+                        entry.getRow().updateBubbleButton();
+                    }
+                });
             }
 
             @Override
             public void updateNotificationSuppression(String key) {
-                final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(
-                        key);
-                if (entry != null) {
-                    mNotificationGroupManager.updateSuppression(entry);
-                }
+                sysuiMainExecutor.execute(() -> {
+                    final NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    if (entry != null) {
+                        mNotificationGroupManager.updateSuppression(entry);
+                    }
+                });
             }
 
             @Override
             public void onStackExpandChanged(boolean shouldExpand) {
-                sysUiState
-                        .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand)
-                        .commitUpdate(mContext.getDisplayId());
+                sysuiMainExecutor.execute(() -> {
+                    sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand)
+                            .commitUpdate(mContext.getDisplayId());
+                });
             }
 
             @Override
             public void onUnbubbleConversation(String key) {
-                final NotificationEntry entry =
-                        mNotificationEntryManager.getPendingOrActiveNotif(key);
-                if (entry != null) {
-                    onUserChangedBubble(entry, false /* shouldBubble */);
-                }
+                sysuiMainExecutor.execute(() -> {
+                    final NotificationEntry entry =
+                            mNotificationEntryManager.getPendingOrActiveNotif(key);
+                    if (entry != null) {
+                        onUserChangedBubble(entry, false /* shouldBubble */);
+                    }
+                });
             }
         };
         mBubbles.setSysuiProxy(mSysuiProxy);
@@ -424,9 +484,8 @@
                         final String groupKey = group.summary != null
                                 ? group.summary.getSbn().getGroupKey()
                                 : null;
-                        if (!suppressed && groupKey != null
-                                && mBubbles.isSummarySuppressed(groupKey)) {
-                            mBubbles.removeSuppressedSummary(groupKey);
+                        if (!suppressed && groupKey != null) {
+                            mBubbles.removeSuppressedSummaryIfNecessary(groupKey, null, null);
                         }
                     }
                 });
@@ -449,19 +508,16 @@
                 // Check if removed bubble has an associated suppressed group summary that needs
                 // to be removed now.
                 final String groupKey = entry.getSbn().getGroupKey();
-                if (mBubbles.isSummarySuppressed(groupKey)) {
-                    mBubbles.removeSuppressedSummary(groupKey);
-
+                mBubbles.removeSuppressedSummaryIfNecessary(groupKey, (summaryKey) -> {
                     final NotificationEntry summary =
-                            mNotificationEntryManager.getActiveNotificationUnfiltered(
-                                    mBubbles.getSummaryKey(groupKey));
+                            mNotificationEntryManager.getActiveNotificationUnfiltered(summaryKey);
                     if (summary != null) {
                         mNotificationEntryManager.performRemoveNotification(
                                 summary.getSbn(),
                                 getDismissedByUserStats(summary, false),
                                 UNDEFINED_DISMISS_REASON);
                     }
-                }
+                }, mSysuiMainExecutor);
 
                 // Check if we still need to remove the summary from NoManGroup because the summary
                 // may not be in the mBubbleData.mSuppressedGroupKeys list and removed above.
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
index 103e6bb..c2e4e14 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
@@ -331,6 +331,7 @@
     @BindsOptionalOf
     abstract AppPairs optionalAppPairs();
 
+    // Note: Handler needed for LauncherApps.register
     @WMSingleton
     @Provides
     static Optional<Bubbles> provideBubbles(Context context,
@@ -341,11 +342,12 @@
             LauncherApps launcherApps,
             UiEventLogger uiEventLogger,
             ShellTaskOrganizer organizer,
-            @ShellMainThread ShellExecutor mainExecutor) {
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler) {
         return Optional.of(BubbleController.create(context, null /* synchronizer */,
                 floatingContentCoordinator, statusBarService, windowManager,
                 windowManagerShellWrapper, launcherApps, uiEventLogger, organizer,
-                mainExecutor));
+                mainExecutor, mainHandler));
     }
 
     // Needs the shell main handler for ContentObserver callbacks
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 3b2e055..21368d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -54,6 +54,8 @@
 import com.android.systemui.statusbar.ScrimView;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.wakelock.DelayedWakeLock;
 import com.android.systemui.utils.os.FakeHandler;
 
@@ -220,7 +222,8 @@
         mScrimController = new ScrimController(mLightBarController,
                 mDozeParamenters, mAlarmManager, mKeyguardStateController, mDelayedWakeLockBuilder,
                 new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor,
-                mDockManager, mBlurUtils, mConfigurationController, mFeatureFlags);
+                mDockManager, mBlurUtils, mConfigurationController, mFeatureFlags,
+                new FakeExecutor(new FakeSystemClock()));
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mScrimInFront, mScrimForBubble);
         mScrimController.setAnimatorListener(mAnimatorListener);
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 ccc2eb3..76269dd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -166,6 +166,7 @@
     private ArgumentCaptor<NotificationRemoveInterceptor> mRemoveInterceptorCaptor;
 
     private BubblesManager mBubblesManager;
+    // TODO(178618782): Move tests on the controller directly to the shell
     private TestableBubbleController mBubbleController;
     private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
     private NotificationEntryListener mEntryListener;
@@ -221,6 +222,9 @@
 
         mTestableLooper = TestableLooper.get(this);
 
+        // For the purposes of this test, just run everything synchronously
+        ShellExecutor syncExecutor = new SyncExecutor();
+
         mContext.addMockSystemService(FaceManager.class, mFaceManager);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
 
@@ -257,8 +261,9 @@
                 mSysUiStateBubblesExpanded =
                         (sysUiFlags & QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED) != 0);
 
+        // TODO: Fix
         mPositioner = new TestableBubblePositioner(mContext, mWindowManager);
-        mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner);
+        mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, syncExecutor);
 
         TestableNotificationInterruptStateProviderImpl interruptionStateProvider =
                 new TestableNotificationInterruptStateProviderImpl(mContext.getContentResolver(),
@@ -273,7 +278,7 @@
                 );
 
         when(mFeatureFlagsOldPipeline.isNewNotifPipelineRenderingEnabled()).thenReturn(false);
-        when(mShellTaskOrganizer.getExecutor()).thenReturn(new FakeExecutor(new FakeSystemClock()));
+        when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
         mBubbleController = new TestableBubbleController(
                 mContext,
                 mBubbleData,
@@ -286,12 +291,13 @@
                 mBubbleLogger,
                 mShellTaskOrganizer,
                 mPositioner,
-                mock(ShellExecutor.class));
+                syncExecutor,
+                mock(Handler.class));
         mBubbleController.setExpandListener(mBubbleExpandListener);
 
         mBubblesManager = new BubblesManager(
                 mContext,
-                mBubbleController,
+                mBubbleController.getImpl(),
                 mNotificationShadeWindowController,
                 mStatusBarStateController,
                 mShadeController,
@@ -306,7 +312,8 @@
                 mNotifPipeline,
                 mSysUiState,
                 mFeatureFlagsOldPipeline,
-                mDumpManager);
+                mDumpManager,
+                syncExecutor);
 
         // Get a reference to the BubbleController's entry listener
         verify(mNotificationEntryManager, atLeastOnce())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
index 00f4e3a..5340ff7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -83,8 +83,6 @@
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.ZenModeController;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.bubbles.BubbleData;
@@ -203,6 +201,9 @@
 
         mTestableLooper = TestableLooper.get(this);
 
+        // For the purposes of this test, just run everything synchronously
+        ShellExecutor syncExecutor = new SyncExecutor();
+
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
 
         mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(mContext,
@@ -227,7 +228,7 @@
         when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);
 
         mPositioner = new TestableBubblePositioner(mContext, mWindowManager);
-        mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner);
+        mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, syncExecutor);
 
         TestableNotificationInterruptStateProviderImpl interruptionStateProvider =
                 new TestableNotificationInterruptStateProviderImpl(mContext.getContentResolver(),
@@ -241,7 +242,7 @@
                         mock(Handler.class)
                 );
         when(mFeatureFlagsNewPipeline.isNewNotifPipelineRenderingEnabled()).thenReturn(true);
-        when(mShellTaskOrganizer.getExecutor()).thenReturn(new FakeExecutor(new FakeSystemClock()));
+        when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
         mBubbleController = new TestableBubbleController(
                 mContext,
                 mBubbleData,
@@ -254,12 +255,13 @@
                 mBubbleLogger,
                 mShellTaskOrganizer,
                 mPositioner,
-                mock(ShellExecutor.class));
+                syncExecutor,
+                mock(Handler.class));
         mBubbleController.setExpandListener(mBubbleExpandListener);
 
         mBubblesManager = new BubblesManager(
                 mContext,
-                mBubbleController,
+                mBubbleController.getImpl(),
                 mNotificationShadeWindowController,
                 mStatusBarStateController,
                 mShadeController,
@@ -274,7 +276,8 @@
                 mNotifPipeline,
                 mSysUiState,
                 mFeatureFlagsNewPipeline,
-                mDumpManager);
+                mDumpManager,
+                syncExecutor);
         mBubblesManager.addNotifCallback(mNotifCallback);
 
         // Get a reference to the BubbleController's entry listener
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/SyncExecutor.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/SyncExecutor.java
new file mode 100644
index 0000000..d40eecf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/SyncExecutor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.wmshell;
+
+import com.android.wm.shell.common.ShellExecutor;
+
+/**
+ * And executor that just executes everything synchronously.  To be removed once we move the
+ * tests of shell behavior over to the shell.
+ */
+public class SyncExecutor implements ShellExecutor {
+    @Override
+    public void execute(Runnable runnable) {
+        runnable.run();
+    }
+
+    @Override
+    public void executeDelayed(Runnable runnable, long delayMillis) {
+        runnable.run();
+    }
+
+    @Override
+    public void removeAllCallbacks() {
+    }
+
+    @Override
+    public void removeCallbacks(Runnable runnable) {
+    }
+
+    @Override
+    public boolean hasCallback(Runnable runnable) {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 3f918e8..cdf47b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -49,10 +49,11 @@
             BubbleLogger bubbleLogger,
             ShellTaskOrganizer shellTaskOrganizer,
             BubblePositioner positioner,
-            ShellExecutor shellMainExecutor) {
+            ShellExecutor shellMainExecutor,
+            Handler shellMainHandler) {
         super(context, data, Runnable::run, floatingContentCoordinator, dataRepository,
                 statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
-                bubbleLogger, shellTaskOrganizer, positioner, shellMainExecutor);
+                bubbleLogger, shellTaskOrganizer, positioner, shellMainExecutor, shellMainHandler);
         setInflateSynchronously(true);
     }
 }