• 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.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         addHeadsUpEntry(headsUp);
173         updateNotification(headsUp, true);
174         headsUp.setInterruption();
175     }
176 
177     /**
178      * Called when updating or posting a notification to the heads up.
179      */
updateNotification(NotificationData.Entry headsUp, boolean alert)180     public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
181         if (DEBUG) Log.v(TAG, "updateNotification");
182 
183         headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
184         headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
185 
186         if (alert) {
187             HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
188             headsUpEntry.updateEntry();
189             setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp));
190         }
191     }
192 
addHeadsUpEntry(NotificationData.Entry entry)193     private void addHeadsUpEntry(NotificationData.Entry entry) {
194         HeadsUpEntry headsUpEntry = mEntryPool.acquire();
195 
196         // This will also add the entry to the sortedList
197         headsUpEntry.setEntry(entry);
198         mHeadsUpEntries.put(entry.key, headsUpEntry);
199         entry.row.setHeadsUp(true);
200         setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(entry));
201         for (OnHeadsUpChangedListener listener : mListeners) {
202             listener.onHeadsUpStateChanged(entry, true);
203         }
204         entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
205     }
206 
shouldHeadsUpBecomePinned(NotificationData.Entry entry)207     private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
208         return !mIsExpanded || hasFullScreenIntent(entry);
209     }
210 
hasFullScreenIntent(NotificationData.Entry entry)211     private boolean hasFullScreenIntent(NotificationData.Entry entry) {
212         return entry.notification.getNotification().fullScreenIntent != null;
213     }
214 
setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned)215     private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) {
216         ExpandableNotificationRow row = headsUpEntry.entry.row;
217         if (row.isPinned() != isPinned) {
218             row.setPinned(isPinned);
219             updatePinnedMode();
220             for (OnHeadsUpChangedListener listener : mListeners) {
221                 if (isPinned) {
222                     listener.onHeadsUpPinned(row);
223                 } else {
224                     listener.onHeadsUpUnPinned(row);
225                 }
226             }
227         }
228     }
229 
removeHeadsUpEntry(NotificationData.Entry entry)230     private void removeHeadsUpEntry(NotificationData.Entry entry) {
231         HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
232         mSortedEntries.remove(remove);
233         entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
234         entry.row.setHeadsUp(false);
235         setEntryPinned(remove, false /* isPinned */);
236         for (OnHeadsUpChangedListener listener : mListeners) {
237             listener.onHeadsUpStateChanged(entry, false);
238         }
239         mEntryPool.release(remove);
240     }
241 
updatePinnedMode()242     private void updatePinnedMode() {
243         boolean hasPinnedNotification = hasPinnedNotificationInternal();
244         if (hasPinnedNotification == mHasPinnedNotification) {
245             return;
246         }
247         mHasPinnedNotification = hasPinnedNotification;
248         if (mHasPinnedNotification) {
249             MetricsLogger.count(mContext, "note_peek", 1);
250         }
251         updateTouchableRegionListener();
252         for (OnHeadsUpChangedListener listener : mListeners) {
253             listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
254         }
255     }
256 
257     /**
258      * React to the removal of the notification in the heads up.
259      *
260      * @return true if the notification was removed and false if it still needs to be kept around
261      * for a bit since it wasn't shown long enough
262      */
removeNotification(String key)263     public boolean removeNotification(String key) {
264         if (DEBUG) Log.v(TAG, "remove");
265         if (wasShownLongEnough(key)) {
266             releaseImmediately(key);
267             return true;
268         } else {
269             getHeadsUpEntry(key).removeAsSoonAsPossible();
270             return false;
271         }
272     }
273 
wasShownLongEnough(String key)274     private boolean wasShownLongEnough(String key) {
275         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
276         HeadsUpEntry topEntry = getTopEntry();
277         if (mSwipedOutKeys.contains(key)) {
278             // We always instantly dismiss views being manually swiped out.
279             mSwipedOutKeys.remove(key);
280             return true;
281         }
282         if (headsUpEntry != topEntry) {
283             return true;
284         }
285         return headsUpEntry.wasShownLongEnough();
286     }
287 
isHeadsUp(String key)288     public boolean isHeadsUp(String key) {
289         return mHeadsUpEntries.containsKey(key);
290     }
291 
292     /**
293      * Push any current Heads Up notification down into the shade.
294      */
releaseAllImmediately()295     public void releaseAllImmediately() {
296         if (DEBUG) Log.v(TAG, "releaseAllImmediately");
297         ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet());
298         for (String key : keys) {
299             releaseImmediately(key);
300         }
301     }
302 
releaseImmediately(String key)303     public void releaseImmediately(String key) {
304         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
305         if (headsUpEntry == null) {
306             return;
307         }
308         NotificationData.Entry shadeEntry = headsUpEntry.entry;
309         removeHeadsUpEntry(shadeEntry);
310     }
311 
isSnoozed(String packageName)312     public boolean isSnoozed(String packageName) {
313         final String key = snoozeKey(packageName, mUser);
314         Long snoozedUntil = mSnoozedPackages.get(key);
315         if (snoozedUntil != null) {
316             if (snoozedUntil > SystemClock.elapsedRealtime()) {
317                 if (DEBUG) Log.v(TAG, key + " snoozed");
318                 return true;
319             }
320             mSnoozedPackages.remove(packageName);
321         }
322         return false;
323     }
324 
snooze()325     public void snooze() {
326         for (String key : mHeadsUpEntries.keySet()) {
327             HeadsUpEntry entry = mHeadsUpEntries.get(key);
328             String packageName = entry.entry.notification.getPackageName();
329             mSnoozedPackages.put(snoozeKey(packageName, mUser),
330                     SystemClock.elapsedRealtime() + mSnoozeLengthMs);
331         }
332         mReleaseOnExpandFinish = true;
333     }
334 
snoozeKey(String packageName, int user)335     private static String snoozeKey(String packageName, int user) {
336         return user + "," + packageName;
337     }
338 
getHeadsUpEntry(String key)339     private HeadsUpEntry getHeadsUpEntry(String key) {
340         return mHeadsUpEntries.get(key);
341     }
342 
getEntry(String key)343     public NotificationData.Entry getEntry(String key) {
344         return mHeadsUpEntries.get(key).entry;
345     }
346 
getSortedEntries()347     public TreeSet<HeadsUpEntry> getSortedEntries() {
348         return mSortedEntries;
349     }
350 
getTopEntry()351     public HeadsUpEntry getTopEntry() {
352         return mSortedEntries.isEmpty() ? null : mSortedEntries.first();
353     }
354 
355     /**
356      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
357      * that a user might have consciously clicked on it.
358      *
359      * @param key the key of the touched notification
360      * @return whether the touch is invalid and should be discarded
361      */
shouldSwallowClick(String key)362     public boolean shouldSwallowClick(String key) {
363         HeadsUpEntry entry = mHeadsUpEntries.get(key);
364         if (entry != null && mClock.currentTimeMillis() < entry.postTime) {
365             return true;
366         }
367         return false;
368     }
369 
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info)370     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
371         if (mIsExpanded) {
372             // The touchable region is always the full area when expanded
373             return;
374         }
375         if (mHasPinnedNotification) {
376             int minX = Integer.MAX_VALUE;
377             int maxX = 0;
378             int minY = Integer.MAX_VALUE;
379             int maxY = 0;
380             for (HeadsUpEntry entry : mSortedEntries) {
381                 ExpandableNotificationRow row = entry.entry.row;
382                 if (row.isPinned()) {
383                     row.getLocationOnScreen(mTmpTwoArray);
384                     minX = Math.min(minX, mTmpTwoArray[0]);
385                     minY = Math.min(minY, 0);
386                     maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth());
387                     maxY = Math.max(maxY, row.getHeadsUpHeight());
388                 }
389             }
390 
391             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
392             info.touchableRegion.set(minX, minY, maxX, maxY + mNotificationsTopPadding);
393         } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
394             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
395             info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
396         }
397     }
398 
setUser(int user)399     public void setUser(int user) {
400         mUser = user;
401     }
402 
dump(FileDescriptor fd, PrintWriter pw, String[] args)403     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
404         pw.println("HeadsUpManager state:");
405         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
406         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
407         pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
408         pw.print("  mUser="); pw.println(mUser);
409         for (HeadsUpEntry entry: mSortedEntries) {
410             pw.print("  HeadsUpEntry="); pw.println(entry.entry);
411         }
412         int N = mSnoozedPackages.size();
413         pw.println("  snoozed packages: " + N);
414         for (int i = 0; i < N; i++) {
415             pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
416             pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
417         }
418     }
419 
hasPinnedHeadsUp()420     public boolean hasPinnedHeadsUp() {
421         return mHasPinnedNotification;
422     }
423 
hasPinnedNotificationInternal()424     private boolean hasPinnedNotificationInternal() {
425         for (String key : mHeadsUpEntries.keySet()) {
426             HeadsUpEntry entry = mHeadsUpEntries.get(key);
427             if (entry.entry.row.isPinned()) {
428                 return true;
429             }
430         }
431         return false;
432     }
433 
434     /**
435      * Notifies that a notification was swiped out and will be removed.
436      *
437      * @param key the notification key
438      */
addSwipedOutNotification(String key)439     public void addSwipedOutNotification(String key) {
440         mSwipedOutKeys.add(key);
441     }
442 
unpinAll()443     public void unpinAll() {
444         for (String key : mHeadsUpEntries.keySet()) {
445             HeadsUpEntry entry = mHeadsUpEntries.get(key);
446             setEntryPinned(entry, false /* isPinned */);
447         }
448     }
449 
onExpandingFinished()450     public void onExpandingFinished() {
451         if (mReleaseOnExpandFinish) {
452             releaseAllImmediately();
453             mReleaseOnExpandFinish = false;
454         } else {
455             for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
456                 removeHeadsUpEntry(entry);
457             }
458         }
459         mEntriesToRemoveAfterExpand.clear();
460     }
461 
setTrackingHeadsUp(boolean trackingHeadsUp)462     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
463         mTrackingHeadsUp = trackingHeadsUp;
464     }
465 
setIsExpanded(boolean isExpanded)466     public void setIsExpanded(boolean isExpanded) {
467         if (isExpanded != mIsExpanded) {
468             mIsExpanded = isExpanded;
469             if (isExpanded) {
470                 // make sure our state is sane
471                 mWaitingOnCollapseWhenGoingAway = false;
472                 mHeadsUpGoingAway = false;
473                 updateTouchableRegionListener();
474             }
475         }
476     }
477 
getTopHeadsUpHeight()478     public int getTopHeadsUpHeight() {
479         HeadsUpEntry topEntry = getTopEntry();
480         return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0;
481     }
482 
483     /**
484      * Compare two entries and decide how they should be ranked.
485      *
486      * @return -1 if the first argument should be ranked higher than the second, 1 if the second
487      * one should be ranked higher and 0 if they are equal.
488      */
compare(NotificationData.Entry a, NotificationData.Entry b)489     public int compare(NotificationData.Entry a, NotificationData.Entry b) {
490         HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
491         HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
492         if (aEntry == null || bEntry == null) {
493             return aEntry == null ? 1 : -1;
494         }
495         return aEntry.compareTo(bEntry);
496     }
497 
498     /**
499      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
500      * animating out. This is used to keep the touchable regions in a sane state.
501      */
setHeadsUpGoingAway(boolean headsUpGoingAway)502     public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
503         if (headsUpGoingAway != mHeadsUpGoingAway) {
504             mHeadsUpGoingAway = headsUpGoingAway;
505             if (!headsUpGoingAway) {
506                 waitForStatusBarLayout();
507             }
508             updateTouchableRegionListener();
509         }
510     }
511 
512     /**
513      * We need to wait on the whole panel to collapse, before we can remove the touchable region
514      * listener.
515      */
waitForStatusBarLayout()516     private void waitForStatusBarLayout() {
517         mWaitingOnCollapseWhenGoingAway = true;
518         mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
519             @Override
520             public void onLayoutChange(View v, int left, int top, int right, int bottom,
521                     int oldLeft,
522                     int oldTop, int oldRight, int oldBottom) {
523                 if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
524                     mStatusBarWindowView.removeOnLayoutChangeListener(this);
525                     mWaitingOnCollapseWhenGoingAway = false;
526                     updateTouchableRegionListener();
527                 }
528             }
529         });
530     }
531 
setIsClickedNotification(View child, boolean clicked)532     public static void setIsClickedNotification(View child, boolean clicked) {
533         child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
534     }
535 
isClickedHeadsUpNotification(View child)536     public static boolean isClickedHeadsUpNotification(View child) {
537         Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
538         return clicked != null && clicked;
539     }
540 
541     /**
542      * This represents a notification and how long it is in a heads up mode. It also manages its
543      * lifecycle automatically when created.
544      */
545     public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
546         public NotificationData.Entry entry;
547         public long postTime;
548         public long earliestRemovaltime;
549         private Runnable mRemoveHeadsUpRunnable;
550 
setEntry(final NotificationData.Entry entry)551         public void setEntry(final NotificationData.Entry entry) {
552             this.entry = entry;
553 
554             // The actual post time will be just after the heads-up really slided in
555             postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay;
556             mRemoveHeadsUpRunnable = new Runnable() {
557                 @Override
558                 public void run() {
559                     if (!mTrackingHeadsUp) {
560                         removeHeadsUpEntry(entry);
561                     } else {
562                         mEntriesToRemoveAfterExpand.add(entry);
563                     }
564                 }
565             };
566             updateEntry();
567         }
568 
updateEntry()569         public void updateEntry() {
570             mSortedEntries.remove(HeadsUpEntry.this);
571             long currentTime = mClock.currentTimeMillis();
572             earliestRemovaltime = currentTime + mMinimumDisplayTime;
573             postTime = Math.max(postTime, currentTime);
574             removeAutoRemovalCallbacks();
575             if (!hasFullScreenIntent(entry)) {
576                 long finishTime = postTime + mHeadsUpNotificationDecay;
577                 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
578                 mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
579             }
580             mSortedEntries.add(HeadsUpEntry.this);
581         }
582 
583         @Override
compareTo(HeadsUpEntry o)584         public int compareTo(HeadsUpEntry o) {
585             boolean selfFullscreen = hasFullScreenIntent(entry);
586             boolean otherFullscreen = hasFullScreenIntent(o.entry);
587             if (selfFullscreen && !otherFullscreen) {
588                 return -1;
589             } else if (!selfFullscreen && otherFullscreen) {
590                 return 1;
591             }
592             return postTime < o.postTime ? 1
593                     : postTime == o.postTime ? entry.key.compareTo(o.entry.key)
594                             : -1;
595         }
596 
removeAutoRemovalCallbacks()597         public void removeAutoRemovalCallbacks() {
598             mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
599         }
600 
wasShownLongEnough()601         public boolean wasShownLongEnough() {
602             return earliestRemovaltime < mClock.currentTimeMillis();
603         }
604 
removeAsSoonAsPossible()605         public void removeAsSoonAsPossible() {
606             removeAutoRemovalCallbacks();
607             mHandler.postDelayed(mRemoveHeadsUpRunnable,
608                     earliestRemovaltime - mClock.currentTimeMillis());
609         }
610 
reset()611         public void reset() {
612             removeAutoRemovalCallbacks();
613             entry = null;
614             mRemoveHeadsUpRunnable = null;
615         }
616     }
617 
618     public static class Clock {
currentTimeMillis()619         public long currentTimeMillis() {
620             return SystemClock.elapsedRealtime();
621         }
622     }
623 
624     public interface OnHeadsUpChangedListener {
625         /**
626          * The state whether there exist pinned heads-ups or not changed.
627          *
628          * @param inPinnedMode whether there are any pinned heads-ups
629          */
onHeadsUpPinnedModeChanged(boolean inPinnedMode)630         void onHeadsUpPinnedModeChanged(boolean inPinnedMode);
631 
632         /**
633          * A notification was just pinned to the top.
634          */
onHeadsUpPinned(ExpandableNotificationRow headsUp)635         void onHeadsUpPinned(ExpandableNotificationRow headsUp);
636 
637         /**
638          * A notification was just unpinned from the top.
639          */
onHeadsUpUnPinned(ExpandableNotificationRow headsUp)640         void onHeadsUpUnPinned(ExpandableNotificationRow headsUp);
641 
642         /**
643          * A notification just became a heads up or turned back to its normal state.
644          *
645          * @param entry the entry of the changed notification
646          * @param isHeadsUp whether the notification is now a headsUp notification
647          */
onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp)648         void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
649     }
650 }
651