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