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