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.Policy.SUPPRESSED_EFFECT_BADGE; 21 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; 22 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; 23 import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS; 24 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; 25 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; 26 import static android.service.notification.NotificationListenerService.REASON_CANCEL; 27 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; 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.statusbar.StatusBarState.SHADE; 35 import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; 36 37 import static java.lang.annotation.ElementType.FIELD; 38 import static java.lang.annotation.ElementType.LOCAL_VARIABLE; 39 import static java.lang.annotation.ElementType.PARAMETER; 40 import static java.lang.annotation.RetentionPolicy.SOURCE; 41 42 import android.app.ActivityManager; 43 import android.app.ActivityManager.RunningTaskInfo; 44 import android.app.Notification; 45 import android.app.NotificationManager; 46 import android.app.PendingIntent; 47 import android.content.Context; 48 import android.content.pm.ActivityInfo; 49 import android.content.pm.ParceledListSlice; 50 import android.content.res.Configuration; 51 import android.graphics.Rect; 52 import android.os.RemoteException; 53 import android.os.ServiceManager; 54 import android.provider.Settings; 55 import android.service.notification.NotificationListenerService.RankingMap; 56 import android.service.notification.StatusBarNotification; 57 import android.service.notification.ZenModeConfig; 58 import android.util.Log; 59 import android.util.Pair; 60 import android.view.Display; 61 import android.view.IPinnedStackController; 62 import android.view.IPinnedStackListener; 63 import android.view.ViewGroup; 64 import android.widget.FrameLayout; 65 66 import androidx.annotation.IntDef; 67 import androidx.annotation.MainThread; 68 import androidx.annotation.Nullable; 69 70 import com.android.internal.annotations.VisibleForTesting; 71 import com.android.internal.statusbar.IStatusBarService; 72 import com.android.systemui.Dependency; 73 import com.android.systemui.R; 74 import com.android.systemui.plugins.statusbar.StatusBarStateController; 75 import com.android.systemui.shared.system.ActivityManagerWrapper; 76 import com.android.systemui.shared.system.TaskStackChangeListener; 77 import com.android.systemui.shared.system.WindowManagerWrapper; 78 import com.android.systemui.statusbar.NotificationRemoveInterceptor; 79 import com.android.systemui.statusbar.notification.NotificationEntryListener; 80 import com.android.systemui.statusbar.notification.NotificationEntryManager; 81 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; 82 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 83 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; 84 import com.android.systemui.statusbar.phone.StatusBarWindowController; 85 import com.android.systemui.statusbar.policy.ConfigurationController; 86 import com.android.systemui.statusbar.policy.ZenModeController; 87 88 import java.lang.annotation.Retention; 89 import java.lang.annotation.Target; 90 import java.util.List; 91 92 import javax.inject.Inject; 93 import javax.inject.Singleton; 94 95 /** 96 * Bubbles are a special type of content that can "float" on top of other apps or System UI. 97 * Bubbles can be expanded to show more content. 98 * 99 * The controller manages addition, removal, and visible state of bubbles on screen. 100 */ 101 @Singleton 102 public class BubbleController implements ConfigurationController.ConfigurationListener { 103 104 private static final String TAG = "BubbleController"; 105 private static final boolean DEBUG = false; 106 107 @Retention(SOURCE) 108 @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, 109 DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE}) 110 @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) 111 @interface DismissReason {} 112 113 static final int DISMISS_USER_GESTURE = 1; 114 static final int DISMISS_AGED = 2; 115 static final int DISMISS_TASK_FINISHED = 3; 116 static final int DISMISS_BLOCKED = 4; 117 static final int DISMISS_NOTIF_CANCEL = 5; 118 static final int DISMISS_ACCESSIBILITY_ACTION = 6; 119 static final int DISMISS_NO_LONGER_BUBBLE = 7; 120 121 public static final int MAX_BUBBLES = 5; // TODO: actually enforce this 122 123 // Enables some subset of notifs to automatically become bubbles 124 public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false; 125 126 /** Flag to enable or disable the entire feature */ 127 private static final String ENABLE_BUBBLES = "experiment_enable_bubbles"; 128 /** Auto bubble flags set whether different notif types should be presented as a bubble */ 129 private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging"; 130 private static final String ENABLE_AUTO_BUBBLE_ONGOING = "experiment_autobubble_ongoing"; 131 private static final String ENABLE_AUTO_BUBBLE_ALL = "experiment_autobubble_all"; 132 133 /** Use an activityView for an auto-bubbled notifs if it has an appropriate content intent */ 134 private static final String ENABLE_BUBBLE_CONTENT_INTENT = "experiment_bubble_content_intent"; 135 136 private static final String BUBBLE_STIFFNESS = "experiment_bubble_stiffness"; 137 private static final String BUBBLE_BOUNCINESS = "experiment_bubble_bounciness"; 138 139 private final Context mContext; 140 private final NotificationEntryManager mNotificationEntryManager; 141 private final BubbleTaskStackListener mTaskStackListener; 142 private BubbleStateChangeListener mStateChangeListener; 143 private BubbleExpandListener mExpandListener; 144 @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; 145 146 private BubbleData mBubbleData; 147 @Nullable private BubbleStackView mStackView; 148 149 // Bubbles get added to the status bar view 150 private final StatusBarWindowController mStatusBarWindowController; 151 private final ZenModeController mZenModeController; 152 private StatusBarStateListener mStatusBarStateListener; 153 154 private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider; 155 private IStatusBarService mBarService; 156 157 // Used for determining view rect for touch interaction 158 private Rect mTempRect = new Rect(); 159 160 /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */ 161 private int mOrientation = Configuration.ORIENTATION_UNDEFINED; 162 163 /** 164 * Listener to be notified when some states of the bubbles change. 165 */ 166 public interface BubbleStateChangeListener { 167 /** 168 * Called when the stack has bubbles or no longer has bubbles. 169 */ 170 void onHasBubblesChanged(boolean hasBubbles); 171 } 172 173 /** 174 * Listener to find out about stack expansion / collapse events. 175 */ 176 public interface BubbleExpandListener { 177 /** 178 * Called when the expansion state of the bubble stack changes. 179 * 180 * @param isExpanding whether it's expanding or collapsing 181 * @param key the notification key associated with bubble being expanded 182 */ 183 void onBubbleExpandChanged(boolean isExpanding, String key); 184 } 185 186 /** 187 * Listens for the current state of the status bar and updates the visibility state 188 * of bubbles as needed. 189 */ 190 private class StatusBarStateListener implements StatusBarStateController.StateListener { 191 private int mState; 192 /** 193 * Returns the current status bar state. 194 */ 195 public int getCurrentState() { 196 return mState; 197 } 198 199 @Override 200 public void onStateChanged(int newState) { 201 mState = newState; 202 boolean shouldCollapse = (mState != SHADE); 203 if (shouldCollapse) { 204 collapseStack(); 205 } 206 updateStack(); 207 } 208 } 209 210 @Inject 211 public BubbleController(Context context, StatusBarWindowController statusBarWindowController, 212 BubbleData data, ConfigurationController configurationController, 213 NotificationInterruptionStateProvider interruptionStateProvider, 214 ZenModeController zenModeController) { 215 this(context, statusBarWindowController, data, null /* synchronizer */, 216 configurationController, interruptionStateProvider, zenModeController); 217 } 218 219 public BubbleController(Context context, StatusBarWindowController statusBarWindowController, 220 BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, 221 ConfigurationController configurationController, 222 NotificationInterruptionStateProvider interruptionStateProvider, 223 ZenModeController zenModeController) { 224 mContext = context; 225 mNotificationInterruptionStateProvider = interruptionStateProvider; 226 mZenModeController = zenModeController; 227 mZenModeController.addCallback(new ZenModeController.Callback() { 228 @Override 229 public void onZenChanged(int zen) { 230 updateStackViewForZenConfig(); 231 } 232 233 @Override 234 public void onConfigChanged(ZenModeConfig config) { 235 updateStackViewForZenConfig(); 236 } 237 }); 238 239 configurationController.addCallback(this /* configurationListener */); 240 241 mBubbleData = data; 242 mBubbleData.setListener(mBubbleDataListener); 243 244 mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); 245 mNotificationEntryManager.addNotificationEntryListener(mEntryListener); 246 mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor); 247 248 mStatusBarWindowController = statusBarWindowController; 249 mStatusBarStateListener = new StatusBarStateListener(); 250 Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener); 251 252 mTaskStackListener = new BubbleTaskStackListener(); 253 ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); 254 255 try { 256 WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener()); 257 } catch (RemoteException e) { 258 e.printStackTrace(); 259 } 260 mSurfaceSynchronizer = synchronizer; 261 262 mBarService = IStatusBarService.Stub.asInterface( 263 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 264 } 265 266 /** 267 * BubbleStackView is lazily created by this method the first time a Bubble is added. This 268 * method initializes the stack view and adds it to the StatusBar just above the scrim. 269 */ 270 private void ensureStackViewCreated() { 271 if (mStackView == null) { 272 mStackView = new BubbleStackView(mContext, mBubbleData, mSurfaceSynchronizer); 273 ViewGroup sbv = mStatusBarWindowController.getStatusBarView(); 274 // TODO(b/130237686): When you expand the shade on top of expanded bubble, there is no 275 // scrim between bubble and the shade 276 int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1; 277 sbv.addView(mStackView, bubblePosition, 278 new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 279 if (mExpandListener != null) { 280 mStackView.setExpandListener(mExpandListener); 281 } 282 283 updateStackViewForZenConfig(); 284 } 285 } 286 287 @Override 288 public void onUiModeChanged() { 289 if (mStackView != null) { 290 mStackView.onThemeChanged(); 291 } 292 } 293 294 @Override 295 public void onOverlayChanged() { 296 if (mStackView != null) { 297 mStackView.onThemeChanged(); 298 } 299 } 300 301 @Override 302 public void onConfigChanged(Configuration newConfig) { 303 if (mStackView != null && newConfig != null && newConfig.orientation != mOrientation) { 304 mStackView.onOrientationChanged(); 305 mOrientation = newConfig.orientation; 306 } 307 } 308 309 /** 310 * Set a listener to be notified when some states of the bubbles change. 311 */ 312 public void setBubbleStateChangeListener(BubbleStateChangeListener listener) { 313 mStateChangeListener = listener; 314 } 315 316 /** 317 * Set a listener to be notified of bubble expand events. 318 */ 319 public void setExpandListener(BubbleExpandListener listener) { 320 mExpandListener = ((isExpanding, key) -> { 321 if (listener != null) { 322 listener.onBubbleExpandChanged(isExpanding, key); 323 } 324 mStatusBarWindowController.setBubbleExpanded(isExpanding); 325 }); 326 if (mStackView != null) { 327 mStackView.setExpandListener(mExpandListener); 328 } 329 } 330 331 /** 332 * Whether or not there are bubbles present, regardless of them being visible on the 333 * screen (e.g. if on AOD). 334 */ 335 public boolean hasBubbles() { 336 if (mStackView == null) { 337 return false; 338 } 339 return mBubbleData.hasBubbles(); 340 } 341 342 /** 343 * Whether the stack of bubbles is expanded or not. 344 */ 345 public boolean isStackExpanded() { 346 return mBubbleData.isExpanded(); 347 } 348 349 /** 350 * Tell the stack of bubbles to expand. 351 */ 352 public void expandStack() { 353 mBubbleData.setExpanded(true); 354 } 355 356 /** 357 * Tell the stack of bubbles to collapse. 358 */ 359 public void collapseStack() { 360 mBubbleData.setExpanded(false /* expanded */); 361 } 362 363 void selectBubble(Bubble bubble) { 364 mBubbleData.setSelectedBubble(bubble); 365 } 366 367 @VisibleForTesting 368 void selectBubble(String key) { 369 Bubble bubble = mBubbleData.getBubbleWithKey(key); 370 selectBubble(bubble); 371 } 372 373 /** 374 * Request the stack expand if needed, then select the specified Bubble as current. 375 * 376 * @param notificationKey the notification key for the bubble to be selected 377 */ 378 public void expandStackAndSelectBubble(String notificationKey) { 379 Bubble bubble = mBubbleData.getBubbleWithKey(notificationKey); 380 if (bubble != null) { 381 mBubbleData.setSelectedBubble(bubble); 382 mBubbleData.setExpanded(true); 383 } 384 } 385 386 /** 387 * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack. 388 */ 389 void dismissStack(@DismissReason int reason) { 390 mBubbleData.dismissAll(reason); 391 } 392 393 /** 394 * Directs a back gesture at the bubble stack. When opened, the current expanded bubble 395 * is forwarded a back key down/up pair. 396 */ 397 public void performBackPressIfNeeded() { 398 if (mStackView != null) { 399 mStackView.performBackPressIfNeeded(); 400 } 401 } 402 403 /** 404 * Adds or updates a bubble associated with the provided notification entry. 405 * 406 * @param notif the notification associated with this bubble. 407 */ 408 void updateBubble(NotificationEntry notif) { 409 // If this is an interruptive notif, mark that it's interrupted 410 if (notif.importance >= NotificationManager.IMPORTANCE_HIGH) { 411 notif.setInterruption(); 412 } 413 mBubbleData.notificationEntryUpdated(notif); 414 } 415 416 /** 417 * Removes the bubble associated with the {@param uri}. 418 * <p> 419 * Must be called from the main thread. 420 */ 421 @MainThread 422 void removeBubble(String key, int reason) { 423 // TEMP: refactor to change this to pass entry 424 Bubble bubble = mBubbleData.getBubbleWithKey(key); 425 if (bubble != null) { 426 mBubbleData.notificationEntryRemoved(bubble.entry, reason); 427 } 428 } 429 430 @SuppressWarnings("FieldCanBeLocal") 431 private final NotificationRemoveInterceptor mRemoveInterceptor = 432 new NotificationRemoveInterceptor() { 433 @Override 434 public boolean onNotificationRemoveRequested(String key, int reason) { 435 if (!mBubbleData.hasBubbleWithKey(key)) { 436 return false; 437 } 438 NotificationEntry entry = mBubbleData.getBubbleWithKey(key).entry; 439 440 final boolean isClearAll = reason == REASON_CANCEL_ALL; 441 final boolean isUserDimiss = reason == REASON_CANCEL; 442 final boolean isAppCancel = reason == REASON_APP_CANCEL 443 || reason == REASON_APP_CANCEL_ALL; 444 445 // Need to check for !appCancel here because the notification may have 446 // previously been dismissed & entry.isRowDismissed would still be true 447 boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel) 448 || isClearAll || isUserDimiss; 449 450 // The bubble notification sticks around in the data as long as the bubble is 451 // not dismissed and the app hasn't cancelled the notification. 452 boolean bubbleExtended = entry.isBubble() && !entry.isBubbleDismissed() 453 && userRemovedNotif; 454 if (bubbleExtended) { 455 entry.setShowInShadeWhenBubble(false); 456 if (mStackView != null) { 457 mStackView.updateDotVisibility(entry.key); 458 } 459 mNotificationEntryManager.updateNotifications(); 460 return true; 461 } else if (!userRemovedNotif && !entry.isBubbleDismissed()) { 462 // This wasn't a user removal so we should remove the bubble as well 463 mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL); 464 return false; 465 } 466 return false; 467 } 468 }; 469 470 @SuppressWarnings("FieldCanBeLocal") 471 private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { 472 @Override 473 public void onPendingEntryAdded(NotificationEntry entry) { 474 if (!areBubblesEnabled(mContext)) { 475 return; 476 } 477 if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry) 478 && canLaunchInActivityView(mContext, entry)) { 479 updateShowInShadeForSuppressNotification(entry); 480 } 481 } 482 483 @Override 484 public void onEntryInflated(NotificationEntry entry, @InflationFlag int inflatedFlags) { 485 if (!areBubblesEnabled(mContext)) { 486 return; 487 } 488 if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry) 489 && canLaunchInActivityView(mContext, entry)) { 490 updateBubble(entry); 491 } 492 } 493 494 @Override 495 public void onPreEntryUpdated(NotificationEntry entry) { 496 if (!areBubblesEnabled(mContext)) { 497 return; 498 } 499 boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry) 500 && canLaunchInActivityView(mContext, entry); 501 if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.key)) { 502 // It was previously a bubble but no longer a bubble -- lets remove it 503 removeBubble(entry.key, DISMISS_NO_LONGER_BUBBLE); 504 } else if (shouldBubble) { 505 updateShowInShadeForSuppressNotification(entry); 506 entry.setBubbleDismissed(false); // updates come back as bubbles even if dismissed 507 updateBubble(entry); 508 } 509 } 510 511 @Override 512 public void onNotificationRankingUpdated(RankingMap rankingMap) { 513 // Forward to BubbleData to block any bubbles which should no longer be shown 514 mBubbleData.notificationRankingUpdated(rankingMap); 515 } 516 }; 517 518 @SuppressWarnings("FieldCanBeLocal") 519 private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { 520 521 @Override 522 public void applyUpdate(BubbleData.Update update) { 523 if (mStackView == null && update.addedBubble != null) { 524 // Lazy init stack view when the first bubble is added. 525 ensureStackViewCreated(); 526 } 527 528 // If not yet initialized, ignore all other changes. 529 if (mStackView == null) { 530 return; 531 } 532 533 if (update.addedBubble != null) { 534 mStackView.addBubble(update.addedBubble); 535 } 536 537 // Collapsing? Do this first before remaining steps. 538 if (update.expandedChanged && !update.expanded) { 539 mStackView.setExpanded(false); 540 } 541 542 // Do removals, if any. 543 for (Pair<Bubble, Integer> removed : update.removedBubbles) { 544 final Bubble bubble = removed.first; 545 @DismissReason final int reason = removed.second; 546 mStackView.removeBubble(bubble); 547 548 if (!mBubbleData.hasBubbleWithKey(bubble.getKey()) 549 && !bubble.entry.showInShadeWhenBubble()) { 550 // The bubble is gone & the notification is gone, time to actually remove it 551 mNotificationEntryManager.performRemoveNotification(bubble.entry.notification, 552 UNDEFINED_DISMISS_REASON); 553 } else { 554 // Update the flag for SysUI 555 bubble.entry.notification.getNotification().flags &= ~FLAG_BUBBLE; 556 557 // Make sure NoMan knows it's not a bubble anymore so anyone querying it will 558 // get right result back 559 try { 560 mBarService.onNotificationBubbleChanged(bubble.getKey(), 561 false /* isBubble */); 562 } catch (RemoteException e) { 563 // Bad things have happened 564 } 565 } 566 } 567 568 if (update.updatedBubble != null) { 569 mStackView.updateBubble(update.updatedBubble); 570 } 571 572 if (update.orderChanged) { 573 mStackView.updateBubbleOrder(update.bubbles); 574 } 575 576 if (update.selectionChanged) { 577 mStackView.setSelectedBubble(update.selectedBubble); 578 } 579 580 // Expanding? Apply this last. 581 if (update.expandedChanged && update.expanded) { 582 mStackView.setExpanded(true); 583 } 584 585 mNotificationEntryManager.updateNotifications(); 586 updateStack(); 587 588 if (DEBUG) { 589 Log.d(TAG, "[BubbleData]"); 590 Log.d(TAG, formatBubblesString(mBubbleData.getBubbles(), 591 mBubbleData.getSelectedBubble())); 592 593 if (mStackView != null) { 594 Log.d(TAG, "[BubbleStackView]"); 595 Log.d(TAG, formatBubblesString(mStackView.getBubblesOnScreen(), 596 mStackView.getExpandedBubble())); 597 } 598 } 599 } 600 }; 601 602 /** 603 * Updates the stack view's suppression flags from the latest config from the zen (do not 604 * disturb) controller. 605 */ 606 private void updateStackViewForZenConfig() { 607 final ZenModeConfig zenModeConfig = mZenModeController.getConfig(); 608 609 if (zenModeConfig == null || mStackView == null) { 610 return; 611 } 612 613 final int suppressedEffects = zenModeConfig.suppressedVisualEffects; 614 final boolean hideNotificationDotsSelected = 615 (suppressedEffects & SUPPRESSED_EFFECT_BADGE) != 0; 616 final boolean dontPopNotifsOnScreenSelected = 617 (suppressedEffects & SUPPRESSED_EFFECT_PEEK) != 0; 618 final boolean hideFromPullDownShadeSelected = 619 (suppressedEffects & SUPPRESSED_EFFECT_NOTIFICATION_LIST) != 0; 620 621 final boolean dndEnabled = mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF; 622 623 mStackView.setSuppressNewDot( 624 dndEnabled && hideNotificationDotsSelected); 625 mStackView.setSuppressFlyout( 626 dndEnabled && (dontPopNotifsOnScreenSelected 627 || hideFromPullDownShadeSelected)); 628 } 629 630 /** 631 * Lets any listeners know if bubble state has changed. 632 * Updates the visibility of the bubbles based on current state. 633 * Does not un-bubble, just hides or un-hides. Notifies any 634 * {@link BubbleStateChangeListener}s of visibility changes. 635 * Updates stack description for TalkBack focus. 636 */ 637 public void updateStack() { 638 if (mStackView == null) { 639 return; 640 } 641 if (mStatusBarStateListener.getCurrentState() == SHADE && hasBubbles()) { 642 // Bubbles only appear in unlocked shade 643 mStackView.setVisibility(hasBubbles() ? VISIBLE : INVISIBLE); 644 } else if (mStackView != null) { 645 mStackView.setVisibility(INVISIBLE); 646 } 647 648 // Let listeners know if bubble state changed. 649 boolean hadBubbles = mStatusBarWindowController.getBubblesShowing(); 650 boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE; 651 mStatusBarWindowController.setBubblesShowing(hasBubblesShowing); 652 if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) { 653 mStateChangeListener.onHasBubblesChanged(hasBubblesShowing); 654 } 655 656 mStackView.updateContentDescription(); 657 } 658 659 /** 660 * Rect indicating the touchable region for the bubble stack / expanded stack. 661 */ 662 public Rect getTouchableRegion() { 663 if (mStackView == null || mStackView.getVisibility() != VISIBLE) { 664 return null; 665 } 666 mStackView.getBoundsOnScreen(mTempRect); 667 return mTempRect; 668 } 669 670 /** 671 * The display id of the expanded view, if the stack is expanded and not occluded by the 672 * status bar, otherwise returns {@link Display#INVALID_DISPLAY}. 673 */ 674 public int getExpandedDisplayId(Context context) { 675 if (mStackView == null) { 676 return INVALID_DISPLAY; 677 } 678 boolean defaultDisplay = context.getDisplay() != null 679 && context.getDisplay().getDisplayId() == DEFAULT_DISPLAY; 680 Bubble b = mStackView.getExpandedBubble(); 681 if (defaultDisplay && b != null && isStackExpanded() 682 && !mStatusBarWindowController.getPanelExpanded()) { 683 return b.expandedView.getVirtualDisplayId(); 684 } 685 return INVALID_DISPLAY; 686 } 687 688 @VisibleForTesting 689 BubbleStackView getStackView() { 690 return mStackView; 691 } 692 693 /** 694 * Whether the notification should automatically bubble or not. Gated by secure settings flags. 695 */ 696 @VisibleForTesting 697 protected boolean shouldAutoBubbleForFlags(Context context, NotificationEntry entry) { 698 if (entry.isBubbleDismissed()) { 699 return false; 700 } 701 StatusBarNotification n = entry.notification; 702 703 boolean autoBubbleMessages = shouldAutoBubbleMessages(context) || DEBUG_ENABLE_AUTO_BUBBLE; 704 boolean autoBubbleOngoing = shouldAutoBubbleOngoing(context) || DEBUG_ENABLE_AUTO_BUBBLE; 705 boolean autoBubbleAll = shouldAutoBubbleAll(context) || DEBUG_ENABLE_AUTO_BUBBLE; 706 707 boolean hasRemoteInput = false; 708 if (n.getNotification().actions != null) { 709 for (Notification.Action action : n.getNotification().actions) { 710 if (action.getRemoteInputs() != null) { 711 hasRemoteInput = true; 712 break; 713 } 714 } 715 } 716 boolean isCall = Notification.CATEGORY_CALL.equals(n.getNotification().category) 717 && n.isOngoing(); 718 boolean isMusic = n.getNotification().hasMediaSession(); 719 boolean isImportantOngoing = isMusic || isCall; 720 721 Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle(); 722 boolean isMessageType = Notification.CATEGORY_MESSAGE.equals(n.getNotification().category); 723 boolean isMessageStyle = Notification.MessagingStyle.class.equals(style); 724 return (((isMessageType && hasRemoteInput) || isMessageStyle) && autoBubbleMessages) 725 || (isImportantOngoing && autoBubbleOngoing) 726 || autoBubbleAll; 727 } 728 729 private void updateShowInShadeForSuppressNotification(NotificationEntry entry) { 730 boolean suppressNotification = entry.getBubbleMetadata() != null 731 && entry.getBubbleMetadata().isNotificationSuppressed() 732 && isForegroundApp(mContext, entry.notification.getPackageName()); 733 entry.setShowInShadeWhenBubble(!suppressNotification); 734 } 735 736 static String formatBubblesString(List<Bubble> bubbles, Bubble selected) { 737 StringBuilder sb = new StringBuilder(); 738 for (Bubble bubble : bubbles) { 739 if (bubble == null) { 740 sb.append(" <null> !!!!!\n"); 741 } else { 742 boolean isSelected = (bubble == selected); 743 sb.append(String.format("%s Bubble{act=%12d, ongoing=%d, key=%s}\n", 744 ((isSelected) ? "->" : " "), 745 bubble.getLastActivity(), 746 (bubble.isOngoing() ? 1 : 0), 747 bubble.getKey())); 748 } 749 } 750 return sb.toString(); 751 } 752 753 /** 754 * Return true if the applications with the package name is running in foreground. 755 * 756 * @param context application context. 757 * @param pkgName application package name. 758 */ 759 public static boolean isForegroundApp(Context context, String pkgName) { 760 ActivityManager am = context.getSystemService(ActivityManager.class); 761 List<RunningTaskInfo> tasks = am.getRunningTasks(1 /* maxNum */); 762 return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName()); 763 } 764 765 /** 766 * This task stack listener is responsible for responding to tasks moved to the front 767 * which are on the default (main) display. When this happens, expanded bubbles must be 768 * collapsed so the user may interact with the app which was just moved to the front. 769 * <p> 770 * This listener is registered with SystemUI's ActivityManagerWrapper which dispatches 771 * these calls via a main thread Handler. 772 */ 773 @MainThread 774 private class BubbleTaskStackListener extends TaskStackChangeListener { 775 776 @Override 777 public void onTaskMovedToFront(RunningTaskInfo taskInfo) { 778 if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) { 779 mBubbleData.setExpanded(false); 780 } 781 } 782 783 @Override 784 public void onActivityLaunchOnSecondaryDisplayRerouted() { 785 if (mStackView != null) { 786 mBubbleData.setExpanded(false); 787 } 788 } 789 790 @Override 791 public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { 792 if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) { 793 mBubbleData.setExpanded(false); 794 } 795 } 796 } 797 798 private static boolean shouldAutoBubbleMessages(Context context) { 799 return Settings.Secure.getInt(context.getContentResolver(), 800 ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0; 801 } 802 803 private static boolean shouldAutoBubbleOngoing(Context context) { 804 return Settings.Secure.getInt(context.getContentResolver(), 805 ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0; 806 } 807 808 private static boolean shouldAutoBubbleAll(Context context) { 809 return Settings.Secure.getInt(context.getContentResolver(), 810 ENABLE_AUTO_BUBBLE_ALL, 0) != 0; 811 } 812 813 static boolean shouldUseContentIntent(Context context) { 814 return Settings.Secure.getInt(context.getContentResolver(), 815 ENABLE_BUBBLE_CONTENT_INTENT, 0) != 0; 816 } 817 818 private static boolean areBubblesEnabled(Context context) { 819 return Settings.Secure.getInt(context.getContentResolver(), 820 ENABLE_BUBBLES, 1) != 0; 821 } 822 823 /** Default stiffness to use for bubble physics animations. */ 824 public static int getBubbleStiffness(Context context, int defaultStiffness) { 825 return Settings.Secure.getInt( 826 context.getContentResolver(), BUBBLE_STIFFNESS, defaultStiffness); 827 } 828 829 /** Default bounciness/damping ratio to use for bubble physics animations. */ 830 public static float getBubbleBounciness(Context context, float defaultBounciness) { 831 return Settings.Secure.getInt( 832 context.getContentResolver(), 833 BUBBLE_BOUNCINESS, 834 (int) (defaultBounciness * 100)) / 100f; 835 } 836 837 /** 838 * Whether an intent is properly configured to display in an {@link android.app.ActivityView}. 839 * 840 * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically 841 * that should filter out any invalid bubbles, but should protect SysUI side just in case. 842 * 843 * @param context the context to use. 844 * @param entry the entry to bubble. 845 */ 846 static boolean canLaunchInActivityView(Context context, NotificationEntry entry) { 847 PendingIntent intent = entry.getBubbleMetadata() != null 848 ? entry.getBubbleMetadata().getIntent() 849 : null; 850 if (intent == null) { 851 Log.w(TAG, "Unable to create bubble -- no intent"); 852 return false; 853 } 854 ActivityInfo info = 855 intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0); 856 if (info == null) { 857 Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: " 858 + intent); 859 return false; 860 } 861 if (!ActivityInfo.isResizeableMode(info.resizeMode)) { 862 Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: " 863 + intent); 864 return false; 865 } 866 if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) { 867 Log.w(TAG, "Unable to send as bubble -- activity is not documentLaunchMode=always " 868 + "for intent: " + intent); 869 return false; 870 } 871 if ((info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) == 0) { 872 Log.w(TAG, "Unable to send as bubble -- activity is not embeddable for intent: " 873 + intent); 874 return false; 875 } 876 return true; 877 } 878 879 /** PinnedStackListener that dispatches IME visibility updates to the stack. */ 880 private class BubblesImeListener extends IPinnedStackListener.Stub { 881 882 @Override 883 public void onListenerRegistered(IPinnedStackController controller) throws RemoteException { 884 } 885 886 @Override 887 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, 888 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, 889 int displayRotation) throws RemoteException {} 890 891 @Override 892 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 893 if (mStackView != null && mStackView.getBubbleCount() > 0) { 894 mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight)); 895 } 896 } 897 898 @Override 899 public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) 900 throws RemoteException {} 901 902 @Override 903 public void onMinimizedStateChanged(boolean isMinimized) throws RemoteException {} 904 905 @Override 906 public void onActionsChanged(ParceledListSlice actions) throws RemoteException {} 907 } 908 } 909