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