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 */ onHasBubblesChanged(boolean hasBubbles)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 */ onBubbleExpandChanged(boolean isExpanding, String key)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 */ getCurrentState()195 public int getCurrentState() { 196 return mState; 197 } 198 199 @Override onStateChanged(int newState)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 BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController)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 BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController)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 */ ensureStackViewCreated()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 onUiModeChanged()288 public void onUiModeChanged() { 289 if (mStackView != null) { 290 mStackView.onThemeChanged(); 291 } 292 } 293 294 @Override onOverlayChanged()295 public void onOverlayChanged() { 296 if (mStackView != null) { 297 mStackView.onThemeChanged(); 298 } 299 } 300 301 @Override onConfigChanged(Configuration newConfig)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 */ setBubbleStateChangeListener(BubbleStateChangeListener listener)312 public void setBubbleStateChangeListener(BubbleStateChangeListener listener) { 313 mStateChangeListener = listener; 314 } 315 316 /** 317 * Set a listener to be notified of bubble expand events. 318 */ setExpandListener(BubbleExpandListener listener)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 */ hasBubbles()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 */ isStackExpanded()345 public boolean isStackExpanded() { 346 return mBubbleData.isExpanded(); 347 } 348 349 /** 350 * Tell the stack of bubbles to expand. 351 */ expandStack()352 public void expandStack() { 353 mBubbleData.setExpanded(true); 354 } 355 356 /** 357 * Tell the stack of bubbles to collapse. 358 */ collapseStack()359 public void collapseStack() { 360 mBubbleData.setExpanded(false /* expanded */); 361 } 362 selectBubble(Bubble bubble)363 void selectBubble(Bubble bubble) { 364 mBubbleData.setSelectedBubble(bubble); 365 } 366 367 @VisibleForTesting selectBubble(String key)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 */ expandStackAndSelectBubble(String notificationKey)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 */ dismissStack(@ismissReason int reason)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 */ performBackPressIfNeeded()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 */ updateBubble(NotificationEntry notif)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 removeBubble(String key, int reason)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 */ updateStackViewForZenConfig()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 */ updateStack()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 */ getTouchableRegion()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 */ getExpandedDisplayId(Context context)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 getStackView()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 shouldAutoBubbleForFlags(Context context, NotificationEntry entry)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 updateShowInShadeForSuppressNotification(NotificationEntry entry)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 formatBubblesString(List<Bubble> bubbles, Bubble selected)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 */ isForegroundApp(Context context, String pkgName)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 onTaskMovedToFront(RunningTaskInfo taskInfo)777 public void onTaskMovedToFront(RunningTaskInfo taskInfo) { 778 if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) { 779 mBubbleData.setExpanded(false); 780 } 781 } 782 783 @Override onActivityLaunchOnSecondaryDisplayRerouted()784 public void onActivityLaunchOnSecondaryDisplayRerouted() { 785 if (mStackView != null) { 786 mBubbleData.setExpanded(false); 787 } 788 } 789 790 @Override onBackPressedOnTaskRoot(RunningTaskInfo taskInfo)791 public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { 792 if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) { 793 mBubbleData.setExpanded(false); 794 } 795 } 796 } 797 shouldAutoBubbleMessages(Context context)798 private static boolean shouldAutoBubbleMessages(Context context) { 799 return Settings.Secure.getInt(context.getContentResolver(), 800 ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0; 801 } 802 shouldAutoBubbleOngoing(Context context)803 private static boolean shouldAutoBubbleOngoing(Context context) { 804 return Settings.Secure.getInt(context.getContentResolver(), 805 ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0; 806 } 807 shouldAutoBubbleAll(Context context)808 private static boolean shouldAutoBubbleAll(Context context) { 809 return Settings.Secure.getInt(context.getContentResolver(), 810 ENABLE_AUTO_BUBBLE_ALL, 0) != 0; 811 } 812 shouldUseContentIntent(Context context)813 static boolean shouldUseContentIntent(Context context) { 814 return Settings.Secure.getInt(context.getContentResolver(), 815 ENABLE_BUBBLE_CONTENT_INTENT, 0) != 0; 816 } 817 areBubblesEnabled(Context context)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. */ getBubbleStiffness(Context context, int defaultStiffness)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. */ getBubbleBounciness(Context context, float defaultBounciness)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 */ canLaunchInActivityView(Context context, NotificationEntry entry)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 onListenerRegistered(IPinnedStackController controller)883 public void onListenerRegistered(IPinnedStackController controller) throws RemoteException { 884 } 885 886 @Override onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)887 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, 888 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, 889 int displayRotation) throws RemoteException {} 890 891 @Override onImeVisibilityChanged(boolean imeVisible, int imeHeight)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 onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)899 public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) 900 throws RemoteException {} 901 902 @Override onMinimizedStateChanged(boolean isMinimized)903 public void onMinimizedStateChanged(boolean isMinimized) throws RemoteException {} 904 905 @Override onActionsChanged(ParceledListSlice actions)906 public void onActionsChanged(ParceledListSlice actions) throws RemoteException {} 907 } 908 } 909