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