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