1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles;
18 
19 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
20 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
21 import static android.view.View.INVISIBLE;
22 import static android.view.View.VISIBLE;
23 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
24 
25 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
26 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
27 import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_BOTTOM;
28 import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_LEFT;
29 import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_NONE;
30 import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_RIGHT;
31 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED;
32 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED;
33 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT;
34 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NOTIF_CANCEL;
35 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_BUBBLE_UP;
36 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE;
37 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
38 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
39 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
40 
41 import android.annotation.NonNull;
42 import android.annotation.UserIdInt;
43 import android.app.ActivityManager;
44 import android.app.Notification;
45 import android.app.PendingIntent;
46 import android.content.Context;
47 import android.content.pm.ActivityInfo;
48 import android.content.pm.LauncherApps;
49 import android.content.pm.PackageManager;
50 import android.content.pm.ShortcutInfo;
51 import android.content.pm.UserInfo;
52 import android.content.res.Configuration;
53 import android.graphics.PixelFormat;
54 import android.graphics.PointF;
55 import android.graphics.Rect;
56 import android.os.Binder;
57 import android.os.Bundle;
58 import android.os.Handler;
59 import android.os.RemoteException;
60 import android.os.ServiceManager;
61 import android.os.UserHandle;
62 import android.service.notification.NotificationListenerService;
63 import android.service.notification.NotificationListenerService.RankingMap;
64 import android.util.ArraySet;
65 import android.util.Log;
66 import android.util.Pair;
67 import android.util.Slog;
68 import android.util.SparseArray;
69 import android.util.SparseSetArray;
70 import android.view.View;
71 import android.view.ViewGroup;
72 import android.view.WindowInsets;
73 import android.view.WindowManager;
74 import android.window.WindowContainerTransaction;
75 
76 import androidx.annotation.MainThread;
77 import androidx.annotation.Nullable;
78 
79 import com.android.internal.annotations.VisibleForTesting;
80 import com.android.internal.logging.UiEventLogger;
81 import com.android.internal.statusbar.IStatusBarService;
82 import com.android.wm.shell.ShellTaskOrganizer;
83 import com.android.wm.shell.WindowManagerShellWrapper;
84 import com.android.wm.shell.common.DisplayChangeController;
85 import com.android.wm.shell.common.DisplayController;
86 import com.android.wm.shell.common.FloatingContentCoordinator;
87 import com.android.wm.shell.common.ShellExecutor;
88 import com.android.wm.shell.common.SyncTransactionQueue;
89 import com.android.wm.shell.common.TaskStackListenerCallback;
90 import com.android.wm.shell.common.TaskStackListenerImpl;
91 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
92 
93 import java.io.FileDescriptor;
94 import java.io.PrintWriter;
95 import java.util.ArrayList;
96 import java.util.HashMap;
97 import java.util.HashSet;
98 import java.util.List;
99 import java.util.Objects;
100 import java.util.concurrent.Executor;
101 import java.util.function.Consumer;
102 import java.util.function.IntConsumer;
103 
104 /**
105  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
106  * Bubbles can be expanded to show more content.
107  *
108  * The controller manages addition, removal, and visible state of bubbles on screen.
109  */
110 public class BubbleController {
111 
112     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
113 
114     // TODO(b/173386799) keep in sync with Launcher3, not hooked up to anything
115     public static final String EXTRA_TASKBAR_CREATED = "taskbarCreated";
116     public static final String EXTRA_BUBBLE_OVERFLOW_OPENED = "bubbleOverflowOpened";
117     public static final String EXTRA_TASKBAR_VISIBLE = "taskbarVisible";
118     public static final String EXTRA_TASKBAR_POSITION = "taskbarPosition";
119     public static final String EXTRA_TASKBAR_ICON_SIZE = "taskbarIconSize";
120     public static final String EXTRA_TASKBAR_BUBBLE_XY = "taskbarBubbleXY";
121     public static final String EXTRA_TASKBAR_SIZE = "taskbarSize";
122     public static final String LEFT_POSITION = "Left";
123     public static final String RIGHT_POSITION = "Right";
124     public static final String BOTTOM_POSITION = "Bottom";
125 
126     private final Context mContext;
127     private final BubblesImpl mImpl = new BubblesImpl();
128     private Bubbles.BubbleExpandListener mExpandListener;
129     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
130     private final FloatingContentCoordinator mFloatingContentCoordinator;
131     private final BubbleDataRepository mDataRepository;
132     private final WindowManagerShellWrapper mWindowManagerShellWrapper;
133     private final LauncherApps mLauncherApps;
134     private final IStatusBarService mBarService;
135     private final WindowManager mWindowManager;
136     private final TaskStackListenerImpl mTaskStackListener;
137     private final ShellTaskOrganizer mTaskOrganizer;
138     private final DisplayController mDisplayController;
139     private final SyncTransactionQueue mSyncQueue;
140 
141     // Used to post to main UI thread
142     private final ShellExecutor mMainExecutor;
143     private final Handler mMainHandler;
144 
145     private BubbleLogger mLogger;
146     private BubbleData mBubbleData;
147     @Nullable private BubbleStackView mStackView;
148     private BubbleIconFactory mBubbleIconFactory;
149     private BubblePositioner mBubblePositioner;
150     private Bubbles.SysuiProxy mSysuiProxy;
151 
152     // Tracks the id of the current (foreground) user.
153     private int mCurrentUserId;
154     // Current profiles of the user (e.g. user with a workprofile)
155     private SparseArray<UserInfo> mCurrentProfiles;
156     // Saves notification keys of active bubbles when users are switched.
157     private final SparseSetArray<String> mSavedBubbleKeysPerUser;
158 
159     // Used when ranking updates occur and we check if things should bubble / unbubble
160     private NotificationListenerService.Ranking mTmpRanking;
161 
162     // Callback that updates BubbleOverflowActivity on data change.
163     @Nullable private BubbleData.Listener mOverflowListener = null;
164 
165     // Typically only load once & after user switches
166     private boolean mOverflowDataLoadNeeded = true;
167 
168     /**
169      * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select
170      * this bubble and expand the stack.
171      */
172     @Nullable private BubbleEntry mNotifEntryToExpandOnShadeUnlock;
173 
174     /** LayoutParams used to add the BubbleStackView to the window manager. */
175     private WindowManager.LayoutParams mWmLayoutParams;
176     /** Whether or not the BubbleStackView has been added to the WindowManager. */
177     private boolean mAddedToWindowManager = false;
178 
179     /** Saved screen density, used to detect display size changes in {@link #onConfigChanged}. */
180     private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED;
181 
182     /** Saved screen bounds, used to detect screen size changes in {@link #onConfigChanged}. **/
183     private Rect mScreenBounds = new Rect();
184 
185     /** Saved font scale, used to detect font size changes in {@link #onConfigChanged}. */
186     private float mFontScale = 0;
187 
188     /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */
189     private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
190 
191     /** Saved insets, used to detect WindowInset changes. */
192     private WindowInsets mWindowInsets;
193 
194     private boolean mInflateSynchronously;
195 
196     /** True when user is in status bar unlock shade. */
197     private boolean mIsStatusBarShade = true;
198 
199     /**
200      * Creates an instance of the BubbleController.
201      */
create(Context context, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, @Nullable IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, LauncherApps launcherApps, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, ShellTaskOrganizer organizer, DisplayController displayController, ShellExecutor mainExecutor, Handler mainHandler, SyncTransactionQueue syncQueue)202     public static BubbleController create(Context context,
203             @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
204             FloatingContentCoordinator floatingContentCoordinator,
205             @Nullable IStatusBarService statusBarService,
206             WindowManager windowManager,
207             WindowManagerShellWrapper windowManagerShellWrapper,
208             LauncherApps launcherApps,
209             TaskStackListenerImpl taskStackListener,
210             UiEventLogger uiEventLogger,
211             ShellTaskOrganizer organizer,
212             DisplayController displayController,
213             ShellExecutor mainExecutor,
214             Handler mainHandler,
215             SyncTransactionQueue syncQueue) {
216         BubbleLogger logger = new BubbleLogger(uiEventLogger);
217         BubblePositioner positioner = new BubblePositioner(context, windowManager);
218         BubbleData data = new BubbleData(context, logger, positioner, mainExecutor);
219         return new BubbleController(context, data, synchronizer, floatingContentCoordinator,
220                 new BubbleDataRepository(context, launcherApps, mainExecutor),
221                 statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
222                 logger, taskStackListener, organizer, positioner, displayController, mainExecutor,
223                 mainHandler, syncQueue);
224     }
225 
226     /**
227      * Testing constructor.
228      */
229     @VisibleForTesting
BubbleController(Context context, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, BubbleDataRepository dataRepository, @Nullable IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, LauncherApps launcherApps, BubbleLogger bubbleLogger, TaskStackListenerImpl taskStackListener, ShellTaskOrganizer organizer, BubblePositioner positioner, DisplayController displayController, ShellExecutor mainExecutor, Handler mainHandler, SyncTransactionQueue syncQueue)230     protected BubbleController(Context context,
231             BubbleData data,
232             @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
233             FloatingContentCoordinator floatingContentCoordinator,
234             BubbleDataRepository dataRepository,
235             @Nullable IStatusBarService statusBarService,
236             WindowManager windowManager,
237             WindowManagerShellWrapper windowManagerShellWrapper,
238             LauncherApps launcherApps,
239             BubbleLogger bubbleLogger,
240             TaskStackListenerImpl taskStackListener,
241             ShellTaskOrganizer organizer,
242             BubblePositioner positioner,
243             DisplayController displayController,
244             ShellExecutor mainExecutor,
245             Handler mainHandler,
246             SyncTransactionQueue syncQueue) {
247         mContext = context;
248         mLauncherApps = launcherApps;
249         mBarService = statusBarService == null
250                 ? IStatusBarService.Stub.asInterface(
251                 ServiceManager.getService(Context.STATUS_BAR_SERVICE))
252                 : statusBarService;
253         mWindowManager = windowManager;
254         mWindowManagerShellWrapper = windowManagerShellWrapper;
255         mFloatingContentCoordinator = floatingContentCoordinator;
256         mDataRepository = dataRepository;
257         mLogger = bubbleLogger;
258         mMainExecutor = mainExecutor;
259         mMainHandler = mainHandler;
260         mTaskStackListener = taskStackListener;
261         mTaskOrganizer = organizer;
262         mSurfaceSynchronizer = synchronizer;
263         mCurrentUserId = ActivityManager.getCurrentUser();
264         mBubblePositioner = positioner;
265         mBubbleData = data;
266         mSavedBubbleKeysPerUser = new SparseSetArray<>();
267         mBubbleIconFactory = new BubbleIconFactory(context);
268         mDisplayController = displayController;
269         mSyncQueue = syncQueue;
270     }
271 
initialize()272     public void initialize() {
273         mBubbleData.setListener(mBubbleDataListener);
274         mBubbleData.setSuppressionChangedListener(this::onBubbleNotificationSuppressionChanged);
275 
276         mBubbleData.setPendingIntentCancelledListener(bubble -> {
277             if (bubble.getBubbleIntent() == null) {
278                 return;
279             }
280             if (bubble.isIntentActive() || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
281                 bubble.setPendingIntentCanceled();
282                 return;
283             }
284             mMainExecutor.execute(() -> removeBubble(bubble.getKey(), DISMISS_INVALID_INTENT));
285         });
286 
287         try {
288             mWindowManagerShellWrapper.addPinnedStackListener(new BubblesImeListener());
289         } catch (RemoteException e) {
290             e.printStackTrace();
291         }
292 
293         mBubbleData.setCurrentUserId(mCurrentUserId);
294 
295         mTaskOrganizer.addLocusIdListener((taskId, locus, visible) ->
296                 mBubbleData.onLocusVisibilityChanged(taskId, locus, visible));
297 
298         mLauncherApps.registerCallback(new LauncherApps.Callback() {
299             @Override
300             public void onPackageAdded(String s, UserHandle userHandle) {}
301 
302             @Override
303             public void onPackageChanged(String s, UserHandle userHandle) {}
304 
305             @Override
306             public void onPackageRemoved(String s, UserHandle userHandle) {
307                 // Remove bubbles with this package name, since it has been uninstalled and attempts
308                 // to open a bubble from an uninstalled app can cause issues.
309                 mBubbleData.removeBubblesWithPackageName(s, DISMISS_PACKAGE_REMOVED);
310             }
311 
312             @Override
313             public void onPackagesAvailable(String[] strings, UserHandle userHandle, boolean b) {}
314 
315             @Override
316             public void onPackagesUnavailable(String[] packages, UserHandle userHandle,
317                     boolean b) {
318                 for (String packageName : packages) {
319                     // Remove bubbles from unavailable apps. This can occur when the app is on
320                     // external storage that has been removed.
321                     mBubbleData.removeBubblesWithPackageName(packageName, DISMISS_PACKAGE_REMOVED);
322                 }
323             }
324 
325             @Override
326             public void onShortcutsChanged(String packageName, List<ShortcutInfo> validShortcuts,
327                     UserHandle user) {
328                 super.onShortcutsChanged(packageName, validShortcuts, user);
329 
330                 // Remove bubbles whose shortcuts aren't in the latest list of valid shortcuts.
331                 mBubbleData.removeBubblesWithInvalidShortcuts(
332                         packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED);
333             }
334         }, mMainHandler);
335 
336         mTaskStackListener.addListener(new TaskStackListenerCallback() {
337             @Override
338             public void onTaskMovedToFront(int taskId) {
339                 if (mSysuiProxy == null) {
340                     return;
341                 }
342 
343                 mSysuiProxy.isNotificationShadeExpand((expand) -> {
344                     mMainExecutor.execute(() -> {
345                         int expandedId = INVALID_TASK_ID;
346                         if (mStackView != null && mStackView.getExpandedBubble() != null
347                                 && isStackExpanded() && !mStackView.isExpansionAnimating()
348                                 && !expand) {
349                             expandedId = mStackView.getExpandedBubble().getTaskId();
350                         }
351 
352                         if (expandedId != INVALID_TASK_ID && expandedId != taskId) {
353                             mBubbleData.setExpanded(false);
354                         }
355                     });
356                 });
357             }
358 
359             @Override
360             public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
361                     boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
362                 for (Bubble b : mBubbleData.getBubbles()) {
363                     if (task.taskId == b.getTaskId()) {
364                         mBubbleData.setSelectedBubble(b);
365                         mBubbleData.setExpanded(true);
366                         return;
367                     }
368                 }
369                 for (Bubble b : mBubbleData.getOverflowBubbles()) {
370                     if (task.taskId == b.getTaskId()) {
371                         promoteBubbleFromOverflow(b);
372                         mBubbleData.setExpanded(true);
373                         return;
374                     }
375                 }
376             }
377         });
378 
379         mDisplayController.addDisplayChangingController(
380                 new DisplayChangeController.OnDisplayChangingListener() {
381                     @Override
382                     public void onRotateDisplay(int displayId, int fromRotation, int toRotation,
383                             WindowContainerTransaction t) {
384                         // This is triggered right before the rotation is applied
385                         if (fromRotation != toRotation) {
386                             mBubblePositioner.setRotation(toRotation);
387                             if (mStackView != null) {
388                                 // Layout listener set on stackView will update the positioner
389                                 // once the rotation is applied
390                                 mStackView.onOrientationChanged();
391                             }
392                         }
393                     }
394                 });
395     }
396 
397     @VisibleForTesting
asBubbles()398     public Bubbles asBubbles() {
399         return mImpl;
400     }
401 
402     @VisibleForTesting
getImplCachedState()403     public BubblesImpl.CachedState getImplCachedState() {
404         return mImpl.mCachedState;
405     }
406 
getMainExecutor()407     public ShellExecutor getMainExecutor() {
408         return mMainExecutor;
409     }
410 
411     /**
412      * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal.
413      */
hideCurrentInputMethod()414     void hideCurrentInputMethod() {
415         try {
416             mBarService.hideCurrentInputMethodForBubbles();
417         } catch (RemoteException e) {
418             e.printStackTrace();
419         }
420     }
421 
openBubbleOverflow()422     private void openBubbleOverflow() {
423         ensureStackViewCreated();
424         mBubbleData.setShowingOverflow(true);
425         mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
426         mBubbleData.setExpanded(true);
427     }
428 
429     /** Called when any taskbar state changes (e.g. visibility, position, sizes). */
onTaskbarChanged(Bundle b)430     private void onTaskbarChanged(Bundle b) {
431         if (b == null) {
432             return;
433         }
434         boolean isVisible = b.getBoolean(EXTRA_TASKBAR_VISIBLE, false /* default */);
435         String position = b.getString(EXTRA_TASKBAR_POSITION, RIGHT_POSITION /* default */);
436         @BubblePositioner.TaskbarPosition int taskbarPosition = TASKBAR_POSITION_NONE;
437         switch (position) {
438             case LEFT_POSITION:
439                 taskbarPosition = TASKBAR_POSITION_LEFT;
440                 break;
441             case RIGHT_POSITION:
442                 taskbarPosition = TASKBAR_POSITION_RIGHT;
443                 break;
444             case BOTTOM_POSITION:
445                 taskbarPosition = TASKBAR_POSITION_BOTTOM;
446                 break;
447         }
448         int[] itemPosition = b.getIntArray(EXTRA_TASKBAR_BUBBLE_XY);
449         int iconSize = b.getInt(EXTRA_TASKBAR_ICON_SIZE);
450         int taskbarSize = b.getInt(EXTRA_TASKBAR_SIZE);
451         Log.w(TAG, "onTaskbarChanged:"
452                 + " isVisible: " + isVisible
453                 + " position: " + position
454                 + " itemPosition: " + itemPosition[0] + "," + itemPosition[1]
455                 + " iconSize: " + iconSize);
456         PointF point = new PointF(itemPosition[0], itemPosition[1]);
457         mBubblePositioner.setPinnedLocation(isVisible ? point : null);
458         mBubblePositioner.updateForTaskbar(iconSize, taskbarPosition, isVisible, taskbarSize);
459         if (mStackView != null) {
460             if (isVisible && b.getBoolean(EXTRA_TASKBAR_CREATED, false /* default */)) {
461                 // If taskbar was created, add and remove the window so that bubbles display on top
462                 removeFromWindowManagerMaybe();
463                 addToWindowManagerMaybe();
464             }
465             mStackView.updateStackPosition();
466             mBubbleIconFactory = new BubbleIconFactory(mContext);
467             mStackView.onDisplaySizeChanged();
468         }
469         if (b.getBoolean(EXTRA_BUBBLE_OVERFLOW_OPENED, false)) {
470             openBubbleOverflow();
471         }
472     }
473 
474     /**
475      * Called when the status bar has become visible or invisible (either permanently or
476      * temporarily).
477      */
onStatusBarVisibilityChanged(boolean visible)478     private void onStatusBarVisibilityChanged(boolean visible) {
479         if (mStackView != null) {
480             // Hide the stack temporarily if the status bar has been made invisible, and the stack
481             // is collapsed. An expanded stack should remain visible until collapsed.
482             mStackView.setTemporarilyInvisible(!visible && !isStackExpanded());
483         }
484     }
485 
onZenStateChanged()486     private void onZenStateChanged() {
487         for (Bubble b : mBubbleData.getBubbles()) {
488             b.setShowDot(b.showInShade());
489         }
490     }
491 
onStatusBarStateChanged(boolean isShade)492     private void onStatusBarStateChanged(boolean isShade) {
493         mIsStatusBarShade = isShade;
494         if (!mIsStatusBarShade) {
495             collapseStack();
496         }
497 
498         if (mNotifEntryToExpandOnShadeUnlock != null) {
499             expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
500             mNotifEntryToExpandOnShadeUnlock = null;
501         }
502 
503         updateStack();
504     }
505 
506     @VisibleForTesting
onBubbleNotificationSuppressionChanged(Bubble bubble)507     public void onBubbleNotificationSuppressionChanged(Bubble bubble) {
508         // Make sure NoMan knows suppression state so that anyone querying it can tell.
509         try {
510             mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(),
511                     !bubble.showInShade(), bubble.isSuppressed());
512         } catch (RemoteException e) {
513             // Bad things have happened
514         }
515         mImpl.mCachedState.updateBubbleSuppressedState(bubble);
516     }
517 
518     /** Called when the current user changes. */
519     @VisibleForTesting
onUserChanged(int newUserId)520     public void onUserChanged(int newUserId) {
521         saveBubbles(mCurrentUserId);
522         mCurrentUserId = newUserId;
523 
524         mBubbleData.dismissAll(DISMISS_USER_CHANGED);
525         mBubbleData.clearOverflow();
526         mOverflowDataLoadNeeded = true;
527 
528         restoreBubbles(newUserId);
529         mBubbleData.setCurrentUserId(newUserId);
530     }
531 
532     /** Called when the profiles for the current user change. **/
onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles)533     public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
534         mCurrentProfiles = currentProfiles;
535     }
536 
537     /** Whether this userId belongs to the current user. */
isCurrentProfile(int userId)538     private boolean isCurrentProfile(int userId) {
539         return userId == UserHandle.USER_ALL
540                 || (mCurrentProfiles != null && mCurrentProfiles.get(userId) != null);
541     }
542 
543     /**
544      * Sets whether to perform inflation on the same thread as the caller. This method should only
545      * be used in tests, not in production.
546      */
547     @VisibleForTesting
setInflateSynchronously(boolean inflateSynchronously)548     public void setInflateSynchronously(boolean inflateSynchronously) {
549         mInflateSynchronously = inflateSynchronously;
550     }
551 
552     /** Set a listener to be notified of when overflow view update. */
setOverflowListener(BubbleData.Listener listener)553     public void setOverflowListener(BubbleData.Listener listener) {
554         mOverflowListener = listener;
555     }
556 
557     /**
558      * @return Bubbles for updating overflow.
559      */
getOverflowBubbles()560     List<Bubble> getOverflowBubbles() {
561         return mBubbleData.getOverflowBubbles();
562     }
563 
564     /** The task listener for events in bubble tasks. */
getTaskOrganizer()565     public ShellTaskOrganizer getTaskOrganizer() {
566         return mTaskOrganizer;
567     }
568 
getSyncTransactionQueue()569     SyncTransactionQueue getSyncTransactionQueue() {
570         return mSyncQueue;
571     }
572 
573     /** Contains information to help position things on the screen.  */
getPositioner()574     BubblePositioner getPositioner() {
575         return mBubblePositioner;
576     }
577 
getSysuiProxy()578     Bubbles.SysuiProxy getSysuiProxy() {
579         return mSysuiProxy;
580     }
581 
582     /**
583      * BubbleStackView is lazily created by this method the first time a Bubble is added. This
584      * method initializes the stack view and adds it to window manager.
585      */
ensureStackViewCreated()586     private void ensureStackViewCreated() {
587         if (mStackView == null) {
588             mStackView = new BubbleStackView(
589                     mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
590                     mMainExecutor);
591             mStackView.onOrientationChanged();
592             if (mExpandListener != null) {
593                 mStackView.setExpandListener(mExpandListener);
594             }
595             mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
596         }
597 
598         addToWindowManagerMaybe();
599     }
600 
601     /** Adds the BubbleStackView to the WindowManager if it's not already there. */
addToWindowManagerMaybe()602     private void addToWindowManagerMaybe() {
603         // If the stack is null, or already added, don't add it.
604         if (mStackView == null || mAddedToWindowManager) {
605             return;
606         }
607 
608         mWmLayoutParams = new WindowManager.LayoutParams(
609                 // Fill the screen so we can use translation animations to position the bubble
610                 // stack. We'll use touchable regions to ignore touches that are not on the bubbles
611                 // themselves.
612                 ViewGroup.LayoutParams.MATCH_PARENT,
613                 ViewGroup.LayoutParams.MATCH_PARENT,
614                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
615                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
616                     | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
617                     | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
618                 PixelFormat.TRANSLUCENT);
619 
620         mWmLayoutParams.setTrustedOverlay();
621         mWmLayoutParams.setFitInsetsTypes(0);
622         mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
623         mWmLayoutParams.token = new Binder();
624         mWmLayoutParams.setTitle("Bubbles!");
625         mWmLayoutParams.packageName = mContext.getPackageName();
626         mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
627         mWmLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
628 
629         try {
630             mAddedToWindowManager = true;
631             mBubbleData.getOverflow().initialize(this);
632             mWindowManager.addView(mStackView, mWmLayoutParams);
633             mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
634                 if (!windowInsets.equals(mWindowInsets)) {
635                     mWindowInsets = windowInsets;
636                     mBubblePositioner.update();
637                     mStackView.onDisplaySizeChanged();
638                 }
639                 return windowInsets;
640             });
641         } catch (IllegalStateException e) {
642             // This means the stack has already been added. This shouldn't happen...
643             e.printStackTrace();
644         }
645     }
646 
647     /**
648      * In some situations bubble's should be able to receive key events for back:
649      * - when the bubble overflow is showing
650      * - when the user education for the stack is showing.
651      *
652      * @param interceptBack whether back should be intercepted or not.
653      */
updateWindowFlagsForBackpress(boolean interceptBack)654     void updateWindowFlagsForBackpress(boolean interceptBack) {
655         if (mStackView != null && mAddedToWindowManager) {
656             mWmLayoutParams.flags = interceptBack
657                     ? 0
658                     : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
659                             | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
660             mWmLayoutParams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
661             mWindowManager.updateViewLayout(mStackView, mWmLayoutParams);
662         }
663     }
664 
665     /** Removes the BubbleStackView from the WindowManager if it's there. */
removeFromWindowManagerMaybe()666     private void removeFromWindowManagerMaybe() {
667         if (!mAddedToWindowManager) {
668             return;
669         }
670 
671         try {
672             mAddedToWindowManager = false;
673             if (mStackView != null) {
674                 mWindowManager.removeView(mStackView);
675                 mBubbleData.getOverflow().cleanUpExpandedState();
676             } else {
677                 Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
678             }
679         } catch (IllegalArgumentException e) {
680             // This means the stack has already been removed - it shouldn't happen, but ignore if it
681             // does, since we wanted it removed anyway.
682             e.printStackTrace();
683         }
684     }
685 
686     /**
687      * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been
688      * added in the meantime.
689      */
onAllBubblesAnimatedOut()690     void onAllBubblesAnimatedOut() {
691         if (mStackView != null) {
692             mStackView.setVisibility(INVISIBLE);
693             removeFromWindowManagerMaybe();
694         }
695     }
696 
697     /**
698      * Records the notification key for any active bubbles. These are used to restore active
699      * bubbles when the user returns to the foreground.
700      *
701      * @param userId the id of the user
702      */
saveBubbles(@serIdInt int userId)703     private void saveBubbles(@UserIdInt int userId) {
704         // First clear any existing keys that might be stored.
705         mSavedBubbleKeysPerUser.remove(userId);
706         // Add in all active bubbles for the current user.
707         for (Bubble bubble: mBubbleData.getBubbles()) {
708             mSavedBubbleKeysPerUser.add(userId, bubble.getKey());
709         }
710     }
711 
712     /**
713      * Promotes existing notifications to Bubbles if they were previously bubbles.
714      *
715      * @param userId the id of the user
716      */
restoreBubbles(@serIdInt int userId)717     private void restoreBubbles(@UserIdInt int userId) {
718         ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId);
719         if (savedBubbleKeys == null) {
720             // There were no bubbles saved for this used.
721             return;
722         }
723         mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys, (entries) -> {
724             mMainExecutor.execute(() -> {
725                 for (BubbleEntry e : entries) {
726                     if (canLaunchInTaskView(mContext, e)) {
727                         updateBubble(e, true /* suppressFlyout */, false /* showInShade */);
728                     }
729                 }
730             });
731         });
732         // Finally, remove the entries for this user now that bubbles are restored.
733         mSavedBubbleKeysPerUser.remove(userId);
734     }
735 
updateForThemeChanges()736     private void updateForThemeChanges() {
737         if (mStackView != null) {
738             mStackView.onThemeChanged();
739         }
740         mBubbleIconFactory = new BubbleIconFactory(mContext);
741         // Reload each bubble
742         for (Bubble b: mBubbleData.getBubbles()) {
743             b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
744                     false /* skipInflation */);
745         }
746         for (Bubble b: mBubbleData.getOverflowBubbles()) {
747             b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
748                     false /* skipInflation */);
749         }
750     }
751 
onConfigChanged(Configuration newConfig)752     private void onConfigChanged(Configuration newConfig) {
753         if (mBubblePositioner != null) {
754             mBubblePositioner.update();
755         }
756         if (mStackView != null && newConfig != null) {
757             if (newConfig.densityDpi != mDensityDpi
758                     || !newConfig.windowConfiguration.getBounds().equals(mScreenBounds)) {
759                 mDensityDpi = newConfig.densityDpi;
760                 mScreenBounds.set(newConfig.windowConfiguration.getBounds());
761                 mBubbleData.onMaxBubblesChanged();
762                 mBubbleIconFactory = new BubbleIconFactory(mContext);
763                 mStackView.onDisplaySizeChanged();
764             }
765             if (newConfig.fontScale != mFontScale) {
766                 mFontScale = newConfig.fontScale;
767                 mStackView.updateFontScale();
768             }
769             if (newConfig.getLayoutDirection() != mLayoutDirection) {
770                 mLayoutDirection = newConfig.getLayoutDirection();
771                 mStackView.onLayoutDirectionChanged(mLayoutDirection);
772             }
773         }
774     }
775 
setSysuiProxy(Bubbles.SysuiProxy proxy)776     private void setSysuiProxy(Bubbles.SysuiProxy proxy) {
777         mSysuiProxy = proxy;
778     }
779 
780     @VisibleForTesting
setExpandListener(Bubbles.BubbleExpandListener listener)781     public void setExpandListener(Bubbles.BubbleExpandListener listener) {
782         mExpandListener = ((isExpanding, key) -> {
783             if (listener != null) {
784                 listener.onBubbleExpandChanged(isExpanding, key);
785             }
786         });
787         if (mStackView != null) {
788             mStackView.setExpandListener(mExpandListener);
789         }
790     }
791 
792     /**
793      * Whether or not there are bubbles present, regardless of them being visible on the
794      * screen (e.g. if on AOD).
795      */
796     @VisibleForTesting
hasBubbles()797     public boolean hasBubbles() {
798         if (mStackView == null) {
799             return false;
800         }
801         return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
802     }
803 
804     @VisibleForTesting
isStackExpanded()805     public boolean isStackExpanded() {
806         return mBubbleData.isExpanded();
807     }
808 
809     @VisibleForTesting
collapseStack()810     public void collapseStack() {
811         mBubbleData.setExpanded(false /* expanded */);
812     }
813 
814     @VisibleForTesting
isBubbleNotificationSuppressedFromShade(String key, String groupKey)815     public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
816         boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key)
817                 && !mBubbleData.getAnyBubbleWithkey(key).showInShade());
818 
819         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
820         boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey));
821         return (isSummary && isSuppressedSummary) || isSuppressedBubble;
822     }
823 
removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback)824     private void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback) {
825         if (mBubbleData.isSummarySuppressed(groupKey)) {
826             mBubbleData.removeSuppressedSummary(groupKey);
827             if (callback != null) {
828                 callback.accept(mBubbleData.getSummaryKey(groupKey));
829             }
830         }
831     }
832 
833     /** Promote the provided bubble from the overflow view. */
promoteBubbleFromOverflow(Bubble bubble)834     public void promoteBubbleFromOverflow(Bubble bubble) {
835         mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
836         bubble.setInflateSynchronously(mInflateSynchronously);
837         bubble.setShouldAutoExpand(true);
838         bubble.markAsAccessedAt(System.currentTimeMillis());
839         setIsBubble(bubble, true /* isBubble */);
840     }
841 
842     /**
843      * Expands and selects the provided bubble as long as it already exists in the stack or the
844      * overflow.
845      *
846      * This is currently only used when opening a bubble via clicking on a conversation widget.
847      */
expandStackAndSelectBubble(Bubble b)848     public void expandStackAndSelectBubble(Bubble b) {
849         if (b == null) {
850             return;
851         }
852         if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) {
853             // already in the stack
854             mBubbleData.setSelectedBubble(b);
855             mBubbleData.setExpanded(true);
856         } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) {
857             // promote it out of the overflow
858             promoteBubbleFromOverflow(b);
859         }
860     }
861 
862     /**
863      * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble
864      * exists for this entry, and it is able to bubble, a new bubble will be created.
865      *
866      * This is the method to use when opening a bubble via a notification or in a state where
867      * the device might not be unlocked.
868      *
869      * @param entry the entry to use for the bubble.
870      */
expandStackAndSelectBubble(BubbleEntry entry)871     public void expandStackAndSelectBubble(BubbleEntry entry) {
872         if (mIsStatusBarShade) {
873             mNotifEntryToExpandOnShadeUnlock = null;
874 
875             String key = entry.getKey();
876             Bubble bubble = mBubbleData.getBubbleInStackWithKey(key);
877             if (bubble != null) {
878                 mBubbleData.setSelectedBubble(bubble);
879                 mBubbleData.setExpanded(true);
880             } else {
881                 bubble = mBubbleData.getOverflowBubbleWithKey(key);
882                 if (bubble != null) {
883                     promoteBubbleFromOverflow(bubble);
884                 } else if (entry.canBubble()) {
885                     // It can bubble but it's not -- it got aged out of the overflow before it
886                     // was dismissed or opened, make it a bubble again.
887                     setIsBubble(entry, true /* isBubble */, true /* autoExpand */);
888                 }
889             }
890         } else {
891             // Wait until we're unlocked to expand, so that the user can see the expand animation
892             // and also to work around bugs with expansion animation + shade unlock happening at the
893             // same time.
894             mNotifEntryToExpandOnShadeUnlock = entry;
895         }
896     }
897 
898     /**
899      * Adds or updates a bubble associated with the provided notification entry.
900      *
901      * @param notif the notification associated with this bubble.
902      */
903     @VisibleForTesting
updateBubble(BubbleEntry notif)904     public void updateBubble(BubbleEntry notif) {
905         updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
906     }
907 
908     /**
909      * Fills the overflow bubbles by loading them from disk.
910      */
loadOverflowBubblesFromDisk()911     void loadOverflowBubblesFromDisk() {
912         if (!mOverflowDataLoadNeeded) {
913             return;
914         }
915         mOverflowDataLoadNeeded = false;
916         mDataRepository.loadBubbles(mCurrentUserId, (bubbles) -> {
917             bubbles.forEach(bubble -> {
918                 if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) {
919                     // if the bubble is already active, there's no need to push it to overflow
920                     return;
921                 }
922                 bubble.inflate(
923                         (b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble),
924                         mContext, this, mStackView, mBubbleIconFactory, true /* skipInflation */);
925             });
926             return null;
927         });
928     }
929 
930     /**
931      * Adds or updates a bubble associated with the provided notification entry.
932      *
933      * @param notif the notification associated with this bubble.
934      * @param suppressFlyout this bubble suppress flyout or not.
935      * @param showInShade this bubble show in shade or not.
936      */
937     @VisibleForTesting
updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade)938     public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) {
939         // If this is an interruptive notif, mark that it's interrupted
940         mSysuiProxy.setNotificationInterruption(notif.getKey());
941         if (!notif.getRanking().isTextChanged()
942                 && (notif.getBubbleMetadata() != null
943                     && !notif.getBubbleMetadata().getAutoExpandBubble())
944                 && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) {
945             // Update the bubble but don't promote it out of overflow
946             Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey());
947             b.setEntry(notif);
948         } else {
949             Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
950             inflateAndAdd(bubble, suppressFlyout, showInShade);
951         }
952     }
953 
inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade)954     void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
955         // Lazy init stack view when a bubble is created
956         ensureStackViewCreated();
957         bubble.setInflateSynchronously(mInflateSynchronously);
958         bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
959                 mContext, this, mStackView, mBubbleIconFactory, false /* skipInflation */);
960     }
961 
962     /**
963      * Removes the bubble with the given key.
964      * <p>
965      * Must be called from the main thread.
966      */
967     @VisibleForTesting
968     @MainThread
removeBubble(String key, int reason)969     public void removeBubble(String key, int reason) {
970         if (mBubbleData.hasAnyBubbleWithKey(key)) {
971             mBubbleData.dismissBubbleWithKey(key, reason);
972         }
973     }
974 
onEntryAdded(BubbleEntry entry)975     private void onEntryAdded(BubbleEntry entry) {
976         if (canLaunchInTaskView(mContext, entry)) {
977             updateBubble(entry);
978         }
979     }
980 
onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp)981     private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
982         // shouldBubbleUp checks canBubble & for bubble metadata
983         boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry);
984         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
985             // It was previously a bubble but no longer a bubble -- lets remove it
986             removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
987         } else if (shouldBubble && entry.isBubble()) {
988             updateBubble(entry);
989         }
990     }
991 
onEntryRemoved(BubbleEntry entry)992     private void onEntryRemoved(BubbleEntry entry) {
993         if (isSummaryOfBubbles(entry)) {
994             final String groupKey = entry.getStatusBarNotification().getGroupKey();
995             mBubbleData.removeSuppressedSummary(groupKey);
996 
997             // Remove any associated bubble children with the summary
998             final List<Bubble> bubbleChildren = getBubblesInGroup(groupKey);
999             for (int i = 0; i < bubbleChildren.size(); i++) {
1000                 removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED);
1001             }
1002         } else {
1003             removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL);
1004         }
1005     }
1006 
onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey)1007     private void onRankingUpdated(RankingMap rankingMap,
1008             HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) {
1009         if (mTmpRanking == null) {
1010             mTmpRanking = new NotificationListenerService.Ranking();
1011         }
1012         String[] orderedKeys = rankingMap.getOrderedKeys();
1013         for (int i = 0; i < orderedKeys.length; i++) {
1014             String key = orderedKeys[i];
1015             Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key);
1016             BubbleEntry entry = entryData.first;
1017             boolean shouldBubbleUp = entryData.second;
1018 
1019             if (entry != null && !isCurrentProfile(
1020                     entry.getStatusBarNotification().getUser().getIdentifier())) {
1021                 return;
1022             }
1023 
1024             rankingMap.getRanking(key, mTmpRanking);
1025             boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key);
1026             if (isActiveBubble && !mTmpRanking.canBubble()) {
1027                 // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason.
1028                 // This means that the app or channel's ability to bubble has been revoked.
1029                 mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED);
1030             } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) {
1031                 // If this entry is allowed to bubble, but cannot currently bubble up or is
1032                 // suspended, dismiss it. This happens when DND is enabled and configured to hide
1033                 // bubbles, or focus mode is enabled and the app is designated as distracting.
1034                 // Dismissing with the reason DISMISS_NO_BUBBLE_UP will retain the underlying
1035                 // notification, so that the bubble will be re-created if shouldBubbleUp returns
1036                 // true.
1037                 mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP);
1038             } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) {
1039                 entry.setFlagBubble(true);
1040                 onEntryUpdated(entry, shouldBubbleUp && !entry.getRanking().isSuspended());
1041             }
1042         }
1043     }
1044 
1045     /**
1046      * Retrieves any bubbles that are part of the notification group represented by the provided
1047      * group key.
1048      */
getBubblesInGroup(@ullable String groupKey)1049     private ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
1050         ArrayList<Bubble> bubbleChildren = new ArrayList<>();
1051         if (groupKey == null) {
1052             return bubbleChildren;
1053         }
1054         for (Bubble bubble : mBubbleData.getActiveBubbles()) {
1055             if (bubble.getGroupKey() != null && groupKey.equals(bubble.getGroupKey())) {
1056                 bubbleChildren.add(bubble);
1057             }
1058         }
1059         return bubbleChildren;
1060     }
1061 
setIsBubble(@onNull final BubbleEntry entry, final boolean isBubble, final boolean autoExpand)1062     private void setIsBubble(@NonNull final BubbleEntry entry, final boolean isBubble,
1063             final boolean autoExpand) {
1064         Objects.requireNonNull(entry);
1065         entry.setFlagBubble(isBubble);
1066         try {
1067             int flags = 0;
1068             if (autoExpand) {
1069                 flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
1070                 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
1071             }
1072             mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, flags);
1073         } catch (RemoteException e) {
1074             // Bad things have happened
1075         }
1076     }
1077 
setIsBubble(@onNull final Bubble b, final boolean isBubble)1078     private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) {
1079         Objects.requireNonNull(b);
1080         b.setIsBubble(isBubble);
1081         mSysuiProxy.getPendingOrActiveEntry(b.getKey(), (entry) -> {
1082             mMainExecutor.execute(() -> {
1083                 if (entry != null) {
1084                     // Updating the entry to be a bubble will trigger our normal update flow
1085                     setIsBubble(entry, isBubble, b.shouldAutoExpand());
1086                 } else if (isBubble) {
1087                     // If bubble doesn't exist, it's a persisted bubble so we need to add it to the
1088                     // stack ourselves
1089                     Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */);
1090                     inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */,
1091                             !bubble.shouldAutoExpand() /* showInShade */);
1092                 }
1093             });
1094         });
1095     }
1096 
1097     @SuppressWarnings("FieldCanBeLocal")
1098     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
1099 
1100         @Override
1101         public void applyUpdate(BubbleData.Update update) {
1102             ensureStackViewCreated();
1103 
1104             // Lazy load overflow bubbles from disk
1105             loadOverflowBubblesFromDisk();
1106 
1107             mStackView.updateOverflowButtonDot();
1108 
1109             // Update bubbles in overflow.
1110             if (mOverflowListener != null) {
1111                 mOverflowListener.applyUpdate(update);
1112             }
1113 
1114             // Collapsing? Do this first before remaining steps.
1115             if (update.expandedChanged && !update.expanded) {
1116                 mStackView.setExpanded(false);
1117                 mSysuiProxy.requestNotificationShadeTopUi(false, TAG);
1118             }
1119 
1120             // Do removals, if any.
1121             ArrayList<Pair<Bubble, Integer>> removedBubbles =
1122                     new ArrayList<>(update.removedBubbles);
1123             ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>();
1124             for (Pair<Bubble, Integer> removed : removedBubbles) {
1125                 final Bubble bubble = removed.first;
1126                 @Bubbles.DismissReason final int reason = removed.second;
1127 
1128                 if (mStackView != null) {
1129                     mStackView.removeBubble(bubble);
1130                 }
1131 
1132                 // Leave the notification in place if we're dismissing due to user switching, or
1133                 // because DND is suppressing the bubble. In both of those cases, we need to be able
1134                 // to restore the bubble from the notification later.
1135                 if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) {
1136                     continue;
1137                 }
1138                 if (reason == DISMISS_NOTIF_CANCEL
1139                         || reason == DISMISS_SHORTCUT_REMOVED) {
1140                     bubblesToBeRemovedFromRepository.add(bubble);
1141                 }
1142                 if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
1143                     if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())
1144                             && (!bubble.showInShade()
1145                             || reason == DISMISS_NOTIF_CANCEL
1146                             || reason == DISMISS_GROUP_CANCELLED)) {
1147                         // The bubble is now gone & the notification is hidden from the shade, so
1148                         // time to actually remove it
1149                         mSysuiProxy.notifyRemoveNotification(bubble.getKey(), REASON_CANCEL);
1150                     } else {
1151                         if (bubble.isBubble()) {
1152                             setIsBubble(bubble, false /* isBubble */);
1153                         }
1154                         mSysuiProxy.updateNotificationBubbleButton(bubble.getKey());
1155                     }
1156 
1157                 }
1158                 mSysuiProxy.getPendingOrActiveEntry(bubble.getKey(), (entry) -> {
1159                     mMainExecutor.execute(() -> {
1160                         if (entry != null) {
1161                             final String groupKey = entry.getStatusBarNotification().getGroupKey();
1162                             if (getBubblesInGroup(groupKey).isEmpty()) {
1163                                 // Time to potentially remove the summary
1164                                 mSysuiProxy.notifyMaybeCancelSummary(bubble.getKey());
1165                             }
1166                         }
1167                     });
1168                 });
1169             }
1170             mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository);
1171 
1172             if (update.addedBubble != null && mStackView != null) {
1173                 mDataRepository.addBubble(mCurrentUserId, update.addedBubble);
1174                 mStackView.addBubble(update.addedBubble);
1175             }
1176 
1177             if (update.updatedBubble != null && mStackView != null) {
1178                 mStackView.updateBubble(update.updatedBubble);
1179             }
1180 
1181             // At this point, the correct bubbles are inflated in the stack.
1182             // Make sure the order in bubble data is reflected in bubble row.
1183             if (update.orderChanged && mStackView != null) {
1184                 mDataRepository.addBubbles(mCurrentUserId, update.bubbles);
1185                 mStackView.updateBubbleOrder(update.bubbles);
1186             }
1187 
1188             if (update.selectionChanged && mStackView != null) {
1189                 mStackView.setSelectedBubble(update.selectedBubble);
1190                 if (update.selectedBubble != null) {
1191                     mSysuiProxy.updateNotificationSuppression(update.selectedBubble.getKey());
1192                 }
1193             }
1194 
1195             if (update.suppressedBubble != null && mStackView != null) {
1196                 mStackView.setBubbleVisibility(update.suppressedBubble, false);
1197             }
1198 
1199             if (update.unsuppressedBubble != null && mStackView != null) {
1200                 mStackView.setBubbleVisibility(update.unsuppressedBubble, true);
1201             }
1202 
1203             // Expanding? Apply this last.
1204             if (update.expandedChanged && update.expanded) {
1205                 if (mStackView != null) {
1206                     mStackView.setExpanded(true);
1207                     mSysuiProxy.requestNotificationShadeTopUi(true, TAG);
1208                 }
1209             }
1210 
1211             mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate");
1212             updateStack();
1213 
1214             // Update the cached state for queries from SysUI
1215             mImpl.mCachedState.update(update);
1216         }
1217     };
1218 
handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback)1219     private boolean handleDismissalInterception(BubbleEntry entry,
1220             @Nullable List<BubbleEntry> children, IntConsumer removeCallback) {
1221         if (isSummaryOfBubbles(entry)) {
1222             handleSummaryDismissalInterception(entry, children, removeCallback);
1223         } else {
1224             Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey());
1225             if (bubble == null || !entry.isBubble()) {
1226                 bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey());
1227             }
1228             if (bubble == null) {
1229                 return false;
1230             }
1231             bubble.setSuppressNotification(true);
1232             bubble.setShowDot(false /* show */);
1233         }
1234         // Update the shade
1235         mSysuiProxy.notifyInvalidateNotifications("BubbleController.handleDismissalInterception");
1236         return true;
1237     }
1238 
isSummaryOfBubbles(BubbleEntry entry)1239     private boolean isSummaryOfBubbles(BubbleEntry entry) {
1240         String groupKey = entry.getStatusBarNotification().getGroupKey();
1241         ArrayList<Bubble> bubbleChildren = getBubblesInGroup(groupKey);
1242         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey)
1243                 && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey());
1244         boolean isSummary = entry.getStatusBarNotification().getNotification().isGroupSummary();
1245         return (isSuppressedSummary || isSummary) && !bubbleChildren.isEmpty();
1246     }
1247 
handleSummaryDismissalInterception( BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback)1248     private void handleSummaryDismissalInterception(
1249             BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) {
1250         if (children != null) {
1251             for (int i = 0; i < children.size(); i++) {
1252                 BubbleEntry child = children.get(i);
1253                 if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) {
1254                     // Suppress the bubbled child
1255                     // As far as group manager is concerned, once a child is no longer shown
1256                     // in the shade, it is essentially removed.
1257                     Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey());
1258                     if (bubbleChild != null) {
1259                         mSysuiProxy.removeNotificationEntry(bubbleChild.getKey());
1260                         bubbleChild.setSuppressNotification(true);
1261                         bubbleChild.setShowDot(false /* show */);
1262                     }
1263                 } else {
1264                     // non-bubbled children can be removed
1265                     removeCallback.accept(i);
1266                 }
1267             }
1268         }
1269 
1270         // And since all children are removed, remove the summary.
1271         removeCallback.accept(-1);
1272 
1273         // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated
1274         mBubbleData.addSummaryToSuppress(summary.getStatusBarNotification().getGroupKey(),
1275                 summary.getKey());
1276     }
1277 
1278     /**
1279      * Updates the visibility of the bubbles based on current state.
1280      * Does not un-bubble, just hides or un-hides.
1281      * Updates stack description for TalkBack focus.
1282      */
updateStack()1283     public void updateStack() {
1284         if (mStackView == null) {
1285             return;
1286         }
1287 
1288         if (!mIsStatusBarShade) {
1289             // Bubbles don't appear over the locked shade.
1290             mStackView.setVisibility(INVISIBLE);
1291         } else if (hasBubbles()) {
1292             // If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the
1293             // stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate
1294             // out.
1295             mStackView.setVisibility(VISIBLE);
1296         }
1297 
1298         mStackView.updateContentDescription();
1299     }
1300 
1301     @VisibleForTesting
getStackView()1302     public BubbleStackView getStackView() {
1303         return mStackView;
1304     }
1305 
1306     /**
1307      * Description of current bubble state.
1308      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)1309     private void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1310         pw.println("BubbleController state:");
1311         mBubbleData.dump(fd, pw, args);
1312         pw.println();
1313         if (mStackView != null) {
1314             mStackView.dump(fd, pw, args);
1315         }
1316         pw.println();
1317     }
1318 
1319     /**
1320      * Whether an intent is properly configured to display in a
1321      * {@link com.android.wm.shell.TaskView}.
1322      *
1323      * Keep checks in sync with BubbleExtractor#canLaunchInTaskView. Typically
1324      * that should filter out any invalid bubbles, but should protect SysUI side just in case.
1325      *
1326      * @param context the context to use.
1327      * @param entry the entry to bubble.
1328      */
canLaunchInTaskView(Context context, BubbleEntry entry)1329     static boolean canLaunchInTaskView(Context context, BubbleEntry entry) {
1330         PendingIntent intent = entry.getBubbleMetadata() != null
1331                 ? entry.getBubbleMetadata().getIntent()
1332                 : null;
1333         if (entry.getBubbleMetadata() != null
1334                 && entry.getBubbleMetadata().getShortcutId() != null) {
1335             return true;
1336         }
1337         if (intent == null) {
1338             Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey());
1339             return false;
1340         }
1341         PackageManager packageManager = getPackageManagerForUser(
1342                 context, entry.getStatusBarNotification().getUser().getIdentifier());
1343         ActivityInfo info =
1344                 intent.getIntent().resolveActivityInfo(packageManager, 0);
1345         if (info == null) {
1346             Log.w(TAG, "Unable to send as bubble, "
1347                     + entry.getKey() + " couldn't find activity info for intent: "
1348                     + intent);
1349             return false;
1350         }
1351         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
1352             Log.w(TAG, "Unable to send as bubble, "
1353                     + entry.getKey() + " activity is not resizable for intent: "
1354                     + intent);
1355             return false;
1356         }
1357         return true;
1358     }
1359 
getPackageManagerForUser(Context context, int userId)1360     static PackageManager getPackageManagerForUser(Context context, int userId) {
1361         Context contextForUser = context;
1362         // UserHandle defines special userId as negative values, e.g. USER_ALL
1363         if (userId >= 0) {
1364             try {
1365                 // Create a context for the correct user so if a package isn't installed
1366                 // for user 0 we can still load information about the package.
1367                 contextForUser =
1368                         context.createPackageContextAsUser(context.getPackageName(),
1369                                 Context.CONTEXT_RESTRICTED,
1370                                 new UserHandle(userId));
1371             } catch (PackageManager.NameNotFoundException e) {
1372                 // Shouldn't fail to find the package name for system ui.
1373             }
1374         }
1375         return contextForUser.getPackageManager();
1376     }
1377 
1378     /** PinnedStackListener that dispatches IME visibility updates to the stack. */
1379     //TODO(b/170442945): Better way to do this / insets listener?
1380     private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedTaskListener {
1381         @Override
onImeVisibilityChanged(boolean imeVisible, int imeHeight)1382         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
1383             mBubblePositioner.setImeVisible(imeVisible, imeHeight);
1384             if (mStackView != null) {
1385                 mStackView.animateForIme(imeVisible);
1386             }
1387         }
1388     }
1389 
1390     private class BubblesImpl implements Bubbles {
1391         // Up-to-date cached state of bubbles data for SysUI to query from the calling thread
1392         @VisibleForTesting
1393         public class CachedState {
1394             private boolean mIsStackExpanded;
1395             private String mSelectedBubbleKey;
1396             private HashSet<String> mSuppressedBubbleKeys = new HashSet<>();
1397             private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>();
1398             private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>();
1399 
1400             private ArrayList<Bubble> mTmpBubbles = new ArrayList<>();
1401 
1402             /**
1403              * Updates the cached state based on the last full BubbleData change.
1404              */
update(BubbleData.Update update)1405             synchronized void update(BubbleData.Update update) {
1406                 if (update.selectionChanged) {
1407                     mSelectedBubbleKey = update.selectedBubble != null
1408                             ? update.selectedBubble.getKey()
1409                             : null;
1410                 }
1411                 if (update.expandedChanged) {
1412                     mIsStackExpanded = update.expanded;
1413                 }
1414                 if (update.suppressedSummaryChanged) {
1415                     String summaryKey =
1416                             mBubbleData.getSummaryKey(update.suppressedSummaryGroup);
1417                     if (summaryKey != null) {
1418                         mSuppressedGroupToNotifKeys.put(update.suppressedSummaryGroup, summaryKey);
1419                     } else {
1420                         mSuppressedGroupToNotifKeys.remove(update.suppressedSummaryGroup);
1421                     }
1422                 }
1423 
1424                 mTmpBubbles.clear();
1425                 mTmpBubbles.addAll(update.bubbles);
1426                 mTmpBubbles.addAll(update.overflowBubbles);
1427 
1428                 mSuppressedBubbleKeys.clear();
1429                 mShortcutIdToBubble.clear();
1430                 for (Bubble b : mTmpBubbles) {
1431                     mShortcutIdToBubble.put(b.getShortcutId(), b);
1432                     updateBubbleSuppressedState(b);
1433                 }
1434             }
1435 
1436             /**
1437              * Updates a specific bubble suppressed state.  This is used mainly because notification
1438              * suppression changes don't go through the same BubbleData update mechanism.
1439              */
updateBubbleSuppressedState(Bubble b)1440             synchronized void updateBubbleSuppressedState(Bubble b) {
1441                 if (!b.showInShade()) {
1442                     mSuppressedBubbleKeys.add(b.getKey());
1443                 } else {
1444                     mSuppressedBubbleKeys.remove(b.getKey());
1445                 }
1446             }
1447 
isStackExpanded()1448             public synchronized boolean isStackExpanded() {
1449                 return mIsStackExpanded;
1450             }
1451 
isBubbleExpanded(String key)1452             public synchronized boolean isBubbleExpanded(String key) {
1453                 return mIsStackExpanded && key.equals(mSelectedBubbleKey);
1454             }
1455 
isBubbleNotificationSuppressedFromShade(String key, String groupKey)1456             public synchronized boolean isBubbleNotificationSuppressedFromShade(String key,
1457                     String groupKey) {
1458                 return mSuppressedBubbleKeys.contains(key)
1459                         || (mSuppressedGroupToNotifKeys.containsKey(groupKey)
1460                                 && key.equals(mSuppressedGroupToNotifKeys.get(groupKey)));
1461             }
1462 
1463             @Nullable
getBubbleWithShortcutId(String id)1464             public synchronized Bubble getBubbleWithShortcutId(String id) {
1465                 return mShortcutIdToBubble.get(id);
1466             }
1467 
dump(PrintWriter pw)1468             synchronized void dump(PrintWriter pw) {
1469                 pw.println("BubbleImpl.CachedState state:");
1470 
1471                 pw.println("mIsStackExpanded: " + mIsStackExpanded);
1472                 pw.println("mSelectedBubbleKey: " + mSelectedBubbleKey);
1473 
1474                 pw.print("mSuppressedBubbleKeys: ");
1475                 pw.println(mSuppressedBubbleKeys.size());
1476                 for (String key : mSuppressedBubbleKeys) {
1477                     pw.println("   suppressing: " + key);
1478                 }
1479 
1480                 pw.print("mSuppressedGroupToNotifKeys: ");
1481                 pw.println(mSuppressedGroupToNotifKeys.size());
1482                 for (String key : mSuppressedGroupToNotifKeys.keySet()) {
1483                     pw.println("   suppressing: " + key);
1484                 }
1485             }
1486         }
1487 
1488         private CachedState mCachedState = new CachedState();
1489 
1490         @Override
isBubbleNotificationSuppressedFromShade(String key, String groupKey)1491         public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
1492             return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey);
1493         }
1494 
1495         @Override
isBubbleExpanded(String key)1496         public boolean isBubbleExpanded(String key) {
1497             return mCachedState.isBubbleExpanded(key);
1498         }
1499 
1500         @Override
isStackExpanded()1501         public boolean isStackExpanded() {
1502             return mCachedState.isStackExpanded();
1503         }
1504 
1505         @Override
1506         @Nullable
getBubbleWithShortcutId(String shortcutId)1507         public Bubble getBubbleWithShortcutId(String shortcutId) {
1508             return mCachedState.getBubbleWithShortcutId(shortcutId);
1509         }
1510 
1511         @Override
removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback, Executor callbackExecutor)1512         public void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
1513                 Executor callbackExecutor) {
1514             mMainExecutor.execute(() -> {
1515                 Consumer<String> cb = callback != null
1516                         ? (key) -> callbackExecutor.execute(() -> callback.accept(key))
1517                         : null;
1518                 BubbleController.this.removeSuppressedSummaryIfNecessary(groupKey, cb);
1519             });
1520         }
1521 
1522         @Override
collapseStack()1523         public void collapseStack() {
1524             mMainExecutor.execute(() -> {
1525                 BubbleController.this.collapseStack();
1526             });
1527         }
1528 
1529         @Override
updateForThemeChanges()1530         public void updateForThemeChanges() {
1531             mMainExecutor.execute(() -> {
1532                 BubbleController.this.updateForThemeChanges();
1533             });
1534         }
1535 
1536         @Override
expandStackAndSelectBubble(BubbleEntry entry)1537         public void expandStackAndSelectBubble(BubbleEntry entry) {
1538             mMainExecutor.execute(() -> {
1539                 BubbleController.this.expandStackAndSelectBubble(entry);
1540             });
1541         }
1542 
1543         @Override
expandStackAndSelectBubble(Bubble bubble)1544         public void expandStackAndSelectBubble(Bubble bubble) {
1545             mMainExecutor.execute(() -> {
1546                 BubbleController.this.expandStackAndSelectBubble(bubble);
1547             });
1548         }
1549 
1550         @Override
onTaskbarChanged(Bundle b)1551         public void onTaskbarChanged(Bundle b) {
1552             mMainExecutor.execute(() -> {
1553                 BubbleController.this.onTaskbarChanged(b);
1554             });
1555         }
1556 
1557         @Override
openBubbleOverflow()1558         public void openBubbleOverflow() {
1559             mMainExecutor.execute(() -> {
1560                 BubbleController.this.openBubbleOverflow();
1561             });
1562         }
1563 
1564         @Override
handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback, Executor callbackExecutor)1565         public boolean handleDismissalInterception(BubbleEntry entry,
1566                 @Nullable List<BubbleEntry> children, IntConsumer removeCallback,
1567                 Executor callbackExecutor) {
1568             IntConsumer cb = removeCallback != null
1569                     ? (index) -> callbackExecutor.execute(() -> removeCallback.accept(index))
1570                     : null;
1571             return mMainExecutor.executeBlockingForResult(() -> {
1572                 return BubbleController.this.handleDismissalInterception(entry, children, cb);
1573             }, Boolean.class);
1574         }
1575 
1576         @Override
setSysuiProxy(SysuiProxy proxy)1577         public void setSysuiProxy(SysuiProxy proxy) {
1578             mMainExecutor.execute(() -> {
1579                 BubbleController.this.setSysuiProxy(proxy);
1580             });
1581         }
1582 
1583         @Override
setExpandListener(BubbleExpandListener listener)1584         public void setExpandListener(BubbleExpandListener listener) {
1585             mMainExecutor.execute(() -> {
1586                 BubbleController.this.setExpandListener(listener);
1587             });
1588         }
1589 
1590         @Override
onEntryAdded(BubbleEntry entry)1591         public void onEntryAdded(BubbleEntry entry) {
1592             mMainExecutor.execute(() -> {
1593                 BubbleController.this.onEntryAdded(entry);
1594             });
1595         }
1596 
1597         @Override
onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp)1598         public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) {
1599             mMainExecutor.execute(() -> {
1600                 BubbleController.this.onEntryUpdated(entry, shouldBubbleUp);
1601             });
1602         }
1603 
1604         @Override
onEntryRemoved(BubbleEntry entry)1605         public void onEntryRemoved(BubbleEntry entry) {
1606             mMainExecutor.execute(() -> {
1607                 BubbleController.this.onEntryRemoved(entry);
1608             });
1609         }
1610 
1611         @Override
onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey)1612         public void onRankingUpdated(RankingMap rankingMap,
1613                 HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) {
1614             mMainExecutor.execute(() -> {
1615                 BubbleController.this.onRankingUpdated(rankingMap, entryDataByKey);
1616             });
1617         }
1618 
1619         @Override
onStatusBarVisibilityChanged(boolean visible)1620         public void onStatusBarVisibilityChanged(boolean visible) {
1621             mMainExecutor.execute(() -> {
1622                 BubbleController.this.onStatusBarVisibilityChanged(visible);
1623             });
1624         }
1625 
1626         @Override
onZenStateChanged()1627         public void onZenStateChanged() {
1628             mMainExecutor.execute(() -> {
1629                 BubbleController.this.onZenStateChanged();
1630             });
1631         }
1632 
1633         @Override
onStatusBarStateChanged(boolean isShade)1634         public void onStatusBarStateChanged(boolean isShade) {
1635             mMainExecutor.execute(() -> {
1636                 BubbleController.this.onStatusBarStateChanged(isShade);
1637             });
1638         }
1639 
1640         @Override
onUserChanged(int newUserId)1641         public void onUserChanged(int newUserId) {
1642             mMainExecutor.execute(() -> {
1643                 BubbleController.this.onUserChanged(newUserId);
1644             });
1645         }
1646 
1647         @Override
onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles)1648         public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
1649             mMainExecutor.execute(() -> {
1650                 BubbleController.this.onCurrentProfilesChanged(currentProfiles);
1651             });
1652         }
1653 
1654         @Override
onConfigChanged(Configuration newConfig)1655         public void onConfigChanged(Configuration newConfig) {
1656             mMainExecutor.execute(() -> {
1657                 BubbleController.this.onConfigChanged(newConfig);
1658             });
1659         }
1660 
1661         @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)1662         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1663             try {
1664                 mMainExecutor.executeBlocking(() -> {
1665                     BubbleController.this.dump(fd, pw, args);
1666                     mCachedState.dump(pw);
1667                 });
1668             } catch (InterruptedException e) {
1669                 Slog.e(TAG, "Failed to dump BubbleController in 2s");
1670             }
1671         }
1672     }
1673 }
1674