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_AUTOGROUP_SUMMARY; 20 import static android.app.Notification.FLAG_BUBBLE; 21 import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS; 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.ViewGroup.LayoutParams.MATCH_PARENT; 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.UserIdInt; 46 import android.app.ActivityManager.RunningTaskInfo; 47 import android.app.NotificationManager; 48 import android.app.PendingIntent; 49 import android.content.Context; 50 import android.content.pm.ActivityInfo; 51 import android.content.pm.ParceledListSlice; 52 import android.content.res.Configuration; 53 import android.graphics.Rect; 54 import android.os.RemoteException; 55 import android.os.ServiceManager; 56 import android.provider.Settings; 57 import android.service.notification.NotificationListenerService.RankingMap; 58 import android.service.notification.ZenModeConfig; 59 import android.util.ArraySet; 60 import android.util.Log; 61 import android.util.Pair; 62 import android.util.SparseSetArray; 63 import android.view.Display; 64 import android.view.IPinnedStackController; 65 import android.view.IPinnedStackListener; 66 import android.view.ViewGroup; 67 import android.widget.FrameLayout; 68 69 import androidx.annotation.IntDef; 70 import androidx.annotation.MainThread; 71 import androidx.annotation.Nullable; 72 73 import com.android.internal.annotations.VisibleForTesting; 74 import com.android.internal.statusbar.IStatusBarService; 75 import com.android.systemui.Dependency; 76 import com.android.systemui.R; 77 import com.android.systemui.plugins.statusbar.StatusBarStateController; 78 import com.android.systemui.shared.system.ActivityManagerWrapper; 79 import com.android.systemui.shared.system.TaskStackChangeListener; 80 import com.android.systemui.shared.system.WindowManagerWrapper; 81 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 82 import com.android.systemui.statusbar.NotificationRemoveInterceptor; 83 import com.android.systemui.statusbar.notification.NotificationEntryListener; 84 import com.android.systemui.statusbar.notification.NotificationEntryManager; 85 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; 86 import com.android.systemui.statusbar.notification.collection.NotificationData; 87 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 88 import com.android.systemui.statusbar.phone.NotificationGroupManager; 89 import com.android.systemui.statusbar.phone.StatusBarWindowController; 90 import com.android.systemui.statusbar.policy.ConfigurationController; 91 import com.android.systemui.statusbar.policy.ZenModeController; 92 93 import java.io.FileDescriptor; 94 import java.io.PrintWriter; 95 import java.lang.annotation.Retention; 96 import java.lang.annotation.Target; 97 import java.util.ArrayList; 98 import java.util.List; 99 100 import javax.inject.Inject; 101 import javax.inject.Singleton; 102 103 /** 104 * Bubbles are a special type of content that can "float" on top of other apps or System UI. 105 * Bubbles can be expanded to show more content. 106 * 107 * The controller manages addition, removal, and visible state of bubbles on screen. 108 */ 109 @Singleton 110 public class BubbleController implements ConfigurationController.ConfigurationListener { 111 112 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; 113 114 @Retention(SOURCE) 115 @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, 116 DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, 117 DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT}) 118 @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) 119 @interface DismissReason {} 120 121 static final int DISMISS_USER_GESTURE = 1; 122 static final int DISMISS_AGED = 2; 123 static final int DISMISS_TASK_FINISHED = 3; 124 static final int DISMISS_BLOCKED = 4; 125 static final int DISMISS_NOTIF_CANCEL = 5; 126 static final int DISMISS_ACCESSIBILITY_ACTION = 6; 127 static final int DISMISS_NO_LONGER_BUBBLE = 7; 128 static final int DISMISS_USER_CHANGED = 8; 129 static final int DISMISS_GROUP_CANCELLED = 9; 130 static final int DISMISS_INVALID_INTENT = 10; 131 132 public static final int MAX_BUBBLES = 5; // TODO: actually enforce this 133 134 /** Flag to enable or disable the entire feature */ 135 private static final String ENABLE_BUBBLES = "experiment_enable_bubbles"; 136 137 private final Context mContext; 138 private final NotificationEntryManager mNotificationEntryManager; 139 private final BubbleTaskStackListener mTaskStackListener; 140 private BubbleStateChangeListener mStateChangeListener; 141 private BubbleExpandListener mExpandListener; 142 @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; 143 private final NotificationGroupManager mNotificationGroupManager; 144 145 private BubbleData mBubbleData; 146 @Nullable private BubbleStackView mStackView; 147 148 // Tracks the id of the current (foreground) user. 149 private int mCurrentUserId; 150 // Saves notification keys of active bubbles when users are switched. 151 private final SparseSetArray<String> mSavedBubbleKeysPerUser; 152 153 // Bubbles get added to the status bar view 154 private final StatusBarWindowController mStatusBarWindowController; 155 private final ZenModeController mZenModeController; 156 private StatusBarStateListener mStatusBarStateListener; 157 158 private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider; 159 private IStatusBarService mBarService; 160 161 // Used for determining view rect for touch interaction 162 private Rect mTempRect = new Rect(); 163 164 // Listens to user switch so bubbles can be saved and restored. 165 private final NotificationLockscreenUserManager mNotifUserManager; 166 167 /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */ 168 private int mOrientation = Configuration.ORIENTATION_UNDEFINED; 169 170 /** 171 * Listener to be notified when some states of the bubbles change. 172 */ 173 public interface BubbleStateChangeListener { 174 /** 175 * Called when the stack has bubbles or no longer has bubbles. 176 */ onHasBubblesChanged(boolean hasBubbles)177 void onHasBubblesChanged(boolean hasBubbles); 178 } 179 180 /** 181 * Listener to find out about stack expansion / collapse events. 182 */ 183 public interface BubbleExpandListener { 184 /** 185 * Called when the expansion state of the bubble stack changes. 186 * 187 * @param isExpanding whether it's expanding or collapsing 188 * @param key the notification key associated with bubble being expanded 189 */ onBubbleExpandChanged(boolean isExpanding, String key)190 void onBubbleExpandChanged(boolean isExpanding, String key); 191 } 192 193 /** 194 * Listens for the current state of the status bar and updates the visibility state 195 * of bubbles as needed. 196 */ 197 private class StatusBarStateListener implements StatusBarStateController.StateListener { 198 private int mState; 199 /** 200 * Returns the current status bar state. 201 */ getCurrentState()202 public int getCurrentState() { 203 return mState; 204 } 205 206 @Override onStateChanged(int newState)207 public void onStateChanged(int newState) { 208 mState = newState; 209 boolean shouldCollapse = (mState != SHADE); 210 if (shouldCollapse) { 211 collapseStack(); 212 } 213 updateStack(); 214 } 215 } 216 217 @Inject BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager)218 public BubbleController(Context context, StatusBarWindowController statusBarWindowController, 219 BubbleData data, ConfigurationController configurationController, 220 NotificationInterruptionStateProvider interruptionStateProvider, 221 ZenModeController zenModeController, 222 NotificationLockscreenUserManager notifUserManager, 223 NotificationGroupManager groupManager) { 224 this(context, statusBarWindowController, data, null /* synchronizer */, 225 configurationController, interruptionStateProvider, zenModeController, 226 notifUserManager, groupManager); 227 } 228 BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager)229 public BubbleController(Context context, StatusBarWindowController statusBarWindowController, 230 BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, 231 ConfigurationController configurationController, 232 NotificationInterruptionStateProvider interruptionStateProvider, 233 ZenModeController zenModeController, 234 NotificationLockscreenUserManager notifUserManager, 235 NotificationGroupManager groupManager) { 236 mContext = context; 237 mNotificationInterruptionStateProvider = interruptionStateProvider; 238 mNotifUserManager = notifUserManager; 239 mZenModeController = zenModeController; 240 mZenModeController.addCallback(new ZenModeController.Callback() { 241 @Override 242 public void onZenChanged(int zen) { 243 if (mStackView != null) { 244 mStackView.updateDots(); 245 } 246 } 247 248 @Override 249 public void onConfigChanged(ZenModeConfig config) { 250 if (mStackView != null) { 251 mStackView.updateDots(); 252 } 253 } 254 }); 255 256 configurationController.addCallback(this /* configurationListener */); 257 258 mBubbleData = data; 259 mBubbleData.setListener(mBubbleDataListener); 260 261 mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); 262 mNotificationEntryManager.addNotificationEntryListener(mEntryListener); 263 mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor); 264 mNotificationGroupManager = groupManager; 265 mNotificationGroupManager.addOnGroupChangeListener( 266 new NotificationGroupManager.OnGroupChangeListener() { 267 @Override 268 public void onGroupSuppressionChanged( 269 NotificationGroupManager.NotificationGroup group, 270 boolean suppressed) { 271 // More notifications could be added causing summary to no longer 272 // be suppressed -- in this case need to remove the key. 273 final String groupKey = group.summary != null 274 ? group.summary.notification.getGroupKey() 275 : null; 276 if (!suppressed && groupKey != null 277 && mBubbleData.isSummarySuppressed(groupKey)) { 278 mBubbleData.removeSuppressedSummary(groupKey); 279 } 280 } 281 }); 282 283 mStatusBarWindowController = statusBarWindowController; 284 mStatusBarStateListener = new StatusBarStateListener(); 285 Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener); 286 287 mTaskStackListener = new BubbleTaskStackListener(); 288 ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); 289 290 try { 291 WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener()); 292 } catch (RemoteException e) { 293 e.printStackTrace(); 294 } 295 mSurfaceSynchronizer = synchronizer; 296 297 mBarService = IStatusBarService.Stub.asInterface( 298 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 299 300 mSavedBubbleKeysPerUser = new SparseSetArray<>(); 301 mCurrentUserId = mNotifUserManager.getCurrentUserId(); 302 mNotifUserManager.addUserChangedListener( 303 newUserId -> { 304 saveBubbles(mCurrentUserId); 305 mBubbleData.dismissAll(DISMISS_USER_CHANGED); 306 restoreBubbles(newUserId); 307 mCurrentUserId = newUserId; 308 }); 309 } 310 311 /** 312 * BubbleStackView is lazily created by this method the first time a Bubble is added. This 313 * method initializes the stack view and adds it to the StatusBar just above the scrim. 314 */ ensureStackViewCreated()315 private void ensureStackViewCreated() { 316 if (mStackView == null) { 317 mStackView = new BubbleStackView(mContext, mBubbleData, mSurfaceSynchronizer); 318 ViewGroup sbv = mStatusBarWindowController.getStatusBarView(); 319 // TODO(b/130237686): When you expand the shade on top of expanded bubble, there is no 320 // scrim between bubble and the shade 321 int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1; 322 sbv.addView(mStackView, bubblePosition, 323 new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 324 if (mExpandListener != null) { 325 mStackView.setExpandListener(mExpandListener); 326 } 327 } 328 } 329 330 /** 331 * Records the notification key for any active bubbles. These are used to restore active 332 * bubbles when the user returns to the foreground. 333 * 334 * @param userId the id of the user 335 */ saveBubbles(@serIdInt int userId)336 private void saveBubbles(@UserIdInt int userId) { 337 // First clear any existing keys that might be stored. 338 mSavedBubbleKeysPerUser.remove(userId); 339 // Add in all active bubbles for the current user. 340 for (Bubble bubble: mBubbleData.getBubbles()) { 341 mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); 342 } 343 } 344 345 /** 346 * Promotes existing notifications to Bubbles if they were previously bubbles. 347 * 348 * @param userId the id of the user 349 */ restoreBubbles(@serIdInt int userId)350 private void restoreBubbles(@UserIdInt int userId) { 351 NotificationData notificationData = 352 mNotificationEntryManager.getNotificationData(); 353 ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId); 354 if (savedBubbleKeys == null) { 355 // There were no bubbles saved for this used. 356 return; 357 } 358 for (NotificationEntry e : notificationData.getNotificationsForCurrentUser()) { 359 if (savedBubbleKeys.contains(e.key) 360 && mNotificationInterruptionStateProvider.shouldBubbleUp(e) 361 && canLaunchInActivityView(mContext, e)) { 362 updateBubble(e, /* suppressFlyout= */ true); 363 } 364 } 365 // Finally, remove the entries for this user now that bubbles are restored. 366 mSavedBubbleKeysPerUser.remove(mCurrentUserId); 367 } 368 369 @Override onUiModeChanged()370 public void onUiModeChanged() { 371 if (mStackView != null) { 372 mStackView.onThemeChanged(); 373 } 374 } 375 376 @Override onOverlayChanged()377 public void onOverlayChanged() { 378 if (mStackView != null) { 379 mStackView.onThemeChanged(); 380 } 381 } 382 383 @Override onConfigChanged(Configuration newConfig)384 public void onConfigChanged(Configuration newConfig) { 385 if (mStackView != null && newConfig != null && newConfig.orientation != mOrientation) { 386 mOrientation = newConfig.orientation; 387 mStackView.onOrientationChanged(newConfig.orientation); 388 } 389 } 390 391 /** 392 * Set a listener to be notified when some states of the bubbles change. 393 */ setBubbleStateChangeListener(BubbleStateChangeListener listener)394 public void setBubbleStateChangeListener(BubbleStateChangeListener listener) { 395 mStateChangeListener = listener; 396 } 397 398 /** 399 * Set a listener to be notified of bubble expand events. 400 */ setExpandListener(BubbleExpandListener listener)401 public void setExpandListener(BubbleExpandListener listener) { 402 mExpandListener = ((isExpanding, key) -> { 403 if (listener != null) { 404 listener.onBubbleExpandChanged(isExpanding, key); 405 } 406 mStatusBarWindowController.setBubbleExpanded(isExpanding); 407 }); 408 if (mStackView != null) { 409 mStackView.setExpandListener(mExpandListener); 410 } 411 } 412 413 /** 414 * Whether or not there are bubbles present, regardless of them being visible on the 415 * screen (e.g. if on AOD). 416 */ hasBubbles()417 public boolean hasBubbles() { 418 if (mStackView == null) { 419 return false; 420 } 421 return mBubbleData.hasBubbles(); 422 } 423 424 /** 425 * Whether the stack of bubbles is expanded or not. 426 */ isStackExpanded()427 public boolean isStackExpanded() { 428 return mBubbleData.isExpanded(); 429 } 430 431 /** 432 * Tell the stack of bubbles to expand. 433 */ expandStack()434 public void expandStack() { 435 mBubbleData.setExpanded(true); 436 } 437 438 /** 439 * Tell the stack of bubbles to collapse. 440 */ collapseStack()441 public void collapseStack() { 442 mBubbleData.setExpanded(false /* expanded */); 443 } 444 445 /** 446 * True if either: 447 * (1) There is a bubble associated with the provided key and if its notification is hidden 448 * from the shade. 449 * (2) There is a group summary associated with the provided key that is hidden from the shade 450 * because it has been dismissed but still has child bubbles active. 451 * 452 * False otherwise. 453 */ isBubbleNotificationSuppressedFromShade(String key)454 public boolean isBubbleNotificationSuppressedFromShade(String key) { 455 boolean isBubbleAndSuppressed = mBubbleData.hasBubbleWithKey(key) 456 && !mBubbleData.getBubbleWithKey(key).showInShadeWhenBubble(); 457 NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key); 458 String groupKey = entry != null ? entry.notification.getGroupKey() : null; 459 boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey); 460 boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey)); 461 return (isSummary && isSuppressedSummary) || isBubbleAndSuppressed; 462 } 463 selectBubble(Bubble bubble)464 void selectBubble(Bubble bubble) { 465 mBubbleData.setSelectedBubble(bubble); 466 } 467 468 @VisibleForTesting selectBubble(String key)469 void selectBubble(String key) { 470 Bubble bubble = mBubbleData.getBubbleWithKey(key); 471 selectBubble(bubble); 472 } 473 474 /** 475 * Request the stack expand if needed, then select the specified Bubble as current. 476 * 477 * @param notificationKey the notification key for the bubble to be selected 478 */ expandStackAndSelectBubble(String notificationKey)479 public void expandStackAndSelectBubble(String notificationKey) { 480 Bubble bubble = mBubbleData.getBubbleWithKey(notificationKey); 481 if (bubble != null) { 482 mBubbleData.setSelectedBubble(bubble); 483 mBubbleData.setExpanded(true); 484 } 485 } 486 487 /** 488 * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack. 489 */ dismissStack(@ismissReason int reason)490 void dismissStack(@DismissReason int reason) { 491 mBubbleData.dismissAll(reason); 492 } 493 494 /** 495 * Directs a back gesture at the bubble stack. When opened, the current expanded bubble 496 * is forwarded a back key down/up pair. 497 */ performBackPressIfNeeded()498 public void performBackPressIfNeeded() { 499 if (mStackView != null) { 500 mStackView.performBackPressIfNeeded(); 501 } 502 } 503 504 /** 505 * Adds or updates a bubble associated with the provided notification entry. 506 * 507 * @param notif the notification associated with this bubble. 508 */ updateBubble(NotificationEntry notif)509 void updateBubble(NotificationEntry notif) { 510 updateBubble(notif, /* supressFlyout */ false); 511 } 512 updateBubble(NotificationEntry notif, boolean suppressFlyout)513 void updateBubble(NotificationEntry notif, boolean suppressFlyout) { 514 // If this is an interruptive notif, mark that it's interrupted 515 if (notif.importance >= NotificationManager.IMPORTANCE_HIGH) { 516 notif.setInterruption(); 517 } 518 mBubbleData.notificationEntryUpdated(notif, suppressFlyout); 519 } 520 521 /** 522 * Removes the bubble associated with the {@param uri}. 523 * <p> 524 * Must be called from the main thread. 525 */ 526 @MainThread removeBubble(String key, int reason)527 void removeBubble(String key, int reason) { 528 // TEMP: refactor to change this to pass entry 529 Bubble bubble = mBubbleData.getBubbleWithKey(key); 530 if (bubble != null) { 531 mBubbleData.notificationEntryRemoved(bubble.getEntry(), reason); 532 } 533 } 534 535 @SuppressWarnings("FieldCanBeLocal") 536 private final NotificationRemoveInterceptor mRemoveInterceptor = 537 new NotificationRemoveInterceptor() { 538 @Override 539 public boolean onNotificationRemoveRequested(String key, int reason) { 540 NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key); 541 String groupKey = entry != null ? entry.notification.getGroupKey() : null; 542 ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey); 543 544 boolean inBubbleData = mBubbleData.hasBubbleWithKey(key); 545 boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey) 546 && mBubbleData.getSummaryKey(groupKey).equals(key)); 547 boolean isSummary = entry != null 548 && entry.notification.getNotification().isGroupSummary(); 549 boolean isSummaryOfBubbles = (isSuppressedSummary || isSummary) 550 && bubbleChildren != null && !bubbleChildren.isEmpty(); 551 552 if (!inBubbleData && !isSummaryOfBubbles) { 553 return false; 554 } 555 556 final boolean isClearAll = reason == REASON_CANCEL_ALL; 557 final boolean isUserDimiss = reason == REASON_CANCEL || reason == REASON_CLICK; 558 final boolean isAppCancel = reason == REASON_APP_CANCEL 559 || reason == REASON_APP_CANCEL_ALL; 560 final boolean isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED; 561 562 // Need to check for !appCancel here because the notification may have 563 // previously been dismissed & entry.isRowDismissed would still be true 564 boolean userRemovedNotif = (entry != null && entry.isRowDismissed() && !isAppCancel) 565 || isClearAll || isUserDimiss || isSummaryCancel; 566 567 if (isSummaryOfBubbles) { 568 return handleSummaryRemovalInterception(entry, userRemovedNotif); 569 } 570 571 // The bubble notification sticks around in the data as long as the bubble is 572 // not dismissed and the app hasn't cancelled the notification. 573 Bubble bubble = mBubbleData.getBubbleWithKey(key); 574 boolean bubbleExtended = entry != null && entry.isBubble() && userRemovedNotif; 575 if (bubbleExtended) { 576 bubble.setShowInShadeWhenBubble(false); 577 bubble.setShowBubbleDot(false); 578 if (mStackView != null) { 579 mStackView.updateDotVisibility(entry.key); 580 } 581 mNotificationEntryManager.updateNotifications(); 582 return true; 583 } else if (!userRemovedNotif && entry != null) { 584 // This wasn't a user removal so we should remove the bubble as well 585 mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL); 586 return false; 587 } 588 return false; 589 } 590 }; 591 handleSummaryRemovalInterception(NotificationEntry summary, boolean userRemovedNotif)592 private boolean handleSummaryRemovalInterception(NotificationEntry summary, 593 boolean userRemovedNotif) { 594 String groupKey = summary.notification.getGroupKey(); 595 ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey); 596 597 if (userRemovedNotif) { 598 // If it's a user dismiss we mark the children to be hidden from the shade. 599 for (int i = 0; i < bubbleChildren.size(); i++) { 600 Bubble bubbleChild = bubbleChildren.get(i); 601 // As far as group manager is concerned, once a child is no longer shown 602 // in the shade, it is essentially removed. 603 mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry()); 604 bubbleChild.setShowInShadeWhenBubble(false); 605 bubbleChild.setShowBubbleDot(false); 606 if (mStackView != null) { 607 mStackView.updateDotVisibility(bubbleChild.getKey()); 608 } 609 } 610 // And since all children are removed, remove the summary. 611 mNotificationGroupManager.onEntryRemoved(summary); 612 613 // If the summary was auto-generated we don't need to keep that notification around 614 // because apps can't cancel it; so we only intercept & suppress real summaries. 615 boolean isAutogroupSummary = (summary.notification.getNotification().flags 616 & FLAG_AUTOGROUP_SUMMARY) != 0; 617 if (!isAutogroupSummary) { 618 mBubbleData.addSummaryToSuppress(summary.notification.getGroupKey(), 619 summary.key); 620 // Tell shade to update for the suppression 621 mNotificationEntryManager.updateNotifications(); 622 } 623 return !isAutogroupSummary; 624 } else { 625 // If it's not a user dismiss it's a cancel. 626 mBubbleData.removeSuppressedSummary(groupKey); 627 628 // Remove any associated bubble children. 629 for (int i = 0; i < bubbleChildren.size(); i++) { 630 Bubble bubbleChild = bubbleChildren.get(i); 631 mBubbleData.notificationEntryRemoved(bubbleChild.getEntry(), 632 DISMISS_GROUP_CANCELLED); 633 } 634 return false; 635 } 636 } 637 638 @SuppressWarnings("FieldCanBeLocal") 639 private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { 640 @Override 641 public void onPendingEntryAdded(NotificationEntry entry) { 642 if (!areBubblesEnabled(mContext)) { 643 return; 644 } 645 if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry) 646 && canLaunchInActivityView(mContext, entry)) { 647 updateBubble(entry); 648 } 649 } 650 651 @Override 652 public void onPreEntryUpdated(NotificationEntry entry) { 653 if (!areBubblesEnabled(mContext)) { 654 return; 655 } 656 boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry) 657 && canLaunchInActivityView(mContext, entry); 658 if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.key)) { 659 // It was previously a bubble but no longer a bubble -- lets remove it 660 removeBubble(entry.key, DISMISS_NO_LONGER_BUBBLE); 661 } else if (shouldBubble) { 662 Bubble b = mBubbleData.getBubbleWithKey(entry.key); 663 updateBubble(entry); 664 } 665 } 666 667 @Override 668 public void onNotificationRankingUpdated(RankingMap rankingMap) { 669 // Forward to BubbleData to block any bubbles which should no longer be shown 670 mBubbleData.notificationRankingUpdated(rankingMap); 671 } 672 }; 673 674 @SuppressWarnings("FieldCanBeLocal") 675 private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { 676 677 @Override 678 public void applyUpdate(BubbleData.Update update) { 679 if (mStackView == null && update.addedBubble != null) { 680 // Lazy init stack view when the first bubble is added. 681 ensureStackViewCreated(); 682 } 683 684 // If not yet initialized, ignore all other changes. 685 if (mStackView == null) { 686 return; 687 } 688 689 if (update.addedBubble != null) { 690 mStackView.addBubble(update.addedBubble); 691 } 692 693 // Collapsing? Do this first before remaining steps. 694 if (update.expandedChanged && !update.expanded) { 695 mStackView.setExpanded(false); 696 } 697 698 // Do removals, if any. 699 ArrayList<Pair<Bubble, Integer>> removedBubbles = 700 new ArrayList<>(update.removedBubbles); 701 for (Pair<Bubble, Integer> removed : removedBubbles) { 702 final Bubble bubble = removed.first; 703 @DismissReason final int reason = removed.second; 704 mStackView.removeBubble(bubble); 705 706 // If the bubble is removed for user switching, leave the notification in place. 707 if (reason != DISMISS_USER_CHANGED) { 708 if (!mBubbleData.hasBubbleWithKey(bubble.getKey()) 709 && !bubble.showInShadeWhenBubble()) { 710 // The bubble is gone & the notification is gone, time to actually remove it 711 mNotificationEntryManager.performRemoveNotification( 712 bubble.getEntry().notification, UNDEFINED_DISMISS_REASON); 713 } else { 714 // Update the flag for SysUI 715 bubble.getEntry().notification.getNotification().flags &= ~FLAG_BUBBLE; 716 717 // Make sure NoMan knows it's not a bubble anymore so anyone querying it 718 // will get right result back 719 try { 720 mBarService.onNotificationBubbleChanged(bubble.getKey(), 721 false /* isBubble */); 722 } catch (RemoteException e) { 723 // Bad things have happened 724 } 725 } 726 727 // Check if removed bubble has an associated suppressed group summary that needs 728 // to be removed now. 729 final String groupKey = bubble.getEntry().notification.getGroupKey(); 730 if (mBubbleData.isSummarySuppressed(groupKey) 731 && mBubbleData.getBubblesInGroup(groupKey).isEmpty()) { 732 // Time to actually remove the summary. 733 String notifKey = mBubbleData.getSummaryKey(groupKey); 734 mBubbleData.removeSuppressedSummary(groupKey); 735 NotificationEntry entry = 736 mNotificationEntryManager.getNotificationData().get(notifKey); 737 mNotificationEntryManager.performRemoveNotification( 738 entry.notification, UNDEFINED_DISMISS_REASON); 739 } 740 741 // Check if summary should be removed from NoManGroup 742 NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary( 743 bubble.getEntry().notification); 744 if (summary != null) { 745 ArrayList<NotificationEntry> summaryChildren = 746 mNotificationGroupManager.getLogicalChildren(summary.notification); 747 boolean isSummaryThisNotif = summary.key.equals(bubble.getEntry().key); 748 if (!isSummaryThisNotif 749 && (summaryChildren == null || summaryChildren.isEmpty())) { 750 mNotificationEntryManager.performRemoveNotification( 751 summary.notification, UNDEFINED_DISMISS_REASON); 752 } 753 } 754 } 755 } 756 757 if (update.updatedBubble != null) { 758 mStackView.updateBubble(update.updatedBubble); 759 } 760 761 if (update.orderChanged) { 762 mStackView.updateBubbleOrder(update.bubbles); 763 } 764 765 if (update.selectionChanged) { 766 mStackView.setSelectedBubble(update.selectedBubble); 767 if (update.selectedBubble != null) { 768 mNotificationGroupManager.updateSuppression( 769 update.selectedBubble.getEntry()); 770 } 771 } 772 773 // Expanding? Apply this last. 774 if (update.expandedChanged && update.expanded) { 775 mStackView.setExpanded(true); 776 } 777 778 mNotificationEntryManager.updateNotifications(); 779 updateStack(); 780 781 if (DEBUG_BUBBLE_CONTROLLER) { 782 Log.d(TAG, "[BubbleData]"); 783 Log.d(TAG, formatBubblesString(mBubbleData.getBubbles(), 784 mBubbleData.getSelectedBubble())); 785 786 if (mStackView != null) { 787 Log.d(TAG, "[BubbleStackView]"); 788 Log.d(TAG, formatBubblesString(mStackView.getBubblesOnScreen(), 789 mStackView.getExpandedBubble())); 790 } 791 } 792 } 793 }; 794 795 /** 796 * Lets any listeners know if bubble state has changed. 797 * Updates the visibility of the bubbles based on current state. 798 * Does not un-bubble, just hides or un-hides. Notifies any 799 * {@link BubbleStateChangeListener}s of visibility changes. 800 * Updates stack description for TalkBack focus. 801 */ updateStack()802 public void updateStack() { 803 if (mStackView == null) { 804 return; 805 } 806 if (mStatusBarStateListener.getCurrentState() == SHADE && hasBubbles()) { 807 // Bubbles only appear in unlocked shade 808 mStackView.setVisibility(hasBubbles() ? VISIBLE : INVISIBLE); 809 } else if (mStackView != null) { 810 mStackView.setVisibility(INVISIBLE); 811 } 812 813 // Let listeners know if bubble state changed. 814 boolean hadBubbles = mStatusBarWindowController.getBubblesShowing(); 815 boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE; 816 mStatusBarWindowController.setBubblesShowing(hasBubblesShowing); 817 if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) { 818 mStateChangeListener.onHasBubblesChanged(hasBubblesShowing); 819 } 820 821 mStackView.updateContentDescription(); 822 } 823 824 /** 825 * Rect indicating the touchable region for the bubble stack / expanded stack. 826 */ getTouchableRegion()827 public Rect getTouchableRegion() { 828 if (mStackView == null || mStackView.getVisibility() != VISIBLE) { 829 return null; 830 } 831 mStackView.getBoundsOnScreen(mTempRect); 832 return mTempRect; 833 } 834 835 /** 836 * The display id of the expanded view, if the stack is expanded and not occluded by the 837 * status bar, otherwise returns {@link Display#INVALID_DISPLAY}. 838 */ getExpandedDisplayId(Context context)839 public int getExpandedDisplayId(Context context) { 840 final Bubble bubble = getExpandedBubble(context); 841 return bubble != null ? bubble.getDisplayId() : INVALID_DISPLAY; 842 } 843 844 @Nullable getExpandedBubble(Context context)845 private Bubble getExpandedBubble(Context context) { 846 if (mStackView == null) { 847 return null; 848 } 849 final boolean defaultDisplay = context.getDisplay() != null 850 && context.getDisplay().getDisplayId() == DEFAULT_DISPLAY; 851 final Bubble expandedBubble = mStackView.getExpandedBubble(); 852 if (defaultDisplay && expandedBubble != null && isStackExpanded() 853 && !mStatusBarWindowController.getPanelExpanded()) { 854 return expandedBubble; 855 } 856 return null; 857 } 858 859 @VisibleForTesting getStackView()860 BubbleStackView getStackView() { 861 return mStackView; 862 } 863 864 /** 865 * Description of current bubble state. 866 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)867 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 868 pw.println("BubbleController state:"); 869 mBubbleData.dump(fd, pw, args); 870 pw.println(); 871 if (mStackView != null) { 872 mStackView.dump(fd, pw, args); 873 } 874 pw.println(); 875 } 876 formatBubblesString(List<Bubble> bubbles, Bubble selected)877 static String formatBubblesString(List<Bubble> bubbles, Bubble selected) { 878 StringBuilder sb = new StringBuilder(); 879 for (Bubble bubble : bubbles) { 880 if (bubble == null) { 881 sb.append(" <null> !!!!!\n"); 882 } else { 883 boolean isSelected = (bubble == selected); 884 sb.append(String.format("%s Bubble{act=%12d, ongoing=%d, key=%s}\n", 885 ((isSelected) ? "->" : " "), 886 bubble.getLastActivity(), 887 (bubble.isOngoing() ? 1 : 0), 888 bubble.getKey())); 889 } 890 } 891 return sb.toString(); 892 } 893 894 /** 895 * This task stack listener is responsible for responding to tasks moved to the front 896 * which are on the default (main) display. When this happens, expanded bubbles must be 897 * collapsed so the user may interact with the app which was just moved to the front. 898 * <p> 899 * This listener is registered with SystemUI's ActivityManagerWrapper which dispatches 900 * these calls via a main thread Handler. 901 */ 902 @MainThread 903 private class BubbleTaskStackListener extends TaskStackChangeListener { 904 905 @Override onTaskMovedToFront(RunningTaskInfo taskInfo)906 public void onTaskMovedToFront(RunningTaskInfo taskInfo) { 907 if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) { 908 if (!mStackView.isExpansionAnimating()) { 909 mBubbleData.setExpanded(false); 910 } 911 } 912 } 913 914 @Override onActivityLaunchOnSecondaryDisplayRerouted()915 public void onActivityLaunchOnSecondaryDisplayRerouted() { 916 if (mStackView != null) { 917 mBubbleData.setExpanded(false); 918 } 919 } 920 921 @Override onBackPressedOnTaskRoot(RunningTaskInfo taskInfo)922 public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { 923 if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) { 924 mBubbleData.setExpanded(false); 925 } 926 } 927 928 @Override onSingleTaskDisplayDrawn(int displayId)929 public void onSingleTaskDisplayDrawn(int displayId) { 930 final Bubble expandedBubble = mStackView != null 931 ? mStackView.getExpandedBubble() 932 : null; 933 if (expandedBubble != null && expandedBubble.getDisplayId() == displayId) { 934 expandedBubble.setContentVisibility(true); 935 } 936 } 937 938 @Override onSingleTaskDisplayEmpty(int displayId)939 public void onSingleTaskDisplayEmpty(int displayId) { 940 final Bubble expandedBubble = mStackView != null 941 ? mStackView.getExpandedBubble() 942 : null; 943 int expandedId = expandedBubble != null ? expandedBubble.getDisplayId() : -1; 944 if (mStackView != null && mStackView.isExpanded() && expandedId == displayId) { 945 mBubbleData.setExpanded(false); 946 } 947 mBubbleData.notifyDisplayEmpty(displayId); 948 } 949 } 950 areBubblesEnabled(Context context)951 private static boolean areBubblesEnabled(Context context) { 952 return Settings.Secure.getInt(context.getContentResolver(), 953 ENABLE_BUBBLES, 1) != 0; 954 } 955 956 /** 957 * Whether an intent is properly configured to display in an {@link android.app.ActivityView}. 958 * 959 * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically 960 * that should filter out any invalid bubbles, but should protect SysUI side just in case. 961 * 962 * @param context the context to use. 963 * @param entry the entry to bubble. 964 */ canLaunchInActivityView(Context context, NotificationEntry entry)965 static boolean canLaunchInActivityView(Context context, NotificationEntry entry) { 966 PendingIntent intent = entry.getBubbleMetadata() != null 967 ? entry.getBubbleMetadata().getIntent() 968 : null; 969 if (intent == null) { 970 Log.w(TAG, "Unable to create bubble -- no intent"); 971 return false; 972 } 973 ActivityInfo info = 974 intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0); 975 if (info == null) { 976 Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: " 977 + intent); 978 return false; 979 } 980 if (!ActivityInfo.isResizeableMode(info.resizeMode)) { 981 Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: " 982 + intent); 983 return false; 984 } 985 if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) { 986 Log.w(TAG, "Unable to send as bubble -- activity is not documentLaunchMode=always " 987 + "for intent: " + intent); 988 return false; 989 } 990 if ((info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) == 0) { 991 Log.w(TAG, "Unable to send as bubble -- activity is not embeddable for intent: " 992 + intent); 993 return false; 994 } 995 return true; 996 } 997 998 /** PinnedStackListener that dispatches IME visibility updates to the stack. */ 999 private class BubblesImeListener extends IPinnedStackListener.Stub { 1000 1001 @Override onListenerRegistered(IPinnedStackController controller)1002 public void onListenerRegistered(IPinnedStackController controller) throws RemoteException { 1003 } 1004 1005 @Override onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)1006 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, 1007 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, 1008 int displayRotation) throws RemoteException {} 1009 1010 @Override onImeVisibilityChanged(boolean imeVisible, int imeHeight)1011 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 1012 if (mStackView != null && mStackView.getBubbleCount() > 0) { 1013 mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight)); 1014 } 1015 } 1016 1017 @Override onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)1018 public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) 1019 throws RemoteException {} 1020 1021 @Override onMinimizedStateChanged(boolean isMinimized)1022 public void onMinimizedStateChanged(boolean isMinimized) throws RemoteException {} 1023 1024 @Override onActionsChanged(ParceledListSlice actions)1025 public void onActionsChanged(ParceledListSlice actions) throws RemoteException {} 1026 } 1027 } 1028