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