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.policy;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.database.ContentObserver;
22 import android.os.Handler;
23 import android.os.SystemClock;
24 import android.provider.Settings;
25 import android.util.ArrayMap;
26 import android.util.Log;
27 import android.util.Pools;
28 import android.view.View;
29 import android.view.ViewTreeObserver;
30 import android.view.accessibility.AccessibilityEvent;
31 
32 import com.android.internal.logging.MetricsLogger;
33 import com.android.systemui.R;
34 import com.android.systemui.statusbar.ExpandableNotificationRow;
35 import com.android.systemui.statusbar.NotificationData;
36 import com.android.systemui.statusbar.phone.PhoneStatusBar;
37 
38 import java.io.FileDescriptor;
39 import java.io.PrintWriter;
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.HashSet;
43 import java.util.Stack;
44 import java.util.TreeSet;
45 
46 /**
47  * A manager which handles heads up notifications which is a special mode where
48  * they simply peek from the top of the screen.
49  */
50 public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener {
51     private static final String TAG = "HeadsUpManager";
52     private static final boolean DEBUG = false;
53     private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
54     private static final int TAG_CLICKED_NOTIFICATION = R.id.is_clicked_heads_up_tag;
55 
56     private final int mHeadsUpNotificationDecay;
57     private final int mMinimumDisplayTime;
58 
59     private final int mTouchAcceptanceDelay;
60     private final ArrayMap<String, Long> mSnoozedPackages;
61     private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
62     private final int mDefaultSnoozeLengthMs;
63     private final Handler mHandler = new Handler();
64     private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
65 
66         private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
67 
68         @Override
69         public HeadsUpEntry acquire() {
70             if (!mPoolObjects.isEmpty()) {
71                 return mPoolObjects.pop();
72             }
73             return new HeadsUpEntry();
74         }
75 
76         @Override
77         public boolean release(HeadsUpEntry instance) {
78             instance.reset();
79             mPoolObjects.push(instance);
80             return true;
81         }
82     };
83 
84     private final View mStatusBarWindowView;
85     private final int mStatusBarHeight;
86     private final int mNotificationsTopPadding;
87     private final Context mContext;
88     private PhoneStatusBar mBar;
89     private int mSnoozeLengthMs;
90     private ContentObserver mSettingsObserver;
91     private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>();
92     private TreeSet<HeadsUpEntry> mSortedEntries = new TreeSet<>();
93     private HashSet<String> mSwipedOutKeys = new HashSet<>();
94     private int mUser;
95     private Clock mClock;
96     private boolean mReleaseOnExpandFinish;
97     private boolean mTrackingHeadsUp;
98     private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
99     private boolean mIsExpanded;
100     private boolean mHasPinnedNotification;
101     private int[] mTmpTwoArray = new int[2];
102     private boolean mHeadsUpGoingAway;
103     private boolean mWaitingOnCollapseWhenGoingAway;
104     private boolean mIsObserving;
105 
HeadsUpManager(final Context context, View statusBarWindowView)106     public HeadsUpManager(final Context context, View statusBarWindowView) {
107         mContext = context;
108         Resources resources = mContext.getResources();
109         mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
110         mSnoozedPackages = new ArrayMap<>();
111         mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
112         mSnoozeLengthMs = mDefaultSnoozeLengthMs;
113         mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
114         mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
115         mClock = new Clock();
116 
117         mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
118                 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
119         mSettingsObserver = new ContentObserver(mHandler) {
120             @Override
121             public void onChange(boolean selfChange) {
122                 final int packageSnoozeLengthMs = Settings.Global.getInt(
123                         context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
124                 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
125                     mSnoozeLengthMs = packageSnoozeLengthMs;
126                     if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
127                 }
128             }
129         };
130         context.getContentResolver().registerContentObserver(
131                 Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
132                 mSettingsObserver);
133         mStatusBarWindowView = statusBarWindowView;
134         mStatusBarHeight = resources.getDimensionPixelSize(
135                 com.android.internal.R.dimen.status_bar_height);
136         mNotificationsTopPadding = context.getResources()
137                 .getDimensionPixelSize(R.dimen.notifications_top_padding);
138     }
139 
updateTouchableRegionListener()140     private void updateTouchableRegionListener() {
141         boolean shouldObserve = mHasPinnedNotification || mHeadsUpGoingAway
142                 || mWaitingOnCollapseWhenGoingAway;
143         if (shouldObserve == mIsObserving) {
144             return;
145         }
146         if (shouldObserve) {
147             mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
148             mStatusBarWindowView.requestLayout();
149         } else {
150             mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
151         }
152         mIsObserving = shouldObserve;
153     }
154 
setBar(PhoneStatusBar bar)155     public void setBar(PhoneStatusBar bar) {
156         mBar = bar;
157     }
158 
addListener(OnHeadsUpChangedListener listener)159     public void addListener(OnHeadsUpChangedListener listener) {
160         mListeners.add(listener);
161     }
162 
getBar()163     public PhoneStatusBar getBar() {
164         return mBar;
165     }
166 
167     /**
168      * Called when posting a new notification to the heads up.
169      */
showNotification(NotificationData.Entry headsUp)170     public void showNotification(NotificationData.Entry headsUp) {
171         if (DEBUG) Log.v(TAG, "showNotification");
172         MetricsLogger.count(mContext, "note_peek", 1);
173         addHeadsUpEntry(headsUp);
174         updateNotification(headsUp, true);
175         headsUp.setInterruption();
176     }
177 
178     /**
179      * Called when updating or posting a notification to the heads up.
180      */
updateNotification(NotificationData.Entry headsUp, boolean alert)181     public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
182         if (DEBUG) Log.v(TAG, "updateNotification");
183 
184         headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
185         headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
186 
187         if (alert) {
188             HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
189             headsUpEntry.updateEntry();
190             setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp));
191         }
192     }
193 
addHeadsUpEntry(NotificationData.Entry entry)194     private void addHeadsUpEntry(NotificationData.Entry entry) {
195         HeadsUpEntry headsUpEntry = mEntryPool.acquire();
196 
197         // This will also add the entry to the sortedList
198         headsUpEntry.setEntry(entry);
199         mHeadsUpEntries.put(entry.key, headsUpEntry);
200         entry.row.setHeadsUp(true);
201         setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(entry));
202         for (OnHeadsUpChangedListener listener : mListeners) {
203             listener.onHeadsUpStateChanged(entry, true);
204         }
205         entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
206     }
207 
shouldHeadsUpBecomePinned(NotificationData.Entry entry)208     private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
209         return !mIsExpanded || hasFullScreenIntent(entry);
210     }
211 
hasFullScreenIntent(NotificationData.Entry entry)212     private boolean hasFullScreenIntent(NotificationData.Entry entry) {
213         return entry.notification.getNotification().fullScreenIntent != null;
214     }
215 
setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned)216     private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) {
217         ExpandableNotificationRow row = headsUpEntry.entry.row;
218         if (row.isPinned() != isPinned) {
219             row.setPinned(isPinned);
220             updatePinnedMode();
221             for (OnHeadsUpChangedListener listener : mListeners) {
222                 if (isPinned) {
223                     listener.onHeadsUpPinned(row);
224                 } else {
225                     listener.onHeadsUpUnPinned(row);
226                 }
227             }
228         }
229     }
230 
removeHeadsUpEntry(NotificationData.Entry entry)231     private void removeHeadsUpEntry(NotificationData.Entry entry) {
232         HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
233         mSortedEntries.remove(remove);
234         entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
235         entry.row.setHeadsUp(false);
236         setEntryPinned(remove, false /* isPinned */);
237         for (OnHeadsUpChangedListener listener : mListeners) {
238             listener.onHeadsUpStateChanged(entry, false);
239         }
240         mEntryPool.release(remove);
241     }
242 
updatePinnedMode()243     private void updatePinnedMode() {
244         boolean hasPinnedNotification = hasPinnedNotificationInternal();
245         if (hasPinnedNotification == mHasPinnedNotification) {
246             return;
247         }
248         mHasPinnedNotification = hasPinnedNotification;
249         updateTouchableRegionListener();
250         for (OnHeadsUpChangedListener listener : mListeners) {
251             listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
252         }
253     }
254 
255     /**
256      * React to the removal of the notification in the heads up.
257      *
258      * @return true if the notification was removed and false if it still needs to be kept around
259      * for a bit since it wasn't shown long enough
260      */
removeNotification(String key)261     public boolean removeNotification(String key) {
262         if (DEBUG) Log.v(TAG, "remove");
263         if (wasShownLongEnough(key)) {
264             releaseImmediately(key);
265             return true;
266         } else {
267             getHeadsUpEntry(key).removeAsSoonAsPossible();
268             return false;
269         }
270     }
271 
wasShownLongEnough(String key)272     private boolean wasShownLongEnough(String key) {
273         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
274         HeadsUpEntry topEntry = getTopEntry();
275         if (mSwipedOutKeys.contains(key)) {
276             // We always instantly dismiss views being manually swiped out.
277             mSwipedOutKeys.remove(key);
278             return true;
279         }
280         if (headsUpEntry != topEntry) {
281             return true;
282         }
283         return headsUpEntry.wasShownLongEnough();
284     }
285 
isHeadsUp(String key)286     public boolean isHeadsUp(String key) {
287         return mHeadsUpEntries.containsKey(key);
288     }
289 
290     /**
291      * Push any current Heads Up notification down into the shade.
292      */
releaseAllImmediately()293     public void releaseAllImmediately() {
294         if (DEBUG) Log.v(TAG, "releaseAllImmediately");
295         ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet());
296         for (String key : keys) {
297             releaseImmediately(key);
298         }
299     }
300 
releaseImmediately(String key)301     public void releaseImmediately(String key) {
302         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
303         if (headsUpEntry == null) {
304             return;
305         }
306         NotificationData.Entry shadeEntry = headsUpEntry.entry;
307         removeHeadsUpEntry(shadeEntry);
308     }
309 
isSnoozed(String packageName)310     public boolean isSnoozed(String packageName) {
311         final String key = snoozeKey(packageName, mUser);
312         Long snoozedUntil = mSnoozedPackages.get(key);
313         if (snoozedUntil != null) {
314             if (snoozedUntil > SystemClock.elapsedRealtime()) {
315                 if (DEBUG) Log.v(TAG, key + " snoozed");
316                 return true;
317             }
318             mSnoozedPackages.remove(packageName);
319         }
320         return false;
321     }
322 
snooze()323     public void snooze() {
324         for (String key : mHeadsUpEntries.keySet()) {
325             HeadsUpEntry entry = mHeadsUpEntries.get(key);
326             String packageName = entry.entry.notification.getPackageName();
327             mSnoozedPackages.put(snoozeKey(packageName, mUser),
328                     SystemClock.elapsedRealtime() + mSnoozeLengthMs);
329         }
330         mReleaseOnExpandFinish = true;
331     }
332 
snoozeKey(String packageName, int user)333     private static String snoozeKey(String packageName, int user) {
334         return user + "," + packageName;
335     }
336 
getHeadsUpEntry(String key)337     private HeadsUpEntry getHeadsUpEntry(String key) {
338         return mHeadsUpEntries.get(key);
339     }
340 
getEntry(String key)341     public NotificationData.Entry getEntry(String key) {
342         return mHeadsUpEntries.get(key).entry;
343     }
344 
getSortedEntries()345     public TreeSet<HeadsUpEntry> getSortedEntries() {
346         return mSortedEntries;
347     }
348 
getTopEntry()349     public HeadsUpEntry getTopEntry() {
350         return mSortedEntries.isEmpty() ? null : mSortedEntries.first();
351     }
352 
353     /**
354      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
355      * that a user might have consciously clicked on it.
356      *
357      * @param key the key of the touched notification
358      * @return whether the touch is invalid and should be discarded
359      */
shouldSwallowClick(String key)360     public boolean shouldSwallowClick(String key) {
361         HeadsUpEntry entry = mHeadsUpEntries.get(key);
362         if (entry != null && mClock.currentTimeMillis() < entry.postTime) {
363             return true;
364         }
365         return false;
366     }
367 
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info)368     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
369         if (mIsExpanded) {
370             // The touchable region is always the full area when expanded
371             return;
372         }
373         if (mHasPinnedNotification) {
374             int minX = Integer.MAX_VALUE;
375             int maxX = 0;
376             int minY = Integer.MAX_VALUE;
377             int maxY = 0;
378             for (HeadsUpEntry entry : mSortedEntries) {
379                 ExpandableNotificationRow row = entry.entry.row;
380                 if (row.isPinned()) {
381                     row.getLocationOnScreen(mTmpTwoArray);
382                     minX = Math.min(minX, mTmpTwoArray[0]);
383                     minY = Math.min(minY, 0);
384                     maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth());
385                     maxY = Math.max(maxY, row.getHeadsUpHeight());
386                 }
387             }
388 
389             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
390             info.touchableRegion.set(minX, minY, maxX, maxY + mNotificationsTopPadding);
391         } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
392             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
393             info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
394         }
395     }
396 
setUser(int user)397     public void setUser(int user) {
398         mUser = user;
399     }
400 
dump(FileDescriptor fd, PrintWriter pw, String[] args)401     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
402         pw.println("HeadsUpManager state:");
403         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
404         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
405         pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
406         pw.print("  mUser="); pw.println(mUser);
407         for (HeadsUpEntry entry: mSortedEntries) {
408             pw.print("  HeadsUpEntry="); pw.println(entry.entry);
409         }
410         int N = mSnoozedPackages.size();
411         pw.println("  snoozed packages: " + N);
412         for (int i = 0; i < N; i++) {
413             pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
414             pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
415         }
416     }
417 
hasPinnedHeadsUp()418     public boolean hasPinnedHeadsUp() {
419         return mHasPinnedNotification;
420     }
421 
hasPinnedNotificationInternal()422     private boolean hasPinnedNotificationInternal() {
423         for (String key : mHeadsUpEntries.keySet()) {
424             HeadsUpEntry entry = mHeadsUpEntries.get(key);
425             if (entry.entry.row.isPinned()) {
426                 return true;
427             }
428         }
429         return false;
430     }
431 
432     /**
433      * Notifies that a notification was swiped out and will be removed.
434      *
435      * @param key the notification key
436      */
addSwipedOutNotification(String key)437     public void addSwipedOutNotification(String key) {
438         mSwipedOutKeys.add(key);
439     }
440 
unpinAll()441     public void unpinAll() {
442         for (String key : mHeadsUpEntries.keySet()) {
443             HeadsUpEntry entry = mHeadsUpEntries.get(key);
444             setEntryPinned(entry, false /* isPinned */);
445         }
446     }
447 
onExpandingFinished()448     public void onExpandingFinished() {
449         if (mReleaseOnExpandFinish) {
450             releaseAllImmediately();
451             mReleaseOnExpandFinish = false;
452         } else {
453             for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
454                 removeHeadsUpEntry(entry);
455             }
456         }
457         mEntriesToRemoveAfterExpand.clear();
458     }
459 
setTrackingHeadsUp(boolean trackingHeadsUp)460     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
461         mTrackingHeadsUp = trackingHeadsUp;
462     }
463 
setIsExpanded(boolean isExpanded)464     public void setIsExpanded(boolean isExpanded) {
465         if (isExpanded != mIsExpanded) {
466             mIsExpanded = isExpanded;
467             if (isExpanded) {
468                 // make sure our state is sane
469                 mWaitingOnCollapseWhenGoingAway = false;
470                 mHeadsUpGoingAway = false;
471                 updateTouchableRegionListener();
472             }
473         }
474     }
475 
getTopHeadsUpHeight()476     public int getTopHeadsUpHeight() {
477         HeadsUpEntry topEntry = getTopEntry();
478         return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0;
479     }
480 
481     /**
482      * Compare two entries and decide how they should be ranked.
483      *
484      * @return -1 if the first argument should be ranked higher than the second, 1 if the second
485      * one should be ranked higher and 0 if they are equal.
486      */
compare(NotificationData.Entry a, NotificationData.Entry b)487     public int compare(NotificationData.Entry a, NotificationData.Entry b) {
488         HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
489         HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
490         if (aEntry == null || bEntry == null) {
491             return aEntry == null ? 1 : -1;
492         }
493         return aEntry.compareTo(bEntry);
494     }
495 
496     /**
497      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
498      * animating out. This is used to keep the touchable regions in a sane state.
499      */
setHeadsUpGoingAway(boolean headsUpGoingAway)500     public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
501         if (headsUpGoingAway != mHeadsUpGoingAway) {
502             mHeadsUpGoingAway = headsUpGoingAway;
503             if (!headsUpGoingAway) {
504                 waitForStatusBarLayout();
505             }
506             updateTouchableRegionListener();
507         }
508     }
509 
510     /**
511      * We need to wait on the whole panel to collapse, before we can remove the touchable region
512      * listener.
513      */
waitForStatusBarLayout()514     private void waitForStatusBarLayout() {
515         mWaitingOnCollapseWhenGoingAway = true;
516         mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
517             @Override
518             public void onLayoutChange(View v, int left, int top, int right, int bottom,
519                     int oldLeft,
520                     int oldTop, int oldRight, int oldBottom) {
521                 if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
522                     mStatusBarWindowView.removeOnLayoutChangeListener(this);
523                     mWaitingOnCollapseWhenGoingAway = false;
524                     updateTouchableRegionListener();
525                 }
526             }
527         });
528     }
529 
setIsClickedNotification(View child, boolean clicked)530     public static void setIsClickedNotification(View child, boolean clicked) {
531         child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
532     }
533 
isClickedHeadsUpNotification(View child)534     public static boolean isClickedHeadsUpNotification(View child) {
535         Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
536         return clicked != null && clicked;
537     }
538 
539     /**
540      * This represents a notification and how long it is in a heads up mode. It also manages its
541      * lifecycle automatically when created.
542      */
543     public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
544         public NotificationData.Entry entry;
545         public long postTime;
546         public long earliestRemovaltime;
547         private Runnable mRemoveHeadsUpRunnable;
548 
setEntry(final NotificationData.Entry entry)549         public void setEntry(final NotificationData.Entry entry) {
550             this.entry = entry;
551 
552             // The actual post time will be just after the heads-up really slided in
553             postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay;
554             mRemoveHeadsUpRunnable = new Runnable() {
555                 @Override
556                 public void run() {
557                     if (!mTrackingHeadsUp) {
558                         removeHeadsUpEntry(entry);
559                     } else {
560                         mEntriesToRemoveAfterExpand.add(entry);
561                     }
562                 }
563             };
564             updateEntry();
565         }
566 
updateEntry()567         public void updateEntry() {
568             mSortedEntries.remove(HeadsUpEntry.this);
569             long currentTime = mClock.currentTimeMillis();
570             earliestRemovaltime = currentTime + mMinimumDisplayTime;
571             postTime = Math.max(postTime, currentTime);
572             removeAutoRemovalCallbacks();
573             if (!hasFullScreenIntent(entry)) {
574                 long finishTime = postTime + mHeadsUpNotificationDecay;
575                 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
576                 mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
577             }
578             mSortedEntries.add(HeadsUpEntry.this);
579         }
580 
581         @Override
compareTo(HeadsUpEntry o)582         public int compareTo(HeadsUpEntry o) {
583             return postTime < o.postTime ? 1
584                     : postTime == o.postTime ? entry.key.compareTo(o.entry.key)
585                             : -1;
586         }
587 
removeAutoRemovalCallbacks()588         public void removeAutoRemovalCallbacks() {
589             mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
590         }
591 
wasShownLongEnough()592         public boolean wasShownLongEnough() {
593             return earliestRemovaltime < mClock.currentTimeMillis();
594         }
595 
removeAsSoonAsPossible()596         public void removeAsSoonAsPossible() {
597             removeAutoRemovalCallbacks();
598             mHandler.postDelayed(mRemoveHeadsUpRunnable,
599                     earliestRemovaltime - mClock.currentTimeMillis());
600         }
601 
reset()602         public void reset() {
603             removeAutoRemovalCallbacks();
604             entry = null;
605             mRemoveHeadsUpRunnable = null;
606         }
607     }
608 
609     public static class Clock {
currentTimeMillis()610         public long currentTimeMillis() {
611             return SystemClock.elapsedRealtime();
612         }
613     }
614 
615     public interface OnHeadsUpChangedListener {
616         /**
617          * The state whether there exist pinned heads-ups or not changed.
618          *
619          * @param inPinnedMode whether there are any pinned heads-ups
620          */
onHeadsUpPinnedModeChanged(boolean inPinnedMode)621         void onHeadsUpPinnedModeChanged(boolean inPinnedMode);
622 
623         /**
624          * A notification was just pinned to the top.
625          */
onHeadsUpPinned(ExpandableNotificationRow headsUp)626         void onHeadsUpPinned(ExpandableNotificationRow headsUp);
627 
628         /**
629          * A notification was just unpinned from the top.
630          */
onHeadsUpUnPinned(ExpandableNotificationRow headsUp)631         void onHeadsUpUnPinned(ExpandableNotificationRow headsUp);
632 
633         /**
634          * A notification just became a heads up or turned back to its normal state.
635          *
636          * @param entry the entry of the changed notification
637          * @param isHeadsUp whether the notification is now a headsUp notification
638          */
onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp)639         void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
640     }
641 }
642