• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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