1 /* 2 * Copyright (C) 2015 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.statusbar.notification.headsup; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Notification; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.database.ContentObserver; 25 import android.graphics.Region; 26 import android.os.Handler; 27 import android.util.ArrayMap; 28 import android.util.ArraySet; 29 import android.util.Log; 30 import android.util.Pools; 31 import android.view.accessibility.AccessibilityEvent; 32 import android.view.accessibility.AccessibilityManager; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.logging.MetricsLogger; 36 import com.android.internal.logging.UiEvent; 37 import com.android.internal.logging.UiEventLogger; 38 import com.android.internal.policy.SystemBarUtils; 39 import com.android.systemui.EventLogTags; 40 import com.android.systemui.dagger.SysUISingleton; 41 import com.android.systemui.dagger.qualifiers.Main; 42 import com.android.systemui.plugins.statusbar.StatusBarStateController; 43 import com.android.systemui.res.R; 44 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 45 import com.android.systemui.shade.ShadeDisplayAware; 46 import com.android.systemui.shade.domain.interactor.ShadeInteractor; 47 import com.android.systemui.statusbar.StatusBarState; 48 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; 49 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 50 import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator; 51 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; 52 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener; 53 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; 54 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; 55 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; 56 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; 57 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 58 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 59 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; 60 import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; 61 import com.android.systemui.statusbar.phone.KeyguardBypassController; 62 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; 63 import com.android.systemui.statusbar.policy.ConfigurationController; 64 import com.android.systemui.util.ListenerSet; 65 import com.android.systemui.util.concurrency.DelayableExecutor; 66 import com.android.systemui.util.kotlin.JavaAdapter; 67 import com.android.systemui.util.settings.GlobalSettings; 68 import com.android.systemui.util.time.SystemClock; 69 70 import kotlinx.coroutines.flow.Flow; 71 import kotlinx.coroutines.flow.MutableStateFlow; 72 import kotlinx.coroutines.flow.StateFlow; 73 import kotlinx.coroutines.flow.StateFlowKt; 74 75 import org.jetbrains.annotations.NotNull; 76 77 import java.io.PrintWriter; 78 import java.util.ArrayList; 79 import java.util.HashSet; 80 import java.util.List; 81 import java.util.Objects; 82 import java.util.Set; 83 import java.util.Stack; 84 import java.util.stream.Stream; 85 86 import javax.inject.Inject; 87 88 /** 89 * A manager which handles heads up notifications which is a special mode where 90 * they simply peek from the top of the screen. 91 */ 92 @SysUISingleton 93 public class HeadsUpManagerImpl 94 implements HeadsUpManager, HeadsUpRepository, OnHeadsUpChangedListener { 95 private static final String TAG = "BaseHeadsUpManager"; 96 private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; 97 private static final String REASON_REORDER_ALLOWED = "mOnReorderingAllowedListener"; 98 private final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>(); 99 100 private final Context mContext; 101 102 private final int mTouchAcceptanceDelay; 103 private int mSnoozeLengthMs; 104 private boolean mHasPinnedNotification; 105 private PinnedStatus mPinnedNotificationStatus = PinnedStatus.NotPinned; 106 private int mUser; 107 108 private final ArrayMap<String, Long> mSnoozedPackages; 109 private final AccessibilityManagerWrapper mAccessibilityMgr; 110 111 private final UiEventLogger mUiEventLogger; 112 private AvalancheController mAvalancheController; 113 private final KeyguardBypassController mBypassController; 114 private final GroupMembershipManager mGroupMembershipManager; 115 private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); 116 private final VisualStabilityProvider mVisualStabilityProvider; 117 118 private final SystemClock mSystemClock; 119 @VisibleForTesting 120 final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); 121 private final HeadsUpManagerLogger mLogger; 122 private final int mMinimumDisplayTimeDefault; 123 private final int mMinimumDisplayTimeForUserInitiated; 124 private final int mStickyForSomeTimeAutoDismissTime; 125 private final int mAutoDismissTime; 126 private final DelayableExecutor mExecutor; 127 128 private final int mExtensionTime; 129 130 // TODO(b/328393698) move the topHeadsUpRow logic to an interactor 131 private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow = 132 StateFlowKt.MutableStateFlow(null); 133 private final MutableStateFlow<Set<HeadsUpRowRepository>> mHeadsUpNotificationRows = 134 StateFlowKt.MutableStateFlow(new HashSet<>()); 135 private final MutableStateFlow<Boolean> mHeadsUpAnimatingAway = 136 StateFlowKt.MutableStateFlow(false); 137 private final MutableStateFlow<Boolean> mTrackingHeadsUp = 138 StateFlowKt.MutableStateFlow(false); 139 private final HashSet<String> mSwipedOutKeys = new HashSet<>(); 140 private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>(); 141 @VisibleForTesting 142 final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed 143 = new ArraySet<>(); 144 145 private boolean mReleaseOnExpandFinish; 146 private boolean mIsShadeOrQsExpanded; 147 private boolean mIsQsExpanded; 148 private int mStatusBarState; 149 private AnimationStateHandler mAnimationStateHandler; 150 private int mHeadsUpInset; 151 152 // Used for determining the region for touch interaction 153 private final Region mTouchableRegion = new Region(); 154 155 private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<>() { 156 private final Stack<HeadsUpEntry> mPoolObjects = new Stack<>(); 157 158 @Override 159 public HeadsUpEntry acquire() { 160 NotificationThrottleHun.assertInLegacyMode(); 161 if (!mPoolObjects.isEmpty()) { 162 return mPoolObjects.pop(); 163 } 164 return new HeadsUpEntry(); 165 } 166 167 @Override 168 public boolean release(@NonNull HeadsUpEntry instance) { 169 NotificationThrottleHun.assertInLegacyMode(); 170 mPoolObjects.push(instance); 171 return true; 172 } 173 }; 174 175 /** 176 * Enum entry for notification peek logged from this class. 177 */ 178 enum NotificationPeekEvent implements UiEventLogger.UiEventEnum { 179 @UiEvent(doc = "Heads-up notification peeked on screen.") 180 NOTIFICATION_PEEK(801); 181 182 private final int mId; NotificationPeekEvent(int id)183 NotificationPeekEvent(int id) { 184 mId = id; 185 } getId()186 @Override public int getId() { 187 return mId; 188 } 189 } 190 191 @Inject HeadsUpManagerImpl( @onNull @hadeDisplayAware final Context context, HeadsUpManagerLogger logger, StatusBarStateController statusBarStateController, KeyguardBypassController bypassController, GroupMembershipManager groupMembershipManager, VisualStabilityProvider visualStabilityProvider, @ShadeDisplayAware ConfigurationController configurationController, @Main Handler handler, GlobalSettings globalSettings, SystemClock systemClock, @Main DelayableExecutor executor, AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger, JavaAdapter javaAdapter, ShadeInteractor shadeInteractor, AvalancheController avalancheController)192 public HeadsUpManagerImpl( 193 @NonNull @ShadeDisplayAware final Context context, 194 HeadsUpManagerLogger logger, 195 StatusBarStateController statusBarStateController, 196 KeyguardBypassController bypassController, 197 GroupMembershipManager groupMembershipManager, 198 VisualStabilityProvider visualStabilityProvider, 199 @ShadeDisplayAware ConfigurationController configurationController, 200 @Main Handler handler, 201 GlobalSettings globalSettings, 202 SystemClock systemClock, 203 @Main DelayableExecutor executor, 204 AccessibilityManagerWrapper accessibilityManagerWrapper, 205 UiEventLogger uiEventLogger, 206 JavaAdapter javaAdapter, 207 ShadeInteractor shadeInteractor, 208 AvalancheController avalancheController) { 209 mLogger = logger; 210 mExecutor = executor; 211 mSystemClock = systemClock; 212 mContext = context; 213 mAccessibilityMgr = accessibilityManagerWrapper; 214 mUiEventLogger = uiEventLogger; 215 mAvalancheController = avalancheController; 216 mAvalancheController.setBaseEntryMapStr(this::getEntryMapStr); 217 mBypassController = bypassController; 218 mGroupMembershipManager = groupMembershipManager; 219 mVisualStabilityProvider = visualStabilityProvider; 220 Resources resources = context.getResources(); 221 mMinimumDisplayTimeDefault = NotificationThrottleHun.isEnabled() 222 ? resources.getInteger(R.integer.heads_up_notification_minimum_time_with_throttling) 223 : resources.getInteger(R.integer.heads_up_notification_minimum_time); 224 mMinimumDisplayTimeForUserInitiated = resources.getInteger( 225 R.integer.heads_up_notification_minimum_time_for_user_initiated); 226 mStickyForSomeTimeAutoDismissTime = resources.getInteger( 227 R.integer.sticky_heads_up_notification_time); 228 mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay); 229 mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time); 230 mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); 231 mSnoozedPackages = new ArrayMap<>(); 232 int defaultSnoozeLengthMs = 233 resources.getInteger(R.integer.heads_up_default_snooze_length_ms); 234 235 mSnoozeLengthMs = globalSettings.getInt(SETTING_HEADS_UP_SNOOZE_LENGTH_MS, 236 defaultSnoozeLengthMs); 237 ContentObserver settingsObserver = new ContentObserver(handler) { 238 @Override 239 public void onChange(boolean selfChange) { 240 final int packageSnoozeLengthMs = globalSettings.getInt( 241 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); 242 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { 243 mSnoozeLengthMs = packageSnoozeLengthMs; 244 mLogger.logSnoozeLengthChange(packageSnoozeLengthMs); 245 } 246 } 247 }; 248 globalSettings.registerContentObserverSync( 249 globalSettings.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), 250 /* notifyForDescendants = */ false, 251 settingsObserver); 252 253 statusBarStateController.addCallback(mStatusBarStateListener); 254 updateResources(); 255 configurationController.addCallback(new ConfigurationController.ConfigurationListener() { 256 @Override 257 public void onDensityOrFontScaleChanged() { 258 updateResources(); 259 } 260 261 @Override 262 public void onThemeChanged() { 263 updateResources(); 264 } 265 }); 266 javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), 267 this::onShadeOrQsExpanded); 268 if (SceneContainerFlag.isEnabled()) { 269 javaAdapter.alwaysCollectFlow(shadeInteractor.isQsExpanded(), 270 this::onQsExpanded); 271 } 272 if (NotificationThrottleHun.isEnabled()) { 273 mVisualStabilityProvider.addPersistentReorderingBannedListener( 274 mOnReorderingBannedListener); 275 mVisualStabilityProvider.addPersistentReorderingAllowedListener( 276 mOnReorderingAllowedListener); 277 } 278 } 279 280 /** 281 * Adds an OnHeadUpChangedListener to observe events. 282 */ 283 @Override addListener(@onNull OnHeadsUpChangedListener listener)284 public void addListener(@NonNull OnHeadsUpChangedListener listener) { 285 mListeners.addIfAbsent(listener); 286 } 287 288 /** 289 * Removes the OnHeadUpChangedListener from the observer list. 290 */ 291 @Override removeListener(@onNull OnHeadsUpChangedListener listener)292 public void removeListener(@NonNull OnHeadsUpChangedListener listener) { 293 mListeners.remove(listener); 294 } 295 296 /** 297 * Add a listener to receive callbacks {@link #setHeadsUpAnimatingAway(boolean)} 298 */ 299 @Override addHeadsUpPhoneListener(@onNull OnHeadsUpPhoneListenerChange listener)300 public void addHeadsUpPhoneListener(@NonNull OnHeadsUpPhoneListenerChange listener) { 301 mHeadsUpPhoneListeners.add(listener); 302 } 303 304 @Override setAnimationStateHandler(@onNull AnimationStateHandler handler)305 public void setAnimationStateHandler(@NonNull AnimationStateHandler handler) { 306 mAnimationStateHandler = handler; 307 } 308 updateResources()309 private void updateResources() { 310 Resources resources = mContext.getResources(); 311 mHeadsUpInset = SystemBarUtils.getStatusBarHeight(mContext) 312 + resources.getDimensionPixelSize(R.dimen.heads_up_status_bar_padding); 313 } 314 315 @Override showNotification( @onNull NotificationEntry entry, boolean isPinnedByUser)316 public void showNotification( 317 @NonNull NotificationEntry entry, boolean isPinnedByUser) { 318 HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry); 319 320 mLogger.logShowNotificationRequest(entry, isPinnedByUser); 321 322 PinnedStatus requestedPinnedStatus = 323 isPinnedByUser 324 ? PinnedStatus.PinnedByUser 325 : PinnedStatus.PinnedBySystem; 326 headsUpEntry.setRequestedPinnedStatus(requestedPinnedStatus); 327 328 Runnable runnable = () -> { 329 mLogger.logShowNotification(entry, isPinnedByUser); 330 331 // Add new entry and begin managing it 332 mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry); 333 onEntryAdded(headsUpEntry, requestedPinnedStatus); 334 // TODO(b/328390331) move accessibility events to the view layer 335 entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 336 if (!NotificationBundleUi.isEnabled()) { 337 entry.setIsHeadsUpEntry(true); 338 } 339 340 updateNotificationInternal(entry.getKey(), requestedPinnedStatus); 341 entry.setInterruption(); 342 }; 343 mAvalancheController.update(headsUpEntry, runnable, "showNotification"); 344 } 345 346 @Override removeNotification( @onNull String key, boolean releaseImmediately, boolean animate, @NonNull String reason)347 public boolean removeNotification( 348 @NonNull String key, 349 boolean releaseImmediately, 350 boolean animate, 351 @NonNull String reason) { 352 if (animate) { 353 return removeNotification(key, releaseImmediately, 354 "removeNotification(animate: true), reason: " + reason); 355 } else { 356 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); 357 final boolean removed = removeNotification(key, releaseImmediately, 358 "removeNotification(animate: false), reason: " + reason); 359 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); 360 return removed; 361 } 362 } 363 364 @Override removeNotification(@otNull String key, boolean releaseImmediately, @NonNull String reason)365 public boolean removeNotification(@NotNull String key, boolean releaseImmediately, 366 @NonNull String reason) { 367 final boolean isWaiting = mAvalancheController.isWaiting(key); 368 mLogger.logRemoveNotification(key, releaseImmediately, isWaiting, reason); 369 370 if (mAvalancheController.isWaiting(key)) { 371 removeEntry(key, "removeNotification (isWaiting)"); 372 return true; 373 } 374 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 375 if (headsUpEntry == null) { 376 mLogger.logNullEntry(key, reason); 377 return true; 378 } 379 if (releaseImmediately) { 380 removeEntry(key, "removeNotification (releaseImmediately)"); 381 return true; 382 } 383 if (canRemoveImmediately(key)) { 384 removeEntry(key, "removeNotification (canRemoveImmediately)"); 385 return true; 386 } 387 headsUpEntry.removeAsSoonAsPossible(); 388 return false; 389 } 390 391 @Override updateNotification( @onNull String key, @NonNull PinnedStatus requestedPinnedStatus)392 public void updateNotification( 393 @NonNull String key, @NonNull PinnedStatus requestedPinnedStatus) { 394 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 395 mLogger.logUpdateNotificationRequest(key, requestedPinnedStatus, headsUpEntry != null); 396 397 Runnable runnable = () -> updateNotificationInternal(key, requestedPinnedStatus); 398 mAvalancheController.update(headsUpEntry, runnable, "updateNotification"); 399 } 400 updateNotificationInternal( @onNull String key, PinnedStatus requestedPinnedStatus)401 private void updateNotificationInternal( 402 @NonNull String key, PinnedStatus requestedPinnedStatus) { 403 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 404 mLogger.logUpdateNotification(key, requestedPinnedStatus, headsUpEntry != null); 405 if (headsUpEntry == null) { 406 // the entry was released before this update (i.e by a listener) This can happen 407 // with the groupmanager 408 return; 409 } 410 // TODO(b/328390331) move accessibility events to the view layer 411 if (headsUpEntry.mEntry != null) { 412 headsUpEntry.mEntry.sendAccessibilityEvent( 413 AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 414 } 415 if (requestedPinnedStatus.isPinned()) { 416 headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification"); 417 PinnedStatus pinnedStatus = 418 getNewPinnedStatusForEntry(headsUpEntry, requestedPinnedStatus); 419 setEntryPinned(headsUpEntry, pinnedStatus, "updateNotificationInternal"); 420 } 421 } 422 423 @Override setTrackingHeadsUp(boolean isTrackingHeadsUp)424 public void setTrackingHeadsUp(boolean isTrackingHeadsUp) { 425 mTrackingHeadsUp.setValue(isTrackingHeadsUp); 426 } 427 428 @Override shouldSwallowClick(@onNull String key)429 public boolean shouldSwallowClick(@NonNull String key) { 430 HeadsUpManagerImpl.HeadsUpEntry entry = getHeadsUpEntry(key); 431 return entry != null && mSystemClock.elapsedRealtime() < entry.mPostTime; 432 } 433 434 @Override releaseAfterExpansion()435 public void releaseAfterExpansion() { 436 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; 437 onExpandingFinished(); 438 } 439 440 @Override onExpandingFinished()441 public void onExpandingFinished() { 442 if (mReleaseOnExpandFinish) { 443 releaseAllImmediately(); 444 mReleaseOnExpandFinish = false; 445 } else { 446 for (NotificationEntry entry : getAllEntries().toList()) { 447 entry.setSeenInShade(true); 448 } 449 for (NotificationEntry entry : mEntriesToRemoveAfterExpand) { 450 if (isHeadsUpEntry(entry.getKey())) { 451 // Maybe the heads-up was removed already 452 removeEntry(entry.getKey(), "onExpandingFinished"); 453 } 454 } 455 } 456 mEntriesToRemoveAfterExpand.clear(); 457 } 458 459 /** 460 * Clears all managed notifications. 461 */ releaseAllImmediately()462 public void releaseAllImmediately() { 463 mLogger.logReleaseAllImmediately(); 464 // A copy is necessary here as we are changing the underlying map. This would cause 465 // undefined behavior if we iterated over the key set directly. 466 ArraySet<String> keysToRemove = new ArraySet<>(mHeadsUpEntryMap.keySet()); 467 468 // Must get waiting keys before calling removeEntry, which clears waiting entries in 469 // AvalancheController 470 List<String> waitingKeysToRemove = mAvalancheController.getWaitingKeys(); 471 472 for (String key : keysToRemove) { 473 removeEntry(key, "releaseAllImmediately (keysToRemove)"); 474 } 475 for (String key : waitingKeysToRemove) { 476 removeEntry(key, "releaseAllImmediately (waitingKeysToRemove)"); 477 } 478 } 479 480 /** 481 * Returns the entry if it is managed by this manager. 482 * @param key key of notification 483 * @return the entry 484 */ 485 @Nullable getEntry(@onNull String key)486 public NotificationEntry getEntry(@NonNull String key) { 487 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 488 return headsUpEntry != null ? headsUpEntry.mEntry : null; 489 } 490 491 /** 492 * Returns the stream of all current notifications managed by this manager. 493 * @return all entries 494 */ 495 @NonNull 496 @Override getAllEntries()497 public Stream<NotificationEntry> getAllEntries() { 498 return getHeadsUpEntryList().stream().map(headsUpEntry -> headsUpEntry.mEntry); 499 } 500 getHeadsUpEntryList()501 public List<HeadsUpEntry> getHeadsUpEntryList() { 502 List<HeadsUpEntry> entryList = new ArrayList<>(mHeadsUpEntryMap.values()); 503 entryList.addAll(mAvalancheController.getWaitingEntryList()); 504 return entryList; 505 } 506 507 /** 508 * Whether or not there are any active notifications. 509 * @return true if there is an entry, false otherwise 510 */ 511 @Override hasNotifications()512 public boolean hasNotifications() { 513 return !mHeadsUpEntryMap.isEmpty() 514 || !mAvalancheController.getWaitingEntryList().isEmpty(); 515 } 516 517 @Override isHeadsUpEntry(@onNull String key)518 public boolean isHeadsUpEntry(@NonNull String key) { 519 return mHeadsUpEntryMap.containsKey(key) || mAvalancheController.isWaiting(key); 520 } 521 522 /** 523 * @return When a HUN entry with the given key should be removed in milliseconds from now 524 */ 525 @Override getEarliestRemovalTime(String key)526 public long getEarliestRemovalTime(String key) { 527 HeadsUpEntry entry = mHeadsUpEntryMap.get(key); 528 if (entry != null) { 529 return Math.max(0, entry.mEarliestRemovalTime - mSystemClock.elapsedRealtime()); 530 } 531 return 0; 532 } 533 534 @VisibleForTesting shouldHeadsUpBecomePinned(@ullable NotificationEntry entry)535 boolean shouldHeadsUpBecomePinned(@Nullable NotificationEntry entry) { 536 if (entry == null) { 537 return false; 538 } 539 boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsShadeOrQsExpanded; 540 if (SceneContainerFlag.isEnabled()) { 541 pin |= mIsQsExpanded; 542 } 543 if (mBypassController.getBypassEnabled()) { 544 pin |= mStatusBarState == StatusBarState.KEYGUARD; 545 } 546 if (pin) { 547 return true; 548 } 549 550 final HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 551 if (headsUpEntry == null) { 552 // This should not happen since shouldHeadsUpBecomePinned is always called after adding 553 // the NotificationEntry into mHeadsUpEntryMap. 554 return hasFullScreenIntent(entry); 555 } 556 return hasFullScreenIntent(entry) && !headsUpEntry.mWasUnpinned; 557 } 558 hasFullScreenIntent(@onNull NotificationEntry entry)559 private boolean hasFullScreenIntent(@NonNull NotificationEntry entry) { 560 if (entry.getSbn().getNotification() == null) { 561 return false; 562 } 563 return entry.getSbn().getNotification().fullScreenIntent != null; 564 } 565 setEntryPinned( @onNull HeadsUpManagerImpl.HeadsUpEntry headsUpEntry, PinnedStatus pinnedStatus, String reason)566 private void setEntryPinned( 567 @NonNull HeadsUpManagerImpl.HeadsUpEntry headsUpEntry, PinnedStatus pinnedStatus, 568 String reason) { 569 NotificationEntry entry = headsUpEntry.requireEntry(); 570 mLogger.logSetEntryPinned(entry, pinnedStatus, reason); 571 boolean isPinned = pinnedStatus.isPinned(); 572 if (!isPinned) { 573 headsUpEntry.mWasUnpinned = true; 574 } 575 if (headsUpEntry.getPinnedStatus().getValue() != pinnedStatus) { 576 headsUpEntry.setRowPinnedStatus(pinnedStatus); 577 updatePinnedMode(); 578 if (isPinned) { 579 mUiEventLogger.logWithInstanceId( 580 NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(), 581 entry.getSbn().getPackageName(), entry.getSbn().getInstanceId()); 582 } 583 // TODO(b/325936094) use the isPinned Flow instead 584 for (OnHeadsUpChangedListener listener : mListeners) { 585 if (isPinned) { 586 listener.onHeadsUpPinned(entry); 587 } else { 588 listener.onHeadsUpUnPinned(entry); 589 } 590 } 591 } 592 } 593 594 /** 595 * Manager-specific logic that should occur when an entry is added. 596 * @param headsUpEntry entry added 597 */ 598 @VisibleForTesting onEntryAdded(HeadsUpEntry headsUpEntry, PinnedStatus requestedPinnedStatus)599 void onEntryAdded(HeadsUpEntry headsUpEntry, PinnedStatus requestedPinnedStatus) { 600 NotificationEntry entry = headsUpEntry.requireEntry(); 601 entry.setHeadsUp(true); 602 603 PinnedStatus pinnedStatus = getNewPinnedStatusForEntry(headsUpEntry, requestedPinnedStatus); 604 setEntryPinned(headsUpEntry, pinnedStatus, "onEntryAdded"); 605 EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */); 606 for (OnHeadsUpChangedListener listener : mListeners) { 607 // TODO(b/382509804): It's odd that if pinnedStatus == PinnedStatus.NotPinned, then we 608 // still send isHeadsUp=true to listeners. Is this causing bugs? 609 listener.onHeadsUpStateChanged(entry, true); 610 } 611 updateTopHeadsUpFlow(); 612 updateHeadsUpFlow(); 613 } 614 getNewPinnedStatusForEntry( HeadsUpEntry headsUpEntry, PinnedStatus requestedPinnedStatus)615 private PinnedStatus getNewPinnedStatusForEntry( 616 HeadsUpEntry headsUpEntry, PinnedStatus requestedPinnedStatus) { 617 NotificationEntry entry = headsUpEntry.mEntry; 618 if (entry == null) { 619 return PinnedStatus.NotPinned; 620 } 621 boolean shouldBecomePinned = shouldHeadsUpBecomePinned(entry); 622 if (!shouldBecomePinned) { 623 return PinnedStatus.NotPinned; 624 } 625 626 if (!StatusBarNotifChips.isEnabled() 627 && requestedPinnedStatus == PinnedStatus.PinnedByUser) { 628 Log.wtf(TAG, "PinnedStatus.PinnedByUser not allowed if StatusBarNotifChips flag off"); 629 return PinnedStatus.NotPinned; 630 } 631 632 return requestedPinnedStatus; 633 } 634 635 /** 636 * Remove a notification from the alerting entries. 637 * @param key key of notification to remove 638 */ removeEntry(@onNull String key, String reason)639 private void removeEntry(@NonNull String key, String reason) { 640 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 641 boolean isWaiting; 642 if (headsUpEntry == null) { 643 headsUpEntry = mAvalancheController.getWaitingEntry(key); 644 isWaiting = true; 645 } else { 646 isWaiting = false; 647 } 648 mLogger.logRemoveEntryRequest(key, reason, isWaiting); 649 HeadsUpEntry finalHeadsUpEntry = headsUpEntry; 650 Runnable runnable = () -> { 651 mLogger.logRemoveEntry(key, reason, isWaiting); 652 653 if (finalHeadsUpEntry == null) { 654 return; 655 } 656 NotificationEntry entry = finalHeadsUpEntry.requireEntry(); 657 658 // If the notification is animating, we will remove it at the end of the animation. 659 if (entry.isExpandAnimationRunning()) { 660 return; 661 } 662 entry.demoteStickyHun(); 663 mHeadsUpEntryMap.remove(key); 664 onEntryRemoved(finalHeadsUpEntry, reason); 665 // TODO(b/328390331) move accessibility events to the view layer 666 entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 667 if (NotificationThrottleHun.isEnabled()) { 668 finalHeadsUpEntry.cancelAutoRemovalCallbacks("removeEntry"); 669 } else { 670 finalHeadsUpEntry.reset(); 671 } 672 }; 673 mAvalancheController.delete(headsUpEntry, runnable, "removeEntry"); 674 } 675 676 /** 677 * Manager-specific logic that should occur when an entry is removed. 678 * @param headsUpEntry entry removed 679 * @param reason why onEntryRemoved was called 680 */ 681 @VisibleForTesting onEntryRemoved(@onNull HeadsUpEntry headsUpEntry, String reason)682 void onEntryRemoved(@NonNull HeadsUpEntry headsUpEntry, String reason) { 683 NotificationEntry entry = headsUpEntry.requireEntry(); 684 entry.setHeadsUp(false); 685 setEntryPinned(headsUpEntry, PinnedStatus.NotPinned, "onEntryRemoved"); 686 EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */); 687 mLogger.logNotificationActuallyRemoved(entry); 688 for (OnHeadsUpChangedListener listener : mListeners) { 689 listener.onHeadsUpStateChanged(entry, false); 690 } 691 if (!NotificationThrottleHun.isEnabled()) { 692 mEntryPool.release(headsUpEntry); 693 } 694 updateTopHeadsUpFlow(); 695 updateHeadsUpFlow(); 696 if (NotificationThrottleHun.isEnabled()) { 697 NotificationEntry notifEntry = headsUpEntry.mEntry; 698 if (notifEntry == null) { 699 return; 700 } 701 // If reorder was just allowed and we called onEntryRemoved while iterating over 702 // mEntriesToRemoveWhenReorderingAllowed, we should not remove from this list (and cause 703 // ArrayIndexOutOfBoundsException). We don't need to in this case anyway, because we 704 // clear mEntriesToRemoveWhenReorderingAllowed after removing these entries. 705 if (!reason.equals(REASON_REORDER_ALLOWED)) { 706 mEntriesToRemoveWhenReorderingAllowed.remove(notifEntry); 707 } 708 } 709 } 710 updateTopHeadsUpFlow()711 private void updateTopHeadsUpFlow() { 712 mTopHeadsUpRow.setValue(getTopHeadsUpEntry()); 713 } 714 updateHeadsUpFlow()715 private void updateHeadsUpFlow() { 716 mHeadsUpNotificationRows.setValue(new HashSet<>(mHeadsUpEntryMap.values())); 717 } 718 719 @Override 720 @NonNull getTopHeadsUpRow()721 public Flow<HeadsUpRowRepository> getTopHeadsUpRow() { 722 return mTopHeadsUpRow; 723 } 724 725 @Override 726 @NonNull getActiveHeadsUpRows()727 public Flow<Set<HeadsUpRowRepository>> getActiveHeadsUpRows() { 728 return mHeadsUpNotificationRows; 729 } 730 731 @Override 732 @NonNull isHeadsUpAnimatingAway()733 public StateFlow<Boolean> isHeadsUpAnimatingAway() { 734 return mHeadsUpAnimatingAway; 735 } 736 737 @Override isHeadsUpAnimatingAwayValue()738 public boolean isHeadsUpAnimatingAwayValue() { 739 return mHeadsUpAnimatingAway.getValue(); 740 } 741 742 /** 743 * Called to notify the listeners that the HUN animating away animation has ended. 744 */ 745 @Override onEntryAnimatingAwayEnded(@onNull NotificationEntry entry)746 public void onEntryAnimatingAwayEnded(@NonNull NotificationEntry entry) { 747 for (OnHeadsUpChangedListener listener : mListeners) { 748 listener.onHeadsUpAnimatingAwayEnded(entry); 749 } 750 } 751 updatePinnedMode()752 private void updatePinnedMode() { 753 boolean hasPinnedNotification = hasPinnedNotificationInternal(); 754 mPinnedNotificationStatus = pinnedNotificationStatusInternal(); 755 if (hasPinnedNotification == mHasPinnedNotification) { 756 return; 757 } 758 mLogger.logUpdatePinnedMode(hasPinnedNotification, mPinnedNotificationStatus); 759 mHasPinnedNotification = hasPinnedNotification; 760 if (mHasPinnedNotification) { 761 MetricsLogger.count(mContext, "note_peek", 1); 762 } 763 for (OnHeadsUpChangedListener listener : mListeners) { 764 listener.onHeadsUpPinnedModeChanged(hasPinnedNotification); 765 } 766 } 767 768 /** 769 * Returns if the given notification is snoozed or not. 770 */ isSnoozed(@onNull String packageName)771 public boolean isSnoozed(@NonNull String packageName) { 772 final String key = snoozeKey(packageName, mUser); 773 Long snoozedUntil = mSnoozedPackages.get(key); 774 if (snoozedUntil != null) { 775 if (snoozedUntil > mSystemClock.elapsedRealtime()) { 776 mLogger.logIsSnoozedReturned(key); 777 return true; 778 } 779 mLogger.logPackageUnsnoozed(key); 780 mSnoozedPackages.remove(key); 781 } 782 return false; 783 } 784 785 /** 786 * Snoozes all current Heads Up Notifications. 787 */ 788 @Override snooze()789 public void snooze() { 790 List<String> keySet = new ArrayList<>(mHeadsUpEntryMap.keySet()); 791 keySet.addAll(mAvalancheController.getWaitingKeys()); 792 for (String key : keySet) { 793 HeadsUpEntry entry = getHeadsUpEntry(key); 794 if (entry == null || entry.mEntry == null) { 795 continue; 796 } 797 String packageName = entry.mEntry.getSbn().getPackageName(); 798 String snoozeKey = snoozeKey(packageName, mUser); 799 mLogger.logPackageSnoozed(snoozeKey); 800 mSnoozedPackages.put(snoozeKey, mSystemClock.elapsedRealtime() + mSnoozeLengthMs); 801 } 802 mReleaseOnExpandFinish = true; 803 } 804 805 @NonNull snoozeKey(@onNull String packageName, int user)806 private static String snoozeKey(@NonNull String packageName, int user) { 807 return user + "," + packageName; 808 } 809 810 @Override addSwipedOutNotification(@onNull String key)811 public void addSwipedOutNotification(@NonNull String key) { 812 mSwipedOutKeys.add(key); 813 } 814 815 @Nullable 816 @VisibleForTesting getHeadsUpEntry(@onNull String key)817 HeadsUpEntry getHeadsUpEntry(@NonNull String key) { 818 if (mHeadsUpEntryMap.containsKey(key)) { 819 return mHeadsUpEntryMap.get(key); 820 } 821 return mAvalancheController.getWaitingEntry(key); 822 } 823 824 /** 825 * Returns the top Heads Up Notification, which appears to show at first. 826 */ 827 @Nullable getTopEntry()828 public NotificationEntry getTopEntry() { 829 HeadsUpEntry topEntry = getTopHeadsUpEntry(); 830 return (topEntry != null) ? topEntry.mEntry : null; 831 } 832 833 @Nullable getTopHeadsUpEntry()834 private HeadsUpEntry getTopHeadsUpEntry() { 835 if (mHeadsUpEntryMap.isEmpty()) { 836 return null; 837 } 838 HeadsUpEntry topEntry = null; 839 for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { 840 if (topEntry == null || entry.compareTo(topEntry) < 0) { 841 topEntry = entry; 842 } 843 } 844 return topEntry; 845 } 846 847 /** 848 * Sets the current user. 849 */ setUser(int user)850 public void setUser(int user) { 851 mUser = user; 852 } 853 854 /** Returns the ID of the current user. */ getUser()855 public int getUser() { 856 return mUser; 857 } 858 getEntryMapStr()859 private String getEntryMapStr() { 860 if (mHeadsUpEntryMap.isEmpty()) { 861 return ""; 862 } 863 StringBuilder entryMapStr = new StringBuilder(); 864 for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { 865 entryMapStr.append("\n ").append( 866 entry.mEntry == null ? "null" : entry.mEntry.getKey()); 867 } 868 return entryMapStr.toString(); 869 } 870 871 @Override getTouchableRegion()872 public @Nullable Region getTouchableRegion() { 873 NotificationEntry topEntry = getTopEntry(); 874 875 // This call could be made in an inconsistent state while the pinnedMode hasn't been 876 // updated yet, but callbacks leading out of the headsUp manager, querying it. Let's 877 // therefore also check if the topEntry is null. 878 if (!hasPinnedHeadsUp() || topEntry == null) { 879 return null; 880 } else { 881 ExpandableNotificationRow topRow = topEntry.getRow(); 882 if (topEntry.rowIsChildInGroup()) { 883 if (NotificationBundleUi.isEnabled()) { 884 if (topRow.getNotificationParent() != null) { 885 topRow = topRow.getNotificationParent(); 886 } 887 } else { 888 final NotificationEntry groupSummary = 889 mGroupMembershipManager.getGroupSummary(topEntry); 890 if (groupSummary != null) { 891 topEntry = groupSummary; 892 topRow = topEntry.getRow(); 893 } 894 } 895 } 896 897 int[] tmpArray = new int[2]; 898 topRow.getLocationOnScreen(tmpArray); 899 int minX = tmpArray[0]; 900 int maxX = tmpArray[0] + topRow.getWidth(); 901 int height = topRow.getIntrinsicHeight(); 902 final boolean stretchToTop = tmpArray[1] <= mHeadsUpInset; 903 mTouchableRegion.set(minX, stretchToTop ? 0 : tmpArray[1], maxX, tmpArray[1] + height); 904 return mTouchableRegion; 905 } 906 } 907 908 @Override setHeadsUpAnimatingAway(boolean headsUpAnimatingAway)909 public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { 910 if (headsUpAnimatingAway != mHeadsUpAnimatingAway.getValue()) { 911 for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) { 912 listener.onHeadsUpAnimatingAwayStateChanged(headsUpAnimatingAway); 913 } 914 mHeadsUpAnimatingAway.setValue(headsUpAnimatingAway); 915 } 916 } 917 onShadeOrQsExpanded(Boolean isExpanded)918 private void onShadeOrQsExpanded(Boolean isExpanded) { 919 if (isExpanded != mIsShadeOrQsExpanded) { 920 mIsShadeOrQsExpanded = isExpanded; 921 if (!SceneContainerFlag.isEnabled() && isExpanded) { 922 mHeadsUpAnimatingAway.setValue(false); 923 } 924 } 925 } 926 onQsExpanded(Boolean isQsExpanded)927 private void onQsExpanded(Boolean isQsExpanded) { 928 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; 929 if (isQsExpanded != mIsQsExpanded) mIsQsExpanded = isQsExpanded; 930 } 931 932 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)933 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 934 pw.println("HeadsUpManager state:"); 935 dumpInternal(pw, args); 936 } 937 dumpInternal(@onNull PrintWriter pw, @NonNull String[] args)938 private void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) { 939 pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); 940 pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); 941 pw.print(" now="); pw.println(mSystemClock.elapsedRealtime()); 942 pw.print(" mUser="); pw.println(mUser); 943 for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { 944 pw.println(entry.mEntry == null ? "null" : entry.mEntry); 945 } 946 int n = mSnoozedPackages.size(); 947 pw.println(" snoozed packages: " + n); 948 for (int i = 0; i < n; i++) { 949 pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); 950 pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); 951 } 952 pw.print(" mBarState="); 953 pw.println(mStatusBarState); 954 pw.print(" mTouchableRegion="); 955 pw.println(mTouchableRegion); 956 } 957 958 @Override hasPinnedHeadsUp()959 public boolean hasPinnedHeadsUp() { 960 return mHasPinnedNotification; 961 } 962 963 @Override 964 @NonNull pinnedHeadsUpStatus()965 public PinnedStatus pinnedHeadsUpStatus() { 966 if (!StatusBarNotifChips.isEnabled()) { 967 return mHasPinnedNotification ? PinnedStatus.PinnedBySystem : PinnedStatus.NotPinned; 968 } 969 return mPinnedNotificationStatus; 970 } 971 hasPinnedNotificationInternal()972 private boolean hasPinnedNotificationInternal() { 973 for (String key : mHeadsUpEntryMap.keySet()) { 974 HeadsUpEntry entry = getHeadsUpEntry(key); 975 if (entry != null && entry.mEntry != null && entry.mEntry.isRowPinned()) { 976 return true; 977 } 978 } 979 return false; 980 } 981 pinnedNotificationStatusInternal()982 private PinnedStatus pinnedNotificationStatusInternal() { 983 for (String key : mHeadsUpEntryMap.keySet()) { 984 HeadsUpEntry entry = getHeadsUpEntry(key); 985 if (entry.mEntry != null && entry.mEntry.isRowPinned()) { 986 return entry.mEntry.getPinnedStatus(); 987 } 988 } 989 return PinnedStatus.NotPinned; 990 } 991 992 /** 993 * Unpins all pinned Heads Up Notifications. 994 * @param userUnPinned The unpinned action is trigger by user real operation. 995 */ 996 @Override unpinAll(boolean userUnPinned)997 public void unpinAll(boolean userUnPinned) { 998 for (String key : mHeadsUpEntryMap.keySet()) { 999 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 1000 if (headsUpEntry == null) { 1001 Log.wtf(TAG, "Couldn't find entry " + key + " in unpinAll"); 1002 continue; 1003 } 1004 mLogger.logUnpinEntryRequest(key); 1005 Runnable runnable = () -> { 1006 mLogger.logUnpinEntry(key); 1007 1008 setEntryPinned(headsUpEntry, PinnedStatus.NotPinned, "unpinAll"); 1009 // maybe it got un sticky 1010 headsUpEntry.updateEntry(false /* updatePostTime */, "unpinAll"); 1011 1012 // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay 1013 // on the screen. 1014 if (userUnPinned 1015 && headsUpEntry.mEntry != null 1016 && headsUpEntry.mEntry.mustStayOnScreen()) { 1017 headsUpEntry.mEntry.setHeadsUpIsVisible(); 1018 } 1019 }; 1020 mAvalancheController.delete(headsUpEntry, runnable, "unpinAll"); 1021 } 1022 } 1023 1024 @Override setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)1025 public void setRemoteInputActive( 1026 @NonNull NotificationEntry entry, boolean remoteInputActive) { 1027 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(entry.getKey()); 1028 if (headsUpEntry != null && headsUpEntry.mRemoteInputActive != remoteInputActive) { 1029 headsUpEntry.mRemoteInputActive = remoteInputActive; 1030 if (ExpandHeadsUpOnInlineReply.isEnabled() && remoteInputActive) { 1031 headsUpEntry.mRemoteInputActivatedAtLeastOnce = true; 1032 } 1033 if (remoteInputActive) { 1034 headsUpEntry.cancelAutoRemovalCallbacks("setRemoteInputActive(true)"); 1035 } else { 1036 headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)"); 1037 } 1038 updateTopHeadsUpFlow(); 1039 } 1040 } 1041 1042 @Override setGutsShown(@onNull NotificationEntry entry, boolean gutsShown)1043 public void setGutsShown(@NonNull NotificationEntry entry, boolean gutsShown) { 1044 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 1045 if (headsUpEntry == null) return; 1046 if (entry.isRowPinned() || !gutsShown) { 1047 headsUpEntry.setGutsShownPinned(gutsShown); 1048 } 1049 } 1050 1051 @Override extendHeadsUp()1052 public void extendHeadsUp() { 1053 HeadsUpEntry topEntry = getTopHeadsUpEntryPhone(); 1054 if (topEntry == null) { 1055 return; 1056 } 1057 topEntry.extendPulse(); 1058 } 1059 1060 @Nullable getTopHeadsUpEntryPhone()1061 private HeadsUpEntry getTopHeadsUpEntryPhone() { 1062 if (SceneContainerFlag.isEnabled()) { 1063 return (HeadsUpEntry) mTopHeadsUpRow.getValue(); 1064 } else { 1065 return getTopHeadsUpEntry(); 1066 } 1067 } 1068 1069 @NonNull 1070 @Override isTrackingHeadsUp()1071 public StateFlow<Boolean> isTrackingHeadsUp() { 1072 return mTrackingHeadsUp; 1073 } 1074 1075 /** 1076 * Compare two entries and decide how they should be ranked. 1077 * 1078 * @return -1 if the first argument should be ranked higher than the second, 1 if the second 1079 * one should be ranked higher and 0 if they are equal. 1080 */ compare(@ullable NotificationEntry a, @Nullable NotificationEntry b)1081 public int compare(@Nullable NotificationEntry a, @Nullable NotificationEntry b) { 1082 if (a == null || b == null) { 1083 return Boolean.compare(a == null, b == null); 1084 } 1085 HeadsUpEntry aEntry = getHeadsUpEntry(a.getKey()); 1086 HeadsUpEntry bEntry = getHeadsUpEntry(b.getKey()); 1087 if (aEntry == null || bEntry == null) { 1088 return Boolean.compare(aEntry == null, bEntry == null); 1089 } 1090 return aEntry.compareTo(bEntry); 1091 } 1092 1093 /** 1094 * Set an entry to be expanded and therefore stick in the heads up area if it's pinned 1095 * until it's collapsed again. 1096 */ 1097 @Override setExpanded(@onNull String entryKey, @NonNull ExpandableNotificationRow row, boolean expanded)1098 public void setExpanded(@NonNull String entryKey, @NonNull ExpandableNotificationRow row, 1099 boolean expanded) { 1100 NotificationBundleUi.unsafeAssertInNewMode(); 1101 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey); 1102 if (headsUpEntry != null && row.getPinnedStatus().isPinned()) { 1103 headsUpEntry.setExpanded(expanded); 1104 } 1105 } 1106 1107 /** 1108 * Set an entry to be expanded and therefore stick in the heads up area if it's pinned 1109 * until it's collapsed again. 1110 */ 1111 @Override setExpanded(@onNull NotificationEntry entry, boolean expanded)1112 public void setExpanded(@NonNull NotificationEntry entry, boolean expanded) { 1113 NotificationBundleUi.assertInLegacyMode(); 1114 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 1115 if (headsUpEntry != null && entry.isRowPinned()) { 1116 headsUpEntry.setExpanded(expanded); 1117 } 1118 } 1119 1120 /** 1121 * Notes that the user took an action on an entry that might indirectly cause the system or the 1122 * app to remove the notification. 1123 * 1124 * @param entry the entry that might be indirectly removed by the user's action 1125 * 1126 * @see HeadsUpCoordinator.mActionPressListener 1127 * @see #canRemoveImmediately(String) 1128 */ setUserActionMayIndirectlyRemove(@onNull String entryKey)1129 public void setUserActionMayIndirectlyRemove(@NonNull String entryKey) { 1130 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey); 1131 if (headsUpEntry != null) { 1132 headsUpEntry.mUserActionMayIndirectlyRemove = true; 1133 } 1134 } 1135 1136 /** 1137 * Whether or not the entry can be removed currently. If it hasn't been on screen long enough 1138 * it should not be removed unless forced 1139 * @param key the key to check if removable 1140 * @return true if the entry can be removed 1141 */ 1142 @Override canRemoveImmediately(@onNull String key)1143 public boolean canRemoveImmediately(@NonNull String key) { 1144 if (mSwipedOutKeys.contains(key)) { 1145 // We always instantly dismiss views being manually swiped out. 1146 mSwipedOutKeys.remove(key); 1147 return true; 1148 } 1149 1150 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 1151 HeadsUpEntry topEntry = getTopHeadsUpEntryPhone(); 1152 1153 if (headsUpEntry == null || headsUpEntry != topEntry) { 1154 return true; 1155 } 1156 1157 if (headsUpEntry.mUserActionMayIndirectlyRemove) { 1158 return true; 1159 } 1160 return headsUpEntry.wasShownLongEnough() 1161 || (headsUpEntry.mEntry != null && headsUpEntry.mEntry.isRowDismissed()); 1162 } 1163 1164 /** 1165 * @return true if the entry with the given key is (pinned and expanded) or (has an active 1166 * remote input) 1167 */ 1168 @Override isSticky(String key)1169 public boolean isSticky(String key) { 1170 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 1171 if (headsUpEntry != null) { 1172 return headsUpEntry.isSticky(); 1173 } 1174 return false; 1175 } 1176 1177 @NonNull 1178 @VisibleForTesting createHeadsUpEntry(NotificationEntry entry)1179 HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { 1180 if (NotificationThrottleHun.isEnabled()) { 1181 return new HeadsUpEntry(entry); 1182 } else { 1183 HeadsUpEntry headsUpEntry = mEntryPool.acquire(); 1184 headsUpEntry.setEntry(entry); 1185 return headsUpEntry; 1186 } 1187 } 1188 1189 /** 1190 * Determines if the notification is for a critical call that must display on top of an active 1191 * input notification. 1192 * The call isOngoing check is for a special case of incoming calls (see b/164291424). 1193 */ isCriticalCallNotif(NotificationEntry entry)1194 private static boolean isCriticalCallNotif(NotificationEntry entry) { 1195 Notification n = entry.getSbn().getNotification(); 1196 boolean isIncomingCall = n.isStyle(Notification.CallStyle.class) && n.extras.getInt( 1197 Notification.EXTRA_CALL_TYPE) == Notification.CallStyle.CALL_TYPE_INCOMING; 1198 return isIncomingCall || (entry.getSbn().isOngoing() 1199 && Notification.CATEGORY_CALL.equals(n.category)); 1200 } 1201 1202 @VisibleForTesting 1203 final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { 1204 if (NotificationThrottleHun.isEnabled()) { 1205 mAvalancheController.setEnableAtRuntime(true); 1206 if (mEntriesToRemoveWhenReorderingAllowed.isEmpty()) { 1207 return; 1208 } 1209 } 1210 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); 1211 for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { 1212 if (entry != null && isHeadsUpEntry(entry.getKey())) { 1213 // Maybe the heads-up was removed already 1214 removeEntry(entry.getKey(), REASON_REORDER_ALLOWED); 1215 } 1216 } 1217 mEntriesToRemoveWhenReorderingAllowed.clear(); 1218 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); 1219 }; 1220 1221 private final OnReorderingBannedListener mOnReorderingBannedListener = () -> { 1222 if (mAvalancheController != null) { 1223 // In open shade the first HUN is pinned, and visual stability logic prevents us from 1224 // unpinning this first HUN as long as the shade remains open. AvalancheController only 1225 // shows the next HUN when the currently showing HUN is unpinned, so we must disable 1226 // throttling here so that the incoming HUN stream is not forever paused. This is reset 1227 // when reorder becomes allowed. 1228 mAvalancheController.setEnableAtRuntime(false); 1229 1230 // Note that we cannot do the above when 1231 // 1) The remove runnable runs because its delay means it may not run before shade close 1232 // 2) Reordering is allowed again (when shade closes) because the HUN appear animation 1233 // will have started by then 1234 } 1235 }; 1236 1237 private final StatusBarStateController.StateListener 1238 mStatusBarStateListener = new StatusBarStateController.StateListener() { 1239 @Override 1240 public void onStateChanged(int newState) { 1241 boolean wasKeyguard = mStatusBarState == StatusBarState.KEYGUARD; 1242 boolean isKeyguard = newState == StatusBarState.KEYGUARD; 1243 mStatusBarState = newState; 1244 1245 if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) { 1246 ArrayList<String> keysToRemove = new ArrayList<>(); 1247 for (HeadsUpEntry entry : getHeadsUpEntryList()) { 1248 if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) { 1249 keysToRemove.add(entry.mEntry.getKey()); 1250 } 1251 } 1252 for (String key : keysToRemove) { 1253 removeEntry(key, "mStatusBarStateListener"); 1254 } 1255 } 1256 } 1257 1258 @Override 1259 public void onDozingChanged(boolean isDozing) { 1260 if (!isDozing) { 1261 // Let's make sure all huns we got while dozing time out within the normal timeout 1262 // duration. Otherwise they could get stuck for a very long time 1263 for (HeadsUpEntry entry : getHeadsUpEntryList()) { 1264 entry.updateEntry(true /* updatePostTime */, "onDozingChanged(false)"); 1265 } 1266 } 1267 } 1268 }; 1269 1270 /** 1271 * This represents a notification and how long it is in a heads up mode. It also manages its 1272 * lifecycle automatically when created. This class is public because it is exposed by methods 1273 * of AvalancheController that take it as param. 1274 */ 1275 public class HeadsUpEntry implements Comparable<HeadsUpEntry>, HeadsUpRowRepository { 1276 public boolean mRemoteInputActivatedAtLeastOnce; 1277 public boolean mRemoteInputActive; 1278 public boolean mUserActionMayIndirectlyRemove; 1279 1280 private boolean mExpanded; 1281 @VisibleForTesting 1282 boolean mWasUnpinned; 1283 1284 @Nullable public NotificationEntry mEntry; 1285 public long mPostTime; 1286 public long mEarliestRemovalTime; 1287 1288 @Nullable private Runnable mRemoveRunnable; 1289 1290 @Nullable private Runnable mCancelRemoveRunnable; 1291 1292 private boolean mGutsShownPinned; 1293 /** The *current* pinned status of this HUN. */ 1294 private final MutableStateFlow<PinnedStatus> mPinnedStatus = 1295 StateFlowKt.MutableStateFlow(PinnedStatus.NotPinned); 1296 1297 /** 1298 * The *requested* pinned status of this HUN. {@link AvalancheController} uses this value to 1299 * know if the current HUN needs to be removed so that a pinned-by-user HUN can show. 1300 */ 1301 private PinnedStatus mRequestedPinnedStatus = PinnedStatus.NotPinned; 1302 1303 /** 1304 * If the time this entry has been on was extended 1305 */ 1306 private boolean extended; 1307 HeadsUpEntry()1308 public HeadsUpEntry() { 1309 NotificationThrottleHun.assertInLegacyMode(); 1310 } 1311 HeadsUpEntry(NotificationEntry entry)1312 public HeadsUpEntry(NotificationEntry entry) { 1313 // Attach NotificationEntry for AvalancheController to log key and 1314 // record mPostTime for AvalancheController sorting 1315 setEntry(entry, createRemoveRunnable(entry)); 1316 } 1317 1318 @Override 1319 @NonNull getKey()1320 public String getKey() { 1321 return requireEntry().getKey(); 1322 } 1323 1324 @Override 1325 @NonNull getElementKey()1326 public Object getElementKey() { 1327 return requireEntry().getRow(); 1328 } 1329 requireEntry()1330 private NotificationEntry requireEntry() { 1331 return Objects.requireNonNull(mEntry); 1332 } 1333 1334 @Override 1335 @NonNull getPinnedStatus()1336 public StateFlow<PinnedStatus> getPinnedStatus() { 1337 return mPinnedStatus; 1338 } 1339 1340 /** Attach a NotificationEntry. */ setEntry(@onNull final NotificationEntry entry)1341 public void setEntry(@NonNull final NotificationEntry entry) { 1342 NotificationThrottleHun.assertInLegacyMode(); 1343 setEntry(entry, createRemoveRunnable(entry)); 1344 } 1345 setEntry( @onNull final NotificationEntry entry, @Nullable Runnable removeRunnable)1346 private void setEntry( 1347 @NonNull final NotificationEntry entry, 1348 @Nullable Runnable removeRunnable) { 1349 mEntry = entry; 1350 mRemoveRunnable = removeRunnable; 1351 1352 mPostTime = calculatePostTime(); 1353 updateEntry(true /* updatePostTime */, "setEntry"); 1354 1355 if (NotificationThrottleHun.isEnabled()) { 1356 mEntriesToRemoveWhenReorderingAllowed.add(entry); 1357 if (!mVisualStabilityProvider.isReorderingAllowed()) { 1358 entry.setSeenInShade(true); 1359 } 1360 } 1361 } 1362 1363 /** Sets what pinned status this HUN is requesting. */ setRequestedPinnedStatus(PinnedStatus pinnedStatus)1364 void setRequestedPinnedStatus(PinnedStatus pinnedStatus) { 1365 if (!StatusBarNotifChips.isEnabled() && pinnedStatus == PinnedStatus.PinnedByUser) { 1366 Log.w(TAG, "PinnedByUser status not allowed if StatusBarNotifChips is disabled"); 1367 mRequestedPinnedStatus = PinnedStatus.NotPinned; 1368 } else { 1369 mRequestedPinnedStatus = pinnedStatus; 1370 } 1371 } 1372 getRequestedPinnedStatus()1373 PinnedStatus getRequestedPinnedStatus() { 1374 return mRequestedPinnedStatus; 1375 } 1376 1377 @VisibleForTesting setRowPinnedStatus(PinnedStatus pinnedStatus)1378 void setRowPinnedStatus(PinnedStatus pinnedStatus) { 1379 if (mEntry != null) mEntry.setRowPinnedStatus(pinnedStatus); 1380 mPinnedStatus.setValue(pinnedStatus); 1381 } 1382 1383 /** 1384 * An interface that returns the amount of time left this HUN should show. 1385 */ 1386 private interface FinishTimeUpdater { updateAndGetTimeRemaining()1387 long updateAndGetTimeRemaining(); 1388 } 1389 1390 /** 1391 * Updates an entry's removal time. 1392 * @param updatePostTime whether or not to refresh the post time 1393 */ updateEntry(boolean updatePostTime, @Nullable String reason)1394 public void updateEntry(boolean updatePostTime, @Nullable String reason) { 1395 updateEntry(updatePostTime, /* updateEarliestRemovalTime= */ true, reason); 1396 } 1397 1398 /** 1399 * Updates an entry's removal time. 1400 * @param updatePostTime whether or not to refresh the post time 1401 * @param updateEarliestRemovalTime whether this update should further delay removal 1402 */ updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime, @Nullable String reason)1403 public void updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime, 1404 @Nullable String reason) { 1405 Runnable runnable = () -> { 1406 if (mEntry == null) { 1407 Log.wtf(TAG, "#updateEntry called with null mEntry; returning early"); 1408 return; 1409 } 1410 mLogger.logUpdateEntry(mEntry, updatePostTime, reason); 1411 1412 final long now = mSystemClock.elapsedRealtime(); 1413 if (updateEarliestRemovalTime) { 1414 if (StatusBarNotifChips.isEnabled() 1415 && mPinnedStatus.getValue() == PinnedStatus.PinnedByUser) { 1416 mEarliestRemovalTime = now + mMinimumDisplayTimeForUserInitiated; 1417 } else { 1418 mEarliestRemovalTime = now + mMinimumDisplayTimeDefault; 1419 } 1420 } 1421 1422 if (updatePostTime) { 1423 mPostTime = Math.max(mPostTime, now); 1424 } 1425 }; 1426 mAvalancheController.update(this, runnable, "updateEntry reason:" 1427 + reason + " updatePostTime:" + updatePostTime); 1428 1429 if (isSticky()) { 1430 cancelAutoRemovalCallbacks("updateEntry (sticky)"); 1431 return; 1432 } 1433 1434 FinishTimeUpdater finishTimeCalculator = () -> { 1435 RemainingDuration remainingDuration = 1436 mAvalancheController.getDuration(this, mAutoDismissTime); 1437 1438 if (remainingDuration instanceof RemainingDuration.HideImmediately) { 1439 /* Check if */ StatusBarNotifChips.isUnexpectedlyInLegacyMode(); 1440 return 0; 1441 } 1442 1443 int remainingTimeoutMs; 1444 if (isStickyForSomeTime()) { 1445 remainingTimeoutMs = mStickyForSomeTimeAutoDismissTime; 1446 } else { 1447 remainingTimeoutMs = 1448 ((RemainingDuration.UpdatedDuration) remainingDuration).getDuration(); 1449 } 1450 final long duration = getRecommendedHeadsUpTimeoutMs(remainingTimeoutMs); 1451 final long timeoutTimestamp = 1452 mPostTime + duration + (extended ? mExtensionTime : 0); 1453 1454 final long now = mSystemClock.elapsedRealtime(); 1455 return NotificationThrottleHun.isEnabled() 1456 ? Math.max(timeoutTimestamp, mEarliestRemovalTime) - now 1457 : Math.max(timeoutTimestamp - now, mMinimumDisplayTimeDefault); 1458 }; 1459 scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); 1460 1461 // Notify the manager, that the posted time has changed. 1462 updateTopHeadsUpFlow(); 1463 1464 mEntriesToRemoveAfterExpand.remove(mEntry); 1465 if (!NotificationThrottleHun.isEnabled()) { 1466 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); 1467 } 1468 } 1469 extendPulse()1470 private void extendPulse() { 1471 if (!extended) { 1472 extended = true; 1473 updateEntry(false, "extendPulse()"); 1474 } 1475 } 1476 1477 /** 1478 * Whether or not the notification is "sticky" i.e. should stay on screen regardless 1479 * of the timer (forever) and should be removed externally. 1480 * @return true if the notification is sticky 1481 */ isSticky()1482 public boolean isSticky() { 1483 if (mGutsShownPinned) return true; 1484 1485 if (mEntry == null) return false; 1486 1487 if (ExpandHeadsUpOnInlineReply.isEnabled()) { 1488 // we don't consider pinned and expanded huns as sticky after the remote input 1489 // has been activated for them 1490 if (!mRemoteInputActive && mRemoteInputActivatedAtLeastOnce) { 1491 return false; 1492 } 1493 } 1494 1495 // Promoted notifications are always shown as expanded, and we don't want them to ever 1496 // be sticky. 1497 boolean isStickyDueToExpansion = 1498 mEntry.isRowPinned() && mExpanded && !mEntry.isPromotedOngoing(); 1499 1500 return isStickyDueToExpansion 1501 || mRemoteInputActive 1502 || hasFullScreenIntent(mEntry); 1503 } 1504 isStickyForSomeTime()1505 public boolean isStickyForSomeTime() { 1506 if (mEntry == null) return false; 1507 1508 return mEntry.isStickyAndNotDemoted(); 1509 } 1510 1511 /** 1512 * Whether the notification has been on screen long enough and can be removed. 1513 * @return true if the notification has been on screen long enough 1514 */ wasShownLongEnough()1515 public boolean wasShownLongEnough() { 1516 return mEarliestRemovalTime < mSystemClock.elapsedRealtime(); 1517 } 1518 compareNonTimeFields(HeadsUpEntry headsUpEntry)1519 public int compareNonTimeFields(HeadsUpEntry headsUpEntry) { 1520 if (mEntry == null && headsUpEntry.mEntry == null) { 1521 return 0; 1522 } else if (headsUpEntry.mEntry == null) { 1523 return -1; 1524 } else if (mEntry == null) { 1525 return 1; 1526 } 1527 1528 boolean selfFullscreen = hasFullScreenIntent(mEntry); 1529 boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry); 1530 if (selfFullscreen && !otherFullscreen) { 1531 return -1; 1532 } else if (!selfFullscreen && otherFullscreen) { 1533 return 1; 1534 } 1535 1536 boolean selfCall = isCriticalCallNotif(mEntry); 1537 boolean otherCall = isCriticalCallNotif(headsUpEntry.mEntry); 1538 1539 if (selfCall && !otherCall) { 1540 return -1; 1541 } else if (!selfCall && otherCall) { 1542 return 1; 1543 } 1544 1545 if (mRemoteInputActive && !headsUpEntry.mRemoteInputActive) { 1546 return -1; 1547 } else if (!mRemoteInputActive && headsUpEntry.mRemoteInputActive) { 1548 return 1; 1549 } 1550 return 0; 1551 } 1552 compareTo(@onNull HeadsUpEntry headsUpEntry)1553 public int compareTo(@NonNull HeadsUpEntry headsUpEntry) { 1554 if (mEntry == null && headsUpEntry.mEntry == null) { 1555 return 0; 1556 } else if (headsUpEntry.mEntry == null) { 1557 return -1; 1558 } else if (mEntry == null) { 1559 return 1; 1560 } 1561 boolean isPinned = mEntry.isRowPinned(); 1562 boolean otherPinned = headsUpEntry.mEntry.isRowPinned(); 1563 if (isPinned && !otherPinned) { 1564 return -1; 1565 } else if (!isPinned && otherPinned) { 1566 return 1; 1567 } 1568 int nonTimeCompareResult = compareNonTimeFields(headsUpEntry); 1569 if (nonTimeCompareResult != 0) { 1570 return nonTimeCompareResult; 1571 } 1572 if (mPostTime > headsUpEntry.mPostTime) { 1573 return -1; 1574 } else if (mPostTime == headsUpEntry.mPostTime) { 1575 return mEntry.getKey().compareTo(headsUpEntry.mEntry.getKey()); 1576 } else { 1577 return 1; 1578 } 1579 } 1580 1581 @Override hashCode()1582 public int hashCode() { 1583 if (mEntry == null) return super.hashCode(); 1584 int result = mEntry.getKey().hashCode(); 1585 result = 31 * result; 1586 return result; 1587 } 1588 1589 @Override equals(@ullable Object o)1590 public boolean equals(@Nullable Object o) { 1591 if (this == o) return true; 1592 if (!(o instanceof HeadsUpEntry otherHeadsUpEntry)) return false; 1593 if (mEntry != null && otherHeadsUpEntry.mEntry != null) { 1594 return mEntry.getKey().equals(otherHeadsUpEntry.mEntry.getKey()); 1595 } 1596 return false; 1597 } 1598 setExpanded(boolean expanded)1599 public void setExpanded(boolean expanded) { 1600 if (this.mExpanded == expanded) { 1601 return; 1602 } 1603 1604 this.mExpanded = expanded; 1605 if (expanded) { 1606 cancelAutoRemovalCallbacks("setExpanded(true)"); 1607 } else { 1608 updateEntry(false /* updatePostTime */, "setExpanded(false)"); 1609 } 1610 } 1611 setGutsShownPinned(boolean gutsShownPinned)1612 public void setGutsShownPinned(boolean gutsShownPinned) { 1613 if (mGutsShownPinned == gutsShownPinned) { 1614 return; 1615 } 1616 1617 mGutsShownPinned = gutsShownPinned; 1618 if (gutsShownPinned) { 1619 cancelAutoRemovalCallbacks("setGutsShownPinned(true)"); 1620 } else { 1621 updateEntry(false /* updatePostTime */, "setGutsShownPinned(false)"); 1622 } 1623 } 1624 reset()1625 public void reset() { 1626 NotificationThrottleHun.assertInLegacyMode(); 1627 cancelAutoRemovalCallbacks("reset()"); 1628 mEntry = null; 1629 mRemoveRunnable = null; 1630 mExpanded = false; 1631 mRemoteInputActive = false; 1632 mGutsShownPinned = false; 1633 extended = false; 1634 } 1635 1636 /** 1637 * Clear any pending removal runnables. 1638 */ cancelAutoRemovalCallbacks(@ullable String reason)1639 public void cancelAutoRemovalCallbacks(@Nullable String reason) { 1640 Runnable runnable = () -> { 1641 final boolean removed = cancelAutoRemovalCallbackInternal(); 1642 1643 if (removed) { 1644 mLogger.logAutoRemoveCanceled(mEntry, reason); 1645 } 1646 }; 1647 if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) { 1648 mLogger.logAutoRemoveCancelRequest(this.mEntry, reason); 1649 mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks"); 1650 } else { 1651 // Just removed 1652 runnable.run(); 1653 } 1654 } 1655 scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, @NonNull String reason)1656 private void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, 1657 @NonNull String reason) { 1658 if (mEntry == null) { 1659 Log.wtf(TAG, "#scheduleAutoRemovalCallback with null mEntry; returning early"); 1660 return; 1661 } 1662 mLogger.logAutoRemoveRequest(mEntry, reason); 1663 Runnable runnable = () -> { 1664 long delayMs = finishTimeCalculator.updateAndGetTimeRemaining(); 1665 1666 if (mRemoveRunnable == null) { 1667 Log.wtf(TAG, "scheduleAutoRemovalCallback with no callback set"); 1668 return; 1669 } 1670 1671 final boolean deletedExistingRemovalRunnable = cancelAutoRemovalCallbackInternal(); 1672 mCancelRemoveRunnable = mExecutor.executeDelayed(mRemoveRunnable, 1673 delayMs); 1674 1675 if (deletedExistingRemovalRunnable) { 1676 mLogger.logAutoRemoveRescheduled(mEntry, delayMs, reason); 1677 } else { 1678 mLogger.logAutoRemoveScheduled(mEntry, delayMs, reason); 1679 } 1680 }; 1681 mAvalancheController.update(this, runnable, 1682 reason + " scheduleAutoRemovalCallback"); 1683 } 1684 cancelAutoRemovalCallbackInternal()1685 public boolean cancelAutoRemovalCallbackInternal() { 1686 final boolean scheduled = (mCancelRemoveRunnable != null); 1687 1688 if (scheduled) { 1689 mCancelRemoveRunnable.run(); // Delete removal runnable from Executor queue 1690 mCancelRemoveRunnable = null; 1691 } 1692 1693 return scheduled; 1694 } 1695 1696 /** 1697 * Remove the entry at the earliest allowed removal time. 1698 */ removeAsSoonAsPossible()1699 public void removeAsSoonAsPossible() { 1700 if (mRemoveRunnable != null) { 1701 1702 FinishTimeUpdater finishTimeCalculator = () -> 1703 mEarliestRemovalTime - mSystemClock.elapsedRealtime(); 1704 scheduleAutoRemovalCallback(finishTimeCalculator, "removeAsSoonAsPossible"); 1705 } 1706 } 1707 1708 /** Creates a runnable to remove this notification from the alerting entries. */ createRemoveRunnable(NotificationEntry entry)1709 private Runnable createRemoveRunnable(NotificationEntry entry) { 1710 return () -> { 1711 if (!NotificationThrottleHun.isEnabled() 1712 && !mVisualStabilityProvider.isReorderingAllowed() 1713 // We don't want to allow reordering while pulsing, but headsup need to 1714 // time out anyway 1715 && !entry.showingPulsing()) { 1716 mEntriesToRemoveWhenReorderingAllowed.add(entry); 1717 mVisualStabilityProvider.addTemporaryReorderingAllowedListener( 1718 mOnReorderingAllowedListener); 1719 } else if (mTrackingHeadsUp.getValue()) { 1720 mEntriesToRemoveAfterExpand.add(entry); 1721 mLogger.logRemoveEntryAfterExpand(entry); 1722 } else if (mVisualStabilityProvider.isReorderingAllowed() 1723 || entry.showingPulsing()) { 1724 removeEntry(entry.getKey(), "createRemoveRunnable"); 1725 } 1726 }; 1727 } 1728 1729 /** 1730 * Calculate what the post time of a notification is at some current time. 1731 * @return the post time 1732 */ calculatePostTime()1733 private long calculatePostTime() { 1734 // The actual post time will be just after the heads-up really slided in 1735 return mSystemClock.elapsedRealtime() + mTouchAcceptanceDelay; 1736 } 1737 1738 /** 1739 * Get user-preferred or default timeout duration. The larger one will be returned. 1740 * @return milliseconds before auto-dismiss 1741 */ getRecommendedHeadsUpTimeoutMs(int requestedTimeout)1742 private int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) { 1743 return mAccessibilityMgr.getRecommendedTimeoutMillis( 1744 requestedTimeout, 1745 AccessibilityManager.FLAG_CONTENT_CONTROLS 1746 | AccessibilityManager.FLAG_CONTENT_ICONS 1747 | AccessibilityManager.FLAG_CONTENT_TEXT); 1748 } 1749 } 1750 } 1751