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