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.Configuration;
23 import android.content.res.Resources;
24 import android.graphics.Rect;
25 import android.graphics.Region;
26 import android.util.Log;
27 import android.util.Pools;
28 import android.view.DisplayCutout;
29 import android.view.Gravity;
30 import android.view.View;
31 import android.view.ViewTreeObserver;
32 
33 import androidx.collection.ArraySet;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.systemui.Dumpable;
37 import com.android.systemui.R;
38 import com.android.systemui.ScreenDecorations;
39 import com.android.systemui.plugins.statusbar.StatusBarStateController;
40 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
41 import com.android.systemui.statusbar.StatusBarState;
42 import com.android.systemui.statusbar.notification.VisualStabilityManager;
43 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
44 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
45 import com.android.systemui.statusbar.policy.ConfigurationController;
46 import com.android.systemui.statusbar.policy.HeadsUpManager;
47 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
48 
49 import java.io.FileDescriptor;
50 import java.io.PrintWriter;
51 import java.util.HashSet;
52 import java.util.Stack;
53 
54 import javax.inject.Inject;
55 import javax.inject.Singleton;
56 
57 /**
58  * A implementation of HeadsUpManager for phone and car.
59  */
60 @Singleton
61 public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
62         VisualStabilityManager.Callback, OnHeadsUpChangedListener,
63         ConfigurationController.ConfigurationListener, StateListener {
64     private static final String TAG = "HeadsUpManagerPhone";
65 
66     @VisibleForTesting
67     final int mExtensionTime;
68     private final StatusBarStateController mStatusBarStateController;
69     private final KeyguardBypassController mBypassController;
70     private final int mAutoHeadsUpNotificationDecay;
71     private View mStatusBarWindowView;
72     private NotificationGroupManager mGroupManager;
73     private VisualStabilityManager mVisualStabilityManager;
74     private StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
75     private boolean mReleaseOnExpandFinish;
76 
77     private int mStatusBarHeight;
78     private int mHeadsUpInset;
79     private int mDisplayCutoutTouchableRegionSize;
80     private boolean mTrackingHeadsUp;
81     private HashSet<String> mSwipedOutKeys = new HashSet<>();
82     private HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
83     private HashSet<String> mKeysToRemoveWhenLeavingKeyguard = new HashSet<>();
84     private ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
85             = new ArraySet<>();
86     private boolean mIsExpanded;
87     private int[] mTmpTwoArray = new int[2];
88     private boolean mHeadsUpGoingAway;
89     private int mStatusBarState;
90     private Region mTouchableRegion = new Region();
91 
92     private AnimationStateHandler mAnimationStateHandler;
93 
94     private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() {
95         private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>();
96 
97         @Override
98         public HeadsUpEntryPhone acquire() {
99             if (!mPoolObjects.isEmpty()) {
100                 return mPoolObjects.pop();
101             }
102             return new HeadsUpEntryPhone();
103         }
104 
105         @Override
106         public boolean release(@NonNull HeadsUpEntryPhone instance) {
107             mPoolObjects.push(instance);
108             return true;
109         }
110     };
111 
112     ///////////////////////////////////////////////////////////////////////////////////////////////
113     //  Constructor:
114 
115     @Inject
HeadsUpManagerPhone(@onNull final Context context, StatusBarStateController statusBarStateController, KeyguardBypassController bypassController)116     public HeadsUpManagerPhone(@NonNull final Context context,
117             StatusBarStateController statusBarStateController,
118             KeyguardBypassController bypassController) {
119         super(context);
120         Resources resources = mContext.getResources();
121         mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time);
122         mAutoHeadsUpNotificationDecay = resources.getInteger(
123                 R.integer.auto_heads_up_notification_decay);
124         mStatusBarStateController = statusBarStateController;
125         mStatusBarStateController.addCallback(this);
126         mBypassController = bypassController;
127 
128         initResources();
129     }
130 
131 
setUp(@onNull View statusBarWindowView, @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar, @NonNull VisualStabilityManager visualStabilityManager)132     public void setUp(@NonNull View statusBarWindowView,
133             @NonNull NotificationGroupManager groupManager,
134             @NonNull StatusBar bar,
135             @NonNull VisualStabilityManager visualStabilityManager) {
136         mStatusBarWindowView = statusBarWindowView;
137         mStatusBarTouchableRegionManager = new StatusBarTouchableRegionManager(mContext, this, bar,
138                 statusBarWindowView);
139         mGroupManager = groupManager;
140         mVisualStabilityManager = visualStabilityManager;
141 
142         addListener(new OnHeadsUpChangedListener() {
143             @Override
144             public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
145                 if (Log.isLoggable(TAG, Log.WARN)) {
146                     Log.w(TAG, "onHeadsUpPinnedModeChanged");
147                 }
148                 mStatusBarTouchableRegionManager.updateTouchableRegion();
149             }
150         });
151     }
152 
setAnimationStateHandler(AnimationStateHandler handler)153     public void setAnimationStateHandler(AnimationStateHandler handler) {
154         mAnimationStateHandler = handler;
155     }
156 
initResources()157     private void initResources() {
158         Resources resources = mContext.getResources();
159         mStatusBarHeight = resources.getDimensionPixelSize(
160                 com.android.internal.R.dimen.status_bar_height);
161         mHeadsUpInset = mStatusBarHeight + resources.getDimensionPixelSize(
162                 R.dimen.heads_up_status_bar_padding);
163         mDisplayCutoutTouchableRegionSize = resources.getDimensionPixelSize(
164                 com.android.internal.R.dimen.display_cutout_touchable_region_size);
165     }
166 
167     @Override
onDensityOrFontScaleChanged()168     public void onDensityOrFontScaleChanged() {
169         super.onDensityOrFontScaleChanged();
170         initResources();
171     }
172 
173     @Override
onOverlayChanged()174     public void onOverlayChanged() {
175         initResources();
176     }
177 
178     ///////////////////////////////////////////////////////////////////////////////////////////////
179     //  Public methods:
180 
181     /**
182      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
183      * that a user might have consciously clicked on it.
184      *
185      * @param key the key of the touched notification
186      * @return whether the touch is invalid and should be discarded
187      */
shouldSwallowClick(@onNull String key)188     public boolean shouldSwallowClick(@NonNull String key) {
189         HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
190         return entry != null && mClock.currentTimeMillis() < entry.mPostTime;
191     }
192 
onExpandingFinished()193     public void onExpandingFinished() {
194         if (mReleaseOnExpandFinish) {
195             releaseAllImmediately();
196             mReleaseOnExpandFinish = false;
197         } else {
198             for (NotificationEntry entry : mEntriesToRemoveAfterExpand) {
199                 if (isAlerting(entry.key)) {
200                     // Maybe the heads-up was removed already
201                     removeAlertEntry(entry.key);
202                 }
203             }
204         }
205         mEntriesToRemoveAfterExpand.clear();
206     }
207 
208     /**
209      * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
210      * from the list even after a Heads Up Notification is gone.
211      */
setTrackingHeadsUp(boolean trackingHeadsUp)212     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
213         mTrackingHeadsUp = trackingHeadsUp;
214     }
215 
216     /**
217      * Notify that the status bar panel gets expanded or collapsed.
218      *
219      * @param isExpanded True to notify expanded, false to notify collapsed.
220      */
setIsPanelExpanded(boolean isExpanded)221     public void setIsPanelExpanded(boolean isExpanded) {
222         if (isExpanded != mIsExpanded) {
223             mIsExpanded = isExpanded;
224             if (isExpanded) {
225                 mHeadsUpGoingAway = false;
226             }
227             mStatusBarTouchableRegionManager.setIsStatusBarExpanded(isExpanded);
228             mStatusBarTouchableRegionManager.updateTouchableRegion();
229         }
230     }
231 
232     @Override
onStateChanged(int newState)233     public void onStateChanged(int newState) {
234         boolean wasKeyguard = mStatusBarState == StatusBarState.KEYGUARD;
235         boolean isKeyguard = newState == StatusBarState.KEYGUARD;
236         mStatusBarState = newState;
237         if (wasKeyguard && !isKeyguard && mKeysToRemoveWhenLeavingKeyguard.size() != 0) {
238             String[] keys = mKeysToRemoveWhenLeavingKeyguard.toArray(new String[0]);
239             for (String key : keys) {
240                 removeAlertEntry(key);
241             }
242             mKeysToRemoveWhenLeavingKeyguard.clear();
243         }
244     }
245 
246     @Override
onDozingChanged(boolean isDozing)247     public void onDozingChanged(boolean isDozing) {
248         if (!isDozing) {
249             // Let's make sure all huns we got while dozing time out within the normal timeout
250             // duration. Otherwise they could get stuck for a very long time
251             for (AlertEntry entry : mAlertEntries.values()) {
252                 entry.updateEntry(true /* updatePostTime */);
253             }
254         }
255     }
256 
257     @Override
isEntryAutoHeadsUpped(String key)258     public boolean isEntryAutoHeadsUpped(String key) {
259         HeadsUpEntryPhone headsUpEntryPhone = getHeadsUpEntryPhone(key);
260         if (headsUpEntryPhone == null) {
261             return false;
262         }
263         return headsUpEntryPhone.isAutoHeadsUp();
264     }
265 
266     /**
267      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
268      * animating out. This is used to keep the touchable regions in a reasonable state.
269      */
setHeadsUpGoingAway(boolean headsUpGoingAway)270     public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
271         if (headsUpGoingAway != mHeadsUpGoingAway) {
272             mHeadsUpGoingAway = headsUpGoingAway;
273             if (!headsUpGoingAway) {
274                 mStatusBarTouchableRegionManager.updateTouchableRegionAfterLayout();
275             } else {
276                 mStatusBarTouchableRegionManager.updateTouchableRegion();
277             }
278         }
279     }
280 
isHeadsUpGoingAway()281     public boolean isHeadsUpGoingAway() {
282         return mHeadsUpGoingAway;
283     }
284 
285     /**
286      * Notifies that a remote input textbox in notification gets active or inactive.
287      *
288      * @param entry             The entry of the target notification.
289      * @param remoteInputActive True to notify active, False to notify inactive.
290      */
setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)291     public void setRemoteInputActive(
292             @NonNull NotificationEntry entry, boolean remoteInputActive) {
293         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key);
294         if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
295             headsUpEntry.remoteInputActive = remoteInputActive;
296             if (remoteInputActive) {
297                 headsUpEntry.removeAutoRemovalCallbacks();
298             } else {
299                 headsUpEntry.updateEntry(false /* updatePostTime */);
300             }
301         }
302     }
303 
304     /**
305      * Sets whether an entry's menu row is exposed and therefore it should stick in the heads up
306      * area if it's pinned until it's hidden again.
307      */
setMenuShown(@onNull NotificationEntry entry, boolean menuShown)308     public void setMenuShown(@NonNull NotificationEntry entry, boolean menuShown) {
309         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.key);
310         if (headsUpEntry instanceof HeadsUpEntryPhone && entry.isRowPinned()) {
311             ((HeadsUpEntryPhone) headsUpEntry).setMenuShownPinned(menuShown);
312         }
313     }
314 
315     /**
316      * Extends the lifetime of the currently showing pulsing notification so that the pulse lasts
317      * longer.
318      */
extendHeadsUp()319     public void extendHeadsUp() {
320         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
321         if (topEntry == null) {
322             return;
323         }
324         topEntry.extendPulse();
325     }
326 
327     ///////////////////////////////////////////////////////////////////////////////////////////////
328     //  HeadsUpManager public methods overrides:
329 
330     @Override
isTrackingHeadsUp()331     public boolean isTrackingHeadsUp() {
332         return mTrackingHeadsUp;
333     }
334 
335     @Override
snooze()336     public void snooze() {
337         super.snooze();
338         mReleaseOnExpandFinish = true;
339     }
340 
addSwipedOutNotification(@onNull String key)341     public void addSwipedOutNotification(@NonNull String key) {
342         mSwipedOutKeys.add(key);
343     }
344 
345     ///////////////////////////////////////////////////////////////////////////////////////////////
346     //  Dumpable overrides:
347 
348     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)349     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
350         pw.println("HeadsUpManagerPhone state:");
351         dumpInternal(fd, pw, args);
352     }
353 
354     /**
355      * Update touch insets to include any area needed for touching a heads up notification.
356      *
357      * @param info Insets that will include heads up notification touch area after execution.
358      */
359     @Nullable
updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info)360     public void updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info) {
361         info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
362         info.touchableRegion.set(calculateTouchableRegion());
363     }
364 
calculateTouchableRegion()365     public Region calculateTouchableRegion() {
366         NotificationEntry topEntry = getTopEntry();
367         // This call could be made in an inconsistent state while the pinnedMode hasn't been
368         // updated yet, but callbacks leading out of the headsUp manager, querying it. Let's
369         // therefore also check if the topEntry is null.
370         if (!hasPinnedHeadsUp() || topEntry == null) {
371             mTouchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
372             updateRegionForNotch(mTouchableRegion);
373 
374         } else {
375             if (topEntry.isChildInGroup()) {
376                 final NotificationEntry groupSummary =
377                         mGroupManager.getGroupSummary(topEntry.notification);
378                 if (groupSummary != null) {
379                     topEntry = groupSummary;
380                 }
381             }
382             ExpandableNotificationRow topRow = topEntry.getRow();
383             topRow.getLocationOnScreen(mTmpTwoArray);
384             int minX = mTmpTwoArray[0];
385             int maxX = mTmpTwoArray[0] + topRow.getWidth();
386             int height = topRow.getIntrinsicHeight();
387             mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
388         }
389         return mTouchableRegion;
390     }
391 
updateRegionForNotch(Region region)392     private void updateRegionForNotch(Region region) {
393         DisplayCutout cutout = mStatusBarWindowView.getRootWindowInsets().getDisplayCutout();
394         if (cutout == null) {
395             return;
396         }
397 
398         // Expand touchable region such that we also catch touches that just start below the notch
399         // area.
400         Rect bounds = new Rect();
401         ScreenDecorations.DisplayCutoutView.boundsFromDirection(cutout, Gravity.TOP, bounds);
402         bounds.offset(0, mDisplayCutoutTouchableRegionSize);
403         region.union(bounds);
404     }
405 
406     @Override
shouldExtendLifetime(NotificationEntry entry)407     public boolean shouldExtendLifetime(NotificationEntry entry) {
408         // We should not defer the removal if reordering isn't allowed since otherwise
409         // these won't disappear until reordering is allowed again, which happens only once
410         // the notification panel is collapsed again.
411         return mVisualStabilityManager.isReorderingAllowed() && super.shouldExtendLifetime(entry);
412     }
413 
414     @Override
onConfigChanged(Configuration newConfig)415     public void onConfigChanged(Configuration newConfig) {
416         initResources();
417     }
418 
419     ///////////////////////////////////////////////////////////////////////////////////////////////
420     //  VisualStabilityManager.Callback overrides:
421 
422     @Override
onReorderingAllowed()423     public void onReorderingAllowed() {
424         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
425         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
426             if (isAlerting(entry.key)) {
427                 // Maybe the heads-up was removed already
428                 removeAlertEntry(entry.key);
429             }
430         }
431         mEntriesToRemoveWhenReorderingAllowed.clear();
432         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
433     }
434 
435     ///////////////////////////////////////////////////////////////////////////////////////////////
436     //  HeadsUpManager utility (protected) methods overrides:
437 
438     @Override
createAlertEntry()439     protected HeadsUpEntry createAlertEntry() {
440         return mEntryPool.acquire();
441     }
442 
443     @Override
onAlertEntryRemoved(AlertEntry alertEntry)444     protected void onAlertEntryRemoved(AlertEntry alertEntry) {
445         mKeysToRemoveWhenLeavingKeyguard.remove(alertEntry.mEntry.key);
446         super.onAlertEntryRemoved(alertEntry);
447         mEntryPool.release((HeadsUpEntryPhone) alertEntry);
448     }
449 
450     @Override
shouldHeadsUpBecomePinned(NotificationEntry entry)451     protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) {
452         boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded;
453         if (mBypassController.getBypassEnabled()) {
454             pin |= mStatusBarState == StatusBarState.KEYGUARD;
455         }
456         return pin || super.shouldHeadsUpBecomePinned(entry);
457     }
458 
459     @Override
dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args)460     protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
461         super.dumpInternal(fd, pw, args);
462         pw.print("  mBarState=");
463         pw.println(mStatusBarState);
464         pw.print("  mTouchableRegion=");
465         pw.println(mTouchableRegion);
466     }
467 
468     ///////////////////////////////////////////////////////////////////////////////////////////////
469     //  Private utility methods:
470 
471     @Nullable
getHeadsUpEntryPhone(@onNull String key)472     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
473         return (HeadsUpEntryPhone) mAlertEntries.get(key);
474     }
475 
476     @Nullable
getTopHeadsUpEntryPhone()477     private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
478         return (HeadsUpEntryPhone) getTopHeadsUpEntry();
479     }
480 
481     @Override
canRemoveImmediately(@onNull String key)482     protected boolean canRemoveImmediately(@NonNull String key) {
483         if (mSwipedOutKeys.contains(key)) {
484             // We always instantly dismiss views being manually swiped out.
485             mSwipedOutKeys.remove(key);
486             return true;
487         }
488 
489         HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
490         HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
491 
492         return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
493     }
494 
495     ///////////////////////////////////////////////////////////////////////////////////////////////
496     //  HeadsUpEntryPhone:
497 
498     protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry {
499 
500         private boolean mMenuShownPinned;
501 
502         /**
503          * If the time this entry has been on was extended
504          */
505         private boolean extended;
506 
507         /**
508          * Was this entry received while on keyguard
509          */
510         private boolean mIsAutoHeadsUp;
511 
512 
513         @Override
isSticky()514         protected boolean isSticky() {
515             return super.isSticky() || mMenuShownPinned;
516         }
517 
setEntry(@onNull final NotificationEntry entry)518         public void setEntry(@NonNull final NotificationEntry entry) {
519             Runnable removeHeadsUpRunnable = () -> {
520                 if (!mVisualStabilityManager.isReorderingAllowed()
521                         // We don't want to allow reordering while pulsing, but headsup need to
522                         // time out anyway
523                         && !entry.showingPulsing()) {
524                     mEntriesToRemoveWhenReorderingAllowed.add(entry);
525                     mVisualStabilityManager.addReorderingAllowedCallback(
526                             HeadsUpManagerPhone.this);
527                 } else if (mTrackingHeadsUp) {
528                     mEntriesToRemoveAfterExpand.add(entry);
529                 } else if (mIsAutoHeadsUp && mStatusBarState == StatusBarState.KEYGUARD) {
530                     mKeysToRemoveWhenLeavingKeyguard.add(entry.key);
531                 } else {
532                     removeAlertEntry(entry.key);
533                 }
534             };
535 
536             setEntry(entry, removeHeadsUpRunnable);
537         }
538 
539         @Override
updateEntry(boolean updatePostTime)540         public void updateEntry(boolean updatePostTime) {
541             mIsAutoHeadsUp = mEntry.isAutoHeadsUp();
542             super.updateEntry(updatePostTime);
543 
544             if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
545                 mEntriesToRemoveAfterExpand.remove(mEntry);
546             }
547             if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
548                 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
549             }
550             mKeysToRemoveWhenLeavingKeyguard.remove(mEntry.key);
551         }
552 
553         @Override
setExpanded(boolean expanded)554         public void setExpanded(boolean expanded) {
555             if (this.expanded == expanded) {
556                 return;
557             }
558 
559             this.expanded = expanded;
560             if (expanded) {
561                 removeAutoRemovalCallbacks();
562             } else {
563                 updateEntry(false /* updatePostTime */);
564             }
565         }
566 
setMenuShownPinned(boolean menuShownPinned)567         public void setMenuShownPinned(boolean menuShownPinned) {
568             if (mMenuShownPinned == menuShownPinned) {
569                 return;
570             }
571 
572             mMenuShownPinned = menuShownPinned;
573             if (menuShownPinned) {
574                 removeAutoRemovalCallbacks();
575             } else {
576                 updateEntry(false /* updatePostTime */);
577             }
578         }
579 
580         @Override
reset()581         public void reset() {
582             super.reset();
583             mMenuShownPinned = false;
584             extended = false;
585             mIsAutoHeadsUp = false;
586         }
587 
extendPulse()588         private void extendPulse() {
589             if (!extended) {
590                 extended = true;
591                 updateEntry(false);
592             }
593         }
594 
595         @Override
compareTo(AlertEntry alertEntry)596         public int compareTo(AlertEntry alertEntry) {
597             HeadsUpEntryPhone headsUpEntry = (HeadsUpEntryPhone) alertEntry;
598             boolean autoShown = isAutoHeadsUp();
599             boolean otherAutoShown = headsUpEntry.isAutoHeadsUp();
600             if (autoShown && !otherAutoShown) {
601                 return 1;
602             } else if (!autoShown && otherAutoShown) {
603                 return -1;
604             }
605             return super.compareTo(alertEntry);
606         }
607 
608         @Override
calculateFinishTime()609         protected long calculateFinishTime() {
610             return mPostTime + getDecayDuration() + (extended ? mExtensionTime : 0);
611         }
612 
getDecayDuration()613         private int getDecayDuration() {
614             if (isAutoHeadsUp()) {
615                 return getRecommendedHeadsUpTimeoutMs(mAutoHeadsUpNotificationDecay);
616             } else {
617                 return getRecommendedHeadsUpTimeoutMs(mAutoDismissNotificationDecay);
618             }
619         }
620 
isAutoHeadsUp()621         private boolean isAutoHeadsUp() {
622             return mIsAutoHeadsUp;
623         }
624     }
625 
626     public interface AnimationStateHandler {
setHeadsUpGoingAwayAnimationsAllowed(boolean allowed)627         void setHeadsUpGoingAwayAnimationsAllowed(boolean allowed);
628     }
629 }
630