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