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.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED;
20 import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
21 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
22 import static android.view.View.INVISIBLE;
23 import static android.view.View.VISIBLE;
24 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
25 
26 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
27 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
28 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED;
29 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED;
30 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT;
31 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NOTIF_CANCEL;
32 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_BUBBLE_UP;
33 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE;
34 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
35 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
36 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
37 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
38 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
39 
40 import android.annotation.BinderThread;
41 import android.annotation.NonNull;
42 import android.annotation.UserIdInt;
43 import android.app.ActivityManager;
44 import android.app.Notification;
45 import android.app.NotificationChannel;
46 import android.app.PendingIntent;
47 import android.content.BroadcastReceiver;
48 import android.content.Context;
49 import android.content.Intent;
50 import android.content.IntentFilter;
51 import android.content.pm.ActivityInfo;
52 import android.content.pm.LauncherApps;
53 import android.content.pm.PackageManager;
54 import android.content.pm.ShortcutInfo;
55 import android.content.pm.UserInfo;
56 import android.content.res.Configuration;
57 import android.graphics.PixelFormat;
58 import android.graphics.Point;
59 import android.graphics.Rect;
60 import android.graphics.drawable.Icon;
61 import android.os.Binder;
62 import android.os.Bundle;
63 import android.os.Handler;
64 import android.os.RemoteException;
65 import android.os.ServiceManager;
66 import android.os.UserHandle;
67 import android.os.UserManager;
68 import android.service.notification.NotificationListenerService;
69 import android.service.notification.NotificationListenerService.RankingMap;
70 import android.util.Log;
71 import android.util.Pair;
72 import android.util.SparseArray;
73 import android.view.IWindowManager;
74 import android.view.SurfaceControl;
75 import android.view.View;
76 import android.view.ViewGroup;
77 import android.view.ViewRootImpl;
78 import android.view.WindowInsets;
79 import android.view.WindowManager;
80 import android.window.ScreenCapture;
81 import android.window.ScreenCapture.SynchronousScreenCaptureListener;
82 
83 import androidx.annotation.MainThread;
84 import androidx.annotation.Nullable;
85 
86 import com.android.internal.annotations.VisibleForTesting;
87 import com.android.internal.protolog.common.ProtoLog;
88 import com.android.internal.statusbar.IStatusBarService;
89 import com.android.internal.util.CollectionUtils;
90 import com.android.launcher3.icons.BubbleIconFactory;
91 import com.android.wm.shell.Flags;
92 import com.android.wm.shell.R;
93 import com.android.wm.shell.ShellTaskOrganizer;
94 import com.android.wm.shell.WindowManagerShellWrapper;
95 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
96 import com.android.wm.shell.bubbles.properties.BubbleProperties;
97 import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper;
98 import com.android.wm.shell.common.DisplayController;
99 import com.android.wm.shell.common.ExternalInterfaceBinder;
100 import com.android.wm.shell.common.FloatingContentCoordinator;
101 import com.android.wm.shell.common.RemoteCallable;
102 import com.android.wm.shell.common.ShellExecutor;
103 import com.android.wm.shell.common.SingleInstanceRemoteListener;
104 import com.android.wm.shell.common.SyncTransactionQueue;
105 import com.android.wm.shell.common.TaskStackListenerCallback;
106 import com.android.wm.shell.common.TaskStackListenerImpl;
107 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
108 import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
109 import com.android.wm.shell.draganddrop.DragAndDropController;
110 import com.android.wm.shell.onehanded.OneHandedController;
111 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
112 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
113 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
114 import com.android.wm.shell.shared.annotations.ShellMainThread;
115 import com.android.wm.shell.sysui.ConfigurationChangeListener;
116 import com.android.wm.shell.sysui.ShellCommandHandler;
117 import com.android.wm.shell.sysui.ShellController;
118 import com.android.wm.shell.sysui.ShellInit;
119 import com.android.wm.shell.taskview.TaskView;
120 import com.android.wm.shell.taskview.TaskViewTaskController;
121 import com.android.wm.shell.taskview.TaskViewTransitions;
122 import com.android.wm.shell.transition.Transitions;
123 
124 import java.io.PrintWriter;
125 import java.util.ArrayList;
126 import java.util.HashMap;
127 import java.util.HashSet;
128 import java.util.List;
129 import java.util.Locale;
130 import java.util.Map;
131 import java.util.Objects;
132 import java.util.Optional;
133 import java.util.Set;
134 import java.util.concurrent.Executor;
135 import java.util.function.Consumer;
136 import java.util.function.IntConsumer;
137 
138 /**
139  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
140  * Bubbles can be expanded to show more content.
141  *
142  * The controller manages addition, removal, and visible state of bubbles on screen.
143  */
144 public class BubbleController implements ConfigurationChangeListener,
145         RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider {
146 
147     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
148 
149     // Should match with PhoneWindowManager
150     private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
151     private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav";
152     private static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
153     private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
154 
155     /**
156      * Common interface to send updates to bubble views.
157      */
158     public interface BubbleViewCallback {
159         /** Called when the provided bubble should be removed. */
removeBubble(Bubble removedBubble)160         void removeBubble(Bubble removedBubble);
161         /** Called when the provided bubble should be added. */
addBubble(Bubble addedBubble)162         void addBubble(Bubble addedBubble);
163         /** Called when the provided bubble should be updated. */
updateBubble(Bubble updatedBubble)164         void updateBubble(Bubble updatedBubble);
165         /** Called when the provided bubble should be selected. */
selectionChanged(BubbleViewProvider selectedBubble)166         void selectionChanged(BubbleViewProvider selectedBubble);
167         /** Called when the provided bubble's suppression state has changed. */
suppressionChanged(Bubble bubble, boolean isSuppressed)168         void suppressionChanged(Bubble bubble, boolean isSuppressed);
169         /** Called when the expansion state of bubbles has changed. */
expansionChanged(boolean isExpanded)170         void expansionChanged(boolean isExpanded);
171         /**
172          * Called when the order of the bubble list has changed. Depending on the expanded state
173          * the pointer might need to be updated.
174          */
bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer)175         void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer);
176         /** Called when the bubble overflow empty state changes, used to show/hide the overflow. */
bubbleOverflowChanged(boolean hasBubbles)177         void bubbleOverflowChanged(boolean hasBubbles);
178     }
179 
180     private final Context mContext;
181     private final BubblesImpl mImpl = new BubblesImpl();
182     private Bubbles.BubbleExpandListener mExpandListener;
183     @Nullable private final BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
184     private final FloatingContentCoordinator mFloatingContentCoordinator;
185     private final BubbleDataRepository mDataRepository;
186     private final WindowManagerShellWrapper mWindowManagerShellWrapper;
187     private final UserManager mUserManager;
188     private final LauncherApps mLauncherApps;
189     private final IStatusBarService mBarService;
190     private final WindowManager mWindowManager;
191     private final TaskStackListenerImpl mTaskStackListener;
192     private final ShellTaskOrganizer mTaskOrganizer;
193     private final DisplayController mDisplayController;
194     private final TaskViewTransitions mTaskViewTransitions;
195     private final Transitions mTransitions;
196     private final SyncTransactionQueue mSyncQueue;
197     private final ShellController mShellController;
198     private final ShellCommandHandler mShellCommandHandler;
199     private final IWindowManager mWmService;
200     private final BubbleProperties mBubbleProperties;
201     private final BubbleTaskViewFactory mBubbleTaskViewFactory;
202     private final BubbleExpandedViewManager mExpandedViewManager;
203 
204     // Used to post to main UI thread
205     private final ShellExecutor mMainExecutor;
206     private final Handler mMainHandler;
207     private final ShellExecutor mBackgroundExecutor;
208 
209     private final BubbleLogger mLogger;
210     private final BubbleData mBubbleData;
211     @Nullable private BubbleStackView mStackView;
212     @Nullable private BubbleBarLayerView mLayerView;
213     private BubbleIconFactory mBubbleIconFactory;
214     private final BubblePositioner mBubblePositioner;
215     private Bubbles.SysuiProxy mSysuiProxy;
216 
217     // Tracks the id of the current (foreground) user.
218     private int mCurrentUserId;
219     // Current profiles of the user (e.g. user with a workprofile)
220     private SparseArray<UserInfo> mCurrentProfiles;
221     // Saves data about active bubbles when users are switched.
222     private final SparseArray<UserBubbleData> mSavedUserBubbleData;
223 
224     // Used when ranking updates occur and we check if things should bubble / unbubble
225     private NotificationListenerService.Ranking mTmpRanking;
226 
227     // Callback that updates BubbleOverflowActivity on data change.
228     @Nullable private BubbleData.Listener mOverflowListener = null;
229 
230     // Typically only load once & after user switches
231     private boolean mOverflowDataLoadNeeded = true;
232 
233     /**
234      * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select
235      * this bubble and expand the stack.
236      */
237     @Nullable private BubbleEntry mNotifEntryToExpandOnShadeUnlock;
238 
239     /** LayoutParams used to add the BubbleStackView to the window manager. */
240     private WindowManager.LayoutParams mWmLayoutParams;
241     /** Whether or not the BubbleStackView has been added to the WindowManager. */
242     private boolean mAddedToWindowManager = false;
243 
244     /**
245      * Saved screen density, used to detect display size changes in {@link #onConfigurationChanged}.
246      */
247     private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED;
248 
249     /**
250      * Saved screen bounds, used to detect screen size changes in {@link #onConfigurationChanged}.
251      */
252     private final Rect mScreenBounds = new Rect();
253 
254     /** Saved font scale, used to detect font size changes in {@link #onConfigurationChanged}. */
255     private float mFontScale = 0;
256 
257     /** Saved locale, used to detect local changes in {@link #onConfigurationChanged}. */
258     private Locale mLocale = null;
259 
260     /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */
261     private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
262 
263     /** Saved insets, used to detect WindowInset changes. */
264     private WindowInsets mWindowInsets;
265 
266     private boolean mInflateSynchronously;
267 
268     /** True when user is in status bar unlock shade. */
269     private boolean mIsStatusBarShade = true;
270 
271     /** One handed mode controller to register transition listener. */
272     private final Optional<OneHandedController> mOneHandedOptional;
273     /** Drag and drop controller to register listener for onDragStarted. */
274     private final DragAndDropController mDragAndDropController;
275     /** Used to send bubble events to launcher. */
276     private Bubbles.BubbleStateListener mBubbleStateListener;
277 
278     /** Used to send updates to the views from {@link #mBubbleDataListener}. */
279     private BubbleViewCallback mBubbleViewCallback;
280 
BubbleController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellController shellController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, BubbleDataRepository dataRepository, @Nullable IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, UserManager userManager, LauncherApps launcherApps, BubbleLogger bubbleLogger, TaskStackListenerImpl taskStackListener, ShellTaskOrganizer organizer, BubblePositioner positioner, DisplayController displayController, Optional<OneHandedController> oneHandedOptional, DragAndDropController dragAndDropController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellBackgroundThread ShellExecutor bgExecutor, TaskViewTransitions taskViewTransitions, Transitions transitions, SyncTransactionQueue syncQueue, IWindowManager wmService, BubbleProperties bubbleProperties)281     public BubbleController(Context context,
282             ShellInit shellInit,
283             ShellCommandHandler shellCommandHandler,
284             ShellController shellController,
285             BubbleData data,
286             @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
287             FloatingContentCoordinator floatingContentCoordinator,
288             BubbleDataRepository dataRepository,
289             @Nullable IStatusBarService statusBarService,
290             WindowManager windowManager,
291             WindowManagerShellWrapper windowManagerShellWrapper,
292             UserManager userManager,
293             LauncherApps launcherApps,
294             BubbleLogger bubbleLogger,
295             TaskStackListenerImpl taskStackListener,
296             ShellTaskOrganizer organizer,
297             BubblePositioner positioner,
298             DisplayController displayController,
299             Optional<OneHandedController> oneHandedOptional,
300             DragAndDropController dragAndDropController,
301             @ShellMainThread ShellExecutor mainExecutor,
302             @ShellMainThread Handler mainHandler,
303             @ShellBackgroundThread ShellExecutor bgExecutor,
304             TaskViewTransitions taskViewTransitions,
305             Transitions transitions,
306             SyncTransactionQueue syncQueue,
307             IWindowManager wmService,
308             BubbleProperties bubbleProperties) {
309         mContext = context;
310         mShellCommandHandler = shellCommandHandler;
311         mShellController = shellController;
312         mLauncherApps = launcherApps;
313         mBarService = statusBarService == null
314                 ? IStatusBarService.Stub.asInterface(
315                 ServiceManager.getService(Context.STATUS_BAR_SERVICE))
316                 : statusBarService;
317         mWindowManager = windowManager;
318         mWindowManagerShellWrapper = windowManagerShellWrapper;
319         mUserManager = userManager;
320         mFloatingContentCoordinator = floatingContentCoordinator;
321         mDataRepository = dataRepository;
322         mLogger = bubbleLogger;
323         mMainExecutor = mainExecutor;
324         mMainHandler = mainHandler;
325         mBackgroundExecutor = bgExecutor;
326         mTaskStackListener = taskStackListener;
327         mTaskOrganizer = organizer;
328         mSurfaceSynchronizer = synchronizer;
329         mCurrentUserId = ActivityManager.getCurrentUser();
330         mBubblePositioner = positioner;
331         mBubbleData = data;
332         mSavedUserBubbleData = new SparseArray<>();
333         mBubbleIconFactory = new BubbleIconFactory(context,
334                 context.getResources().getDimensionPixelSize(R.dimen.bubble_size),
335                 context.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
336                 context.getResources().getColor(
337                         com.android.launcher3.icons.R.color.important_conversation),
338                 context.getResources().getDimensionPixelSize(
339                         com.android.internal.R.dimen.importance_ring_stroke_width));
340         mDisplayController = displayController;
341         mTaskViewTransitions = taskViewTransitions;
342         mTransitions = transitions;
343         mOneHandedOptional = oneHandedOptional;
344         mDragAndDropController = dragAndDropController;
345         mSyncQueue = syncQueue;
346         mWmService = wmService;
347         mBubbleProperties = bubbleProperties;
348         shellInit.addInitCallback(this::onInit, this);
349         mBubbleTaskViewFactory = new BubbleTaskViewFactory() {
350             @Override
351             public BubbleTaskView create() {
352                 TaskViewTaskController taskViewTaskController = new TaskViewTaskController(
353                         context, organizer, taskViewTransitions, syncQueue);
354                 TaskView taskView = new TaskView(context, taskViewTaskController);
355                 return new BubbleTaskView(taskView, mainExecutor);
356             }
357         };
358         mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this);
359     }
360 
registerOneHandedState(OneHandedController oneHanded)361     private void registerOneHandedState(OneHandedController oneHanded) {
362         oneHanded.registerTransitionCallback(
363                 new OneHandedTransitionCallback() {
364                     @Override
365                     public void onStartFinished(Rect bounds) {
366                         mMainExecutor.execute(() -> {
367                             if (mStackView != null) {
368                                 mStackView.onVerticalOffsetChanged(bounds.top);
369                             }
370                         });
371                     }
372 
373                     @Override
374                     public void onStopFinished(Rect bounds) {
375                         mMainExecutor.execute(() -> {
376                             if (mStackView != null) {
377                                 mStackView.onVerticalOffsetChanged(bounds.top);
378                             }
379                         });
380                     }
381                 });
382     }
383 
onInit()384     protected void onInit() {
385         mBubbleViewCallback = isShowingAsBubbleBar()
386                 ? mBubbleBarViewCallback
387                 : mBubbleStackViewCallback;
388         mBubbleData.setListener(mBubbleDataListener);
389         mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
390         mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
391 
392         mBubbleData.setPendingIntentCancelledListener(bubble -> {
393             if (bubble.getBubbleIntent() == null) {
394                 return;
395             }
396             if (bubble.isIntentActive() || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
397                 bubble.setPendingIntentCanceled();
398                 return;
399             }
400             mMainExecutor.execute(() -> removeBubble(bubble.getKey(), DISMISS_INVALID_INTENT));
401         });
402 
403         try {
404             mWindowManagerShellWrapper.addPinnedStackListener(new BubblesImeListener());
405         } catch (RemoteException e) {
406             e.printStackTrace();
407         }
408 
409         mBubbleData.setCurrentUserId(mCurrentUserId);
410 
411         mTaskOrganizer.addLocusIdListener((taskId, locus, visible) ->
412                 mBubbleData.onLocusVisibilityChanged(taskId, locus, visible));
413 
414         mLauncherApps.registerCallback(new LauncherApps.Callback() {
415             @Override
416             public void onPackageAdded(String s, UserHandle userHandle) {}
417 
418             @Override
419             public void onPackageChanged(String s, UserHandle userHandle) {}
420 
421             @Override
422             public void onPackageRemoved(String s, UserHandle userHandle) {
423                 // Remove bubbles with this package name, since it has been uninstalled and attempts
424                 // to open a bubble from an uninstalled app can cause issues.
425                 mBubbleData.removeBubblesWithPackageName(s, DISMISS_PACKAGE_REMOVED);
426             }
427 
428             @Override
429             public void onPackagesAvailable(String[] strings, UserHandle userHandle, boolean b) {}
430 
431             @Override
432             public void onPackagesUnavailable(String[] packages, UserHandle userHandle,
433                     boolean b) {
434                 for (String packageName : packages) {
435                     // Remove bubbles from unavailable apps. This can occur when the app is on
436                     // external storage that has been removed.
437                     mBubbleData.removeBubblesWithPackageName(packageName, DISMISS_PACKAGE_REMOVED);
438                 }
439             }
440 
441             @Override
442             public void onShortcutsChanged(String packageName, List<ShortcutInfo> validShortcuts,
443                     UserHandle user) {
444                 super.onShortcutsChanged(packageName, validShortcuts, user);
445 
446                 // Remove bubbles whose shortcuts aren't in the latest list of valid shortcuts.
447                 mBubbleData.removeBubblesWithInvalidShortcuts(
448                         packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED);
449             }
450         }, mMainHandler);
451 
452         mTransitions.registerObserver(new BubblesTransitionObserver(this, mBubbleData));
453 
454         mTaskStackListener.addListener(new TaskStackListenerCallback() {
455             @Override
456             public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
457                     boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
458                 for (Bubble b : mBubbleData.getBubbles()) {
459                     if (task.taskId == b.getTaskId()) {
460                         ProtoLog.d(WM_SHELL_BUBBLES,
461                                 "onActivityRestartAttempt - taskId=%d selecting matching bubble=%s",
462                                 task.taskId, b.getKey());
463                         mBubbleData.setSelectedBubbleAndExpandStack(b);
464                         return;
465                     }
466                 }
467                 for (Bubble b : mBubbleData.getOverflowBubbles()) {
468                     if (task.taskId == b.getTaskId()) {
469                         ProtoLog.d(WM_SHELL_BUBBLES, "onActivityRestartAttempt - taskId=%d "
470                                         + "selecting matching overflow bubble=%s",
471                                 task.taskId, b.getKey());
472                         promoteBubbleFromOverflow(b);
473                         mBubbleData.setExpanded(true);
474                         return;
475                     }
476                 }
477             }
478         });
479 
480         mDisplayController.addDisplayChangingController(
481                 (displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> {
482                     Rect newScreenBounds = new Rect();
483                     if (newDisplayAreaInfo != null) {
484                         newScreenBounds =
485                                 newDisplayAreaInfo.configuration.windowConfiguration.getBounds();
486                     }
487                     // This is triggered right before the rotation or new screen size is applied
488                     if (fromRotation != toRotation || !newScreenBounds.equals(mScreenBounds)) {
489                         if (mStackView != null) {
490                             // Layout listener set on stackView will update the positioner
491                             // once the rotation or screen change is applied
492                             mStackView.onOrientationChanged();
493                         }
494                     }
495                 });
496 
497         mOneHandedOptional.ifPresent(this::registerOneHandedState);
498         mDragAndDropController.addListener(new DragAndDropController.DragAndDropListener() {
499             @Override
500             public void onDragStarted() {
501                 collapseStack();
502             }
503         });
504 
505         // Clear out any persisted bubbles on disk that no longer have a valid user.
506         List<UserInfo> users = mUserManager.getAliveUsers();
507         mDataRepository.sanitizeBubbles(users);
508 
509         // Init profiles
510         SparseArray<UserInfo> userProfiles = new SparseArray<>();
511         for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) {
512             userProfiles.put(user.id, user);
513         }
514         mCurrentProfiles = userProfiles;
515 
516         if (Flags.enableRetrievableBubbles()) {
517             registerShortcutBroadcastReceiver();
518         }
519 
520         mShellController.addConfigurationChangeListener(this);
521         mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES,
522                 this::createExternalInterface, this);
523         mShellCommandHandler.addDumpCallback(this::dump, this);
524     }
525 
createExternalInterface()526     private ExternalInterfaceBinder createExternalInterface() {
527         return new IBubblesImpl(this);
528     }
529 
530     @VisibleForTesting
asBubbles()531     public Bubbles asBubbles() {
532         return mImpl;
533     }
534 
535     @VisibleForTesting
getImplCachedState()536     public BubblesImpl.CachedState getImplCachedState() {
537         return mImpl.mCachedState;
538     }
539 
getMainExecutor()540     public ShellExecutor getMainExecutor() {
541         return mMainExecutor;
542     }
543 
544     @Override
getContext()545     public Context getContext() {
546         return mContext;
547     }
548 
549     @Override
getRemoteCallExecutor()550     public ShellExecutor getRemoteCallExecutor() {
551         return mMainExecutor;
552     }
553 
554     /**
555      * Sets a listener to be notified of bubble updates. This is used by launcher so that
556      * it may render bubbles in itself. Only one listener is supported.
557      *
558      * <p>If bubble bar is supported, bubble views will be updated to switch to bar mode.
559      */
registerBubbleStateListener(Bubbles.BubbleStateListener listener)560     public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) {
561         mBubbleProperties.refresh();
562         if (canShowAsBubbleBar() && listener != null) {
563             // Only set the listener if we can show the bubble bar.
564             mBubbleStateListener = listener;
565             setUpBubbleViewsForMode();
566             sendInitialListenerUpdate();
567         } else {
568             mBubbleStateListener = null;
569         }
570     }
571 
572     /**
573      * Unregisters the {@link Bubbles.BubbleStateListener}.
574      *
575      * <p>If there's an existing listener, then we're switching back to stack mode and bubble views
576      * will be updated accordingly.
577      */
unregisterBubbleStateListener()578     public void unregisterBubbleStateListener() {
579         mBubbleProperties.refresh();
580         if (mBubbleStateListener != null) {
581             mBubbleStateListener = null;
582             setUpBubbleViewsForMode();
583         }
584     }
585 
586     /**
587      * If a {@link Bubbles.BubbleStateListener} is present, this will send the current bubble
588      * state to it.
589      */
sendInitialListenerUpdate()590     private void sendInitialListenerUpdate() {
591         if (mBubbleStateListener != null) {
592             BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar();
593             mBubbleStateListener.onBubbleStateChange(update);
594         }
595     }
596 
597     /**
598      * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal.
599      */
hideCurrentInputMethod()600     void hideCurrentInputMethod() {
601         mBubblePositioner.setImeVisible(false /* visible */, 0 /* height */);
602         int displayId = mWindowManager.getDefaultDisplay().getDisplayId();
603         try {
604             mBarService.hideCurrentInputMethodForBubbles(displayId);
605         } catch (RemoteException e) {
606             Log.e(TAG, "Failed to hide IME", e);
607         }
608     }
609 
610     /**
611      * Called when the status bar has become visible or invisible (either permanently or
612      * temporarily).
613      */
onStatusBarVisibilityChanged(boolean visible)614     private void onStatusBarVisibilityChanged(boolean visible) {
615         if (mStackView != null) {
616             // Hide the stack temporarily if the status bar has been made invisible, and the stack
617             // is collapsed. An expanded stack should remain visible until collapsed.
618             mStackView.setTemporarilyInvisible(!visible && !isStackExpanded());
619             ProtoLog.d(WM_SHELL_BUBBLES, "onStatusBarVisibilityChanged=%b stackExpanded=%b",
620                     visible, isStackExpanded());
621         }
622     }
623 
onZenStateChanged()624     private void onZenStateChanged() {
625         if (hasBubbles()) {
626             ProtoLog.d(WM_SHELL_BUBBLES, "onZenStateChanged");
627         }
628         for (Bubble b : mBubbleData.getBubbles()) {
629             b.setShowDot(b.showInShade());
630         }
631     }
632 
633     @VisibleForTesting
onStatusBarStateChanged(boolean isShade)634     public void onStatusBarStateChanged(boolean isShade) {
635         boolean didChange = mIsStatusBarShade != isShade;
636         ProtoLog.d(WM_SHELL_BUBBLES, "onStatusBarStateChanged "
637                         + "isShade=%b didChange=%b mNotifEntryToExpandOnShadeUnlock=%s",
638                 isShade, didChange, (mNotifEntryToExpandOnShadeUnlock != null
639                         ? mNotifEntryToExpandOnShadeUnlock.getKey() : "null"));
640         mIsStatusBarShade = isShade;
641         if (!mIsStatusBarShade && didChange) {
642             // Only collapse stack on change
643             collapseStack();
644         }
645 
646         if (mNotifEntryToExpandOnShadeUnlock != null) {
647             expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
648         }
649 
650         updateBubbleViews();
651     }
652 
653     @VisibleForTesting
onBubbleMetadataFlagChanged(Bubble bubble)654     public void onBubbleMetadataFlagChanged(Bubble bubble) {
655         ProtoLog.d(WM_SHELL_BUBBLES, "onBubbleMetadataFlagChanged=%s flags=%d",
656                 bubble.getKey(), bubble.getFlags());
657         // Make sure NoMan knows suppression state so that anyone querying it can tell.
658         try {
659             mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags());
660         } catch (RemoteException e) {
661             // Bad things have happened
662         }
663         mImpl.mCachedState.updateBubbleSuppressedState(bubble);
664     }
665 
666     /** Called when the current user changes. */
667     @VisibleForTesting
onUserChanged(int newUserId)668     public void onUserChanged(int newUserId) {
669         ProtoLog.d(WM_SHELL_BUBBLES, "onUserChanged currentUser=%d newUser=%d",
670                 mCurrentUserId, newUserId);
671         saveBubbles(mCurrentUserId);
672         mCurrentUserId = newUserId;
673 
674         mBubbleData.dismissAll(DISMISS_USER_CHANGED);
675         mBubbleData.clearOverflow();
676         mOverflowDataLoadNeeded = true;
677 
678         restoreBubbles(newUserId);
679         mBubbleData.setCurrentUserId(newUserId);
680     }
681 
682     /** Called when the profiles for the current user change. **/
onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles)683     public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
684         mCurrentProfiles = currentProfiles;
685     }
686 
687     /** Called when a user is removed from the device, including work profiles. */
onUserRemoved(int removedUserId)688     public void onUserRemoved(int removedUserId) {
689         UserInfo parent = mUserManager.getProfileParent(removedUserId);
690         int parentUserId = parent != null ? parent.getUserHandle().getIdentifier() : -1;
691         mBubbleData.removeBubblesForUser(removedUserId);
692         // Typically calls from BubbleData would remove bubbles from the DataRepository as well,
693         // however, this gets complicated when users are removed (mCurrentUserId won't necessarily
694         // be correct for this) so we update the repo directly.
695         mDataRepository.removeBubblesForUser(removedUserId, parentUserId);
696     }
697 
698     /** Called when sensitive notification state has changed */
onSensitiveNotificationProtectionStateChanged( boolean sensitiveNotificationProtectionActive)699     public void onSensitiveNotificationProtectionStateChanged(
700             boolean sensitiveNotificationProtectionActive) {
701         if (mStackView != null) {
702             mStackView.onSensitiveNotificationProtectionStateChanged(
703                     sensitiveNotificationProtectionActive);
704             ProtoLog.d(WM_SHELL_BUBBLES, "onSensitiveNotificationProtectionStateChanged=%b",
705                     sensitiveNotificationProtectionActive);
706         }
707     }
708 
709     /** Whether bubbles are showing in the bubble bar. */
isShowingAsBubbleBar()710     public boolean isShowingAsBubbleBar() {
711         return canShowAsBubbleBar() && mBubbleStateListener != null;
712     }
713 
714     /** Whether the current configuration supports showing as bubble bar. */
canShowAsBubbleBar()715     private boolean canShowAsBubbleBar() {
716         return mBubbleProperties.isBubbleBarEnabled() && mBubblePositioner.isLargeScreen();
717     }
718 
719     /**
720      * Returns current {@link BubbleBarLocation} if bubble bar is being used.
721      * Otherwise returns <code>null</code>
722      */
723     @Nullable
getBubbleBarLocation()724     public BubbleBarLocation getBubbleBarLocation() {
725         if (canShowAsBubbleBar()) {
726             return mBubblePositioner.getBubbleBarLocation();
727         }
728         return null;
729     }
730 
731     /**
732      * Update bubble bar location and trigger and update to listeners
733      */
setBubbleBarLocation(BubbleBarLocation bubbleBarLocation)734     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
735         if (canShowAsBubbleBar()) {
736             mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
737             BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
738             bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation;
739             mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
740         }
741     }
742 
743     /**
744      * Animate bubble bar to the given location. The location change is transient. It does not
745      * update the state of the bubble bar.
746      * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}.
747      */
animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation)748     public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
749         if (canShowAsBubbleBar()) {
750             mBubbleStateListener.animateBubbleBarLocation(bubbleBarLocation);
751         }
752     }
753 
754     /** Whether this userId belongs to the current user. */
isCurrentProfile(int userId)755     private boolean isCurrentProfile(int userId) {
756         return userId == UserHandle.USER_ALL
757                 || (mCurrentProfiles != null && mCurrentProfiles.get(userId) != null);
758     }
759 
760     /**
761      * Sets whether to perform inflation on the same thread as the caller. This method should only
762      * be used in tests, not in production.
763      */
764     @VisibleForTesting
setInflateSynchronously(boolean inflateSynchronously)765     public void setInflateSynchronously(boolean inflateSynchronously) {
766         mInflateSynchronously = inflateSynchronously;
767     }
768 
769     /** Set a listener to be notified of when overflow view update. */
setOverflowListener(BubbleData.Listener listener)770     public void setOverflowListener(BubbleData.Listener listener) {
771         mOverflowListener = listener;
772     }
773 
774     /**
775      * @return Bubbles for updating overflow.
776      */
getOverflowBubbles()777     List<Bubble> getOverflowBubbles() {
778         return mBubbleData.getOverflowBubbles();
779     }
780 
781     /** The task listener for events in bubble tasks. */
getTaskOrganizer()782     public ShellTaskOrganizer getTaskOrganizer() {
783         return mTaskOrganizer;
784     }
785 
getSyncTransactionQueue()786     SyncTransactionQueue getSyncTransactionQueue() {
787         return mSyncQueue;
788     }
789 
getTaskViewTransitions()790     TaskViewTransitions getTaskViewTransitions() {
791         return mTaskViewTransitions;
792     }
793 
794     /** Contains information to help position things on the screen. */
795     @VisibleForTesting
getPositioner()796     public BubblePositioner getPositioner() {
797         return mBubblePositioner;
798     }
799 
getIconFactory()800     BubbleIconFactory getIconFactory() {
801         return mBubbleIconFactory;
802     }
803 
804     @Override
getSysuiProxy()805     public Bubbles.SysuiProxy getSysuiProxy() {
806         return mSysuiProxy;
807     }
808 
809     /**
810      * The view and window for bubbles is lazily created by this method the first time a Bubble
811      * is added. Depending on the device state, this method will:
812      * - initialize a {@link BubbleStackView} and add it to window manager OR
813      * - initialize a {@link com.android.wm.shell.bubbles.bar.BubbleBarLayerView} and adds
814      *   it to window manager.
815      */
ensureBubbleViewsAndWindowCreated()816     private void ensureBubbleViewsAndWindowCreated() {
817         mBubblePositioner.setShowingInBubbleBar(isShowingAsBubbleBar());
818         if (isShowingAsBubbleBar()) {
819             // When we're showing in launcher / bubble bar is enabled, we don't have bubble stack
820             // view, instead we just show the expanded bubble view as necessary. We still need a
821             // window to show this in, but we use a separate code path.
822             // TODO(b/273312602): consider foldables where we do need a stack view when folded
823             if (mLayerView == null) {
824                 mLayerView = new BubbleBarLayerView(mContext, this, mBubbleData);
825                 mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
826             }
827         } else {
828             if (mStackView == null) {
829                 BubbleStackViewManager bubbleStackViewManager =
830                         BubbleStackViewManager.fromBubbleController(this);
831                 mStackView = new BubbleStackView(
832                         mContext, bubbleStackViewManager, mBubblePositioner, mBubbleData,
833                         mSurfaceSynchronizer, mFloatingContentCoordinator, this, mMainExecutor);
834                 mStackView.onOrientationChanged();
835                 if (mExpandListener != null) {
836                     mStackView.setExpandListener(mExpandListener);
837                 }
838                 mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
839             }
840         }
841         addToWindowManagerMaybe();
842     }
843 
844     /** Adds the appropriate view to WindowManager if it's not already there. */
addToWindowManagerMaybe()845     private void addToWindowManagerMaybe() {
846         // If already added, don't add it.
847         if (mAddedToWindowManager) {
848             return;
849         }
850         // If the appropriate view is null, don't add it.
851         if (isShowingAsBubbleBar() && mLayerView == null) {
852             return;
853         } else if (!isShowingAsBubbleBar() && mStackView == null) {
854             return;
855         }
856 
857         mWmLayoutParams = new WindowManager.LayoutParams(
858                 // Fill the screen so we can use translation animations to position the bubble
859                 // views. We'll use touchable regions to ignore touches that are not on the bubbles
860                 // themselves.
861                 ViewGroup.LayoutParams.MATCH_PARENT,
862                 ViewGroup.LayoutParams.MATCH_PARENT,
863                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
864                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
865                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
866                         | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
867                 PixelFormat.TRANSLUCENT);
868 
869         mWmLayoutParams.setTrustedOverlay();
870         mWmLayoutParams.setFitInsetsTypes(0);
871         mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
872         mWmLayoutParams.token = new Binder();
873         mWmLayoutParams.setTitle("Bubbles!");
874         mWmLayoutParams.packageName = mContext.getPackageName();
875         mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
876         mWmLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
877 
878         try {
879             mAddedToWindowManager = true;
880             registerBroadcastReceiver();
881             if (isShowingAsBubbleBar()) {
882                 mBubbleData.getOverflow().initializeForBubbleBar(
883                         mExpandedViewManager, mBubblePositioner);
884             } else {
885                 mBubbleData.getOverflow().initialize(
886                         mExpandedViewManager, mStackView, mBubblePositioner);
887             }
888             // (TODO: b/273314541) some duplication in the inset listener
889             if (isShowingAsBubbleBar()) {
890                 mWindowManager.addView(mLayerView, mWmLayoutParams);
891                 mLayerView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
892                     if (!windowInsets.equals(mWindowInsets) && mLayerView != null) {
893                         mWindowInsets = windowInsets;
894                         mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager));
895                         mLayerView.onDisplaySizeChanged();
896                     }
897                     return windowInsets;
898                 });
899             } else {
900                 mWindowManager.addView(mStackView, mWmLayoutParams);
901                 mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
902                     if (!windowInsets.equals(mWindowInsets) && mStackView != null) {
903                         mWindowInsets = windowInsets;
904                         mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager));
905                         mStackView.onDisplaySizeChanged();
906                     }
907                     return windowInsets;
908                 });
909             }
910         } catch (IllegalStateException e) {
911             // This means the view has already been added. This shouldn't happen...
912             e.printStackTrace();
913         }
914     }
915 
916     /**
917      * In some situations bubble's should be able to receive key events for back:
918      * - when the bubble overflow is showing
919      * - when the user education for the stack is showing.
920      *
921      * @param interceptBack whether back should be intercepted or not.
922      */
updateWindowFlagsForBackpress(boolean interceptBack)923     void updateWindowFlagsForBackpress(boolean interceptBack) {
924         if (mAddedToWindowManager) {
925             ProtoLog.d(WM_SHELL_BUBBLES, "updateFlagsForBackPress interceptBack=%b", interceptBack);
926             mWmLayoutParams.flags = interceptBack
927                     ? 0
928                     : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
929                             | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
930             mWmLayoutParams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
931             if (mStackView != null) {
932                 mWindowManager.updateViewLayout(mStackView, mWmLayoutParams);
933             } else if (mLayerView != null) {
934                 mWindowManager.updateViewLayout(mLayerView, mWmLayoutParams);
935             }
936         }
937     }
938 
939     /** Removes any bubble views from the WindowManager that exist. */
removeFromWindowManagerMaybe()940     private void removeFromWindowManagerMaybe() {
941         if (!mAddedToWindowManager) {
942             return;
943         }
944 
945         mAddedToWindowManager = false;
946         // Put on background for this binder call, was causing jank
947         mBackgroundExecutor.execute(() -> {
948             try {
949                 mContext.unregisterReceiver(mBroadcastReceiver);
950             } catch (IllegalArgumentException e) {
951                 // Not sure if this happens in production, but was happening in tests
952                 // (b/253647225)
953                 e.printStackTrace();
954             }
955         });
956         try {
957             if (mStackView != null) {
958                 mWindowManager.removeView(mStackView);
959                 mBubbleData.getOverflow().cleanUpExpandedState();
960             }
961             if (mLayerView != null) {
962                 mWindowManager.removeView(mLayerView);
963                 mBubbleData.getOverflow().cleanUpExpandedState();
964             }
965         } catch (IllegalArgumentException e) {
966             // This means the stack has already been removed - it shouldn't happen, but ignore if it
967             // does, since we wanted it removed anyway.
968             e.printStackTrace();
969         }
970     }
971 
registerBroadcastReceiver()972     private void registerBroadcastReceiver() {
973         IntentFilter filter = new IntentFilter();
974         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
975         filter.addAction(Intent.ACTION_SCREEN_OFF);
976         mContext.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED);
977     }
978 
979     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
980         @Override
981         public void onReceive(Context context, Intent intent) {
982             if (!isStackExpanded()) return; // Nothing to do
983 
984             String action = intent.getAction();
985             String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
986             boolean validReasonToCollapse = SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason)
987                     || SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)
988                     || SYSTEM_DIALOG_REASON_GESTURE_NAV.equals(reason);
989             if ((Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action) && validReasonToCollapse)
990                     || Intent.ACTION_SCREEN_OFF.equals(action)) {
991                 mMainExecutor.execute(() -> collapseStack());
992             }
993         }
994     };
995 
registerShortcutBroadcastReceiver()996     private void registerShortcutBroadcastReceiver() {
997         IntentFilter shortcutFilter = new IntentFilter();
998         shortcutFilter.addAction(BubbleShortcutHelper.ACTION_SHOW_BUBBLES);
999         ProtoLog.d(WM_SHELL_BUBBLES, "register broadcast receive for bubbles shortcut");
1000         mContext.registerReceiver(mShortcutBroadcastReceiver, shortcutFilter,
1001                 Context.RECEIVER_NOT_EXPORTED);
1002     }
1003 
1004     private final BroadcastReceiver mShortcutBroadcastReceiver = new BroadcastReceiver() {
1005         @Override
1006         public void onReceive(Context context, Intent intent) {
1007             ProtoLog.v(WM_SHELL_BUBBLES, "receive broadcast to show bubbles %s",
1008                     intent.getAction());
1009             if (BubbleShortcutHelper.ACTION_SHOW_BUBBLES.equals(intent.getAction())) {
1010                 mMainExecutor.execute(() -> showBubblesFromShortcut());
1011             }
1012         }
1013     };
1014 
1015     /**
1016      * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been
1017      * added in the meantime.
1018      */
onAllBubblesAnimatedOut()1019     public void onAllBubblesAnimatedOut() {
1020         if (mStackView != null) {
1021             mStackView.setVisibility(INVISIBLE);
1022             removeFromWindowManagerMaybe();
1023         } else if (mLayerView != null) {
1024             mLayerView.setVisibility(INVISIBLE);
1025             removeFromWindowManagerMaybe();
1026         }
1027     }
1028 
1029     /**
1030      * Records the notification key for any active bubbles. These are used to restore active
1031      * bubbles when the user returns to the foreground.
1032      *
1033      * @param userId the id of the user
1034      */
saveBubbles(@serIdInt int userId)1035     private void saveBubbles(@UserIdInt int userId) {
1036         // First clear any existing keys that might be stored.
1037         mSavedUserBubbleData.remove(userId);
1038         UserBubbleData userBubbleData = new UserBubbleData();
1039         // Add in all active bubbles for the current user.
1040         for (Bubble bubble : mBubbleData.getBubbles()) {
1041             userBubbleData.add(bubble.getKey(), bubble.showInShade());
1042         }
1043         mSavedUserBubbleData.put(userId, userBubbleData);
1044     }
1045 
1046     /**
1047      * Promotes existing notifications to Bubbles if they were previously bubbles.
1048      *
1049      * @param userId the id of the user
1050      */
restoreBubbles(@serIdInt int userId)1051     private void restoreBubbles(@UserIdInt int userId) {
1052         UserBubbleData savedBubbleData = mSavedUserBubbleData.get(userId);
1053         if (savedBubbleData == null) {
1054             // There were no bubbles saved for this used.
1055             return;
1056         }
1057         mSysuiProxy.getShouldRestoredEntries(savedBubbleData.getKeys(), (entries) -> {
1058             mMainExecutor.execute(() -> {
1059                 for (BubbleEntry e : entries) {
1060                     if (canLaunchInTaskView(mContext, e)) {
1061                         boolean showInShade = savedBubbleData.isShownInShade(e.getKey());
1062                         updateBubble(e, true /* suppressFlyout */, showInShade);
1063                     }
1064                 }
1065             });
1066         });
1067         // Finally, remove the entries for this user now that bubbles are restored.
1068         mSavedUserBubbleData.remove(userId);
1069     }
1070 
1071     @Override
onThemeChanged()1072     public void onThemeChanged() {
1073         if (mStackView != null) {
1074             mStackView.onThemeChanged();
1075         }
1076         mBubbleIconFactory = new BubbleIconFactory(mContext,
1077                 mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size),
1078                 mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
1079                 mContext.getResources().getColor(
1080                         com.android.launcher3.icons.R.color.important_conversation),
1081                 mContext.getResources().getDimensionPixelSize(
1082                         com.android.internal.R.dimen.importance_ring_stroke_width));
1083 
1084         // Reload each bubble
1085         for (Bubble b : mBubbleData.getBubbles()) {
1086             b.inflate(null /* callback */,
1087                     mContext,
1088                     mExpandedViewManager,
1089                     mBubbleTaskViewFactory,
1090                     mBubblePositioner,
1091                     mStackView,
1092                     mLayerView,
1093                     mBubbleIconFactory,
1094                     false /* skipInflation */);
1095         }
1096         for (Bubble b : mBubbleData.getOverflowBubbles()) {
1097             b.inflate(null /* callback */,
1098                     mContext,
1099                     mExpandedViewManager,
1100                     mBubbleTaskViewFactory,
1101                     mBubblePositioner,
1102                     mStackView,
1103                     mLayerView,
1104                     mBubbleIconFactory,
1105                     false /* skipInflation */);
1106         }
1107     }
1108 
1109     @Override
onConfigurationChanged(Configuration newConfig)1110     public void onConfigurationChanged(Configuration newConfig) {
1111         if (mBubblePositioner != null) {
1112             mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager));
1113         }
1114         if (mStackView != null && newConfig != null) {
1115             if (newConfig.densityDpi != mDensityDpi
1116                     || !newConfig.windowConfiguration.getBounds().equals(mScreenBounds)) {
1117                 mDensityDpi = newConfig.densityDpi;
1118                 mScreenBounds.set(newConfig.windowConfiguration.getBounds());
1119                 mBubbleData.onMaxBubblesChanged();
1120                 mBubbleIconFactory = new BubbleIconFactory(mContext,
1121                         mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size),
1122                         mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size),
1123                         mContext.getResources().getColor(
1124                                 com.android.launcher3.icons.R.color.important_conversation),
1125                         mContext.getResources().getDimensionPixelSize(
1126                                 com.android.internal.R.dimen.importance_ring_stroke_width));
1127                 mStackView.onDisplaySizeChanged();
1128             }
1129             if (newConfig.fontScale != mFontScale) {
1130                 mFontScale = newConfig.fontScale;
1131                 mStackView.updateFontScale();
1132             }
1133             if (newConfig.getLayoutDirection() != mLayoutDirection) {
1134                 mLayoutDirection = newConfig.getLayoutDirection();
1135                 mStackView.onLayoutDirectionChanged(mLayoutDirection);
1136             }
1137             Locale newLocale = newConfig.locale;
1138             if (newLocale != null && !newLocale.equals(mLocale)) {
1139                 mLocale = newLocale;
1140                 mStackView.updateLocale();
1141             }
1142         }
1143     }
1144 
onNotificationPanelExpandedChanged(boolean expanded)1145     private void onNotificationPanelExpandedChanged(boolean expanded) {
1146         if (mStackView != null && mStackView.isExpanded()) {
1147             ProtoLog.d(WM_SHELL_BUBBLES,
1148                     "onNotificationPanelExpandedChanged expanded=%b", expanded);
1149             if (expanded) {
1150                 mStackView.stopMonitoringSwipeUpGesture();
1151             } else {
1152                 mStackView.startMonitoringSwipeUpGesture();
1153             }
1154         }
1155     }
1156 
setSysuiProxy(Bubbles.SysuiProxy proxy)1157     private void setSysuiProxy(Bubbles.SysuiProxy proxy) {
1158         mSysuiProxy = proxy;
1159     }
1160 
1161     @VisibleForTesting
setExpandListener(Bubbles.BubbleExpandListener listener)1162     public void setExpandListener(Bubbles.BubbleExpandListener listener) {
1163         mExpandListener = ((isExpanding, key) -> {
1164             if (listener != null) {
1165                 listener.onBubbleExpandChanged(isExpanding, key);
1166             }
1167         });
1168         if (mStackView != null) {
1169             mStackView.setExpandListener(mExpandListener);
1170         }
1171     }
1172 
1173     /**
1174      * Whether or not there are bubbles present, regardless of them being visible on the
1175      * screen (e.g. if on AOD).
1176      */
1177     @VisibleForTesting
hasBubbles()1178     public boolean hasBubbles() {
1179         if (mStackView == null && mLayerView == null) {
1180             return false;
1181         }
1182         return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
1183     }
1184 
isStackExpanded()1185     public boolean isStackExpanded() {
1186         return mBubbleData.isExpanded();
1187     }
1188 
collapseStack()1189     public void collapseStack() {
1190         mBubbleData.setExpanded(false /* expanded */);
1191     }
1192 
1193     /**
1194      * A bubble is being dragged in Launcher.
1195      * Will be called only when bubble bar is expanded.
1196      *
1197      * @param bubbleKey key of the bubble being dragged
1198      */
startBubbleDrag(String bubbleKey)1199     public void startBubbleDrag(String bubbleKey) {
1200         if (mBubbleData.getSelectedBubble() != null) {
1201             mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ false);
1202         }
1203         if (mBubbleStateListener != null) {
1204             boolean overflow = BubbleOverflow.KEY.equals(bubbleKey);
1205             Rect rect = new Rect();
1206             mBubblePositioner.getBubbleBarExpandedViewBounds(mBubblePositioner.isBubbleBarOnLeft(),
1207                     overflow, rect);
1208             BubbleBarUpdate update = new BubbleBarUpdate();
1209             update.expandedViewDropTargetSize = new Point(rect.width(), rect.height());
1210             mBubbleStateListener.onBubbleStateChange(update);
1211         }
1212     }
1213 
1214     /**
1215      * A bubble is no longer being dragged in Launcher. And was released in given location.
1216      * Will be called only when bubble bar is expanded.
1217      *
1218      * @param location location where bubble was released
1219      * @param topOnScreen      top coordinate of the bubble bar on the screen after release
1220      */
stopBubbleDrag(BubbleBarLocation location, int topOnScreen)1221     public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) {
1222         mBubblePositioner.setBubbleBarLocation(location);
1223         mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen);
1224         if (mBubbleData.getSelectedBubble() != null) {
1225             mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true);
1226         }
1227     }
1228 
1229     /**
1230      * A bubble was dragged and is released in dismiss target in Launcher.
1231      *
1232      * @param bubbleKey key of the bubble being dragged to dismiss target
1233      */
dragBubbleToDismiss(String bubbleKey)1234     public void dragBubbleToDismiss(String bubbleKey) {
1235         String selectedBubbleKey = mBubbleData.getSelectedBubbleKey();
1236         removeBubble(bubbleKey, Bubbles.DISMISS_USER_GESTURE);
1237         if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) {
1238             // We did not remove the selected bubble. Expand it again
1239             mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true);
1240         }
1241     }
1242 
1243     /**
1244      * Show bubble bar user education relative to the reference position.
1245      * @param position the reference position in Screen coordinates.
1246      */
showUserEducation(Point position)1247     public void showUserEducation(Point position) {
1248         if (mLayerView == null) return;
1249         mLayerView.showUserEducation(position);
1250     }
1251 
1252     @VisibleForTesting
isBubbleNotificationSuppressedFromShade(String key, String groupKey)1253     public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
1254         boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key)
1255                 && !mBubbleData.getAnyBubbleWithkey(key).showInShade());
1256 
1257         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
1258         boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey));
1259         return (isSummary && isSuppressedSummary) || isSuppressedBubble;
1260     }
1261 
1262     /** Promote the provided bubble from the overflow view. */
promoteBubbleFromOverflow(Bubble bubble)1263     public void promoteBubbleFromOverflow(Bubble bubble) {
1264         mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
1265         ProtoLog.d(WM_SHELL_BUBBLES, "promoteBubbleFromOverflow=%s", bubble.getKey());
1266         bubble.setInflateSynchronously(mInflateSynchronously);
1267         bubble.setShouldAutoExpand(true);
1268         bubble.markAsAccessedAt(System.currentTimeMillis());
1269         setIsBubble(bubble, true /* isBubble */);
1270     }
1271 
1272     /**
1273      * Expands and selects the provided bubble as long as it already exists in the stack or the
1274      * overflow.
1275      *
1276      * <p>This is used by external callers (launcher).
1277      */
1278     @VisibleForTesting
expandStackAndSelectBubbleFromLauncher(String key, int topOnScreen)1279     public void expandStackAndSelectBubbleFromLauncher(String key, int topOnScreen) {
1280         mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen);
1281 
1282         if (BubbleOverflow.KEY.equals(key)) {
1283             mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
1284             mLayerView.showExpandedView(mBubbleData.getOverflow());
1285             return;
1286         }
1287 
1288         Bubble b = mBubbleData.getAnyBubbleWithkey(key);
1289         if (b == null) {
1290             return;
1291         }
1292         if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) {
1293             // already in the stack
1294             mBubbleData.setSelectedBubbleFromLauncher(b);
1295             mLayerView.showExpandedView(b);
1296         } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) {
1297             // TODO: (b/271468319) handle overflow
1298         } else {
1299             Log.w(TAG, "didn't add bubble from launcher: " + key);
1300         }
1301     }
1302 
1303     /**
1304      * Expands the stack if the selected bubble is present. This is currently used when user
1305      * education view is clicked to expand the selected bubble.
1306      */
expandStackWithSelectedBubble()1307     public void expandStackWithSelectedBubble() {
1308         if (mBubbleData.getSelectedBubble() != null) {
1309             mBubbleData.setExpanded(true);
1310         }
1311     }
1312 
1313     /**
1314      * Expands and selects the provided bubble as long as it already exists in the stack or the
1315      * overflow. This is currently used when opening a bubble via clicking on a conversation widget.
1316      */
expandStackAndSelectBubble(Bubble b)1317     public void expandStackAndSelectBubble(Bubble b) {
1318         if (b == null) {
1319             return;
1320         }
1321         if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) {
1322             // already in the stack
1323             mBubbleData.setSelectedBubbleAndExpandStack(b);
1324         } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) {
1325             // promote it out of the overflow
1326             promoteBubbleFromOverflow(b);
1327         }
1328     }
1329 
1330     /**
1331      * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble
1332      * exists for this entry, and it is able to bubble, a new bubble will be created.
1333      *
1334      * <p>This is the method to use when opening a bubble via a notification or in a state where
1335      * the device might not be unlocked.
1336      *
1337      * @param entry the entry to use for the bubble.
1338      */
expandStackAndSelectBubble(BubbleEntry entry)1339     public void expandStackAndSelectBubble(BubbleEntry entry) {
1340         ProtoLog.d(WM_SHELL_BUBBLES, "opening bubble from notification key=%s mIsStatusBarShade=%b",
1341                 entry.getKey(), mIsStatusBarShade);
1342         if (mIsStatusBarShade) {
1343             mNotifEntryToExpandOnShadeUnlock = null;
1344 
1345             String key = entry.getKey();
1346             Bubble bubble = mBubbleData.getBubbleInStackWithKey(key);
1347             if (bubble != null) {
1348                 mBubbleData.setSelectedBubbleAndExpandStack(bubble);
1349             } else {
1350                 bubble = mBubbleData.getOverflowBubbleWithKey(key);
1351                 if (bubble != null) {
1352                     promoteBubbleFromOverflow(bubble);
1353                 } else if (entry.canBubble()) {
1354                     // It can bubble but it's not -- it got aged out of the overflow before it
1355                     // was dismissed or opened, make it a bubble again.
1356                     setIsBubble(entry, true /* isBubble */, true /* autoExpand */);
1357                 }
1358             }
1359         } else {
1360             // Wait until we're unlocked to expand, so that the user can see the expand animation
1361             // and also to work around bugs with expansion animation + shade unlock happening at the
1362             // same time.
1363             mNotifEntryToExpandOnShadeUnlock = entry;
1364         }
1365     }
1366 
1367     /**
1368      * Adds or updates a bubble associated with the provided notification entry.
1369      *
1370      * @param notif the notification associated with this bubble.
1371      */
1372     @VisibleForTesting
updateBubble(BubbleEntry notif)1373     public void updateBubble(BubbleEntry notif) {
1374         int bubbleUserId = notif.getStatusBarNotification().getUserId();
1375         if (isCurrentProfile(bubbleUserId)) {
1376             updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
1377         } else {
1378             // Skip update, but store it in user bubbles so it gets restored after user switch
1379             mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(),
1380                     true /* shownInShade */);
1381             Log.w(TAG, "updateBubble, ignore update for non-active user=" + bubbleUserId
1382                     + " currentUser=" + mCurrentUserId);
1383         }
1384     }
1385 
1386     /**
1387      * This method has different behavior depending on:
1388      *    - if an app bubble exists
1389      *    - if an app bubble is expanded
1390      *
1391      * If no app bubble exists, this will add and expand a bubble with the provided intent. The
1392      * intent must be explicit (i.e. include a package name or fully qualified component class name)
1393      * and the activity for it should be resizable.
1394      *
1395      * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is
1396      * expanded, calling this method will collapse it. If the app bubble is not expanded, calling
1397      * this method will expand it.
1398      *
1399      * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses
1400      * the bubble or bubble stack.
1401      *
1402      * Some notes:
1403      *    - Only one app bubble is supported at a time, regardless of users. Multi-users support is
1404      *      tracked in b/273533235.
1405      *    - Calling this method with a different intent than the existing app bubble will do nothing
1406      *
1407      * @param intent the intent to display in the bubble expanded view.
1408      * @param user the {@link UserHandle} of the user to start this activity for.
1409      * @param icon the {@link Icon} to use for the bubble view.
1410      */
showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon)1411     public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) {
1412         if (intent == null || intent.getPackage() == null) {
1413             Log.w(TAG, "App bubble failed to show, invalid intent: " + intent
1414                     + ((intent != null) ? " with package: " + intent.getPackage() : " "));
1415             return;
1416         }
1417 
1418         String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
1419         PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier());
1420         if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; // logs errors
1421 
1422         Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey);
1423         ProtoLog.d(WM_SHELL_BUBBLES,
1424                 "showOrHideAppBubble, key=%s existingAppBubble=%s stackVisibility=%s "
1425                         + "statusBarShade=%s",
1426                 appBubbleKey, existingAppBubble,
1427                 (mStackView != null ? mStackView.getVisibility() : "null"),
1428                 mIsStatusBarShade);
1429 
1430         if (existingAppBubble != null) {
1431             BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
1432             if (isStackExpanded()) {
1433                 if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) {
1434                     ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", appBubbleKey);
1435                     // App bubble is expanded, lets collapse
1436                     collapseStack();
1437                 } else {
1438                     ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", appBubbleKey);
1439                     // App bubble is not selected, select it
1440                     mBubbleData.setSelectedBubble(existingAppBubble);
1441                 }
1442             } else {
1443                 ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", appBubbleKey);
1444                 // App bubble is not selected, select it & expand
1445                 mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble);
1446             }
1447         } else {
1448             // Check if it exists in the overflow
1449             Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey);
1450             if (b != null) {
1451                 // It's in the overflow, so remove it & reinflate
1452                 mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
1453             } else {
1454                 // App bubble does not exist, lets add and expand it
1455                 b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
1456             }
1457             ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey);
1458             b.setShouldAutoExpand(true);
1459             inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
1460         }
1461     }
1462 
1463     /**
1464      * Dismiss bubble if it exists and remove it from the stack
1465      */
dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason)1466     public void dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason) {
1467         dismissBubble(bubble.getKey(), reason);
1468     }
1469 
1470     /**
1471      * Dismiss bubble with given key if it exists and remove it from the stack
1472      */
dismissBubble(String key, @Bubbles.DismissReason int reason)1473     public void dismissBubble(String key, @Bubbles.DismissReason int reason) {
1474         mBubbleData.dismissBubbleWithKey(key, reason);
1475     }
1476 
1477     /**
1478      * Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot
1479      * can be access via the supplied {@link SynchronousScreenCaptureListener#getBuffer()}
1480      * asynchronously.
1481      */
getScreenshotExcludingBubble(int displayId, SynchronousScreenCaptureListener screenCaptureListener)1482     public void getScreenshotExcludingBubble(int displayId,
1483             SynchronousScreenCaptureListener screenCaptureListener) {
1484         try {
1485             ScreenCapture.CaptureArgs args = null;
1486             View viewToUse = mStackView != null ? mStackView : mLayerView;
1487             if (viewToUse != null) {
1488                 ViewRootImpl viewRoot = viewToUse.getViewRootImpl();
1489                 if (viewRoot != null) {
1490                     SurfaceControl bubbleLayer = viewRoot.getSurfaceControl();
1491                     if (bubbleLayer != null) {
1492                         args = new ScreenCapture.CaptureArgs.Builder<>()
1493                                 .setExcludeLayers(new SurfaceControl[] {bubbleLayer})
1494                                 .build();
1495                     }
1496                 }
1497             }
1498 
1499             mWmService.captureDisplay(displayId, args, screenCaptureListener);
1500         } catch (RemoteException e) {
1501             Log.e(TAG, "Failed to capture screenshot");
1502         }
1503     }
1504 
1505     /** Sets the app bubble's taskId which is cached for SysUI. */
setAppBubbleTaskId(String key, int taskId)1506     public void setAppBubbleTaskId(String key, int taskId) {
1507         mImpl.mCachedState.setAppBubbleTaskId(key, taskId);
1508     }
1509 
1510     /**
1511      * Fills the overflow bubbles by loading them from disk.
1512      */
loadOverflowBubblesFromDisk()1513     void loadOverflowBubblesFromDisk() {
1514         if (!mOverflowDataLoadNeeded) {
1515             return;
1516         }
1517         mOverflowDataLoadNeeded = false;
1518         List<UserInfo> users = mUserManager.getAliveUsers();
1519         List<Integer> userIds = users.stream().map(userInfo -> userInfo.id).toList();
1520         mDataRepository.loadBubbles(mCurrentUserId, userIds, (bubbles) -> {
1521             bubbles.forEach(bubble -> {
1522                 if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) {
1523                     // if the bubble is already active, there's no need to push it to overflow
1524                     return;
1525                 }
1526                 bubble.inflate(
1527                         (b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble),
1528                         mContext,
1529                         mExpandedViewManager,
1530                         mBubbleTaskViewFactory,
1531                         mBubblePositioner,
1532                         mStackView,
1533                         mLayerView,
1534                         mBubbleIconFactory,
1535                         true /* skipInflation */);
1536             });
1537             return null;
1538         });
1539     }
1540 
setUpBubbleViewsForMode()1541     void setUpBubbleViewsForMode() {
1542         mBubbleViewCallback = isShowingAsBubbleBar()
1543                 ? mBubbleBarViewCallback
1544                 : mBubbleStackViewCallback;
1545 
1546         // reset the overflow so that it can be re-added later if needed.
1547         if (mStackView != null) {
1548             mStackView.resetOverflowView();
1549             mStackView.removeAllViews();
1550         }
1551         // cleanup existing bubble views so they can be recreated later if needed, but retain
1552         // TaskView.
1553         mBubbleData.getBubbles().forEach(b -> b.cleanupViews(/* cleanupTaskView= */ false));
1554 
1555         // remove the current bubble container from window manager, null it out, and create a new
1556         // container based on the current mode.
1557         removeFromWindowManagerMaybe();
1558         mLayerView = null;
1559         mStackView = null;
1560 
1561         if (!mBubbleData.hasBubbles()) {
1562             // if there are no bubbles, don't create the stack or layer views. they will be created
1563             // later when the first bubble is added.
1564             return;
1565         }
1566 
1567         ensureBubbleViewsAndWindowCreated();
1568 
1569         // inflate bubble views
1570         BubbleViewInfoTask.Callback callback = null;
1571         if (!isShowingAsBubbleBar()) {
1572             callback = b -> {
1573                 if (mStackView != null) {
1574                     mStackView.addBubble(b);
1575                     mStackView.setSelectedBubble(b);
1576                 } else {
1577                     Log.w(TAG, "Tried to add a bubble to the stack but the stack is null");
1578                 }
1579             };
1580         } else if (mBubbleData.isExpanded() && mBubbleData.getSelectedBubble() != null) {
1581             callback = b -> {
1582                 if (b.getKey().equals(mBubbleData.getSelectedBubbleKey())) {
1583                     mLayerView.showExpandedView(b);
1584                 }
1585             };
1586         }
1587         for (int i = mBubbleData.getBubbles().size() - 1; i >= 0; i--) {
1588             Bubble bubble = mBubbleData.getBubbles().get(i);
1589             bubble.inflate(callback,
1590                     mContext,
1591                     mExpandedViewManager,
1592                     mBubbleTaskViewFactory,
1593                     mBubblePositioner,
1594                     mStackView,
1595                     mLayerView,
1596                     mBubbleIconFactory,
1597                     false /* skipInflation */);
1598         }
1599     }
1600 
1601     /**
1602      * Adds or updates a bubble associated with the provided notification entry.
1603      *
1604      * @param notif          the notification associated with this bubble.
1605      * @param suppressFlyout this bubble suppress flyout or not.
1606      * @param showInShade    this bubble show in shade or not.
1607      */
1608     @VisibleForTesting
updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade)1609     public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) {
1610         // If this is an interruptive notif, mark that it's interrupted
1611         mSysuiProxy.setNotificationInterruption(notif.getKey());
1612         boolean isNonInterruptiveNotExpanding = !notif.getRanking().isTextChanged()
1613                 && (notif.getBubbleMetadata() != null
1614                 && !notif.getBubbleMetadata().getAutoExpandBubble());
1615         if (isNonInterruptiveNotExpanding
1616                 && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) {
1617             // Update the bubble but don't promote it out of overflow
1618             Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey());
1619             if (notif.isBubble()) {
1620                 notif.setFlagBubble(false);
1621             }
1622             updateNotNotifyingEntry(b, notif, showInShade);
1623         } else if (mBubbleData.hasAnyBubbleWithKey(notif.getKey())
1624                 && isNonInterruptiveNotExpanding) {
1625             Bubble b = mBubbleData.getAnyBubbleWithkey(notif.getKey());
1626             if (b != null) {
1627                 updateNotNotifyingEntry(b, notif, showInShade);
1628             }
1629         } else if (mBubbleData.isSuppressedWithLocusId(notif.getLocusId())) {
1630             // Update the bubble but don't promote it out of overflow
1631             Bubble b = mBubbleData.getSuppressedBubbleWithKey(notif.getKey());
1632             if (b != null) {
1633                 updateNotNotifyingEntry(b, notif, showInShade);
1634             }
1635         } else {
1636             Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
1637             if (notif.shouldSuppressNotificationList()) {
1638                 // If we're suppressing notifs for DND, we don't want the bubbles to randomly
1639                 // expand when DND turns off so flip the flag.
1640                 if (bubble.shouldAutoExpand()) {
1641                     bubble.setShouldAutoExpand(false);
1642                 }
1643                 mImpl.mCachedState.updateBubbleSuppressedState(bubble);
1644             } else {
1645                 inflateAndAdd(bubble, suppressFlyout, showInShade);
1646             }
1647         }
1648     }
1649 
updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade)1650     void updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade) {
1651         boolean showInShadeBefore = b.showInShade();
1652         boolean isBubbleSelected = Objects.equals(b, mBubbleData.getSelectedBubble());
1653         boolean isBubbleExpandedAndSelected = isStackExpanded() && isBubbleSelected;
1654         b.setEntry(entry);
1655         boolean suppress = isBubbleExpandedAndSelected || !showInShade || !b.showInShade();
1656         b.setSuppressNotification(suppress);
1657         b.setShowDot(!isBubbleExpandedAndSelected);
1658         if (showInShadeBefore != b.showInShade()) {
1659             mImpl.mCachedState.updateBubbleSuppressedState(b);
1660         }
1661     }
1662 
1663     @VisibleForTesting
inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade)1664     public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
1665         // Lazy init stack view when a bubble is created
1666         ensureBubbleViewsAndWindowCreated();
1667         bubble.setInflateSynchronously(mInflateSynchronously);
1668         bubble.inflate(
1669                 b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
1670                 mContext,
1671                 mExpandedViewManager,
1672                 mBubbleTaskViewFactory,
1673                 mBubblePositioner,
1674                 mStackView,
1675                 mLayerView,
1676                 mBubbleIconFactory,
1677                 false /* skipInflation */);
1678     }
1679 
1680     /**
1681      * Removes the bubble with the given key.
1682      * <p>
1683      * Must be called from the main thread.
1684      */
1685     @MainThread
removeBubble(String key, int reason)1686     public void removeBubble(String key, int reason) {
1687         if (mBubbleData.hasAnyBubbleWithKey(key)) {
1688             mBubbleData.dismissBubbleWithKey(key, reason);
1689         }
1690     }
1691 
1692     /**
1693      * Removes all the bubbles.
1694      * <p>
1695      * Must be called from the main thread.
1696      */
1697     @VisibleForTesting
1698     @MainThread
removeAllBubbles(@ubbles.DismissReason int reason)1699     public void removeAllBubbles(@Bubbles.DismissReason int reason) {
1700         mBubbleData.dismissAll(reason);
1701     }
1702 
onEntryAdded(BubbleEntry entry)1703     private void onEntryAdded(BubbleEntry entry) {
1704         if (canLaunchInTaskView(mContext, entry)) {
1705             updateBubble(entry);
1706         }
1707     }
1708 
1709     @VisibleForTesting
onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem)1710     public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) {
1711         if (!fromSystem) {
1712             return;
1713         }
1714         // shouldBubbleUp checks canBubble & for bubble metadata
1715         boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry);
1716         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
1717             // It was previously a bubble but no longer a bubble -- lets remove it
1718             removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
1719         } else if (shouldBubble && entry.isBubble()) {
1720             updateBubble(entry);
1721         }
1722     }
1723 
onEntryRemoved(BubbleEntry entry)1724     private void onEntryRemoved(BubbleEntry entry) {
1725         if (isSummaryOfBubbles(entry)) {
1726             final String groupKey = entry.getStatusBarNotification().getGroupKey();
1727             mBubbleData.removeSuppressedSummary(groupKey);
1728 
1729             // Remove any associated bubble children with the summary
1730             final List<Bubble> bubbleChildren = getBubblesInGroup(groupKey);
1731             for (int i = 0; i < bubbleChildren.size(); i++) {
1732                 removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED);
1733             }
1734         } else {
1735             removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL);
1736         }
1737     }
1738 
1739     @VisibleForTesting
onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey)1740     public void onRankingUpdated(RankingMap rankingMap,
1741             HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) {
1742         if (mTmpRanking == null) {
1743             mTmpRanking = new NotificationListenerService.Ranking();
1744         }
1745         String[] orderedKeys = rankingMap.getOrderedKeys();
1746         for (int i = 0; i < orderedKeys.length; i++) {
1747             String key = orderedKeys[i];
1748             Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key);
1749             BubbleEntry entry = entryData.first;
1750             boolean shouldBubbleUp = entryData.second;
1751             if (entry != null && !isCurrentProfile(
1752                     entry.getStatusBarNotification().getUser().getIdentifier())) {
1753                 return;
1754             }
1755             if (entry != null && (entry.shouldSuppressNotificationList()
1756                     || entry.getRanking().isSuspended())) {
1757                 shouldBubbleUp = false;
1758             }
1759             rankingMap.getRanking(key, mTmpRanking);
1760             boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key);
1761             boolean isActive = mBubbleData.hasBubbleInStackWithKey(key);
1762             if (isActiveOrInOverflow && !mTmpRanking.canBubble()) {
1763                 // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason.
1764                 // This means that the app or channel's ability to bubble has been revoked.
1765                 mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED);
1766             } else if (isActiveOrInOverflow && !shouldBubbleUp) {
1767                 // If this entry is allowed to bubble, but cannot currently bubble up or is
1768                 // suspended, dismiss it. This happens when DND is enabled and configured to hide
1769                 // bubbles, or focus mode is enabled and the app is designated as distracting.
1770                 // Dismissing with the reason DISMISS_NO_BUBBLE_UP will retain the underlying
1771                 // notification, so that the bubble will be re-created if shouldBubbleUp returns
1772                 // true.
1773                 mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP);
1774             } else if (entry != null && mTmpRanking.isBubble() && !isActiveOrInOverflow) {
1775                 entry.setFlagBubble(true);
1776                 onEntryUpdated(entry, shouldBubbleUp, /* fromSystem= */ true);
1777             }
1778         }
1779     }
1780 
1781     @VisibleForTesting
onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, int modificationType)1782     public void onNotificationChannelModified(String pkg, UserHandle user,
1783             NotificationChannel channel, int modificationType) {
1784         // Only query overflow bubbles here because active bubbles will have an active notification
1785         // and channel changes we care about would result in a ranking update.
1786         List<Bubble> overflowBubbles = new ArrayList<>(mBubbleData.getOverflowBubbles());
1787         for (int i = 0; i < overflowBubbles.size(); i++) {
1788             Bubble b = overflowBubbles.get(i);
1789             if (Objects.equals(b.getShortcutId(), channel.getConversationId())
1790                     && b.getPackageName().equals(pkg)
1791                     && b.getUser().getIdentifier() == user.getIdentifier()) {
1792                 if (!channel.canBubble() || channel.isDeleted()) {
1793                     mBubbleData.dismissBubbleWithKey(b.getKey(), DISMISS_NO_LONGER_BUBBLE);
1794                 }
1795             }
1796         }
1797     }
1798 
1799     /**
1800      * Retrieves any bubbles that are part of the notification group represented by the provided
1801      * group key.
1802      */
getBubblesInGroup(@ullable String groupKey)1803     private ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
1804         ArrayList<Bubble> bubbleChildren = new ArrayList<>();
1805         if (groupKey == null) {
1806             return bubbleChildren;
1807         }
1808         for (Bubble bubble : mBubbleData.getBubbles()) {
1809             if (bubble.getGroupKey() != null && groupKey.equals(bubble.getGroupKey())) {
1810                 bubbleChildren.add(bubble);
1811             }
1812         }
1813         return bubbleChildren;
1814     }
1815 
setIsBubble(@onNull final BubbleEntry entry, final boolean isBubble, final boolean autoExpand)1816     private void setIsBubble(@NonNull final BubbleEntry entry, final boolean isBubble,
1817             final boolean autoExpand) {
1818         Objects.requireNonNull(entry);
1819         entry.setFlagBubble(isBubble);
1820         try {
1821             int flags = 0;
1822             if (autoExpand) {
1823                 flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
1824                 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
1825             }
1826             mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, flags);
1827         } catch (RemoteException e) {
1828             // Bad things have happened
1829         }
1830     }
1831 
setIsBubble(@onNull final Bubble b, final boolean isBubble)1832     private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) {
1833         Objects.requireNonNull(b);
1834         b.setIsBubble(isBubble);
1835         mSysuiProxy.getPendingOrActiveEntry(b.getKey(), (entry) -> {
1836             mMainExecutor.execute(() -> {
1837                 if (entry != null) {
1838                     // Updating the entry to be a bubble will trigger our normal update flow
1839                     setIsBubble(entry, isBubble, b.shouldAutoExpand());
1840                 } else if (isBubble) {
1841                     // If bubble doesn't exist, it's a persisted bubble so we need to add it to the
1842                     // stack ourselves
1843                     Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */);
1844                     inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */,
1845                             !bubble.shouldAutoExpand() /* showInShade */);
1846                 }
1847             });
1848         });
1849     }
1850 
1851     /** When bubbles are floating, this will be used to notify the floating views. */
1852     private final BubbleViewCallback mBubbleStackViewCallback = new BubbleViewCallback() {
1853         @Override
1854         public void removeBubble(Bubble removedBubble) {
1855             if (mStackView != null) {
1856                 mStackView.removeBubble(removedBubble);
1857             }
1858         }
1859 
1860         @Override
1861         public void addBubble(Bubble addedBubble) {
1862             if (mStackView != null) {
1863                 mStackView.addBubble(addedBubble);
1864             }
1865         }
1866 
1867         @Override
1868         public void updateBubble(Bubble updatedBubble) {
1869             if (mStackView != null) {
1870                 mStackView.updateBubble(updatedBubble);
1871             }
1872         }
1873 
1874         @Override
1875         public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) {
1876             if (mStackView != null) {
1877                 mStackView.updateBubbleOrder(bubbleOrder, updatePointer);
1878             }
1879         }
1880 
1881         @Override
1882         public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
1883             if (mStackView != null) {
1884                 mStackView.setBubbleSuppressed(bubble, isSuppressed);
1885             }
1886         }
1887 
1888         @Override
1889         public void expansionChanged(boolean isExpanded) {
1890             if (mStackView != null) {
1891                 mStackView.setExpanded(isExpanded);
1892             }
1893         }
1894 
1895         @Override
1896         public void selectionChanged(BubbleViewProvider selectedBubble) {
1897             if (mStackView != null) {
1898                 mStackView.setSelectedBubble(selectedBubble);
1899             }
1900 
1901         }
1902 
1903         @Override
1904         public void bubbleOverflowChanged(boolean hasBubbles) {
1905             if (Flags.enableOptionalBubbleOverflow()) {
1906                 if (mStackView != null) {
1907                     mStackView.showOverflow(hasBubbles);
1908                 }
1909             }
1910         }
1911     };
1912 
1913     /** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */
1914     private final BubbleViewCallback mBubbleBarViewCallback = new BubbleViewCallback() {
1915         @Override
1916         public void removeBubble(Bubble removedBubble) {
1917             if (mLayerView != null) {
1918                 mLayerView.removeBubble(removedBubble, () -> {
1919                     if (!mBubbleData.hasBubbles() && !isStackExpanded()) {
1920                         mLayerView.setVisibility(INVISIBLE);
1921                         removeFromWindowManagerMaybe();
1922                     }
1923                 });
1924             }
1925         }
1926 
1927         @Override
1928         public void addBubble(Bubble addedBubble) {
1929             // Nothing to do for adds, these are handled by launcher / in the bubble bar.
1930         }
1931 
1932         @Override
1933         public void updateBubble(Bubble updatedBubble) {
1934             // Nothing to do for updates, these are handled by launcher / in the bubble bar.
1935         }
1936 
1937         @Override
1938         public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) {
1939             // Nothing to do for order changes, these are handled by launcher / in the bubble bar.
1940         }
1941 
1942         @Override
1943         public void bubbleOverflowChanged(boolean hasBubbles) {
1944             // Nothing to do for our views, handled by launcher / in the bubble bar.
1945         }
1946 
1947         @Override
1948         public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
1949             if (mLayerView != null) {
1950                 // TODO (b/273316505) handle suppression changes, although might not need to
1951                 //  to do anything on the layerview side for this...
1952             }
1953         }
1954 
1955         @Override
1956         public void expansionChanged(boolean isExpanded) {
1957             if (mLayerView != null) {
1958                 if (!isExpanded) {
1959                     mLayerView.collapse();
1960                 } else {
1961                     BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
1962                     if (selectedBubble != null) {
1963                         mLayerView.showExpandedView(selectedBubble);
1964                     }
1965                 }
1966             }
1967         }
1968 
1969         @Override
1970         public void selectionChanged(BubbleViewProvider selectedBubble) {
1971             // Only need to update the layer view if we're currently expanded for selection changes.
1972             if (mLayerView != null && isStackExpanded()) {
1973                 mLayerView.showExpandedView(selectedBubble);
1974             }
1975         }
1976     };
1977 
1978     @SuppressWarnings("FieldCanBeLocal")
1979     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
1980 
1981         @Override
1982         public void applyUpdate(BubbleData.Update update) {
1983             ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:"
1984                     + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b"
1985                     + " expanded=%b selectionChanged=%b selected=%s"
1986                     + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b",
1987                     update.addedBubble != null ? update.addedBubble.getKey() : "null",
1988                     !update.removedBubbles.isEmpty(),
1989                     update.updatedBubble != null ? update.updatedBubble.getKey() : "null",
1990                     update.orderChanged, update.expandedChanged, update.expanded,
1991                     update.selectionChanged,
1992                     update.selectedBubble != null ? update.selectedBubble.getKey() : "null",
1993                     update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null",
1994                     update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null",
1995                     update.shouldShowEducation, update.showOverflowChanged);
1996 
1997             ensureBubbleViewsAndWindowCreated();
1998 
1999             // Lazy load overflow bubbles from disk
2000             loadOverflowBubblesFromDisk();
2001 
2002             if (update.showOverflowChanged) {
2003                 mBubbleViewCallback.bubbleOverflowChanged(!update.overflowBubbles.isEmpty());
2004             }
2005 
2006             // If bubbles in the overflow have a dot, make sure the overflow shows a dot
2007             updateOverflowButtonDot();
2008 
2009             // Update bubbles in overflow.
2010             if (mOverflowListener != null) {
2011                 mOverflowListener.applyUpdate(update);
2012             }
2013 
2014             // Do removals, if any.
2015             ArrayList<Pair<Bubble, Integer>> removedBubbles =
2016                     new ArrayList<>(update.removedBubbles);
2017             ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>();
2018             for (Pair<Bubble, Integer> removed : removedBubbles) {
2019                 final Bubble bubble = removed.first;
2020                 @Bubbles.DismissReason final int reason = removed.second;
2021 
2022                 mBubbleViewCallback.removeBubble(bubble);
2023 
2024                 // Leave the notification in place if we're dismissing due to user switching, or
2025                 // because DND is suppressing the bubble. In both of those cases, we need to be able
2026                 // to restore the bubble from the notification later.
2027                 if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) {
2028                     continue;
2029                 }
2030                 if (reason == DISMISS_NOTIF_CANCEL
2031                         || reason == DISMISS_SHORTCUT_REMOVED) {
2032                     bubblesToBeRemovedFromRepository.add(bubble);
2033                 }
2034                 if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
2035                     if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())
2036                             && (!bubble.showInShade()
2037                             || reason == DISMISS_NOTIF_CANCEL
2038                             || reason == DISMISS_GROUP_CANCELLED)) {
2039                         // The bubble is now gone & the notification is hidden from the shade, so
2040                         // time to actually remove it
2041                         mSysuiProxy.notifyRemoveNotification(bubble.getKey(), REASON_CANCEL);
2042                     } else {
2043                         if (bubble.isBubble()) {
2044                             setIsBubble(bubble, false /* isBubble */);
2045                         }
2046                         mSysuiProxy.updateNotificationBubbleButton(bubble.getKey());
2047                     }
2048                 }
2049             }
2050             mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository);
2051 
2052             if (update.addedBubble != null) {
2053                 mDataRepository.addBubble(mCurrentUserId, update.addedBubble);
2054                 mBubbleViewCallback.addBubble(update.addedBubble);
2055             }
2056 
2057             if (update.updatedBubble != null) {
2058                 mBubbleViewCallback.updateBubble(update.updatedBubble);
2059             }
2060 
2061             if (update.suppressedBubble != null) {
2062                 mBubbleViewCallback.suppressionChanged(update.suppressedBubble, true);
2063             }
2064 
2065             if (update.unsuppressedBubble != null) {
2066                 mBubbleViewCallback.suppressionChanged(update.unsuppressedBubble, false);
2067             }
2068 
2069             boolean collapseStack = update.expandedChanged && !update.expanded;
2070 
2071             // At this point, the correct bubbles are inflated in the stack.
2072             // Make sure the order in bubble data is reflected in bubble row.
2073             if (update.orderChanged) {
2074                 mDataRepository.addBubbles(mCurrentUserId, update.bubbles);
2075                 // if the stack is going to be collapsed, do not update pointer position
2076                 // after reordering
2077                 mBubbleViewCallback.bubbleOrderChanged(update.bubbles, !collapseStack);
2078             }
2079 
2080             if (collapseStack) {
2081                 mBubbleViewCallback.expansionChanged(/* expanded= */ false);
2082                 mSysuiProxy.requestNotificationShadeTopUi(false, TAG);
2083             }
2084 
2085             if (update.selectionChanged) {
2086                 mBubbleViewCallback.selectionChanged(update.selectedBubble);
2087             }
2088 
2089             // Expanding? Apply this last.
2090             if (update.expandedChanged && update.expanded) {
2091                 mBubbleViewCallback.expansionChanged(/* expanded= */ true);
2092                 mSysuiProxy.requestNotificationShadeTopUi(true, TAG);
2093             }
2094 
2095             mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate");
2096             updateBubbleViews();
2097 
2098             // Update the cached state for queries from SysUI
2099             mImpl.mCachedState.update(update);
2100 
2101             if (isShowingAsBubbleBar()) {
2102                 BubbleBarUpdate bubbleBarUpdate = update.toBubbleBarUpdate();
2103                 // Some updates aren't relevant to the bubble bar so check first.
2104                 if (bubbleBarUpdate.anythingChanged()) {
2105                     mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
2106                 }
2107             }
2108         }
2109     };
2110 
updateOverflowButtonDot()2111     private void updateOverflowButtonDot() {
2112         BubbleOverflow overflow = mBubbleData.getOverflow();
2113         if (overflow == null) return;
2114 
2115         for (Bubble b : mBubbleData.getOverflowBubbles()) {
2116             if (b.showDot()) {
2117                 overflow.setShowDot(true);
2118                 return;
2119             }
2120         }
2121         overflow.setShowDot(false);
2122     }
2123 
handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback)2124     private boolean handleDismissalInterception(BubbleEntry entry,
2125             @Nullable List<BubbleEntry> children, IntConsumer removeCallback) {
2126         if (isSummaryOfBubbles(entry)) {
2127             handleSummaryDismissalInterception(entry, children, removeCallback);
2128         } else {
2129             Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey());
2130             if (bubble == null || !entry.isBubble()) {
2131                 bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey());
2132             }
2133             if (bubble == null) {
2134                 return false;
2135             }
2136             bubble.setSuppressNotification(true);
2137             bubble.setShowDot(false /* show */);
2138         }
2139         // Update the shade
2140         mSysuiProxy.notifyInvalidateNotifications("BubbleController.handleDismissalInterception");
2141         return true;
2142     }
2143 
isSummaryOfBubbles(BubbleEntry entry)2144     private boolean isSummaryOfBubbles(BubbleEntry entry) {
2145         String groupKey = entry.getStatusBarNotification().getGroupKey();
2146         ArrayList<Bubble> bubbleChildren = getBubblesInGroup(groupKey);
2147         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey)
2148                 && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey());
2149         boolean isSummary = entry.getStatusBarNotification().getNotification().isGroupSummary();
2150         return (isSuppressedSummary || isSummary) && !bubbleChildren.isEmpty();
2151     }
2152 
handleSummaryDismissalInterception( BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback)2153     private void handleSummaryDismissalInterception(
2154             BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) {
2155         if (children != null) {
2156             for (int i = 0; i < children.size(); i++) {
2157                 BubbleEntry child = children.get(i);
2158                 if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) {
2159                     // Suppress the bubbled child
2160                     // As far as group manager is concerned, once a child is no longer shown
2161                     // in the shade, it is essentially removed.
2162                     Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey());
2163                     if (bubbleChild != null) {
2164                         bubbleChild.setSuppressNotification(true);
2165                         bubbleChild.setShowDot(false /* show */);
2166                     }
2167                 } else {
2168                     // non-bubbled children can be removed
2169                     removeCallback.accept(i);
2170                 }
2171             }
2172         }
2173 
2174         // And since all children are removed, remove the summary.
2175         removeCallback.accept(-1);
2176 
2177         // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated
2178         mBubbleData.addSummaryToSuppress(summary.getStatusBarNotification().getGroupKey(),
2179                 summary.getKey());
2180     }
2181 
2182     /**
2183      * Updates the visibility of the bubbles based on current state.
2184      * Does not un-bubble, just hides or un-hides the views themselves.
2185      *
2186      * Updates view description for TalkBack focus.
2187      * Updates bubbles' icon views clickable states (when floating).
2188      */
updateBubbleViews()2189     public void updateBubbleViews() {
2190         if (mStackView == null && mLayerView == null) {
2191             return;
2192         }
2193         ProtoLog.v(WM_SHELL_BUBBLES, "updateBubbleViews mIsStatusBarShade=%s hasBubbles=%s",
2194                 mIsStatusBarShade, hasBubbles());
2195         if (!mIsStatusBarShade) {
2196             // Bubbles don't appear when the device is locked.
2197             if (mStackView != null) {
2198                 mStackView.setVisibility(INVISIBLE);
2199             }
2200             if (mLayerView != null) {
2201                 mLayerView.setVisibility(INVISIBLE);
2202             }
2203         } else if (hasBubbles()) {
2204             // If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the
2205             // stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate
2206             // out.
2207             if (mStackView != null) {
2208                 mStackView.setVisibility(VISIBLE);
2209             }
2210             if (mLayerView != null) {
2211                 mLayerView.setVisibility(VISIBLE);
2212             }
2213         }
2214 
2215         if (mStackView != null) {
2216             mStackView.updateContentDescription();
2217             mStackView.updateBubblesAcessibillityStates();
2218         } else if (mLayerView != null) {
2219             // TODO(b/273313561): handle a11y for BubbleBarLayerView
2220         }
2221     }
2222 
2223     /**
2224      * Returns whether the stack is animating or not.
2225      */
isStackAnimating()2226     public boolean isStackAnimating() {
2227         return mStackView != null
2228                 && (mStackView.isExpansionAnimating()
2229                 || mStackView.isSwitchAnimating());
2230     }
2231 
2232     @VisibleForTesting
2233     @Nullable
getStackView()2234     public BubbleStackView getStackView() {
2235         return mStackView;
2236     }
2237 
2238     @VisibleForTesting
2239     @Nullable
getLayerView()2240     public BubbleBarLayerView getLayerView() {
2241         return mLayerView;
2242     }
2243 
2244     /**
2245      * Check if notification panel is in an expanded state.
2246      * Makes a call to System UI process and delivers the result via {@code callback} on the
2247      * WM Shell main thread.
2248      *
2249      * @param callback callback that has the result of notification panel expanded state
2250      */
isNotificationPanelExpanded(Consumer<Boolean> callback)2251     public void isNotificationPanelExpanded(Consumer<Boolean> callback) {
2252         mSysuiProxy.isNotificationPanelExpand(expanded ->
2253                 mMainExecutor.execute(() -> callback.accept(expanded)));
2254     }
2255 
2256     /**
2257      * Show bubbles UI when triggered via shortcut.
2258      *
2259      * <p>When there are bubbles visible, expands the top-most bubble. When there are no bubbles
2260      * visible, opens the bubbles overflow UI.
2261      */
showBubblesFromShortcut()2262     public void showBubblesFromShortcut() {
2263         if (isStackExpanded()) {
2264             ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: stack visible, skip");
2265             return;
2266         }
2267         if (mBubbleData.getSelectedBubble() != null) {
2268             ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: open selected bubble");
2269             expandStackWithSelectedBubble();
2270             return;
2271         }
2272         BubbleViewProvider bubbleToSelect = CollectionUtils.firstOrNull(mBubbleData.getBubbles());
2273         if (bubbleToSelect == null) {
2274             ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: no bubbles");
2275             // make sure overflow bubbles are loaded
2276             loadOverflowBubblesFromDisk();
2277             bubbleToSelect = mBubbleData.getOverflow();
2278         }
2279         ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: select and open %s",
2280                 bubbleToSelect.getKey());
2281         mBubbleData.setSelectedBubbleAndExpandStack(bubbleToSelect);
2282     }
2283 
2284     /**
2285      * Description of current bubble state.
2286      */
dump(PrintWriter pw, String prefix)2287     private void dump(PrintWriter pw, String prefix) {
2288         pw.print(prefix); pw.println("BubbleController state:");
2289         pw.print(prefix); pw.println("  currentUserId= " + mCurrentUserId);
2290         pw.print(prefix); pw.println("  isStatusBarShade= " + mIsStatusBarShade);
2291         pw.print(prefix); pw.println("  isShowingAsBubbleBar= " + isShowingAsBubbleBar());
2292         pw.print(prefix); pw.println("  isImeVisible= " + mBubblePositioner.isImeVisible());
2293         pw.println();
2294 
2295         mBubbleData.dump(pw);
2296         pw.println();
2297 
2298         if (mStackView != null) {
2299             mStackView.dump(pw);
2300         }
2301         pw.println();
2302 
2303         mImpl.mCachedState.dump(pw);
2304     }
2305 
2306     /**
2307      * Whether an intent is properly configured to display in a
2308      * {@link TaskView}.
2309      *
2310      * Keep checks in sync with BubbleExtractor#canLaunchInTaskView. Typically
2311      * that should filter out any invalid bubbles, but should protect SysUI side just in case.
2312      *
2313      * @param context the context to use.
2314      * @param entry   the entry to bubble.
2315      */
canLaunchInTaskView(Context context, BubbleEntry entry)2316     static boolean canLaunchInTaskView(Context context, BubbleEntry entry) {
2317         PendingIntent intent = entry.getBubbleMetadata() != null
2318                 ? entry.getBubbleMetadata().getIntent()
2319                 : null;
2320         if (entry.getBubbleMetadata() != null
2321                 && entry.getBubbleMetadata().getShortcutId() != null) {
2322             return true;
2323         }
2324         if (intent == null) {
2325             Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey());
2326             return false;
2327         }
2328         PackageManager packageManager = getPackageManagerForUser(
2329                 context, entry.getStatusBarNotification().getUser().getIdentifier());
2330         return isResizableActivity(intent.getIntent(), packageManager, entry.getKey());
2331     }
2332 
isResizableActivity(Intent intent, PackageManager packageManager, String key)2333     static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) {
2334         if (intent == null) {
2335             Log.w(TAG, "Unable to send as bubble: " + key + " null intent");
2336             return false;
2337         }
2338         ActivityInfo info = intent.resolveActivityInfo(packageManager, 0);
2339         if (info == null) {
2340             Log.w(TAG, "Unable to send as bubble: " + key
2341                     + " couldn't find activity info for intent: " + intent);
2342             return false;
2343         }
2344         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
2345             Log.w(TAG, "Unable to send as bubble: " + key
2346                     + " activity is not resizable for intent: " + intent);
2347             return false;
2348         }
2349         return true;
2350     }
2351 
getPackageManagerForUser(Context context, int userId)2352     static PackageManager getPackageManagerForUser(Context context, int userId) {
2353         Context contextForUser = context;
2354         // UserHandle defines special userId as negative values, e.g. USER_ALL
2355         if (userId >= 0) {
2356             try {
2357                 // Create a context for the correct user so if a package isn't installed
2358                 // for user 0 we can still load information about the package.
2359                 contextForUser =
2360                         context.createPackageContextAsUser(context.getPackageName(),
2361                                 Context.CONTEXT_RESTRICTED,
2362                                 new UserHandle(userId));
2363             } catch (PackageManager.NameNotFoundException e) {
2364                 // Shouldn't fail to find the package name for system ui.
2365             }
2366         }
2367         return contextForUser.getPackageManager();
2368     }
2369 
2370     /** PinnedStackListener that dispatches IME visibility updates to the stack. */
2371     //TODO(b/170442945): Better way to do this / insets listener?
2372     private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedTaskListener {
2373         @Override
onImeVisibilityChanged(boolean imeVisible, int imeHeight)2374         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
2375             mBubblePositioner.setImeVisible(imeVisible, imeHeight);
2376             if (mStackView != null) {
2377                 mStackView.setImeVisible(imeVisible);
2378             }
2379         }
2380     }
2381 
2382     /**
2383      * The interface for calls from outside the host process.
2384      */
2385     @BinderThread
2386     private class IBubblesImpl extends IBubbles.Stub implements ExternalInterfaceBinder {
2387         private BubbleController mController;
2388         private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener;
2389         private final Bubbles.BubbleStateListener mBubbleListener =
2390                 new Bubbles.BubbleStateListener() {
2391                     @Override
2392                     public void onBubbleStateChange(BubbleBarUpdate update) {
2393                         Bundle b = new Bundle();
2394                         b.setClassLoader(BubbleBarUpdate.class.getClassLoader());
2395                         b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update);
2396                         mListener.call(l -> l.onBubbleStateChange(b));
2397                     }
2398 
2399                     @Override
2400                     public void animateBubbleBarLocation(BubbleBarLocation location) {
2401                         mListener.call(l -> l.animateBubbleBarLocation(location));
2402                     }
2403                 };
2404 
IBubblesImpl(BubbleController controller)2405         IBubblesImpl(BubbleController controller) {
2406             mController = controller;
2407             mListener = new SingleInstanceRemoteListener<>(mController,
2408                     c -> c.registerBubbleStateListener(mBubbleListener),
2409                     c -> c.unregisterBubbleStateListener());
2410         }
2411 
2412         /**
2413          * Invalidates this instance, preventing future calls from updating the controller.
2414          */
2415         @Override
invalidate()2416         public void invalidate() {
2417             mController = null;
2418             // Unregister the listeners to ensure any binder death recipients are unlinked
2419             mListener.unregister();
2420         }
2421 
2422         @Override
registerBubbleListener(IBubblesListener listener)2423         public void registerBubbleListener(IBubblesListener listener) {
2424             mMainExecutor.execute(() -> mListener.register(listener));
2425         }
2426 
2427         @Override
unregisterBubbleListener(IBubblesListener listener)2428         public void unregisterBubbleListener(IBubblesListener listener) {
2429             mMainExecutor.execute(mListener::unregister);
2430         }
2431 
2432         @Override
showBubble(String key, int topOnScreen)2433         public void showBubble(String key, int topOnScreen) {
2434             mMainExecutor.execute(
2435                     () -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen));
2436         }
2437 
2438         @Override
removeAllBubbles()2439         public void removeAllBubbles() {
2440             mMainExecutor.execute(() -> mController.removeAllBubbles(Bubbles.DISMISS_USER_GESTURE));
2441         }
2442 
2443         @Override
collapseBubbles()2444         public void collapseBubbles() {
2445             mMainExecutor.execute(() -> mController.collapseStack());
2446         }
2447 
2448         @Override
startBubbleDrag(String bubbleKey)2449         public void startBubbleDrag(String bubbleKey) {
2450             mMainExecutor.execute(() -> mController.startBubbleDrag(bubbleKey));
2451         }
2452 
2453         @Override
stopBubbleDrag(BubbleBarLocation location, int topOnScreen)2454         public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) {
2455             mMainExecutor.execute(() -> mController.stopBubbleDrag(location, topOnScreen));
2456         }
2457 
2458         @Override
dragBubbleToDismiss(String key)2459         public void dragBubbleToDismiss(String key) {
2460             mMainExecutor.execute(() -> mController.dragBubbleToDismiss(key));
2461         }
2462 
2463         @Override
showUserEducation(int positionX, int positionY)2464         public void showUserEducation(int positionX, int positionY) {
2465             mMainExecutor.execute(() ->
2466                     mController.showUserEducation(new Point(positionX, positionY)));
2467         }
2468 
2469         @Override
setBubbleBarLocation(BubbleBarLocation location)2470         public void setBubbleBarLocation(BubbleBarLocation location) {
2471             mMainExecutor.execute(() ->
2472                     mController.setBubbleBarLocation(location));
2473         }
2474 
2475         @Override
updateBubbleBarTopOnScreen(int topOnScreen)2476         public void updateBubbleBarTopOnScreen(int topOnScreen) {
2477             mMainExecutor.execute(() -> {
2478                 mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen);
2479                 if (mLayerView != null) mLayerView.updateExpandedView();
2480             });
2481         }
2482     }
2483 
2484     private class BubblesImpl implements Bubbles {
2485         // Up-to-date cached state of bubbles data for SysUI to query from the calling thread
2486         @VisibleForTesting
2487         public class CachedState {
2488             private boolean mIsStackExpanded;
2489             private String mSelectedBubbleKey;
2490             private HashSet<String> mSuppressedBubbleKeys = new HashSet<>();
2491             private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>();
2492             private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>();
2493 
2494             private HashMap<String, Integer> mAppBubbleTaskIds = new HashMap();
2495 
2496             private ArrayList<Bubble> mTmpBubbles = new ArrayList<>();
2497 
2498             /**
2499              * Updates the cached state based on the last full BubbleData change.
2500              */
update(BubbleData.Update update)2501             synchronized void update(BubbleData.Update update) {
2502                 if (update.selectionChanged) {
2503                     mSelectedBubbleKey = update.selectedBubble != null
2504                             ? update.selectedBubble.getKey()
2505                             : null;
2506                 }
2507                 if (update.expandedChanged) {
2508                     mIsStackExpanded = update.expanded;
2509                 }
2510                 if (update.suppressedSummaryChanged) {
2511                     String summaryKey =
2512                             mBubbleData.getSummaryKey(update.suppressedSummaryGroup);
2513                     if (summaryKey != null) {
2514                         mSuppressedGroupToNotifKeys.put(update.suppressedSummaryGroup, summaryKey);
2515                     } else {
2516                         mSuppressedGroupToNotifKeys.remove(update.suppressedSummaryGroup);
2517                     }
2518                 }
2519 
2520                 mTmpBubbles.clear();
2521                 mTmpBubbles.addAll(update.bubbles);
2522                 mTmpBubbles.addAll(update.overflowBubbles);
2523 
2524                 mSuppressedBubbleKeys.clear();
2525                 mShortcutIdToBubble.clear();
2526                 mAppBubbleTaskIds.clear();
2527                 for (Bubble b : mTmpBubbles) {
2528                     mShortcutIdToBubble.put(b.getShortcutId(), b);
2529                     updateBubbleSuppressedState(b);
2530 
2531                     if (b.isAppBubble()) {
2532                         mAppBubbleTaskIds.put(b.getKey(), b.getTaskId());
2533                     }
2534                 }
2535             }
2536 
2537             /** Sets the app bubble's taskId which is cached for SysUI. */
setAppBubbleTaskId(String key, int taskId)2538             synchronized void setAppBubbleTaskId(String key, int taskId) {
2539                 mAppBubbleTaskIds.put(key, taskId);
2540             }
2541 
2542             /**
2543              * Updates a specific bubble suppressed state.  This is used mainly because notification
2544              * suppression changes don't go through the same BubbleData update mechanism.
2545              */
updateBubbleSuppressedState(Bubble b)2546             synchronized void updateBubbleSuppressedState(Bubble b) {
2547                 if (!b.showInShade()) {
2548                     mSuppressedBubbleKeys.add(b.getKey());
2549                 } else {
2550                     mSuppressedBubbleKeys.remove(b.getKey());
2551                 }
2552             }
2553 
isStackExpanded()2554             public synchronized boolean isStackExpanded() {
2555                 return mIsStackExpanded;
2556             }
2557 
isBubbleExpanded(String key)2558             public synchronized boolean isBubbleExpanded(String key) {
2559                 return mIsStackExpanded && key.equals(mSelectedBubbleKey);
2560             }
2561 
isBubbleNotificationSuppressedFromShade(String key, String groupKey)2562             public synchronized boolean isBubbleNotificationSuppressedFromShade(String key,
2563                     String groupKey) {
2564                 return mSuppressedBubbleKeys.contains(key)
2565                         || (mSuppressedGroupToNotifKeys.containsKey(groupKey)
2566                         && key.equals(mSuppressedGroupToNotifKeys.get(groupKey)));
2567             }
2568 
2569             @Nullable
getBubbleWithShortcutId(String id)2570             public synchronized Bubble getBubbleWithShortcutId(String id) {
2571                 return mShortcutIdToBubble.get(id);
2572             }
2573 
dump(PrintWriter pw)2574             synchronized void dump(PrintWriter pw) {
2575                 pw.println("BubbleImpl.CachedState state:");
2576 
2577                 pw.println("mIsStackExpanded: " + mIsStackExpanded);
2578                 pw.println("mSelectedBubbleKey: " + mSelectedBubbleKey);
2579 
2580                 pw.println("mSuppressedBubbleKeys: " + mSuppressedBubbleKeys.size());
2581                 for (String key : mSuppressedBubbleKeys) {
2582                     pw.println("   suppressing: " + key);
2583                 }
2584 
2585                 pw.print("mSuppressedGroupToNotifKeys: ");
2586                 pw.println(mSuppressedGroupToNotifKeys.size());
2587                 for (String key : mSuppressedGroupToNotifKeys.keySet()) {
2588                     pw.println("   suppressing: " + key);
2589                 }
2590 
2591                 pw.println("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values());
2592             }
2593         }
2594 
2595         private CachedState mCachedState = new CachedState();
2596 
2597         @Override
isBubbleNotificationSuppressedFromShade(String key, String groupKey)2598         public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
2599             return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey);
2600         }
2601 
2602         @Override
isBubbleExpanded(String key)2603         public boolean isBubbleExpanded(String key) {
2604             return mCachedState.isBubbleExpanded(key);
2605         }
2606 
2607         @Override
2608         @Nullable
getBubbleWithShortcutId(String shortcutId)2609         public Bubble getBubbleWithShortcutId(String shortcutId) {
2610             return mCachedState.getBubbleWithShortcutId(shortcutId);
2611         }
2612 
2613         @Override
collapseStack()2614         public void collapseStack() {
2615             mMainExecutor.execute(() -> {
2616                 BubbleController.this.collapseStack();
2617             });
2618         }
2619 
2620         @Override
expandStackAndSelectBubble(BubbleEntry entry)2621         public void expandStackAndSelectBubble(BubbleEntry entry) {
2622             mMainExecutor.execute(() -> {
2623                 BubbleController.this.expandStackAndSelectBubble(entry);
2624             });
2625         }
2626 
2627         @Override
expandStackAndSelectBubble(Bubble bubble)2628         public void expandStackAndSelectBubble(Bubble bubble) {
2629             mMainExecutor.execute(() -> {
2630                 BubbleController.this.expandStackAndSelectBubble(bubble);
2631             });
2632         }
2633 
2634         @Override
showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon)2635         public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) {
2636             mMainExecutor.execute(
2637                     () -> BubbleController.this.showOrHideAppBubble(intent, user, icon));
2638         }
2639 
2640         @Override
isAppBubbleTaskId(int taskId)2641         public boolean isAppBubbleTaskId(int taskId) {
2642             return mCachedState.mAppBubbleTaskIds.values().contains(taskId);
2643         }
2644 
2645         @Override
2646         @Nullable
getScreenshotExcludingBubble(int displayId)2647         public SynchronousScreenCaptureListener getScreenshotExcludingBubble(int displayId) {
2648             SynchronousScreenCaptureListener screenCaptureListener =
2649                     ScreenCapture.createSyncCaptureListener();
2650 
2651             mMainExecutor.execute(
2652                     () -> BubbleController.this.getScreenshotExcludingBubble(displayId,
2653                             screenCaptureListener));
2654 
2655             return screenCaptureListener;
2656         }
2657 
2658         @Override
handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback, Executor callbackExecutor)2659         public boolean handleDismissalInterception(BubbleEntry entry,
2660                 @Nullable List<BubbleEntry> children, IntConsumer removeCallback,
2661                 Executor callbackExecutor) {
2662             IntConsumer cb = removeCallback != null
2663                     ? (index) -> callbackExecutor.execute(() -> removeCallback.accept(index))
2664                     : null;
2665             return mMainExecutor.executeBlockingForResult(() -> {
2666                 return BubbleController.this.handleDismissalInterception(entry, children, cb);
2667             }, Boolean.class);
2668         }
2669 
2670         @Override
setSysuiProxy(SysuiProxy proxy)2671         public void setSysuiProxy(SysuiProxy proxy) {
2672             mMainExecutor.execute(() -> {
2673                 BubbleController.this.setSysuiProxy(proxy);
2674             });
2675         }
2676 
2677         @Override
setExpandListener(BubbleExpandListener listener)2678         public void setExpandListener(BubbleExpandListener listener) {
2679             mMainExecutor.execute(() -> {
2680                 BubbleController.this.setExpandListener(listener);
2681             });
2682         }
2683 
2684         @Override
onEntryAdded(BubbleEntry entry)2685         public void onEntryAdded(BubbleEntry entry) {
2686             mMainExecutor.execute(() -> {
2687                 BubbleController.this.onEntryAdded(entry);
2688             });
2689         }
2690 
2691         @Override
onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem)2692         public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) {
2693             mMainExecutor.execute(() -> {
2694                 BubbleController.this.onEntryUpdated(entry, shouldBubbleUp, fromSystem);
2695             });
2696         }
2697 
2698         @Override
onEntryRemoved(BubbleEntry entry)2699         public void onEntryRemoved(BubbleEntry entry) {
2700             mMainExecutor.execute(() -> {
2701                 BubbleController.this.onEntryRemoved(entry);
2702             });
2703         }
2704 
2705         @Override
onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey)2706         public void onRankingUpdated(RankingMap rankingMap,
2707                 HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) {
2708             mMainExecutor.execute(() -> {
2709                 BubbleController.this.onRankingUpdated(rankingMap, entryDataByKey);
2710             });
2711         }
2712 
2713         @Override
onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, int modificationType)2714         public void onNotificationChannelModified(String pkg,
2715                 UserHandle user, NotificationChannel channel, int modificationType) {
2716             // Bubbles only cares about updates or deletions.
2717             if (modificationType == NOTIFICATION_CHANNEL_OR_GROUP_UPDATED
2718                     || modificationType == NOTIFICATION_CHANNEL_OR_GROUP_DELETED) {
2719                 mMainExecutor.execute(() -> {
2720                     BubbleController.this.onNotificationChannelModified(pkg, user, channel,
2721                             modificationType);
2722                 });
2723             }
2724         }
2725 
2726         @Override
onStatusBarVisibilityChanged(boolean visible)2727         public void onStatusBarVisibilityChanged(boolean visible) {
2728             mMainExecutor.execute(() -> {
2729                 BubbleController.this.onStatusBarVisibilityChanged(visible);
2730             });
2731         }
2732 
2733         @Override
onZenStateChanged()2734         public void onZenStateChanged() {
2735             mMainExecutor.execute(() -> {
2736                 BubbleController.this.onZenStateChanged();
2737             });
2738         }
2739 
2740         @Override
onStatusBarStateChanged(boolean isShade)2741         public void onStatusBarStateChanged(boolean isShade) {
2742             mMainExecutor.execute(() -> {
2743                 BubbleController.this.onStatusBarStateChanged(isShade);
2744             });
2745         }
2746 
2747         @Override
onUserChanged(int newUserId)2748         public void onUserChanged(int newUserId) {
2749             mMainExecutor.execute(() -> {
2750                 BubbleController.this.onUserChanged(newUserId);
2751             });
2752         }
2753 
2754         @Override
onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles)2755         public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
2756             mMainExecutor.execute(() -> {
2757                 BubbleController.this.onCurrentProfilesChanged(currentProfiles);
2758             });
2759         }
2760 
2761         @Override
onUserRemoved(int removedUserId)2762         public void onUserRemoved(int removedUserId) {
2763             mMainExecutor.execute(() -> {
2764                 BubbleController.this.onUserRemoved(removedUserId);
2765             });
2766         }
2767 
2768         @Override
onNotificationPanelExpandedChanged(boolean expanded)2769         public void onNotificationPanelExpandedChanged(boolean expanded) {
2770             mMainExecutor.execute(
2771                     () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded));
2772         }
2773 
2774         @Override
onSensitiveNotificationProtectionStateChanged( boolean sensitiveNotificationProtectionActive)2775         public void onSensitiveNotificationProtectionStateChanged(
2776                 boolean sensitiveNotificationProtectionActive) {
2777             mMainExecutor.execute(
2778                     () -> BubbleController.this.onSensitiveNotificationProtectionStateChanged(
2779                             sensitiveNotificationProtectionActive));
2780         }
2781 
2782         @Override
canShowBubbleNotification()2783         public boolean canShowBubbleNotification() {
2784             // in bubble bar mode, when the IME is visible we can't animate new bubbles.
2785             if (BubbleController.this.isShowingAsBubbleBar()) {
2786                 return !BubbleController.this.mBubblePositioner.isImeVisible();
2787             }
2788             return true;
2789         }
2790     }
2791 
2792     /**
2793      * Bubble data that is stored per user.
2794      * Used to store and restore active bubbles during user switching.
2795      */
2796     private static class UserBubbleData {
2797         private final Map<String, Boolean> mKeyToShownInShadeMap = new HashMap<>();
2798 
2799         /**
2800          * Add bubble key and whether it should be shown in notification shade
2801          */
add(String key, boolean shownInShade)2802         void add(String key, boolean shownInShade) {
2803             mKeyToShownInShadeMap.put(key, shownInShade);
2804         }
2805 
2806         /**
2807          * Get all bubble keys stored for this user
2808          */
getKeys()2809         Set<String> getKeys() {
2810             return mKeyToShownInShadeMap.keySet();
2811         }
2812 
2813         /**
2814          * Check if this bubble with the given key should be shown in the notification shade
2815          */
isShownInShade(String key)2816         boolean isShownInShade(String key) {
2817             return mKeyToShownInShadeMap.get(key);
2818         }
2819     }
2820 }
2821