1 /* 2 * Copyright (C) 2018 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.systemui.bubbles; 18 19 import static android.app.Notification.FLAG_BUBBLE; 20 import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; 21 import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; 22 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; 23 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; 24 import static android.service.notification.NotificationListenerService.REASON_CANCEL; 25 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; 26 import static android.service.notification.NotificationListenerService.REASON_CLICK; 27 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; 28 import static android.view.Display.DEFAULT_DISPLAY; 29 import static android.view.Display.INVALID_DISPLAY; 30 import static android.view.View.INVISIBLE; 31 import static android.view.View.VISIBLE; 32 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 33 34 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER; 35 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; 36 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 37 import static com.android.systemui.statusbar.StatusBarState.SHADE; 38 import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; 39 40 import static java.lang.annotation.ElementType.FIELD; 41 import static java.lang.annotation.ElementType.LOCAL_VARIABLE; 42 import static java.lang.annotation.ElementType.PARAMETER; 43 import static java.lang.annotation.RetentionPolicy.SOURCE; 44 45 import android.annotation.NonNull; 46 import android.annotation.UserIdInt; 47 import android.app.ActivityManager.RunningTaskInfo; 48 import android.app.INotificationManager; 49 import android.app.Notification; 50 import android.app.NotificationChannel; 51 import android.app.NotificationManager; 52 import android.app.PendingIntent; 53 import android.content.Context; 54 import android.content.pm.ActivityInfo; 55 import android.content.pm.LauncherApps; 56 import android.content.pm.PackageManager; 57 import android.content.pm.ShortcutInfo; 58 import android.content.res.Configuration; 59 import android.graphics.PixelFormat; 60 import android.os.Binder; 61 import android.os.Handler; 62 import android.os.RemoteException; 63 import android.os.ServiceManager; 64 import android.os.UserHandle; 65 import android.service.notification.NotificationListenerService; 66 import android.service.notification.NotificationListenerService.RankingMap; 67 import android.service.notification.ZenModeConfig; 68 import android.util.ArraySet; 69 import android.util.Log; 70 import android.util.Pair; 71 import android.util.SparseSetArray; 72 import android.view.Display; 73 import android.view.View; 74 import android.view.ViewGroup; 75 import android.view.WindowManager; 76 77 import androidx.annotation.IntDef; 78 import androidx.annotation.MainThread; 79 import androidx.annotation.Nullable; 80 81 import com.android.internal.annotations.VisibleForTesting; 82 import com.android.internal.statusbar.IStatusBarService; 83 import com.android.internal.statusbar.NotificationVisibility; 84 import com.android.systemui.Dumpable; 85 import com.android.systemui.bubbles.dagger.BubbleModule; 86 import com.android.systemui.dump.DumpManager; 87 import com.android.systemui.model.SysUiState; 88 import com.android.systemui.plugins.statusbar.StatusBarStateController; 89 import com.android.systemui.shared.system.ActivityManagerWrapper; 90 import com.android.systemui.shared.system.PinnedStackListenerForwarder; 91 import com.android.systemui.shared.system.TaskStackChangeListener; 92 import com.android.systemui.shared.system.WindowManagerWrapper; 93 import com.android.systemui.statusbar.FeatureFlags; 94 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 95 import com.android.systemui.statusbar.NotificationRemoveInterceptor; 96 import com.android.systemui.statusbar.ScrimView; 97 import com.android.systemui.statusbar.notification.NotificationChannelHelper; 98 import com.android.systemui.statusbar.notification.NotificationEntryListener; 99 import com.android.systemui.statusbar.notification.NotificationEntryManager; 100 import com.android.systemui.statusbar.notification.collection.NotifCollection; 101 import com.android.systemui.statusbar.notification.collection.NotifPipeline; 102 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 103 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 104 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; 105 import com.android.systemui.statusbar.phone.NotificationGroupManager; 106 import com.android.systemui.statusbar.phone.NotificationShadeWindowController; 107 import com.android.systemui.statusbar.phone.ScrimController; 108 import com.android.systemui.statusbar.phone.ShadeController; 109 import com.android.systemui.statusbar.phone.StatusBar; 110 import com.android.systemui.statusbar.policy.ConfigurationController; 111 import com.android.systemui.statusbar.policy.ZenModeController; 112 import com.android.systemui.util.FloatingContentCoordinator; 113 114 import java.io.FileDescriptor; 115 import java.io.PrintWriter; 116 import java.lang.annotation.Retention; 117 import java.lang.annotation.Target; 118 import java.util.ArrayList; 119 import java.util.List; 120 import java.util.Objects; 121 122 /** 123 * Bubbles are a special type of content that can "float" on top of other apps or System UI. 124 * Bubbles can be expanded to show more content. 125 * 126 * The controller manages addition, removal, and visible state of bubbles on screen. 127 */ 128 public class BubbleController implements ConfigurationController.ConfigurationListener, Dumpable { 129 130 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; 131 132 @Retention(SOURCE) 133 @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, 134 DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, 135 DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, 136 DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, 137 DISMISS_NO_BUBBLE_UP}) 138 @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) 139 @interface DismissReason {} 140 141 static final int DISMISS_USER_GESTURE = 1; 142 static final int DISMISS_AGED = 2; 143 static final int DISMISS_TASK_FINISHED = 3; 144 static final int DISMISS_BLOCKED = 4; 145 static final int DISMISS_NOTIF_CANCEL = 5; 146 static final int DISMISS_ACCESSIBILITY_ACTION = 6; 147 static final int DISMISS_NO_LONGER_BUBBLE = 7; 148 static final int DISMISS_USER_CHANGED = 8; 149 static final int DISMISS_GROUP_CANCELLED = 9; 150 static final int DISMISS_INVALID_INTENT = 10; 151 static final int DISMISS_OVERFLOW_MAX_REACHED = 11; 152 static final int DISMISS_SHORTCUT_REMOVED = 12; 153 static final int DISMISS_PACKAGE_REMOVED = 13; 154 static final int DISMISS_NO_BUBBLE_UP = 14; 155 156 private final Context mContext; 157 private final NotificationEntryManager mNotificationEntryManager; 158 private final NotifPipeline mNotifPipeline; 159 private final BubbleTaskStackListener mTaskStackListener; 160 private BubbleExpandListener mExpandListener; 161 @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; 162 private final NotificationGroupManager mNotificationGroupManager; 163 private final ShadeController mShadeController; 164 private final FloatingContentCoordinator mFloatingContentCoordinator; 165 private final BubbleDataRepository mDataRepository; 166 private BubbleLogger mLogger = new BubbleLoggerImpl(); 167 168 private BubbleData mBubbleData; 169 private ScrimView mBubbleScrim; 170 @Nullable private BubbleStackView mStackView; 171 private BubbleIconFactory mBubbleIconFactory; 172 173 /** 174 * The relative position of the stack when we removed it and nulled it out. If the stack is 175 * re-created, it will re-appear at this position. 176 */ 177 @Nullable private BubbleStackView.RelativeStackPosition mPositionFromRemovedStack; 178 179 // Tracks the id of the current (foreground) user. 180 private int mCurrentUserId; 181 // Saves notification keys of active bubbles when users are switched. 182 private final SparseSetArray<String> mSavedBubbleKeysPerUser; 183 184 // Used when ranking updates occur and we check if things should bubble / unbubble 185 private NotificationListenerService.Ranking mTmpRanking; 186 187 // Bubbles get added to the status bar view 188 private final NotificationShadeWindowController mNotificationShadeWindowController; 189 private final ZenModeController mZenModeController; 190 private StatusBarStateListener mStatusBarStateListener; 191 private INotificationManager mINotificationManager; 192 193 // Callback that updates BubbleOverflowActivity on data change. 194 @Nullable private Runnable mOverflowCallback = null; 195 196 // Only load overflow data from disk once 197 private boolean mOverflowDataLoaded = false; 198 199 /** 200 * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select 201 * this bubble and expand the stack. 202 */ 203 @Nullable private NotificationEntry mNotifEntryToExpandOnShadeUnlock; 204 205 private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; 206 private IStatusBarService mBarService; 207 private WindowManager mWindowManager; 208 private SysUiState mSysUiState; 209 210 // Used to post to main UI thread 211 private Handler mHandler = new Handler(); 212 213 /** LayoutParams used to add the BubbleStackView to the window manager. */ 214 private WindowManager.LayoutParams mWmLayoutParams; 215 /** Whether or not the BubbleStackView has been added to the WindowManager. */ 216 private boolean mAddedToWindowManager = false; 217 218 // Listens to user switch so bubbles can be saved and restored. 219 private final NotificationLockscreenUserManager mNotifUserManager; 220 221 /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */ 222 private int mOrientation = Configuration.ORIENTATION_UNDEFINED; 223 224 /** 225 * Last known screen density, used to detect display size changes in {@link #onConfigChanged}. 226 */ 227 private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED; 228 229 /** Last known direction, used to detect layout direction changes @link #onConfigChanged}. */ 230 private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED; 231 232 private boolean mInflateSynchronously; 233 234 // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline 235 private final List<NotifCallback> mCallbacks = new ArrayList<>(); 236 237 /** 238 * Whether the IME is visible, as reported by the BubbleStackView. If it is, we'll make the 239 * Bubbles window NOT_FOCUSABLE so that touches on the Bubbles UI doesn't steal focus from the 240 * ActivityView and hide the IME. 241 */ 242 private boolean mImeVisible = false; 243 244 /** 245 * Listener to find out about stack expansion / collapse events. 246 */ 247 public interface BubbleExpandListener { 248 /** 249 * Called when the expansion state of the bubble stack changes. 250 * 251 * @param isExpanding whether it's expanding or collapsing 252 * @param key the notification key associated with bubble being expanded 253 */ onBubbleExpandChanged(boolean isExpanding, String key)254 void onBubbleExpandChanged(boolean isExpanding, String key); 255 } 256 257 /** 258 * Listener to be notified when a bubbles' notification suppression state changes. 259 */ 260 public interface NotificationSuppressionChangedListener { 261 /** 262 * Called when the notification suppression state of a bubble changes. 263 */ onBubbleNotificationSuppressionChange(Bubble bubble)264 void onBubbleNotificationSuppressionChange(Bubble bubble); 265 } 266 267 /** 268 * Listener to be notified when a pending intent has been canceled for a bubble. 269 */ 270 public interface PendingIntentCanceledListener { 271 /** 272 * Called when the pending intent for a bubble has been canceled. 273 */ onPendingIntentCanceled(Bubble bubble)274 void onPendingIntentCanceled(Bubble bubble); 275 } 276 277 /** 278 * Callback for when the BubbleController wants to interact with the notification pipeline to: 279 * - Remove a previously bubbled notification 280 * - Update the notification shade since bubbled notification should/shouldn't be showing 281 */ 282 public interface NotifCallback { 283 /** 284 * Called when a bubbled notification that was hidden from the shade is now being removed 285 * This can happen when an app cancels a bubbled notification or when the user dismisses a 286 * bubble. 287 */ removeNotification(@onNull NotificationEntry entry, int reason)288 void removeNotification(@NonNull NotificationEntry entry, int reason); 289 290 /** 291 * Called when a bubbled notification has changed whether it should be 292 * filtered from the shade. 293 */ invalidateNotifications(@onNull String reason)294 void invalidateNotifications(@NonNull String reason); 295 296 /** 297 * Called on a bubbled entry that has been removed when there are no longer 298 * bubbled entries in its group. 299 * 300 * Checks whether its group has any other (non-bubbled) children. If it doesn't, 301 * removes all remnants of the group's summary from the notification pipeline. 302 * TODO: (b/145659174) Only old pipeline needs this - delete post-migration. 303 */ maybeCancelSummary(@onNull NotificationEntry entry)304 void maybeCancelSummary(@NonNull NotificationEntry entry); 305 } 306 307 /** 308 * Listens for the current state of the status bar and updates the visibility state 309 * of bubbles as needed. 310 */ 311 private class StatusBarStateListener implements StatusBarStateController.StateListener { 312 private int mState; 313 /** 314 * Returns the current status bar state. 315 */ getCurrentState()316 public int getCurrentState() { 317 return mState; 318 } 319 320 @Override onStateChanged(int newState)321 public void onStateChanged(int newState) { 322 mState = newState; 323 boolean shouldCollapse = (mState != SHADE); 324 if (shouldCollapse) { 325 collapseStack(); 326 } 327 328 if (mNotifEntryToExpandOnShadeUnlock != null) { 329 expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock); 330 mNotifEntryToExpandOnShadeUnlock = null; 331 } 332 333 updateStack(); 334 } 335 } 336 337 /** 338 * Injected constructor. See {@link BubbleModule}. 339 */ BubbleController(Context context, NotificationShadeWindowController notificationShadeWindowController, StatusBarStateController statusBarStateController, ShadeController shadeController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, ConfigurationController configurationController, NotificationInterruptStateProvider interruptionStateProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager, NotificationEntryManager entryManager, NotifPipeline notifPipeline, FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, BubbleDataRepository dataRepository, SysUiState sysUiState, INotificationManager notificationManager, @Nullable IStatusBarService statusBarService, WindowManager windowManager, LauncherApps launcherApps)340 public BubbleController(Context context, 341 NotificationShadeWindowController notificationShadeWindowController, 342 StatusBarStateController statusBarStateController, 343 ShadeController shadeController, 344 BubbleData data, 345 @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, 346 ConfigurationController configurationController, 347 NotificationInterruptStateProvider interruptionStateProvider, 348 ZenModeController zenModeController, 349 NotificationLockscreenUserManager notifUserManager, 350 NotificationGroupManager groupManager, 351 NotificationEntryManager entryManager, 352 NotifPipeline notifPipeline, 353 FeatureFlags featureFlags, 354 DumpManager dumpManager, 355 FloatingContentCoordinator floatingContentCoordinator, 356 BubbleDataRepository dataRepository, 357 SysUiState sysUiState, 358 INotificationManager notificationManager, 359 @Nullable IStatusBarService statusBarService, 360 WindowManager windowManager, 361 LauncherApps launcherApps) { 362 dumpManager.registerDumpable(TAG, this); 363 mContext = context; 364 mShadeController = shadeController; 365 mNotificationInterruptStateProvider = interruptionStateProvider; 366 mNotifUserManager = notifUserManager; 367 mZenModeController = zenModeController; 368 mFloatingContentCoordinator = floatingContentCoordinator; 369 mDataRepository = dataRepository; 370 mINotificationManager = notificationManager; 371 mZenModeController.addCallback(new ZenModeController.Callback() { 372 @Override 373 public void onZenChanged(int zen) { 374 for (Bubble b : mBubbleData.getBubbles()) { 375 b.setShowDot(b.showInShade()); 376 } 377 } 378 379 @Override 380 public void onConfigChanged(ZenModeConfig config) { 381 for (Bubble b : mBubbleData.getBubbles()) { 382 b.setShowDot(b.showInShade()); 383 } 384 } 385 }); 386 387 configurationController.addCallback(this /* configurationListener */); 388 mSysUiState = sysUiState; 389 390 mBubbleData = data; 391 mBubbleData.setListener(mBubbleDataListener); 392 mBubbleData.setSuppressionChangedListener(new NotificationSuppressionChangedListener() { 393 @Override 394 public void onBubbleNotificationSuppressionChange(Bubble bubble) { 395 // Make sure NoMan knows it's not showing in the shade anymore so anyone querying it 396 // can tell. 397 try { 398 mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(), 399 !bubble.showInShade()); 400 } catch (RemoteException e) { 401 // Bad things have happened 402 } 403 } 404 }); 405 mBubbleData.setPendingIntentCancelledListener(bubble -> { 406 if (bubble.getBubbleIntent() == null) { 407 return; 408 } 409 if (bubble.isIntentActive() 410 || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 411 bubble.setPendingIntentCanceled(); 412 return; 413 } 414 mHandler.post( 415 () -> removeBubble(bubble.getKey(), 416 BubbleController.DISMISS_INVALID_INTENT)); 417 }); 418 419 mNotificationEntryManager = entryManager; 420 mNotificationGroupManager = groupManager; 421 mNotifPipeline = notifPipeline; 422 423 if (!featureFlags.isNewNotifPipelineRenderingEnabled()) { 424 setupNEM(); 425 } else { 426 setupNotifPipeline(); 427 } 428 429 mNotificationShadeWindowController = notificationShadeWindowController; 430 mStatusBarStateListener = new StatusBarStateListener(); 431 statusBarStateController.addCallback(mStatusBarStateListener); 432 433 mTaskStackListener = new BubbleTaskStackListener(); 434 ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); 435 436 try { 437 WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener()); 438 } catch (RemoteException e) { 439 e.printStackTrace(); 440 } 441 mSurfaceSynchronizer = synchronizer; 442 443 mWindowManager = windowManager; 444 mBarService = statusBarService == null 445 ? IStatusBarService.Stub.asInterface( 446 ServiceManager.getService(Context.STATUS_BAR_SERVICE)) 447 : statusBarService; 448 449 mBubbleScrim = new ScrimView(mContext); 450 mBubbleScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 451 452 mSavedBubbleKeysPerUser = new SparseSetArray<>(); 453 mCurrentUserId = mNotifUserManager.getCurrentUserId(); 454 mNotifUserManager.addUserChangedListener( 455 new NotificationLockscreenUserManager.UserChangedListener() { 456 @Override 457 public void onUserChanged(int newUserId) { 458 BubbleController.this.saveBubbles(mCurrentUserId); 459 mBubbleData.dismissAll(DISMISS_USER_CHANGED); 460 BubbleController.this.restoreBubbles(newUserId); 461 mCurrentUserId = newUserId; 462 } 463 }); 464 465 mBubbleIconFactory = new BubbleIconFactory(context); 466 467 launcherApps.registerCallback(new LauncherApps.Callback() { 468 @Override 469 public void onPackageAdded(String s, UserHandle userHandle) {} 470 471 @Override 472 public void onPackageChanged(String s, UserHandle userHandle) {} 473 474 @Override 475 public void onPackageRemoved(String s, UserHandle userHandle) { 476 // Remove bubbles with this package name, since it has been uninstalled and attempts 477 // to open a bubble from an uninstalled app can cause issues. 478 mBubbleData.removeBubblesWithPackageName(s, DISMISS_PACKAGE_REMOVED); 479 } 480 481 @Override 482 public void onPackagesAvailable(String[] strings, UserHandle userHandle, 483 boolean b) { 484 485 } 486 487 @Override 488 public void onPackagesUnavailable(String[] packages, UserHandle userHandle, 489 boolean b) { 490 for (String packageName : packages) { 491 // Remove bubbles from unavailable apps. This can occur when the app is on 492 // external storage that has been removed. 493 mBubbleData.removeBubblesWithPackageName(packageName, DISMISS_PACKAGE_REMOVED); 494 } 495 } 496 497 @Override 498 public void onShortcutsChanged(String packageName, List<ShortcutInfo> validShortcuts, 499 UserHandle user) { 500 super.onShortcutsChanged(packageName, validShortcuts, user); 501 502 // Remove bubbles whose shortcuts aren't in the latest list of valid shortcuts. 503 mBubbleData.removeBubblesWithInvalidShortcuts( 504 packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED); 505 } 506 }); 507 } 508 509 /** 510 * See {@link NotifCallback}. 511 */ addNotifCallback(NotifCallback callback)512 public void addNotifCallback(NotifCallback callback) { 513 mCallbacks.add(callback); 514 } 515 516 /** 517 * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. 518 */ hideCurrentInputMethod()519 public void hideCurrentInputMethod() { 520 try { 521 mBarService.hideCurrentInputMethodForBubbles(); 522 } catch (RemoteException e) { 523 e.printStackTrace(); 524 } 525 } 526 setupNEM()527 private void setupNEM() { 528 mNotificationEntryManager.addNotificationEntryListener( 529 new NotificationEntryListener() { 530 @Override 531 public void onPendingEntryAdded(NotificationEntry entry) { 532 onEntryAdded(entry); 533 } 534 535 @Override 536 public void onPreEntryUpdated(NotificationEntry entry) { 537 onEntryUpdated(entry); 538 } 539 540 @Override 541 public void onEntryRemoved( 542 NotificationEntry entry, 543 @android.annotation.Nullable NotificationVisibility visibility, 544 boolean removedByUser, 545 int reason) { 546 BubbleController.this.onEntryRemoved(entry); 547 } 548 549 @Override 550 public void onNotificationRankingUpdated(RankingMap rankingMap) { 551 onRankingUpdated(rankingMap); 552 } 553 }); 554 555 mNotificationEntryManager.addNotificationRemoveInterceptor( 556 new NotificationRemoveInterceptor() { 557 @Override 558 public boolean onNotificationRemoveRequested( 559 String key, 560 NotificationEntry entry, 561 int dismissReason) { 562 final boolean isClearAll = dismissReason == REASON_CANCEL_ALL; 563 final boolean isUserDimiss = dismissReason == REASON_CANCEL 564 || dismissReason == REASON_CLICK; 565 final boolean isAppCancel = dismissReason == REASON_APP_CANCEL 566 || dismissReason == REASON_APP_CANCEL_ALL; 567 final boolean isSummaryCancel = 568 dismissReason == REASON_GROUP_SUMMARY_CANCELED; 569 570 // Need to check for !appCancel here because the notification may have 571 // previously been dismissed & entry.isRowDismissed would still be true 572 boolean userRemovedNotif = 573 (entry != null && entry.isRowDismissed() && !isAppCancel) 574 || isClearAll || isUserDimiss || isSummaryCancel; 575 576 if (userRemovedNotif) { 577 return handleDismissalInterception(entry); 578 } 579 return false; 580 } 581 }); 582 583 mNotificationGroupManager.addOnGroupChangeListener( 584 new NotificationGroupManager.OnGroupChangeListener() { 585 @Override 586 public void onGroupSuppressionChanged( 587 NotificationGroupManager.NotificationGroup group, 588 boolean suppressed) { 589 // More notifications could be added causing summary to no longer 590 // be suppressed -- in this case need to remove the key. 591 final String groupKey = group.summary != null 592 ? group.summary.getSbn().getGroupKey() 593 : null; 594 if (!suppressed && groupKey != null 595 && mBubbleData.isSummarySuppressed(groupKey)) { 596 mBubbleData.removeSuppressedSummary(groupKey); 597 } 598 } 599 }); 600 601 addNotifCallback(new NotifCallback() { 602 @Override 603 public void removeNotification(NotificationEntry entry, int reason) { 604 mNotificationEntryManager.performRemoveNotification(entry.getSbn(), reason); 605 } 606 607 @Override 608 public void invalidateNotifications(String reason) { 609 mNotificationEntryManager.updateNotifications(reason); 610 } 611 612 @Override 613 public void maybeCancelSummary(NotificationEntry entry) { 614 // Check if removed bubble has an associated suppressed group summary that needs 615 // to be removed now. 616 final String groupKey = entry.getSbn().getGroupKey(); 617 if (mBubbleData.isSummarySuppressed(groupKey)) { 618 mBubbleData.removeSuppressedSummary(groupKey); 619 620 final NotificationEntry summary = 621 mNotificationEntryManager.getActiveNotificationUnfiltered( 622 mBubbleData.getSummaryKey(groupKey)); 623 if (summary != null) { 624 mNotificationEntryManager.performRemoveNotification(summary.getSbn(), 625 UNDEFINED_DISMISS_REASON); 626 } 627 } 628 629 // Check if we still need to remove the summary from NoManGroup because the summary 630 // may not be in the mBubbleData.mSuppressedGroupKeys list and removed above. 631 // For example: 632 // 1. Bubbled notifications (group) is posted to shade and are visible bubbles 633 // 2. User expands bubbles so now their respective notifications in the shade are 634 // hidden, including the group summary 635 // 3. User removes all bubbles 636 // 4. We expect all the removed bubbles AND the summary (note: the summary was 637 // never added to the suppressedSummary list in BubbleData, so we add this check) 638 NotificationEntry summary = 639 mNotificationGroupManager.getLogicalGroupSummary(entry.getSbn()); 640 if (summary != null) { 641 ArrayList<NotificationEntry> summaryChildren = 642 mNotificationGroupManager.getLogicalChildren(summary.getSbn()); 643 boolean isSummaryThisNotif = summary.getKey().equals(entry.getKey()); 644 if (!isSummaryThisNotif && (summaryChildren == null 645 || summaryChildren.isEmpty())) { 646 mNotificationEntryManager.performRemoveNotification(summary.getSbn(), 647 UNDEFINED_DISMISS_REASON); 648 } 649 } 650 } 651 }); 652 } 653 setupNotifPipeline()654 private void setupNotifPipeline() { 655 mNotifPipeline.addCollectionListener(new NotifCollectionListener() { 656 @Override 657 public void onEntryAdded(NotificationEntry entry) { 658 BubbleController.this.onEntryAdded(entry); 659 } 660 661 @Override 662 public void onEntryUpdated(NotificationEntry entry) { 663 BubbleController.this.onEntryUpdated(entry); 664 } 665 666 @Override 667 public void onRankingUpdate(RankingMap rankingMap) { 668 onRankingUpdated(rankingMap); 669 } 670 671 @Override 672 public void onEntryRemoved(NotificationEntry entry, 673 @NotifCollection.CancellationReason int reason) { 674 BubbleController.this.onEntryRemoved(entry); 675 } 676 }); 677 } 678 679 /** 680 * Returns the scrim drawn behind the bubble stack. This is managed by {@link ScrimController} 681 * since we want the scrim's appearance and behavior to be identical to that of the notification 682 * shade scrim. 683 */ getScrimForBubble()684 public ScrimView getScrimForBubble() { 685 return mBubbleScrim; 686 } 687 688 /** 689 * Called when the status bar has become visible or invisible (either permanently or 690 * temporarily). 691 */ onStatusBarVisibilityChanged(boolean visible)692 public void onStatusBarVisibilityChanged(boolean visible) { 693 if (mStackView != null) { 694 // Hide the stack temporarily if the status bar has been made invisible, and the stack 695 // is collapsed. An expanded stack should remain visible until collapsed. 696 mStackView.setTemporarilyInvisible(!visible && !isStackExpanded()); 697 } 698 } 699 700 /** 701 * Sets whether to perform inflation on the same thread as the caller. This method should only 702 * be used in tests, not in production. 703 */ 704 @VisibleForTesting setInflateSynchronously(boolean inflateSynchronously)705 void setInflateSynchronously(boolean inflateSynchronously) { 706 mInflateSynchronously = inflateSynchronously; 707 } 708 setOverflowCallback(Runnable updateOverflow)709 void setOverflowCallback(Runnable updateOverflow) { 710 mOverflowCallback = updateOverflow; 711 } 712 713 /** 714 * @return Bubbles for updating overflow. 715 */ getOverflowBubbles()716 List<Bubble> getOverflowBubbles() { 717 return mBubbleData.getOverflowBubbles(); 718 } 719 720 /** 721 * BubbleStackView is lazily created by this method the first time a Bubble is added. This 722 * method initializes the stack view and adds it to the StatusBar just above the scrim. 723 */ ensureStackViewCreated()724 private void ensureStackViewCreated() { 725 if (mStackView == null) { 726 mStackView = new BubbleStackView( 727 mContext, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator, 728 mSysUiState, this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged, 729 this::hideCurrentInputMethod); 730 mStackView.setStackStartPosition(mPositionFromRemovedStack); 731 mStackView.addView(mBubbleScrim); 732 if (mExpandListener != null) { 733 mStackView.setExpandListener(mExpandListener); 734 } 735 736 mStackView.setUnbubbleConversationCallback(key -> { 737 final NotificationEntry entry = 738 mNotificationEntryManager.getPendingOrActiveNotif(key); 739 if (entry != null) { 740 onUserChangedBubble(entry, false /* shouldBubble */); 741 } 742 }); 743 } 744 745 addToWindowManagerMaybe(); 746 } 747 748 /** Adds the BubbleStackView to the WindowManager if it's not already there. */ addToWindowManagerMaybe()749 private void addToWindowManagerMaybe() { 750 // If the stack is null, or already added, don't add it. 751 if (mStackView == null || mAddedToWindowManager) { 752 return; 753 } 754 755 mWmLayoutParams = new WindowManager.LayoutParams( 756 // Fill the screen so we can use translation animations to position the bubble 757 // stack. We'll use touchable regions to ignore touches that are not on the bubbles 758 // themselves. 759 ViewGroup.LayoutParams.MATCH_PARENT, 760 ViewGroup.LayoutParams.MATCH_PARENT, 761 WindowManager.LayoutParams.TYPE_TRUSTED_APPLICATION_OVERLAY, 762 // Start not focusable - we'll become focusable when expanded so the ActivityView 763 // can use the IME. 764 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 765 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 766 PixelFormat.TRANSLUCENT); 767 768 mWmLayoutParams.setFitInsetsTypes(0); 769 mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 770 mWmLayoutParams.token = new Binder(); 771 mWmLayoutParams.setTitle("Bubbles!"); 772 mWmLayoutParams.packageName = mContext.getPackageName(); 773 mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 774 775 try { 776 mAddedToWindowManager = true; 777 mWindowManager.addView(mStackView, mWmLayoutParams); 778 } catch (IllegalStateException e) { 779 // This means the stack has already been added. This shouldn't happen, since we keep 780 // track of that, but just in case, update the previously added view's layout params. 781 e.printStackTrace(); 782 updateWmFlags(); 783 } 784 } 785 onImeVisibilityChanged(boolean imeVisible)786 private void onImeVisibilityChanged(boolean imeVisible) { 787 mImeVisible = imeVisible; 788 updateWmFlags(); 789 } 790 791 /** Removes the BubbleStackView from the WindowManager if it's there. */ removeFromWindowManagerMaybe()792 private void removeFromWindowManagerMaybe() { 793 if (!mAddedToWindowManager) { 794 return; 795 } 796 797 try { 798 mAddedToWindowManager = false; 799 if (mStackView != null) { 800 mPositionFromRemovedStack = mStackView.getRelativeStackPosition(); 801 mWindowManager.removeView(mStackView); 802 mStackView.removeView(mBubbleScrim); 803 mStackView = null; 804 } else { 805 Log.w(TAG, "StackView added to WindowManager, but was null when removing!"); 806 } 807 } catch (IllegalArgumentException e) { 808 // This means the stack has already been removed - it shouldn't happen, but ignore if it 809 // does, since we wanted it removed anyway. 810 e.printStackTrace(); 811 } 812 } 813 814 /** 815 * Updates the BubbleStackView's WindowManager.LayoutParams, and updates the WindowManager with 816 * the new params if the stack has been added. 817 */ updateWmFlags()818 private void updateWmFlags() { 819 if (mStackView == null) { 820 return; 821 } 822 if (isStackExpanded() && !mImeVisible) { 823 // If we're expanded, and the IME isn't visible, we want to be focusable. This ensures 824 // that any taps within Bubbles (including on the ActivityView) results in Bubbles 825 // receiving focus and clearing it from any other windows that might have it. 826 mWmLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 827 } else { 828 // If we're collapsed, we don't want to be focusable since tapping on the stack would 829 // steal focus from apps. We also don't want to be focusable if the IME is visible, 830 mWmLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 831 } 832 833 if (mAddedToWindowManager) { 834 try { 835 mWindowManager.updateViewLayout(mStackView, mWmLayoutParams); 836 } catch (IllegalArgumentException e) { 837 // If the stack is somehow not there, ignore the attempt to update it. 838 e.printStackTrace(); 839 } 840 } 841 } 842 843 /** 844 * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been 845 * added in the meantime. 846 */ onAllBubblesAnimatedOut()847 private void onAllBubblesAnimatedOut() { 848 if (mStackView != null) { 849 mStackView.setVisibility(INVISIBLE); 850 removeFromWindowManagerMaybe(); 851 } 852 } 853 854 /** 855 * Records the notification key for any active bubbles. These are used to restore active 856 * bubbles when the user returns to the foreground. 857 * 858 * @param userId the id of the user 859 */ saveBubbles(@serIdInt int userId)860 private void saveBubbles(@UserIdInt int userId) { 861 // First clear any existing keys that might be stored. 862 mSavedBubbleKeysPerUser.remove(userId); 863 // Add in all active bubbles for the current user. 864 for (Bubble bubble: mBubbleData.getBubbles()) { 865 mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); 866 } 867 } 868 869 /** 870 * Promotes existing notifications to Bubbles if they were previously bubbles. 871 * 872 * @param userId the id of the user 873 */ restoreBubbles(@serIdInt int userId)874 private void restoreBubbles(@UserIdInt int userId) { 875 ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId); 876 if (savedBubbleKeys == null) { 877 // There were no bubbles saved for this used. 878 return; 879 } 880 for (NotificationEntry e : 881 mNotificationEntryManager.getActiveNotificationsForCurrentUser()) { 882 if (savedBubbleKeys.contains(e.getKey()) 883 && mNotificationInterruptStateProvider.shouldBubbleUp(e) 884 && e.isBubble() 885 && canLaunchInActivityView(mContext, e)) { 886 updateBubble(e, true /* suppressFlyout */, false /* showInShade */); 887 } 888 } 889 // Finally, remove the entries for this user now that bubbles are restored. 890 mSavedBubbleKeysPerUser.remove(mCurrentUserId); 891 } 892 893 @Override onUiModeChanged()894 public void onUiModeChanged() { 895 updateForThemeChanges(); 896 } 897 898 @Override onOverlayChanged()899 public void onOverlayChanged() { 900 updateForThemeChanges(); 901 } 902 updateForThemeChanges()903 private void updateForThemeChanges() { 904 if (mStackView != null) { 905 mStackView.onThemeChanged(); 906 } 907 mBubbleIconFactory = new BubbleIconFactory(mContext); 908 // Reload each bubble 909 for (Bubble b: mBubbleData.getBubbles()) { 910 b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory, 911 false /* skipInflation */); 912 } 913 for (Bubble b: mBubbleData.getOverflowBubbles()) { 914 b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory, 915 false /* skipInflation */); 916 } 917 } 918 919 @Override onConfigChanged(Configuration newConfig)920 public void onConfigChanged(Configuration newConfig) { 921 if (mStackView != null && newConfig != null) { 922 if (newConfig.orientation != mOrientation) { 923 mOrientation = newConfig.orientation; 924 mStackView.onOrientationChanged(newConfig.orientation); 925 } 926 if (newConfig.densityDpi != mDensityDpi) { 927 mDensityDpi = newConfig.densityDpi; 928 mBubbleIconFactory = new BubbleIconFactory(mContext); 929 mStackView.onDisplaySizeChanged(); 930 } 931 if (newConfig.getLayoutDirection() != mLayoutDirection) { 932 mLayoutDirection = newConfig.getLayoutDirection(); 933 mStackView.onLayoutDirectionChanged(mLayoutDirection); 934 } 935 } 936 } 937 inLandscape()938 boolean inLandscape() { 939 return mOrientation == Configuration.ORIENTATION_LANDSCAPE; 940 } 941 942 /** 943 * Set a listener to be notified of bubble expand events. 944 */ setExpandListener(BubbleExpandListener listener)945 public void setExpandListener(BubbleExpandListener listener) { 946 mExpandListener = ((isExpanding, key) -> { 947 if (listener != null) { 948 listener.onBubbleExpandChanged(isExpanding, key); 949 } 950 951 updateWmFlags(); 952 }); 953 if (mStackView != null) { 954 mStackView.setExpandListener(mExpandListener); 955 } 956 } 957 958 /** 959 * Whether or not there are bubbles present, regardless of them being visible on the 960 * screen (e.g. if on AOD). 961 */ 962 @VisibleForTesting hasBubbles()963 boolean hasBubbles() { 964 if (mStackView == null) { 965 return false; 966 } 967 return mBubbleData.hasBubbles(); 968 } 969 970 /** 971 * Whether the stack of bubbles is expanded or not. 972 */ isStackExpanded()973 public boolean isStackExpanded() { 974 return mBubbleData.isExpanded(); 975 } 976 977 /** 978 * Tell the stack of bubbles to collapse. 979 */ collapseStack()980 public void collapseStack() { 981 mBubbleData.setExpanded(false /* expanded */); 982 } 983 984 /** 985 * True if either: 986 * (1) There is a bubble associated with the provided key and if its notification is hidden 987 * from the shade. 988 * (2) There is a group summary associated with the provided key that is hidden from the shade 989 * because it has been dismissed but still has child bubbles active. 990 * 991 * False otherwise. 992 */ isBubbleNotificationSuppressedFromShade(NotificationEntry entry)993 public boolean isBubbleNotificationSuppressedFromShade(NotificationEntry entry) { 994 String key = entry.getKey(); 995 boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key) 996 && !mBubbleData.getAnyBubbleWithkey(key).showInShade()); 997 998 String groupKey = entry.getSbn().getGroupKey(); 999 boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey); 1000 boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey)); 1001 return (isSummary && isSuppressedSummary) || isSuppressedBubble; 1002 } 1003 1004 /** 1005 * True if: 1006 * (1) The current notification entry same as selected bubble notification entry and the 1007 * stack is currently expanded. 1008 * 1009 * False otherwise. 1010 */ isBubbleExpanded(NotificationEntry entry)1011 public boolean isBubbleExpanded(NotificationEntry entry) { 1012 return isStackExpanded() && mBubbleData != null && mBubbleData.getSelectedBubble() != null 1013 && mBubbleData.getSelectedBubble().getKey().equals(entry.getKey()) ? true : false; 1014 } 1015 promoteBubbleFromOverflow(Bubble bubble)1016 void promoteBubbleFromOverflow(Bubble bubble) { 1017 mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK); 1018 bubble.setInflateSynchronously(mInflateSynchronously); 1019 bubble.setShouldAutoExpand(true); 1020 bubble.markAsAccessedAt(System.currentTimeMillis()); 1021 setIsBubble(bubble, true /* isBubble */); 1022 } 1023 1024 /** 1025 * Request the stack expand if needed, then select the specified Bubble as current. 1026 * If no bubble exists for this entry, one is created. 1027 * 1028 * @param entry the notification for the bubble to be selected 1029 */ expandStackAndSelectBubble(NotificationEntry entry)1030 public void expandStackAndSelectBubble(NotificationEntry entry) { 1031 if (mStatusBarStateListener.getCurrentState() == SHADE) { 1032 mNotifEntryToExpandOnShadeUnlock = null; 1033 1034 String key = entry.getKey(); 1035 Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); 1036 if (bubble != null) { 1037 mBubbleData.setSelectedBubble(bubble); 1038 mBubbleData.setExpanded(true); 1039 } else { 1040 bubble = mBubbleData.getOverflowBubbleWithKey(key); 1041 if (bubble != null) { 1042 promoteBubbleFromOverflow(bubble); 1043 } else if (entry.canBubble()) { 1044 // It can bubble but it's not -- it got aged out of the overflow before it 1045 // was dismissed or opened, make it a bubble again. 1046 setIsBubble(entry, true /* isBubble */, true /* autoExpand */); 1047 } 1048 } 1049 } else { 1050 // Wait until we're unlocked to expand, so that the user can see the expand animation 1051 // and also to work around bugs with expansion animation + shade unlock happening at the 1052 // same time. 1053 mNotifEntryToExpandOnShadeUnlock = entry; 1054 } 1055 } 1056 1057 /** 1058 * When a notification is marked Priority, expand the stack if needed, 1059 * then (maybe create and) select the given bubble. 1060 * 1061 * @param entry the notification for the bubble to show 1062 */ onUserChangedImportance(NotificationEntry entry)1063 public void onUserChangedImportance(NotificationEntry entry) { 1064 try { 1065 int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 1066 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; 1067 mBarService.onNotificationBubbleChanged(entry.getKey(), true, flags); 1068 } catch (RemoteException e) { 1069 Log.e(TAG, e.getMessage()); 1070 } 1071 mShadeController.collapsePanel(true); 1072 if (entry.getRow() != null) { 1073 entry.getRow().updateBubbleButton(); 1074 } 1075 } 1076 1077 /** 1078 * Directs a back gesture at the bubble stack. When opened, the current expanded bubble 1079 * is forwarded a back key down/up pair. 1080 */ performBackPressIfNeeded()1081 public void performBackPressIfNeeded() { 1082 if (mStackView != null) { 1083 mStackView.performBackPressIfNeeded(); 1084 } 1085 } 1086 1087 /** 1088 * Adds or updates a bubble associated with the provided notification entry. 1089 * 1090 * @param notif the notification associated with this bubble. 1091 */ updateBubble(NotificationEntry notif)1092 void updateBubble(NotificationEntry notif) { 1093 updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); 1094 } 1095 1096 /** 1097 * Fills the overflow bubbles by loading them from disk. 1098 */ loadOverflowBubblesFromDisk()1099 void loadOverflowBubblesFromDisk() { 1100 if (!mBubbleData.getOverflowBubbles().isEmpty() || mOverflowDataLoaded) { 1101 // we don't need to load overflow bubbles from disk if it is already in memory 1102 return; 1103 } 1104 mOverflowDataLoaded = true; 1105 mDataRepository.loadBubbles((bubbles) -> { 1106 bubbles.forEach(bubble -> { 1107 if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) { 1108 // if the bubble is already active, there's no need to push it to overflow 1109 return; 1110 } 1111 bubble.inflate((b) -> mBubbleData.overflowBubble(DISMISS_AGED, bubble), 1112 mContext, mStackView, mBubbleIconFactory, true /* skipInflation */); 1113 }); 1114 return null; 1115 }); 1116 } 1117 updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade)1118 void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) { 1119 // If this is an interruptive notif, mark that it's interrupted 1120 if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { 1121 notif.setInterruption(); 1122 } 1123 if (!notif.getRanking().visuallyInterruptive() 1124 && (notif.getBubbleMetadata() != null 1125 && !notif.getBubbleMetadata().getAutoExpandBubble()) 1126 && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { 1127 // Update the bubble but don't promote it out of overflow 1128 Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); 1129 b.setEntry(notif); 1130 } else { 1131 Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); 1132 inflateAndAdd(bubble, suppressFlyout, showInShade); 1133 } 1134 } 1135 inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade)1136 void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { 1137 // Lazy init stack view when a bubble is created 1138 ensureStackViewCreated(); 1139 bubble.setInflateSynchronously(mInflateSynchronously); 1140 bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), 1141 mContext, mStackView, mBubbleIconFactory, false /* skipInflation */); 1142 } 1143 1144 /** 1145 * Called when a user has indicated that an active notification should be shown as a bubble. 1146 * <p> 1147 * This method will collapse the shade, create the bubble without a flyout or dot, and suppress 1148 * the notification from appearing in the shade. 1149 * 1150 * @param entry the notification to change bubble state for. 1151 * @param shouldBubble whether the notification should show as a bubble or not. 1152 */ onUserChangedBubble(@onNull final NotificationEntry entry, boolean shouldBubble)1153 public void onUserChangedBubble(@NonNull final NotificationEntry entry, boolean shouldBubble) { 1154 NotificationChannel channel = entry.getChannel(); 1155 final String appPkg = entry.getSbn().getPackageName(); 1156 final int appUid = entry.getSbn().getUid(); 1157 if (channel == null || appPkg == null) { 1158 return; 1159 } 1160 1161 // Update the state in NotificationManagerService 1162 try { 1163 int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 1164 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; 1165 mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags); 1166 } catch (RemoteException e) { 1167 } 1168 1169 // Change the settings 1170 channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext, 1171 mINotificationManager, entry, channel); 1172 channel.setAllowBubbles(shouldBubble); 1173 try { 1174 int currentPref = mINotificationManager.getBubblePreferenceForPackage(appPkg, appUid); 1175 if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) { 1176 mINotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED); 1177 } 1178 mINotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel); 1179 } catch (RemoteException e) { 1180 Log.e(TAG, e.getMessage()); 1181 } 1182 1183 if (shouldBubble) { 1184 mShadeController.collapsePanel(true); 1185 if (entry.getRow() != null) { 1186 entry.getRow().updateBubbleButton(); 1187 } 1188 } 1189 } 1190 1191 /** 1192 * Removes the bubble with the given key. 1193 * <p> 1194 * Must be called from the main thread. 1195 */ 1196 @MainThread removeBubble(String key, int reason)1197 void removeBubble(String key, int reason) { 1198 if (mBubbleData.hasAnyBubbleWithKey(key)) { 1199 mBubbleData.dismissBubbleWithKey(key, reason); 1200 } 1201 } 1202 onEntryAdded(NotificationEntry entry)1203 private void onEntryAdded(NotificationEntry entry) { 1204 if (mNotificationInterruptStateProvider.shouldBubbleUp(entry) 1205 && entry.isBubble() 1206 && canLaunchInActivityView(mContext, entry)) { 1207 updateBubble(entry); 1208 } 1209 } 1210 onEntryUpdated(NotificationEntry entry)1211 private void onEntryUpdated(NotificationEntry entry) { 1212 // shouldBubbleUp checks canBubble & for bubble metadata 1213 boolean shouldBubble = mNotificationInterruptStateProvider.shouldBubbleUp(entry) 1214 && canLaunchInActivityView(mContext, entry); 1215 if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { 1216 // It was previously a bubble but no longer a bubble -- lets remove it 1217 removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE); 1218 } else if (shouldBubble && entry.isBubble()) { 1219 updateBubble(entry); 1220 } 1221 } 1222 onEntryRemoved(NotificationEntry entry)1223 private void onEntryRemoved(NotificationEntry entry) { 1224 if (isSummaryOfBubbles(entry)) { 1225 final String groupKey = entry.getSbn().getGroupKey(); 1226 mBubbleData.removeSuppressedSummary(groupKey); 1227 1228 // Remove any associated bubble children with the summary 1229 final List<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup( 1230 groupKey, mNotificationEntryManager); 1231 for (int i = 0; i < bubbleChildren.size(); i++) { 1232 removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED); 1233 } 1234 } else { 1235 removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL); 1236 } 1237 } 1238 1239 /** 1240 * Called when NotificationListener has received adjusted notification rank and reapplied 1241 * filtering and sorting. This is used to dismiss or create bubbles based on changes in 1242 * permissions on the notification channel or the global setting. 1243 * 1244 * @param rankingMap the updated ranking map from NotificationListenerService 1245 */ onRankingUpdated(RankingMap rankingMap)1246 private void onRankingUpdated(RankingMap rankingMap) { 1247 if (mTmpRanking == null) { 1248 mTmpRanking = new NotificationListenerService.Ranking(); 1249 } 1250 String[] orderedKeys = rankingMap.getOrderedKeys(); 1251 for (int i = 0; i < orderedKeys.length; i++) { 1252 String key = orderedKeys[i]; 1253 NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(key); 1254 rankingMap.getRanking(key, mTmpRanking); 1255 boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); 1256 if (isActiveBubble && !mTmpRanking.canBubble()) { 1257 // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. 1258 // This means that the app or channel's ability to bubble has been revoked. 1259 mBubbleData.dismissBubbleWithKey( 1260 key, BubbleController.DISMISS_BLOCKED); 1261 } else if (isActiveBubble 1262 && !mNotificationInterruptStateProvider.shouldBubbleUp(entry)) { 1263 // If this entry is allowed to bubble, but cannot currently bubble up, dismiss it. 1264 // This happens when DND is enabled and configured to hide bubbles. Dismissing with 1265 // the reason DISMISS_NO_BUBBLE_UP will retain the underlying notification, so that 1266 // the bubble will be re-created if shouldBubbleUp returns true. 1267 mBubbleData.dismissBubbleWithKey( 1268 key, BubbleController.DISMISS_NO_BUBBLE_UP); 1269 } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { 1270 entry.setFlagBubble(true); 1271 onEntryUpdated(entry); 1272 } 1273 } 1274 } 1275 setIsBubble(@onNull final NotificationEntry entry, final boolean isBubble, final boolean autoExpand)1276 private void setIsBubble(@NonNull final NotificationEntry entry, final boolean isBubble, 1277 final boolean autoExpand) { 1278 Objects.requireNonNull(entry); 1279 if (isBubble) { 1280 entry.getSbn().getNotification().flags |= FLAG_BUBBLE; 1281 } else { 1282 entry.getSbn().getNotification().flags &= ~FLAG_BUBBLE; 1283 } 1284 try { 1285 int flags = 0; 1286 if (autoExpand) { 1287 flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 1288 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; 1289 } 1290 mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, flags); 1291 } catch (RemoteException e) { 1292 // Bad things have happened 1293 } 1294 } 1295 setIsBubble(@onNull final Bubble b, final boolean isBubble)1296 private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) { 1297 Objects.requireNonNull(b); 1298 b.setIsBubble(isBubble); 1299 final NotificationEntry entry = mNotificationEntryManager 1300 .getPendingOrActiveNotif(b.getKey()); 1301 if (entry != null) { 1302 // Updating the entry to be a bubble will trigger our normal update flow 1303 setIsBubble(entry, isBubble, b.shouldAutoExpand()); 1304 } else if (isBubble) { 1305 // If bubble doesn't exist, it's a persisted bubble so we need to add it to the 1306 // stack ourselves 1307 Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */); 1308 inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */, 1309 !bubble.shouldAutoExpand() /* showInShade */); 1310 } 1311 } 1312 1313 @SuppressWarnings("FieldCanBeLocal") 1314 private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { 1315 1316 @Override 1317 public void applyUpdate(BubbleData.Update update) { 1318 ensureStackViewCreated(); 1319 1320 // Lazy load overflow bubbles from disk 1321 loadOverflowBubblesFromDisk(); 1322 // Update bubbles in overflow. 1323 if (mOverflowCallback != null) { 1324 mOverflowCallback.run(); 1325 } 1326 1327 // Collapsing? Do this first before remaining steps. 1328 if (update.expandedChanged && !update.expanded) { 1329 mStackView.setExpanded(false); 1330 mNotificationShadeWindowController.setRequestTopUi(false, TAG); 1331 } 1332 1333 // Do removals, if any. 1334 ArrayList<Pair<Bubble, Integer>> removedBubbles = 1335 new ArrayList<>(update.removedBubbles); 1336 ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>(); 1337 for (Pair<Bubble, Integer> removed : removedBubbles) { 1338 final Bubble bubble = removed.first; 1339 @DismissReason final int reason = removed.second; 1340 1341 if (mStackView != null) { 1342 mStackView.removeBubble(bubble); 1343 } 1344 1345 // Leave the notification in place if we're dismissing due to user switching, or 1346 // because DND is suppressing the bubble. In both of those cases, we need to be able 1347 // to restore the bubble from the notification later. 1348 if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) { 1349 continue; 1350 } 1351 if (reason == DISMISS_NOTIF_CANCEL) { 1352 bubblesToBeRemovedFromRepository.add(bubble); 1353 } 1354 final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif( 1355 bubble.getKey()); 1356 if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 1357 if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey()) 1358 && (!bubble.showInShade() 1359 || reason == DISMISS_NOTIF_CANCEL 1360 || reason == DISMISS_GROUP_CANCELLED)) { 1361 // The bubble is now gone & the notification is hidden from the shade, so 1362 // time to actually remove it 1363 for (NotifCallback cb : mCallbacks) { 1364 if (entry != null) { 1365 cb.removeNotification(entry, REASON_CANCEL); 1366 } 1367 } 1368 } else { 1369 if (bubble.isBubble()) { 1370 setIsBubble(bubble, false /* isBubble */); 1371 } 1372 if (entry != null && entry.getRow() != null) { 1373 entry.getRow().updateBubbleButton(); 1374 } 1375 } 1376 1377 } 1378 if (entry != null) { 1379 final String groupKey = entry.getSbn().getGroupKey(); 1380 if (mBubbleData.getBubblesInGroup( 1381 groupKey, mNotificationEntryManager).isEmpty()) { 1382 // Time to potentially remove the summary 1383 for (NotifCallback cb : mCallbacks) { 1384 cb.maybeCancelSummary(entry); 1385 } 1386 } 1387 } 1388 } 1389 mDataRepository.removeBubbles(bubblesToBeRemovedFromRepository); 1390 1391 if (update.addedBubble != null && mStackView != null) { 1392 mDataRepository.addBubble(update.addedBubble); 1393 mStackView.addBubble(update.addedBubble); 1394 } 1395 1396 if (update.updatedBubble != null && mStackView != null) { 1397 mStackView.updateBubble(update.updatedBubble); 1398 } 1399 1400 // At this point, the correct bubbles are inflated in the stack. 1401 // Make sure the order in bubble data is reflected in bubble row. 1402 if (update.orderChanged && mStackView != null) { 1403 mDataRepository.addBubbles(update.bubbles); 1404 mStackView.updateBubbleOrder(update.bubbles); 1405 } 1406 1407 if (update.selectionChanged && mStackView != null) { 1408 mStackView.setSelectedBubble(update.selectedBubble); 1409 if (update.selectedBubble != null) { 1410 final NotificationEntry entry = mNotificationEntryManager 1411 .getPendingOrActiveNotif(update.selectedBubble.getKey()); 1412 if (entry != null) { 1413 mNotificationGroupManager.updateSuppression(entry); 1414 } 1415 } 1416 } 1417 1418 // Expanding? Apply this last. 1419 if (update.expandedChanged && update.expanded) { 1420 if (mStackView != null) { 1421 mStackView.setExpanded(true); 1422 mNotificationShadeWindowController.setRequestTopUi(true, TAG); 1423 } 1424 } 1425 1426 for (NotifCallback cb : mCallbacks) { 1427 cb.invalidateNotifications("BubbleData.Listener.applyUpdate"); 1428 } 1429 updateStack(); 1430 1431 if (DEBUG_BUBBLE_CONTROLLER) { 1432 Log.d(TAG, "\n[BubbleData] bubbles:"); 1433 Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getBubbles(), 1434 mBubbleData.getSelectedBubble())); 1435 1436 if (mStackView != null) { 1437 Log.d(TAG, "\n[BubbleStackView]"); 1438 Log.d(TAG, BubbleDebugConfig.formatBubblesString(mStackView.getBubblesOnScreen(), 1439 mStackView.getExpandedBubble())); 1440 } 1441 Log.d(TAG, "\n[BubbleData] overflow:"); 1442 Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getOverflowBubbles(), 1443 null) + "\n"); 1444 } 1445 } 1446 }; 1447 1448 /** 1449 * We intercept notification entries (including group summaries) dismissed by the user when 1450 * there is an active bubble associated with it. We do this so that developers can still 1451 * cancel it (and hence the bubbles associated with it). However, these intercepted 1452 * notifications should then be hidden from the shade since the user has cancelled them, so we 1453 * {@link Bubble#setSuppressNotification}. For the case of suppressed summaries, we also add 1454 * {@link BubbleData#addSummaryToSuppress}. 1455 * 1456 * @return true if we want to intercept the dismissal of the entry, else false. 1457 */ handleDismissalInterception(NotificationEntry entry)1458 public boolean handleDismissalInterception(NotificationEntry entry) { 1459 if (entry == null) { 1460 return false; 1461 } 1462 if (isSummaryOfBubbles(entry)) { 1463 handleSummaryDismissalInterception(entry); 1464 } else { 1465 Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey()); 1466 if (bubble == null || !entry.isBubble()) { 1467 bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey()); 1468 } 1469 if (bubble == null) { 1470 return false; 1471 } 1472 bubble.setSuppressNotification(true); 1473 bubble.setShowDot(false /* show */); 1474 } 1475 // Update the shade 1476 for (NotifCallback cb : mCallbacks) { 1477 cb.invalidateNotifications("BubbleController.handleDismissalInterception"); 1478 } 1479 return true; 1480 } 1481 isSummaryOfBubbles(NotificationEntry entry)1482 private boolean isSummaryOfBubbles(NotificationEntry entry) { 1483 if (entry == null) { 1484 return false; 1485 } 1486 1487 String groupKey = entry.getSbn().getGroupKey(); 1488 ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup( 1489 groupKey, mNotificationEntryManager); 1490 boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey) 1491 && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey())); 1492 boolean isSummary = entry.getSbn().getNotification().isGroupSummary(); 1493 return (isSuppressedSummary || isSummary) 1494 && bubbleChildren != null 1495 && !bubbleChildren.isEmpty(); 1496 } 1497 handleSummaryDismissalInterception(NotificationEntry summary)1498 private void handleSummaryDismissalInterception(NotificationEntry summary) { 1499 // current children in the row: 1500 final List<NotificationEntry> children = summary.getAttachedNotifChildren(); 1501 if (children != null) { 1502 for (int i = 0; i < children.size(); i++) { 1503 NotificationEntry child = children.get(i); 1504 if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) { 1505 // Suppress the bubbled child 1506 // As far as group manager is concerned, once a child is no longer shown 1507 // in the shade, it is essentially removed. 1508 Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey()); 1509 if (bubbleChild != null) { 1510 final NotificationEntry entry = mNotificationEntryManager 1511 .getPendingOrActiveNotif(bubbleChild.getKey()); 1512 if (entry != null) { 1513 mNotificationGroupManager.onEntryRemoved(entry); 1514 } 1515 bubbleChild.setSuppressNotification(true); 1516 bubbleChild.setShowDot(false /* show */); 1517 } 1518 } else { 1519 // non-bubbled children can be removed 1520 for (NotifCallback cb : mCallbacks) { 1521 cb.removeNotification(child, REASON_GROUP_SUMMARY_CANCELED); 1522 } 1523 } 1524 } 1525 } 1526 1527 // And since all children are removed, remove the summary. 1528 mNotificationGroupManager.onEntryRemoved(summary); 1529 1530 // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated 1531 mBubbleData.addSummaryToSuppress(summary.getSbn().getGroupKey(), 1532 summary.getKey()); 1533 } 1534 1535 /** 1536 * Updates the visibility of the bubbles based on current state. 1537 * Does not un-bubble, just hides or un-hides. 1538 * Updates stack description for TalkBack focus. 1539 */ updateStack()1540 public void updateStack() { 1541 if (mStackView == null) { 1542 return; 1543 } 1544 1545 if (mStatusBarStateListener.getCurrentState() != SHADE) { 1546 // Bubbles don't appear over the locked shade. 1547 mStackView.setVisibility(INVISIBLE); 1548 } else if (hasBubbles()) { 1549 // If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the 1550 // stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate 1551 // out. 1552 mStackView.setVisibility(VISIBLE); 1553 } 1554 1555 mStackView.updateContentDescription(); 1556 } 1557 1558 /** 1559 * The display id of the expanded view, if the stack is expanded and not occluded by the 1560 * status bar, otherwise returns {@link Display#INVALID_DISPLAY}. 1561 */ getExpandedDisplayId(Context context)1562 public int getExpandedDisplayId(Context context) { 1563 if (mStackView == null) { 1564 return INVALID_DISPLAY; 1565 } 1566 final boolean defaultDisplay = context.getDisplay() != null 1567 && context.getDisplay().getDisplayId() == DEFAULT_DISPLAY; 1568 final BubbleViewProvider expandedViewProvider = mStackView.getExpandedBubble(); 1569 if (defaultDisplay && expandedViewProvider != null && isStackExpanded() 1570 && !mNotificationShadeWindowController.getPanelExpanded()) { 1571 return expandedViewProvider.getDisplayId(); 1572 } 1573 return INVALID_DISPLAY; 1574 } 1575 1576 @VisibleForTesting getStackView()1577 BubbleStackView getStackView() { 1578 return mStackView; 1579 } 1580 1581 /** 1582 * Description of current bubble state. 1583 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)1584 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1585 pw.println("BubbleController state:"); 1586 mBubbleData.dump(fd, pw, args); 1587 pw.println(); 1588 if (mStackView != null) { 1589 mStackView.dump(fd, pw, args); 1590 } 1591 pw.println(); 1592 } 1593 1594 /** 1595 * This task stack listener is responsible for responding to tasks moved to the front 1596 * which are on the default (main) display. When this happens, expanded bubbles must be 1597 * collapsed so the user may interact with the app which was just moved to the front. 1598 * <p> 1599 * This listener is registered with SystemUI's ActivityManagerWrapper which dispatches 1600 * these calls via a main thread Handler. 1601 */ 1602 @MainThread 1603 private class BubbleTaskStackListener extends TaskStackChangeListener { 1604 1605 @Override onTaskMovedToFront(RunningTaskInfo taskInfo)1606 public void onTaskMovedToFront(RunningTaskInfo taskInfo) { 1607 if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) { 1608 if (!mStackView.isExpansionAnimating()) { 1609 mBubbleData.setExpanded(false); 1610 } 1611 } 1612 } 1613 1614 @Override onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible)1615 public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, 1616 boolean clearedTask, boolean wasVisible) { 1617 for (Bubble b : mBubbleData.getBubbles()) { 1618 if (b.getDisplayId() == task.displayId) { 1619 mBubbleData.setSelectedBubble(b); 1620 mBubbleData.setExpanded(true); 1621 return; 1622 } 1623 } 1624 } 1625 1626 @Override onActivityLaunchOnSecondaryDisplayRerouted()1627 public void onActivityLaunchOnSecondaryDisplayRerouted() { 1628 if (mStackView != null) { 1629 mBubbleData.setExpanded(false); 1630 } 1631 } 1632 1633 @Override onBackPressedOnTaskRoot(RunningTaskInfo taskInfo)1634 public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { 1635 if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) { 1636 if (mImeVisible) { 1637 hideCurrentInputMethod(); 1638 } else { 1639 mBubbleData.setExpanded(false); 1640 } 1641 } 1642 } 1643 1644 @Override onSingleTaskDisplayDrawn(int displayId)1645 public void onSingleTaskDisplayDrawn(int displayId) { 1646 if (mStackView == null) { 1647 return; 1648 } 1649 mStackView.showExpandedViewContents(displayId); 1650 } 1651 1652 @Override onSingleTaskDisplayEmpty(int displayId)1653 public void onSingleTaskDisplayEmpty(int displayId) { 1654 final BubbleViewProvider expandedBubble = mStackView != null 1655 ? mStackView.getExpandedBubble() 1656 : null; 1657 int expandedId = expandedBubble != null ? expandedBubble.getDisplayId() : -1; 1658 if (mStackView != null && mStackView.isExpanded() && expandedId == displayId) { 1659 mBubbleData.setExpanded(false); 1660 } 1661 mBubbleData.notifyDisplayEmpty(displayId); 1662 } 1663 } 1664 1665 /** 1666 * Whether an intent is properly configured to display in an {@link android.app.ActivityView}. 1667 * 1668 * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically 1669 * that should filter out any invalid bubbles, but should protect SysUI side just in case. 1670 * 1671 * @param context the context to use. 1672 * @param entry the entry to bubble. 1673 */ canLaunchInActivityView(Context context, NotificationEntry entry)1674 static boolean canLaunchInActivityView(Context context, NotificationEntry entry) { 1675 PendingIntent intent = entry.getBubbleMetadata() != null 1676 ? entry.getBubbleMetadata().getIntent() 1677 : null; 1678 if (entry.getBubbleMetadata() != null 1679 && entry.getBubbleMetadata().getShortcutId() != null) { 1680 return true; 1681 } 1682 if (intent == null) { 1683 Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey()); 1684 return false; 1685 } 1686 PackageManager packageManager = StatusBar.getPackageManagerForUser( 1687 context, entry.getSbn().getUser().getIdentifier()); 1688 ActivityInfo info = 1689 intent.getIntent().resolveActivityInfo(packageManager, 0); 1690 if (info == null) { 1691 Log.w(TAG, "Unable to send as bubble, " 1692 + entry.getKey() + " couldn't find activity info for intent: " 1693 + intent); 1694 return false; 1695 } 1696 if (!ActivityInfo.isResizeableMode(info.resizeMode)) { 1697 Log.w(TAG, "Unable to send as bubble, " 1698 + entry.getKey() + " activity is not resizable for intent: " 1699 + intent); 1700 return false; 1701 } 1702 return true; 1703 } 1704 1705 /** PinnedStackListener that dispatches IME visibility updates to the stack. */ 1706 private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedStackListener { 1707 @Override onImeVisibilityChanged(boolean imeVisible, int imeHeight)1708 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 1709 if (mStackView != null) { 1710 mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight)); 1711 } 1712 } 1713 } 1714 } 1715