1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.phone;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Region;
24 import android.os.Handler;
25 import android.util.ArrayMap;
26 import android.util.Pools;
27 
28 import androidx.collection.ArraySet;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.internal.logging.UiEventLogger;
32 import com.android.internal.policy.SystemBarUtils;
33 import com.android.systemui.dagger.SysUISingleton;
34 import com.android.systemui.dagger.qualifiers.Main;
35 import com.android.systemui.plugins.statusbar.StatusBarStateController;
36 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
37 import com.android.systemui.res.R;
38 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
39 import com.android.systemui.statusbar.StatusBarState;
40 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
41 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
42 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
43 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
44 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
45 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository;
46 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
47 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
48 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
49 import com.android.systemui.statusbar.policy.AnimationStateHandler;
50 import com.android.systemui.statusbar.policy.AvalancheController;
51 import com.android.systemui.statusbar.policy.BaseHeadsUpManager;
52 import com.android.systemui.statusbar.policy.ConfigurationController;
53 import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
54 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
55 import com.android.systemui.statusbar.policy.OnHeadsUpPhoneListenerChange;
56 import com.android.systemui.util.concurrency.DelayableExecutor;
57 import com.android.systemui.util.kotlin.JavaAdapter;
58 import com.android.systemui.util.settings.GlobalSettings;
59 import com.android.systemui.util.time.SystemClock;
60 
61 import kotlinx.coroutines.flow.Flow;
62 import kotlinx.coroutines.flow.MutableStateFlow;
63 import kotlinx.coroutines.flow.StateFlow;
64 import kotlinx.coroutines.flow.StateFlowKt;
65 
66 import java.io.PrintWriter;
67 import java.util.ArrayList;
68 import java.util.HashSet;
69 import java.util.List;
70 import java.util.Objects;
71 import java.util.Set;
72 import java.util.Stack;
73 
74 import javax.inject.Inject;
75 
76 /** A implementation of HeadsUpManager for phone. */
77 @SysUISingleton
78 public class HeadsUpManagerPhone extends BaseHeadsUpManager implements
79         HeadsUpRepository, OnHeadsUpChangedListener {
80     private static final String TAG = "HeadsUpManagerPhone";
81 
82     @VisibleForTesting
83     public final int mExtensionTime;
84     private final KeyguardBypassController mBypassController;
85     private final GroupMembershipManager mGroupMembershipManager;
86     private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>();
87     private final VisualStabilityProvider mVisualStabilityProvider;
88 
89     private final AvalancheController mAvalancheController;
90 
91     // TODO(b/328393698) move the topHeadsUpRow logic to an interactor
92     private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow =
93             StateFlowKt.MutableStateFlow(null);
94     private final MutableStateFlow<Set<HeadsUpRowRepository>> mHeadsUpNotificationRows =
95             StateFlowKt.MutableStateFlow(new HashSet<>());
96     private final MutableStateFlow<Boolean> mHeadsUpAnimatingAway =
97             StateFlowKt.MutableStateFlow(false);
98     private boolean mReleaseOnExpandFinish;
99     private boolean mTrackingHeadsUp;
100     private final HashSet<String> mSwipedOutKeys = new HashSet<>();
101     private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
102     private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
103             = new ArraySet<>();
104     private boolean mIsExpanded;
105     private int mStatusBarState;
106     private AnimationStateHandler mAnimationStateHandler;
107     private int mHeadsUpInset;
108 
109     // Used for determining the region for touch interaction
110     private final Region mTouchableRegion = new Region();
111 
112     private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() {
113         private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>();
114 
115         @Override
116         public HeadsUpEntryPhone acquire() {
117             NotificationsHeadsUpRefactor.assertInLegacyMode();
118             if (!mPoolObjects.isEmpty()) {
119                 return mPoolObjects.pop();
120             }
121             return new HeadsUpEntryPhone();
122         }
123 
124         @Override
125         public boolean release(@NonNull HeadsUpEntryPhone instance) {
126             NotificationsHeadsUpRefactor.assertInLegacyMode();
127             mPoolObjects.push(instance);
128             return true;
129         }
130     };
131 
132     ///////////////////////////////////////////////////////////////////////////////////////////////
133     //  Constructor:
134     @Inject
HeadsUpManagerPhone( @onNull final Context context, HeadsUpManagerLogger logger, StatusBarStateController statusBarStateController, KeyguardBypassController bypassController, GroupMembershipManager groupMembershipManager, VisualStabilityProvider visualStabilityProvider, ConfigurationController configurationController, @Main Handler handler, GlobalSettings globalSettings, SystemClock systemClock, @Main DelayableExecutor executor, AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger, JavaAdapter javaAdapter, ShadeInteractor shadeInteractor, AvalancheController avalancheController)135     public HeadsUpManagerPhone(
136             @NonNull final Context context,
137             HeadsUpManagerLogger logger,
138             StatusBarStateController statusBarStateController,
139             KeyguardBypassController bypassController,
140             GroupMembershipManager groupMembershipManager,
141             VisualStabilityProvider visualStabilityProvider,
142             ConfigurationController configurationController,
143             @Main Handler handler,
144             GlobalSettings globalSettings,
145             SystemClock systemClock,
146             @Main DelayableExecutor executor,
147             AccessibilityManagerWrapper accessibilityManagerWrapper,
148             UiEventLogger uiEventLogger,
149             JavaAdapter javaAdapter,
150             ShadeInteractor shadeInteractor,
151             AvalancheController avalancheController) {
152         super(context, logger, handler, globalSettings, systemClock, executor,
153                 accessibilityManagerWrapper, uiEventLogger, avalancheController);
154         Resources resources = mContext.getResources();
155         mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time);
156         statusBarStateController.addCallback(mStatusBarStateListener);
157         mBypassController = bypassController;
158         mGroupMembershipManager = groupMembershipManager;
159         mVisualStabilityProvider = visualStabilityProvider;
160         mAvalancheController = avalancheController;
161 
162         updateResources();
163         configurationController.addCallback(new ConfigurationController.ConfigurationListener() {
164             @Override
165             public void onDensityOrFontScaleChanged() {
166                 updateResources();
167             }
168 
169             @Override
170             public void onThemeChanged() {
171                 updateResources();
172             }
173         });
174         if (!NotificationsHeadsUpRefactor.isEnabled()) {
175             javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(),
176                     this::onShadeOrQsExpanded);
177         }
178     }
179 
setAnimationStateHandler(AnimationStateHandler handler)180     public void setAnimationStateHandler(AnimationStateHandler handler) {
181         mAnimationStateHandler = handler;
182     }
183 
updateResources()184     private void updateResources() {
185         Resources resources = mContext.getResources();
186         mHeadsUpInset = SystemBarUtils.getStatusBarHeight(mContext)
187                 + resources.getDimensionPixelSize(R.dimen.heads_up_status_bar_padding);
188     }
189 
190     ///////////////////////////////////////////////////////////////////////////////////////////////
191     //  Public methods:
192 
193     /**
194      * Add a listener to receive callbacks {@link #setHeadsUpAnimatingAway(boolean)}
195      */
196     @Override
addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener)197     public void addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener) {
198         mHeadsUpPhoneListeners.add(listener);
199     }
200 
201     /**
202      * Gets the touchable region needed for heads up notifications. Returns null if no touchable
203      * region is required (ie: no heads up notification currently exists).
204      */
205     // TODO(b/347007367): With scene container enabled this method may report outdated regions
206     @Override
getTouchableRegion()207     public @Nullable Region getTouchableRegion() {
208         NotificationEntry topEntry = getTopEntry();
209 
210         // This call could be made in an inconsistent state while the pinnedMode hasn't been
211         // updated yet, but callbacks leading out of the headsUp manager, querying it. Let's
212         // therefore also check if the topEntry is null.
213         if (!hasPinnedHeadsUp() || topEntry == null) {
214             return null;
215         } else {
216             if (topEntry.rowIsChildInGroup()) {
217                 final NotificationEntry groupSummary =
218                         mGroupMembershipManager.getGroupSummary(topEntry);
219                 if (groupSummary != null) {
220                     topEntry = groupSummary;
221                 }
222             }
223             ExpandableNotificationRow topRow = topEntry.getRow();
224             int[] tmpArray = new int[2];
225             topRow.getLocationOnScreen(tmpArray);
226             int minX = tmpArray[0];
227             int maxX = tmpArray[0] + topRow.getWidth();
228             int height = topRow.getIntrinsicHeight();
229             final boolean stretchToTop = tmpArray[1] <= mHeadsUpInset;
230             mTouchableRegion.set(minX, stretchToTop ? 0 : tmpArray[1], maxX, tmpArray[1] + height);
231             return mTouchableRegion;
232         }
233     }
234 
235     /**
236      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
237      * that a user might have consciously clicked on it.
238      *
239      * @param key the key of the touched notification
240      * @return whether the touch is invalid and should be discarded
241      */
242     @Override
shouldSwallowClick(@onNull String key)243     public boolean shouldSwallowClick(@NonNull String key) {
244         BaseHeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
245         return entry != null && mSystemClock.elapsedRealtime() < entry.mPostTime;
246     }
247 
onExpandingFinished()248     public void onExpandingFinished() {
249         if (mReleaseOnExpandFinish) {
250             releaseAllImmediately();
251             mReleaseOnExpandFinish = false;
252         } else {
253             for (NotificationEntry entry : mEntriesToRemoveAfterExpand) {
254                 if (isHeadsUpEntry(entry.getKey())) {
255                     // Maybe the heads-up was removed already
256                     removeEntry(entry.getKey(), "onExpandingFinished");
257                 }
258             }
259         }
260         mEntriesToRemoveAfterExpand.clear();
261     }
262 
263     /**
264      * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
265      * from the list even after a Heads Up Notification is gone.
266      */
setTrackingHeadsUp(boolean trackingHeadsUp)267     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
268         mTrackingHeadsUp = trackingHeadsUp;
269     }
270 
onShadeOrQsExpanded(Boolean isExpanded)271     private void onShadeOrQsExpanded(Boolean isExpanded) {
272         NotificationsHeadsUpRefactor.assertInLegacyMode();
273         if (isExpanded != mIsExpanded) {
274             mIsExpanded = isExpanded;
275             if (isExpanded) {
276                 mHeadsUpAnimatingAway.setValue(false);
277             }
278         }
279     }
280 
281     /**
282      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
283      * animating out. This is used to keep the touchable regions in a reasonable state.
284      */
285     @Override
setHeadsUpAnimatingAway(boolean headsUpAnimatingAway)286     public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
287         if (headsUpAnimatingAway != mHeadsUpAnimatingAway.getValue()) {
288             for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) {
289                 listener.onHeadsUpAnimatingAwayStateChanged(headsUpAnimatingAway);
290             }
291             mHeadsUpAnimatingAway.setValue(headsUpAnimatingAway);
292         }
293     }
294 
295     /**
296      * Notifies that a remote input textbox in notification gets active or inactive.
297      *
298      * @param entry             The entry of the target notification.
299      * @param remoteInputActive True to notify active, False to notify inactive.
300      */
setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)301     public void setRemoteInputActive(
302             @NonNull NotificationEntry entry, boolean remoteInputActive) {
303         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.getKey());
304         if (headsUpEntry != null && headsUpEntry.mRemoteInputActive != remoteInputActive) {
305             headsUpEntry.mRemoteInputActive = remoteInputActive;
306             if (remoteInputActive) {
307                 headsUpEntry.cancelAutoRemovalCallbacks("setRemoteInputActive(true)");
308             } else {
309                 headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)");
310             }
311             onEntryUpdated(headsUpEntry);
312         }
313     }
314 
315     /**
316      * Sets whether an entry's guts are exposed and therefore it should stick in the heads up
317      * area if it's pinned until it's hidden again.
318      */
setGutsShown(@onNull NotificationEntry entry, boolean gutsShown)319     public void setGutsShown(@NonNull NotificationEntry entry, boolean gutsShown) {
320         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
321         if (!(headsUpEntry instanceof HeadsUpEntryPhone)) return;
322         HeadsUpEntryPhone headsUpEntryPhone = (HeadsUpEntryPhone)headsUpEntry;
323         if (entry.isRowPinned() || !gutsShown) {
324             headsUpEntryPhone.setGutsShownPinned(gutsShown);
325         }
326     }
327 
328     /**
329      * Extends the lifetime of the currently showing pulsing notification so that the pulse lasts
330      * longer.
331      */
extendHeadsUp()332     public void extendHeadsUp() {
333         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
334         if (topEntry == null) {
335             return;
336         }
337         topEntry.extendPulse();
338     }
339 
340     ///////////////////////////////////////////////////////////////////////////////////////////////
341     //  HeadsUpManager public methods overrides and overloads:
342 
343     @Override
isTrackingHeadsUp()344     public boolean isTrackingHeadsUp() {
345         return mTrackingHeadsUp;
346     }
347 
348     @Override
snooze()349     public void snooze() {
350         super.snooze();
351         mReleaseOnExpandFinish = true;
352     }
353 
addSwipedOutNotification(@onNull String key)354     public void addSwipedOutNotification(@NonNull String key) {
355         mSwipedOutKeys.add(key);
356     }
357 
358     @Override
removeNotification(@onNull String key, boolean releaseImmediately, boolean animate)359     public boolean removeNotification(@NonNull String key, boolean releaseImmediately,
360             boolean animate) {
361         if (animate) {
362             return removeNotification(key, releaseImmediately);
363         } else {
364             mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
365             boolean removed = removeNotification(key, releaseImmediately);
366             mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
367             return removed;
368         }
369     }
370 
371     ///////////////////////////////////////////////////////////////////////////////////////////////
372     //  Dumpable overrides:
373 
374     @Override
dump(PrintWriter pw, String[] args)375     public void dump(PrintWriter pw, String[] args) {
376         pw.println("HeadsUpManagerPhone state:");
377         dumpInternal(pw, args);
378     }
379 
380     ///////////////////////////////////////////////////////////////////////////////////////////////
381     //  OnReorderingAllowedListener:
382 
383     private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> {
384         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
385         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
386             if (isHeadsUpEntry(entry.getKey())) {
387                 // Maybe the heads-up was removed already
388                 removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
389             }
390         }
391         mEntriesToRemoveWhenReorderingAllowed.clear();
392         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
393     };
394 
395     ///////////////////////////////////////////////////////////////////////////////////////////////
396     //  HeadsUpManager utility (protected) methods overrides:
397 
398     @NonNull
399     @Override
createHeadsUpEntry(NotificationEntry entry)400     protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
401         if (NotificationsHeadsUpRefactor.isEnabled()) {
402             return new HeadsUpEntryPhone(entry);
403         } else {
404             HeadsUpEntryPhone headsUpEntry = mEntryPool.acquire();
405             headsUpEntry.setEntry(entry);
406             return headsUpEntry;
407         }
408     }
409 
410     @Override
onEntryAdded(HeadsUpEntry headsUpEntry)411     protected void onEntryAdded(HeadsUpEntry headsUpEntry) {
412         super.onEntryAdded(headsUpEntry);
413         updateTopHeadsUpFlow();
414         updateHeadsUpFlow();
415     }
416 
417     @Override
onEntryUpdated(HeadsUpEntry headsUpEntry)418     protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
419         super.onEntryUpdated(headsUpEntry);
420         // no need to update the list here
421         updateTopHeadsUpFlow();
422     }
423 
424     @Override
onEntryRemoved(HeadsUpEntry headsUpEntry)425     protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
426         super.onEntryRemoved(headsUpEntry);
427         if (!NotificationsHeadsUpRefactor.isEnabled()) {
428             mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
429         }
430         updateTopHeadsUpFlow();
431         updateHeadsUpFlow();
432     }
433 
updateTopHeadsUpFlow()434     private void updateTopHeadsUpFlow() {
435         mTopHeadsUpRow.setValue((HeadsUpRowRepository) getTopHeadsUpEntry());
436     }
437 
updateHeadsUpFlow()438     private void updateHeadsUpFlow() {
439         mHeadsUpNotificationRows.setValue(new HashSet<>(getHeadsUpEntryPhoneMap().values()));
440     }
441 
442     @Override
shouldHeadsUpBecomePinned(NotificationEntry entry)443     protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) {
444         boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded;
445         if (mBypassController.getBypassEnabled()) {
446             pin |= mStatusBarState == StatusBarState.KEYGUARD;
447         }
448         return pin || super.shouldHeadsUpBecomePinned(entry);
449     }
450 
451     @Override
dumpInternal(PrintWriter pw, String[] args)452     protected void dumpInternal(PrintWriter pw, String[] args) {
453         super.dumpInternal(pw, args);
454         pw.print("  mBarState=");
455         pw.println(mStatusBarState);
456         pw.print("  mTouchableRegion=");
457         pw.println(mTouchableRegion);
458     }
459 
460     ///////////////////////////////////////////////////////////////////////////////////////////////
461     //  Private utility methods:
462 
463     @NonNull
getHeadsUpEntryPhoneMap()464     private ArrayMap<String, HeadsUpEntryPhone> getHeadsUpEntryPhoneMap() {
465         //noinspection unchecked
466         return (ArrayMap<String, HeadsUpEntryPhone>) ((ArrayMap) mHeadsUpEntryMap);
467     }
468 
469     @Nullable
getHeadsUpEntryPhone(@onNull String key)470     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
471         return (HeadsUpEntryPhone) mHeadsUpEntryMap.get(key);
472     }
473 
474     @Nullable
getTopHeadsUpEntryPhone()475     private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
476         if (NotificationsHeadsUpRefactor.isEnabled()) {
477             return (HeadsUpEntryPhone) mTopHeadsUpRow.getValue();
478         } else {
479             return (HeadsUpEntryPhone) getTopHeadsUpEntry();
480         }
481     }
482 
483     @Override
canRemoveImmediately(@onNull String key)484     public boolean canRemoveImmediately(@NonNull String key) {
485         if (mSwipedOutKeys.contains(key)) {
486             // We always instantly dismiss views being manually swiped out.
487             mSwipedOutKeys.remove(key);
488             return true;
489         }
490 
491         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
492         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
493 
494         return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
495     }
496 
497     @Override
498     @NonNull
getTopHeadsUpRow()499     public Flow<HeadsUpRowRepository> getTopHeadsUpRow() {
500         return mTopHeadsUpRow;
501     }
502 
503     @Override
504     @NonNull
getActiveHeadsUpRows()505     public Flow<Set<HeadsUpRowRepository>> getActiveHeadsUpRows() {
506         return mHeadsUpNotificationRows;
507     }
508 
509     @Override
510     @NonNull
isHeadsUpAnimatingAway()511     public StateFlow<Boolean> isHeadsUpAnimatingAway() {
512         return mHeadsUpAnimatingAway;
513     }
514 
515     @Override
isHeadsUpAnimatingAwayValue()516     public boolean isHeadsUpAnimatingAwayValue() {
517         return mHeadsUpAnimatingAway.getValue();
518     }
519 
520     ///////////////////////////////////////////////////////////////////////////////////////////////
521     //  HeadsUpEntryPhone:
522 
523     protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry implements
524             HeadsUpRowRepository {
525 
526         private boolean mGutsShownPinned;
527         private final MutableStateFlow<Boolean> mIsPinned = StateFlowKt.MutableStateFlow(false);
528 
529         /**
530          * If the time this entry has been on was extended
531          */
532         private boolean extended;
533 
534         @Override
isSticky()535         public boolean isSticky() {
536             return super.isSticky() || mGutsShownPinned;
537         }
538 
HeadsUpEntryPhone()539         public HeadsUpEntryPhone() {
540             super();
541         }
542 
HeadsUpEntryPhone(NotificationEntry entry)543         public HeadsUpEntryPhone(NotificationEntry entry) {
544             super(entry);
545         }
546 
547         @Override
548         @NonNull
getKey()549         public String getKey() {
550             return requireEntry().getKey();
551         }
552 
553         @Override
554         @NonNull
isPinned()555         public StateFlow<Boolean> isPinned() {
556             return mIsPinned;
557         }
558 
559         @Override
setRowPinned(boolean pinned)560         protected void setRowPinned(boolean pinned) {
561             // TODO(b/327624082): replace this super call with a ViewBinder
562             super.setRowPinned(pinned);
563             mIsPinned.setValue(pinned);
564         }
565 
566         @Override
createRemoveRunnable(NotificationEntry entry)567         protected Runnable createRemoveRunnable(NotificationEntry entry) {
568             return  () -> {
569                 if (!mVisualStabilityProvider.isReorderingAllowed()
570                         // We don't want to allow reordering while pulsing, but headsup need to
571                         // time out anyway
572                         && !entry.showingPulsing()) {
573                     mEntriesToRemoveWhenReorderingAllowed.add(entry);
574                     mVisualStabilityProvider.addTemporaryReorderingAllowedListener(
575                             mOnReorderingAllowedListener);
576                 } else if (mTrackingHeadsUp) {
577                     mEntriesToRemoveAfterExpand.add(entry);
578                 } else {
579                     removeEntry(entry.getKey(), "createRemoveRunnable");
580                 }
581             };
582         }
583 
584         @Override
updateEntry(boolean updatePostTime, String reason)585         public void updateEntry(boolean updatePostTime, String reason) {
586             super.updateEntry(updatePostTime, reason);
587 
588             if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
589                 mEntriesToRemoveAfterExpand.remove(mEntry);
590             }
591             if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
592                 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
593             }
594         }
595 
596         @Override
setExpanded(boolean expanded)597         public void setExpanded(boolean expanded) {
598             if (this.mExpanded == expanded) {
599                 return;
600             }
601 
602             this.mExpanded = expanded;
603             if (expanded) {
604                 cancelAutoRemovalCallbacks("setExpanded(true)");
605             } else {
606                 updateEntry(false /* updatePostTime */, "setExpanded(false)");
607             }
608         }
609 
setGutsShownPinned(boolean gutsShownPinned)610         public void setGutsShownPinned(boolean gutsShownPinned) {
611             if (mGutsShownPinned == gutsShownPinned) {
612                 return;
613             }
614 
615             mGutsShownPinned = gutsShownPinned;
616             if (gutsShownPinned) {
617                 cancelAutoRemovalCallbacks("setGutsShownPinned(true)");
618             } else {
619                 updateEntry(false /* updatePostTime */, "setGutsShownPinned(false)");
620             }
621         }
622 
623         @Override
reset()624         public void reset() {
625             super.reset();
626             mGutsShownPinned = false;
627             extended = false;
628         }
629 
extendPulse()630         private void extendPulse() {
631             if (!extended) {
632                 extended = true;
633                 updateEntry(false, "extendPulse()");
634             }
635         }
636 
637         @Override
calculateFinishTime()638         protected long calculateFinishTime() {
639             return super.calculateFinishTime() + (extended ? mExtensionTime : 0);
640         }
641 
642         @Override
643         @NonNull
getElementKey()644         public Object getElementKey() {
645             return requireEntry().getRow();
646         }
647 
requireEntry()648         private NotificationEntry requireEntry() {
649             /* check if */ NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode();
650             return Objects.requireNonNull(mEntry);
651         }
652     }
653 
654     private final StateListener mStatusBarStateListener = new StateListener() {
655         @Override
656         public void onStateChanged(int newState) {
657             boolean wasKeyguard = mStatusBarState == StatusBarState.KEYGUARD;
658             boolean isKeyguard = newState == StatusBarState.KEYGUARD;
659             mStatusBarState = newState;
660 
661             if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) {
662                 ArrayList<String> keysToRemove = new ArrayList<>();
663                 for (HeadsUpEntry entry : getHeadsUpEntryList()) {
664                     if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) {
665                         keysToRemove.add(entry.mEntry.getKey());
666                     }
667                 }
668                 for (String key : keysToRemove) {
669                     removeEntry(key, "mStatusBarStateListener");
670                 }
671             }
672         }
673 
674         @Override
675         public void onDozingChanged(boolean isDozing) {
676             if (!isDozing) {
677                 // Let's make sure all huns we got while dozing time out within the normal timeout
678                 // duration. Otherwise they could get stuck for a very long time
679                 for (HeadsUpEntry entry : getHeadsUpEntryList()) {
680                     entry.updateEntry(true /* updatePostTime */, "onDozingChanged(false)");
681                 }
682             }
683         }
684     };
685 }
686