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.NotificationGroupManager;
     37 import com.android.systemui.statusbar.phone.PhoneStatusBar;
     38 
     39 import java.io.FileDescriptor;
     40 import java.io.PrintWriter;
     41 import java.util.ArrayList;
     42 import java.util.Collection;
     43 import java.util.HashMap;
     44 import java.util.HashSet;
     45 import 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  */
     51 public 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             if (headsUpEntry == null) {
    189                 // the entry was released before this update (i.e by a listener) This can happen
    190                 // with the groupmanager
    191                 return;
    192             }
    193             headsUpEntry.updateEntry();
    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         entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    238         entry.row.setHeadsUp(false);
    239         setEntryPinned(remove, false /* isPinned */);
    240         for (OnHeadsUpChangedListener listener : mListeners) {
    241             listener.onHeadsUpStateChanged(entry, false);
    242         }
    243         mEntryPool.release(remove);
    244     }
    245 
    246     private void updatePinnedMode() {
    247         boolean hasPinnedNotification = hasPinnedNotificationInternal();
    248         if (hasPinnedNotification == mHasPinnedNotification) {
    249             return;
    250         }
    251         mHasPinnedNotification = hasPinnedNotification;
    252         if (mHasPinnedNotification) {
    253             MetricsLogger.count(mContext, "note_peek", 1);
    254         }
    255         updateTouchableRegionListener();
    256         for (OnHeadsUpChangedListener listener : mListeners) {
    257             listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
    258         }
    259     }
    260 
    261     /**
    262      * React to the removal of the notification in the heads up.
    263      *
    264      * @return true if the notification was removed and false if it still needs to be kept around
    265      * for a bit since it wasn't shown long enough
    266      */
    267     public boolean removeNotification(String key, boolean ignoreEarliestRemovalTime) {
    268         if (DEBUG) Log.v(TAG, "remove");
    269         if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
    270             releaseImmediately(key);
    271             return true;
    272         } else {
    273             getHeadsUpEntry(key).removeAsSoonAsPossible();
    274             return false;
    275         }
    276     }
    277 
    278     private boolean wasShownLongEnough(String key) {
    279         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
    280         HeadsUpEntry topEntry = getTopEntry();
    281         if (mSwipedOutKeys.contains(key)) {
    282             // We always instantly dismiss views being manually swiped out.
    283             mSwipedOutKeys.remove(key);
    284             return true;
    285         }
    286         if (headsUpEntry != topEntry) {
    287             return true;
    288         }
    289         return headsUpEntry.wasShownLongEnough();
    290     }
    291 
    292     public boolean isHeadsUp(String key) {
    293         return mHeadsUpEntries.containsKey(key);
    294     }
    295 
    296     /**
    297      * Push any current Heads Up notification down into the shade.
    298      */
    299     public void releaseAllImmediately() {
    300         if (DEBUG) Log.v(TAG, "releaseAllImmediately");
    301         ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet());
    302         for (String key : keys) {
    303             releaseImmediately(key);
    304         }
    305     }
    306 
    307     public void releaseImmediately(String key) {
    308         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
    309         if (headsUpEntry == null) {
    310             return;
    311         }
    312         NotificationData.Entry shadeEntry = headsUpEntry.entry;
    313         removeHeadsUpEntry(shadeEntry);
    314     }
    315 
    316     public boolean isSnoozed(String packageName) {
    317         final String key = snoozeKey(packageName, mUser);
    318         Long snoozedUntil = mSnoozedPackages.get(key);
    319         if (snoozedUntil != null) {
    320             if (snoozedUntil > SystemClock.elapsedRealtime()) {
    321                 if (DEBUG) Log.v(TAG, key + " snoozed");
    322                 return true;
    323             }
    324             mSnoozedPackages.remove(packageName);
    325         }
    326         return false;
    327     }
    328 
    329     public void snooze() {
    330         for (String key : mHeadsUpEntries.keySet()) {
    331             HeadsUpEntry entry = mHeadsUpEntries.get(key);
    332             String packageName = entry.entry.notification.getPackageName();
    333             mSnoozedPackages.put(snoozeKey(packageName, mUser),
    334                     SystemClock.elapsedRealtime() + mSnoozeLengthMs);
    335         }
    336         mReleaseOnExpandFinish = true;
    337     }
    338 
    339     private static String snoozeKey(String packageName, int user) {
    340         return user + "," + packageName;
    341     }
    342 
    343     private HeadsUpEntry getHeadsUpEntry(String key) {
    344         return mHeadsUpEntries.get(key);
    345     }
    346 
    347     public NotificationData.Entry getEntry(String key) {
    348         return mHeadsUpEntries.get(key).entry;
    349     }
    350 
    351     public Collection<HeadsUpEntry> getAllEntries() {
    352         return mHeadsUpEntries.values();
    353     }
    354 
    355     public HeadsUpEntry getTopEntry() {
    356         if (mHeadsUpEntries.isEmpty()) {
    357             return null;
    358         }
    359         HeadsUpEntry topEntry = null;
    360         for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
    361             if (topEntry == null || entry.compareTo(topEntry) == -1) {
    362                 topEntry = entry;
    363             }
    364         }
    365         return topEntry;
    366     }
    367 
    368     /**
    369      * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
    370      * that a user might have consciously clicked on it.
    371      *
    372      * @param key the key of the touched notification
    373      * @return whether the touch is invalid and should be discarded
    374      */
    375     public boolean shouldSwallowClick(String key) {
    376         HeadsUpEntry entry = mHeadsUpEntries.get(key);
    377         if (entry != null && mClock.currentTimeMillis() < entry.postTime) {
    378             return true;
    379         }
    380         return false;
    381     }
    382 
    383     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
    384         if (mIsExpanded) {
    385             // The touchable region is always the full area when expanded
    386             return;
    387         }
    388         if (mHasPinnedNotification) {
    389             ExpandableNotificationRow topEntry = getTopEntry().entry.row;
    390             if (topEntry.isChildInGroup()) {
    391                 final ExpandableNotificationRow groupSummary
    392                         = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
    393                 if (groupSummary != null) {
    394                     topEntry = groupSummary;
    395                 }
    396             }
    397             topEntry.getLocationOnScreen(mTmpTwoArray);
    398             int minX = mTmpTwoArray[0];
    399             int maxX = mTmpTwoArray[0] + topEntry.getWidth();
    400             int maxY = topEntry.getIntrinsicHeight();
    401 
    402             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
    403             info.touchableRegion.set(minX, 0, maxX, maxY);
    404         } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) {
    405             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
    406             info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
    407         }
    408     }
    409 
    410     public void setUser(int user) {
    411         mUser = user;
    412     }
    413 
    414     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    415         pw.println("HeadsUpManager state:");
    416         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
    417         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
    418         pw.print("  now="); pw.println(SystemClock.elapsedRealtime());
    419         pw.print("  mUser="); pw.println(mUser);
    420         for (HeadsUpEntry entry: mHeadsUpEntries.values()) {
    421             pw.print("  HeadsUpEntry="); pw.println(entry.entry);
    422         }
    423         int N = mSnoozedPackages.size();
    424         pw.println("  snoozed packages: " + N);
    425         for (int i = 0; i < N; i++) {
    426             pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
    427             pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
    428         }
    429     }
    430 
    431     public boolean hasPinnedHeadsUp() {
    432         return mHasPinnedNotification;
    433     }
    434 
    435     private boolean hasPinnedNotificationInternal() {
    436         for (String key : mHeadsUpEntries.keySet()) {
    437             HeadsUpEntry entry = mHeadsUpEntries.get(key);
    438             if (entry.entry.row.isPinned()) {
    439                 return true;
    440             }
    441         }
    442         return false;
    443     }
    444 
    445     /**
    446      * Notifies that a notification was swiped out and will be removed.
    447      *
    448      * @param key the notification key
    449      */
    450     public void addSwipedOutNotification(String key) {
    451         mSwipedOutKeys.add(key);
    452     }
    453 
    454     public void unpinAll() {
    455         for (String key : mHeadsUpEntries.keySet()) {
    456             HeadsUpEntry entry = mHeadsUpEntries.get(key);
    457             setEntryPinned(entry, false /* isPinned */);
    458             // maybe it got un sticky
    459             entry.updateEntry(false /* updatePostTime */);
    460         }
    461     }
    462 
    463     public void onExpandingFinished() {
    464         if (mReleaseOnExpandFinish) {
    465             releaseAllImmediately();
    466             mReleaseOnExpandFinish = false;
    467         } else {
    468             for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
    469                 if (isHeadsUp(entry.key)) {
    470                     // Maybe the heads-up was removed already
    471                     removeHeadsUpEntry(entry);
    472                 }
    473             }
    474         }
    475         mEntriesToRemoveAfterExpand.clear();
    476     }
    477 
    478     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
    479         mTrackingHeadsUp = trackingHeadsUp;
    480     }
    481 
    482     public boolean isTrackingHeadsUp() {
    483         return mTrackingHeadsUp;
    484     }
    485 
    486     public void setIsExpanded(boolean isExpanded) {
    487         if (isExpanded != mIsExpanded) {
    488             mIsExpanded = isExpanded;
    489             if (isExpanded) {
    490                 // make sure our state is sane
    491                 mWaitingOnCollapseWhenGoingAway = false;
    492                 mHeadsUpGoingAway = false;
    493                 updateTouchableRegionListener();
    494             }
    495         }
    496     }
    497 
    498     /**
    499      * @return the height of the top heads up notification when pinned. This is different from the
    500      *         intrinsic height, which also includes whether the notification is system expanded and
    501      *         is mainly used when dragging down from a heads up notification.
    502      */
    503     public int getTopHeadsUpPinnedHeight() {
    504         HeadsUpEntry topEntry = getTopEntry();
    505         if (topEntry == null || topEntry.entry == null) {
    506             return 0;
    507         }
    508         ExpandableNotificationRow row = topEntry.entry.row;
    509         if (row.isChildInGroup()) {
    510             final ExpandableNotificationRow groupSummary
    511                     = mGroupManager.getGroupSummary(row.getStatusBarNotification());
    512             if (groupSummary != null) {
    513                 row = groupSummary;
    514             }
    515         }
    516         return row.getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
    517     }
    518 
    519     /**
    520      * Compare two entries and decide how they should be ranked.
    521      *
    522      * @return -1 if the first argument should be ranked higher than the second, 1 if the second
    523      * one should be ranked higher and 0 if they are equal.
    524      */
    525     public int compare(NotificationData.Entry a, NotificationData.Entry b) {
    526         HeadsUpEntry aEntry = getHeadsUpEntry(a.key);
    527         HeadsUpEntry bEntry = getHeadsUpEntry(b.key);
    528         if (aEntry == null || bEntry == null) {
    529             return aEntry == null ? 1 : -1;
    530         }
    531         return aEntry.compareTo(bEntry);
    532     }
    533 
    534     /**
    535      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
    536      * animating out. This is used to keep the touchable regions in a sane state.
    537      */
    538     public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
    539         if (headsUpGoingAway != mHeadsUpGoingAway) {
    540             mHeadsUpGoingAway = headsUpGoingAway;
    541             if (!headsUpGoingAway) {
    542                 waitForStatusBarLayout();
    543             }
    544             updateTouchableRegionListener();
    545         }
    546     }
    547 
    548     /**
    549      * We need to wait on the whole panel to collapse, before we can remove the touchable region
    550      * listener.
    551      */
    552     private void waitForStatusBarLayout() {
    553         mWaitingOnCollapseWhenGoingAway = true;
    554         mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    555             @Override
    556             public void onLayoutChange(View v, int left, int top, int right, int bottom,
    557                     int oldLeft,
    558                     int oldTop, int oldRight, int oldBottom) {
    559                 if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
    560                     mStatusBarWindowView.removeOnLayoutChangeListener(this);
    561                     mWaitingOnCollapseWhenGoingAway = false;
    562                     updateTouchableRegionListener();
    563                 }
    564             }
    565         });
    566     }
    567 
    568     public static void setIsClickedNotification(View child, boolean clicked) {
    569         child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
    570     }
    571 
    572     public static boolean isClickedHeadsUpNotification(View child) {
    573         Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
    574         return clicked != null && clicked;
    575     }
    576 
    577     public void setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive) {
    578         HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
    579         if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
    580             headsUpEntry.remoteInputActive = remoteInputActive;
    581             if (remoteInputActive) {
    582                 headsUpEntry.removeAutoRemovalCallbacks();
    583             } else {
    584                 headsUpEntry.updateEntry(false /* updatePostTime */);
    585             }
    586         }
    587     }
    588 
    589     /**
    590      * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
    591      * until it's collapsed again.
    592      */
    593     public void setExpanded(NotificationData.Entry entry, boolean expanded) {
    594         HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key);
    595         if (headsUpEntry != null && headsUpEntry.expanded != expanded) {
    596             headsUpEntry.expanded = expanded;
    597             if (expanded) {
    598                 headsUpEntry.removeAutoRemovalCallbacks();
    599             } else {
    600                 headsUpEntry.updateEntry(false /* updatePostTime */);
    601             }
    602         }
    603     }
    604 
    605     /**
    606      * This represents a notification and how long it is in a heads up mode. It also manages its
    607      * lifecycle automatically when created.
    608      */
    609     public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
    610         public NotificationData.Entry entry;
    611         public long postTime;
    612         public long earliestRemovaltime;
    613         private Runnable mRemoveHeadsUpRunnable;
    614         public boolean remoteInputActive;
    615         public boolean expanded;
    616 
    617         public void setEntry(final NotificationData.Entry entry) {
    618             this.entry = entry;
    619 
    620             // The actual post time will be just after the heads-up really slided in
    621             postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay;
    622             mRemoveHeadsUpRunnable = new Runnable() {
    623                 @Override
    624                 public void run() {
    625                     if (!mTrackingHeadsUp) {
    626                         removeHeadsUpEntry(entry);
    627                     } else {
    628                         mEntriesToRemoveAfterExpand.add(entry);
    629                     }
    630                 }
    631             };
    632             updateEntry();
    633         }
    634 
    635         public void updateEntry() {
    636             updateEntry(true);
    637         }
    638 
    639         public void updateEntry(boolean updatePostTime) {
    640             long currentTime = mClock.currentTimeMillis();
    641             earliestRemovaltime = currentTime + mMinimumDisplayTime;
    642             if (updatePostTime) {
    643                 postTime = Math.max(postTime, currentTime);
    644             }
    645             removeAutoRemovalCallbacks();
    646             if (mEntriesToRemoveAfterExpand.contains(entry)) {
    647                 mEntriesToRemoveAfterExpand.remove(entry);
    648             }
    649             if (!isSticky()) {
    650                 long finishTime = postTime + mHeadsUpNotificationDecay;
    651                 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
    652                 mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
    653             }
    654         }
    655 
    656         private boolean isSticky() {
    657             return (entry.row.isPinned() && expanded)
    658                     || remoteInputActive || hasFullScreenIntent(entry);
    659         }
    660 
    661         @Override
    662         public int compareTo(HeadsUpEntry o) {
    663             boolean isPinned = entry.row.isPinned();
    664             boolean otherPinned = o.entry.row.isPinned();
    665             if (isPinned && !otherPinned) {
    666                 return -1;
    667             } else if (!isPinned && otherPinned) {
    668                 return 1;
    669             }
    670             boolean selfFullscreen = hasFullScreenIntent(entry);
    671             boolean otherFullscreen = hasFullScreenIntent(o.entry);
    672             if (selfFullscreen && !otherFullscreen) {
    673                 return -1;
    674             } else if (!selfFullscreen && otherFullscreen) {
    675                 return 1;
    676             }
    677 
    678             if (remoteInputActive && !o.remoteInputActive) {
    679                 return -1;
    680             } else if (!remoteInputActive && o.remoteInputActive) {
    681                 return 1;
    682             }
    683 
    684             return postTime < o.postTime ? 1
    685                     : postTime == o.postTime ? entry.key.compareTo(o.entry.key)
    686                             : -1;
    687         }
    688 
    689         public void removeAutoRemovalCallbacks() {
    690             mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
    691         }
    692 
    693         public boolean wasShownLongEnough() {
    694             return earliestRemovaltime < mClock.currentTimeMillis();
    695         }
    696 
    697         public void removeAsSoonAsPossible() {
    698             removeAutoRemovalCallbacks();
    699             mHandler.postDelayed(mRemoveHeadsUpRunnable,
    700                     earliestRemovaltime - mClock.currentTimeMillis());
    701         }
    702 
    703         public void reset() {
    704             removeAutoRemovalCallbacks();
    705             entry = null;
    706             mRemoveHeadsUpRunnable = null;
    707             expanded = false;
    708             remoteInputActive = false;
    709         }
    710     }
    711 
    712     public static class Clock {
    713         public long currentTimeMillis() {
    714             return SystemClock.elapsedRealtime();
    715         }
    716     }
    717 
    718     public interface OnHeadsUpChangedListener {
    719         /**
    720          * The state whether there exist pinned heads-ups or not changed.
    721          *
    722          * @param inPinnedMode whether there are any pinned heads-ups
    723          */
    724         void onHeadsUpPinnedModeChanged(boolean inPinnedMode);
    725 
    726         /**
    727          * A notification was just pinned to the top.
    728          */
    729         void onHeadsUpPinned(ExpandableNotificationRow headsUp);
    730 
    731         /**
    732          * A notification was just unpinned from the top.
    733          */
    734         void onHeadsUpUnPinned(ExpandableNotificationRow headsUp);
    735 
    736         /**
    737          * A notification just became a heads up or turned back to its normal state.
    738          *
    739          * @param entry the entry of the changed notification
    740          * @param isHeadsUp whether the notification is now a headsUp notification
    741          */
    742         void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
    743     }
    744 }
    745