Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +13 −4 Original line number Diff line number Diff line Loading @@ -49,6 +49,7 @@ import java.io.FileDescriptor; 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. Loading @@ -58,6 +59,7 @@ public class Bubble implements BubbleViewProvider { private static final String TAG = "Bubble"; private final String mKey; private final Executor mMainExecutor; private long mLastUpdated; private long mLastAccessed; Loading Loading @@ -156,7 +158,8 @@ public class Bubble implements BubbleViewProvider { * 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(); Loading @@ -170,20 +173,25 @@ public class Bubble implements BubbleViewProvider { 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); } mainExecutor.execute(() -> { intentCancelListener.onPendingIntentCanceled(this); }); }; mMainExecutor = mainExecutor; setEntry(entry); } Loading Loading @@ -329,7 +337,8 @@ public class Bubble implements BubbleViewProvider { stackView, iconFactory, skipInflation, callback); callback, mMainExecutor); if (mInflateSynchronously) { mInflationTask.onPostExecute(mInflationTask.doInBackground()); } else { Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +247 −66 Original line number Diff line number Diff line Loading @@ -28,6 +28,16 @@ import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_BOT 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; Loading @@ -45,6 +55,8 @@ import android.graphics.PixelFormat; 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; Loading @@ -53,6 +65,7 @@ import android.service.notification.NotificationListenerService.RankingMap; 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; Loading @@ -75,6 +88,9 @@ import java.io.PrintWriter; 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; /** Loading @@ -83,7 +99,7 @@ import java.util.function.IntConsumer; * * 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; Loading @@ -101,7 +117,8 @@ public class BubbleController implements Bubbles { 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; Loading @@ -111,7 +128,7 @@ public class BubbleController implements Bubbles { @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; Loading Loading @@ -177,7 +194,7 @@ public class BubbleController implements Bubbles { /** * Injected constructor. */ public static BubbleController create(Context context, public static Bubbles create(Context context, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, @Nullable IStatusBarService statusBarService, Loading @@ -186,14 +203,15 @@ public class BubbleController implements Bubbles { 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; } /** Loading @@ -212,7 +230,8 @@ public class BubbleController implements Bubbles { BubbleLogger bubbleLogger, ShellTaskOrganizer organizer, BubblePositioner positioner, ShellExecutor mainExecutor) { ShellExecutor mainExecutor, Handler mainHandler) { mContext = context; mFloatingContentCoordinator = floatingContentCoordinator; mDataRepository = dataRepository; Loading Loading @@ -299,7 +318,12 @@ public class BubbleController implements Bubbles { mBubbleData.removeBubblesWithInvalidShortcuts( packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED); } }); }, mainHandler); } @VisibleForTesting public Bubbles getImpl() { return mImpl; } /** Loading @@ -313,8 +337,7 @@ public class BubbleController implements Bubbles { } } @Override public void openBubbleOverflow() { private void openBubbleOverflow() { ensureStackViewCreated(); mBubbleData.setShowingOverflow(true); mBubbleData.setSelectedBubble(mBubbleData.getOverflow()); Loading @@ -322,8 +345,7 @@ public class BubbleController implements Bubbles { } /** 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; } Loading Loading @@ -371,8 +393,7 @@ public class BubbleController implements Bubbles { * 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. Loading @@ -380,15 +401,13 @@ public class BubbleController implements Bubbles { } } @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(); Loading @@ -402,8 +421,7 @@ public class BubbleController implements Bubbles { updateStack(); } @Override public void onUserChanged(int newUserId) { private void onUserChanged(int newUserId) { saveBubbles(mCurrentUserId); mBubbleData.dismissAll(DISMISS_USER_CHANGED); restoreBubbles(newUserId); Loading Loading @@ -442,7 +460,7 @@ public class BubbleController implements Bubbles { return mBubblePositioner; } SysuiProxy getSysuiProxy() { Bubbles.SysuiProxy getSysuiProxy() { return mSysuiProxy; } Loading @@ -453,7 +471,8 @@ public class BubbleController implements Bubbles { 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); Loading Loading @@ -576,8 +595,7 @@ public class BubbleController implements Bubbles { mSavedBubbleKeysPerUser.remove(mCurrentUserId); } @Override public void updateForThemeChanges() { private void updateForThemeChanges() { if (mStackView != null) { mStackView.onThemeChanged(); } Loading @@ -593,8 +611,7 @@ public class BubbleController implements Bubbles { } } @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); Loading @@ -620,18 +637,19 @@ public class BubbleController implements Bubbles { } } @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); Loading @@ -654,17 +672,17 @@ public class BubbleController implements Bubbles { 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()); Loading @@ -674,23 +692,19 @@ public class BubbleController implements Bubbles { return (isSummary && isSuppressedSummary) || isSuppressedBubble; } @Override public boolean isSummarySuppressed(String groupKey) { return mBubbleData.isSummarySuppressed(groupKey); } @Override public void removeSuppressedSummary(String 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 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); } Loading @@ -704,7 +718,7 @@ public class BubbleController implements Bubbles { setIsBubble(bubble, true /* isBubble */); } @Override @VisibleForTesting public void expandStackAndSelectBubble(BubbleEntry entry) { if (mIsStatusBarShade) { mNotifEntryToExpandOnShadeUnlock = null; Loading Loading @@ -809,15 +823,13 @@ public class BubbleController implements Bubbles { } } @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())) { Loading @@ -828,8 +840,7 @@ public class BubbleController implements Bubbles { } } @Override public void onEntryRemoved(BubbleEntry entry) { private void onEntryRemoved(BubbleEntry entry) { if (isSummaryOfBubbles(entry)) { final String groupKey = entry.getStatusBarNotification().getGroupKey(); mBubbleData.removeSuppressedSummary(groupKey); Loading @@ -844,8 +855,7 @@ public class BubbleController implements Bubbles { } } @Override public void onRankingUpdated(RankingMap rankingMap) { private void onRankingUpdated(RankingMap rankingMap) { if (mTmpRanking == null) { mTmpRanking = new NotificationListenerService.Ranking(); } Loading Loading @@ -882,6 +892,8 @@ public class BubbleController implements Bubbles { 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); Loading Loading @@ -951,7 +963,7 @@ public class BubbleController implements Bubbles { 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); Loading Loading @@ -1029,8 +1041,7 @@ public class BubbleController implements Bubbles { } }; @Override public boolean handleDismissalInterception(BubbleEntry entry, private boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { if (isSummaryOfBubbles(entry)) { handleSummaryDismissalInterception(entry, children, removeCallback); Loading Loading @@ -1137,8 +1148,7 @@ public class BubbleController implements Bubbles { /** * 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(); Loading Loading @@ -1216,4 +1226,175 @@ public class BubbleController implements Bubbles { } } } 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"); } } } } libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +7 −2 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import java.util.HashSet; 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; Loading Loading @@ -117,6 +118,7 @@ public class BubbleData { 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. */ Loading Loading @@ -155,10 +157,12 @@ public class BubbleData { */ 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<>(); Loading Loading @@ -264,7 +268,8 @@ public class BubbleData { 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; Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt +9 −4 Original line number Diff line number Diff line Loading @@ -27,6 +27,8 @@ import android.util.Log 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 Loading @@ -34,12 +36,12 @@ import kotlinx.coroutines.cancelAndJoin 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 /** Loading Loading @@ -109,6 +111,8 @@ internal class BubbleDataRepository(context: Context, private val launcherApps: /** * 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 { Loading Loading @@ -163,10 +167,11 @@ internal class BubbleDataRepository(context: Context, private val launcherApps: shortcutInfo, entity.desiredHeight, entity.desiredHeightResId, entity.title entity.title, mainExecutor ) } } uiScope.launch { cb(bubbles) } mainExecutor.execute { cb(bubbles) } } } Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +13 −4 Original line number Diff line number Diff line Loading @@ -49,6 +49,7 @@ import java.io.FileDescriptor; 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. Loading @@ -58,6 +59,7 @@ public class Bubble implements BubbleViewProvider { private static final String TAG = "Bubble"; private final String mKey; private final Executor mMainExecutor; private long mLastUpdated; private long mLastAccessed; Loading Loading @@ -156,7 +158,8 @@ public class Bubble implements BubbleViewProvider { * 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(); Loading @@ -170,20 +173,25 @@ public class Bubble implements BubbleViewProvider { 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); } mainExecutor.execute(() -> { intentCancelListener.onPendingIntentCanceled(this); }); }; mMainExecutor = mainExecutor; setEntry(entry); } Loading Loading @@ -329,7 +337,8 @@ public class Bubble implements BubbleViewProvider { stackView, iconFactory, skipInflation, callback); callback, mMainExecutor); if (mInflateSynchronously) { mInflationTask.onPostExecute(mInflationTask.doInBackground()); } else { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +247 −66 Original line number Diff line number Diff line Loading @@ -28,6 +28,16 @@ import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_BOT 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; Loading @@ -45,6 +55,8 @@ import android.graphics.PixelFormat; 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; Loading @@ -53,6 +65,7 @@ import android.service.notification.NotificationListenerService.RankingMap; 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; Loading @@ -75,6 +88,9 @@ import java.io.PrintWriter; 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; /** Loading @@ -83,7 +99,7 @@ import java.util.function.IntConsumer; * * 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; Loading @@ -101,7 +117,8 @@ public class BubbleController implements Bubbles { 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; Loading @@ -111,7 +128,7 @@ public class BubbleController implements Bubbles { @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; Loading Loading @@ -177,7 +194,7 @@ public class BubbleController implements Bubbles { /** * Injected constructor. */ public static BubbleController create(Context context, public static Bubbles create(Context context, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, @Nullable IStatusBarService statusBarService, Loading @@ -186,14 +203,15 @@ public class BubbleController implements Bubbles { 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; } /** Loading @@ -212,7 +230,8 @@ public class BubbleController implements Bubbles { BubbleLogger bubbleLogger, ShellTaskOrganizer organizer, BubblePositioner positioner, ShellExecutor mainExecutor) { ShellExecutor mainExecutor, Handler mainHandler) { mContext = context; mFloatingContentCoordinator = floatingContentCoordinator; mDataRepository = dataRepository; Loading Loading @@ -299,7 +318,12 @@ public class BubbleController implements Bubbles { mBubbleData.removeBubblesWithInvalidShortcuts( packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED); } }); }, mainHandler); } @VisibleForTesting public Bubbles getImpl() { return mImpl; } /** Loading @@ -313,8 +337,7 @@ public class BubbleController implements Bubbles { } } @Override public void openBubbleOverflow() { private void openBubbleOverflow() { ensureStackViewCreated(); mBubbleData.setShowingOverflow(true); mBubbleData.setSelectedBubble(mBubbleData.getOverflow()); Loading @@ -322,8 +345,7 @@ public class BubbleController implements Bubbles { } /** 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; } Loading Loading @@ -371,8 +393,7 @@ public class BubbleController implements Bubbles { * 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. Loading @@ -380,15 +401,13 @@ public class BubbleController implements Bubbles { } } @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(); Loading @@ -402,8 +421,7 @@ public class BubbleController implements Bubbles { updateStack(); } @Override public void onUserChanged(int newUserId) { private void onUserChanged(int newUserId) { saveBubbles(mCurrentUserId); mBubbleData.dismissAll(DISMISS_USER_CHANGED); restoreBubbles(newUserId); Loading Loading @@ -442,7 +460,7 @@ public class BubbleController implements Bubbles { return mBubblePositioner; } SysuiProxy getSysuiProxy() { Bubbles.SysuiProxy getSysuiProxy() { return mSysuiProxy; } Loading @@ -453,7 +471,8 @@ public class BubbleController implements Bubbles { 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); Loading Loading @@ -576,8 +595,7 @@ public class BubbleController implements Bubbles { mSavedBubbleKeysPerUser.remove(mCurrentUserId); } @Override public void updateForThemeChanges() { private void updateForThemeChanges() { if (mStackView != null) { mStackView.onThemeChanged(); } Loading @@ -593,8 +611,7 @@ public class BubbleController implements Bubbles { } } @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); Loading @@ -620,18 +637,19 @@ public class BubbleController implements Bubbles { } } @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); Loading @@ -654,17 +672,17 @@ public class BubbleController implements Bubbles { 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()); Loading @@ -674,23 +692,19 @@ public class BubbleController implements Bubbles { return (isSummary && isSuppressedSummary) || isSuppressedBubble; } @Override public boolean isSummarySuppressed(String groupKey) { return mBubbleData.isSummarySuppressed(groupKey); } @Override public void removeSuppressedSummary(String 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 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); } Loading @@ -704,7 +718,7 @@ public class BubbleController implements Bubbles { setIsBubble(bubble, true /* isBubble */); } @Override @VisibleForTesting public void expandStackAndSelectBubble(BubbleEntry entry) { if (mIsStatusBarShade) { mNotifEntryToExpandOnShadeUnlock = null; Loading Loading @@ -809,15 +823,13 @@ public class BubbleController implements Bubbles { } } @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())) { Loading @@ -828,8 +840,7 @@ public class BubbleController implements Bubbles { } } @Override public void onEntryRemoved(BubbleEntry entry) { private void onEntryRemoved(BubbleEntry entry) { if (isSummaryOfBubbles(entry)) { final String groupKey = entry.getStatusBarNotification().getGroupKey(); mBubbleData.removeSuppressedSummary(groupKey); Loading @@ -844,8 +855,7 @@ public class BubbleController implements Bubbles { } } @Override public void onRankingUpdated(RankingMap rankingMap) { private void onRankingUpdated(RankingMap rankingMap) { if (mTmpRanking == null) { mTmpRanking = new NotificationListenerService.Ranking(); } Loading Loading @@ -882,6 +892,8 @@ public class BubbleController implements Bubbles { 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); Loading Loading @@ -951,7 +963,7 @@ public class BubbleController implements Bubbles { 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); Loading Loading @@ -1029,8 +1041,7 @@ public class BubbleController implements Bubbles { } }; @Override public boolean handleDismissalInterception(BubbleEntry entry, private boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { if (isSummaryOfBubbles(entry)) { handleSummaryDismissalInterception(entry, children, removeCallback); Loading Loading @@ -1137,8 +1148,7 @@ public class BubbleController implements Bubbles { /** * 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(); Loading Loading @@ -1216,4 +1226,175 @@ public class BubbleController implements Bubbles { } } } 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"); } } } }
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +7 −2 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import java.util.HashSet; 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; Loading Loading @@ -117,6 +118,7 @@ public class BubbleData { 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. */ Loading Loading @@ -155,10 +157,12 @@ public class BubbleData { */ 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<>(); Loading Loading @@ -264,7 +268,8 @@ public class BubbleData { 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; Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt +9 −4 Original line number Diff line number Diff line Loading @@ -27,6 +27,8 @@ import android.util.Log 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 Loading @@ -34,12 +36,12 @@ import kotlinx.coroutines.cancelAndJoin 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 /** Loading Loading @@ -109,6 +111,8 @@ internal class BubbleDataRepository(context: Context, private val launcherApps: /** * 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 { Loading Loading @@ -163,10 +167,11 @@ internal class BubbleDataRepository(context: Context, private val launcherApps: shortcutInfo, entity.desiredHeight, entity.desiredHeightResId, entity.title entity.title, mainExecutor ) } } uiScope.launch { cb(bubbles) } mainExecutor.execute { cb(bubbles) } } } Loading