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