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 
HeadsUpManager(final Context context, View statusBarWindowView, NotificationGroupManager groupManager)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 
updateTouchableRegionListener()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 
setBar(StatusBar bar)165     public void setBar(StatusBar bar) {
166         mBar = bar;
167     }
168 
addListener(OnHeadsUpChangedListener listener)169     public void addListener(OnHeadsUpChangedListener listener) {
170         mListeners.add(listener);
171     }
172 
removeListener(OnHeadsUpChangedListener listener)173     public void removeListener(OnHeadsUpChangedListener listener) {
174         mListeners.remove(listener);
175     }
176 
getBar()177     public StatusBar getBar() {
178         return mBar;
179     }
180 
181     /**
182      * Called when posting a new notification to the heads up.
183      */
showNotification(NotificationData.Entry headsUp)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      */
updateNotification(NotificationData.Entry headsUp, boolean alert)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 
addHeadsUpEntry(NotificationData.Entry entry)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 
shouldHeadsUpBecomePinned(NotificationData.Entry entry)225     private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
226         return mStatusBarState != StatusBarState.KEYGUARD
227                 && !mIsExpanded || hasFullScreenIntent(entry);
228     }
229 
hasFullScreenIntent(NotificationData.Entry entry)230     private boolean hasFullScreenIntent(NotificationData.Entry entry) {
231         return entry.notification.getNotification().fullScreenIntent != null;
232     }
233 
setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned)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 
removeHeadsUpEntry(NotificationData.Entry entry)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 
updatePinnedMode()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      */
removeNotification(String key, boolean ignoreEarliestRemovalTime)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 
wasShownLongEnough(String key)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 
isHeadsUp(String key)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      */
releaseAllImmediately()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 
releaseImmediately(String key)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 
isSnoozed(String packageName)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 
snooze()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 
snoozeKey(String packageName, int user)353     private static String snoozeKey(String packageName, int user) {
354         return user + "," + packageName;
355     }
356 
getHeadsUpEntry(String key)357     private HeadsUpEntry getHeadsUpEntry(String key) {
358         return mHeadsUpEntries.get(key);
359     }
360 
getEntry(String key)361     public NotificationData.Entry getEntry(String key) {
362         return mHeadsUpEntries.get(key).entry;
363     }
364 
getAllEntries()365     public Collection<HeadsUpEntry> getAllEntries() {
366         return mHeadsUpEntries.values();
367     }
368 
getTopEntry()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      */
shouldSwallowClick(String key)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 
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info)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 
setUser(int user)424     public void setUser(int user) {
425         mUser = user;
426     }
427 
dump(FileDescriptor fd, PrintWriter pw, String[] args)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 
hasPinnedHeadsUp()445     public boolean hasPinnedHeadsUp() {
446         return mHasPinnedNotification;
447     }
448 
hasPinnedNotificationInternal()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      */
addSwipedOutNotification(String key)464     public void addSwipedOutNotification(String key) {
465         mSwipedOutKeys.add(key);
466     }
467 
unpinAll()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 
onExpandingFinished()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 
setTrackingHeadsUp(boolean trackingHeadsUp)492     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
493         mTrackingHeadsUp = trackingHeadsUp;
494     }
495 
isTrackingHeadsUp()496     public boolean isTrackingHeadsUp() {
497         return mTrackingHeadsUp;
498     }
499 
setIsExpanded(boolean isExpanded)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      */
getTopHeadsUpPinnedHeight()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      */
compare(NotificationData.Entry a, NotificationData.Entry b)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      */
setHeadsUpGoingAway(boolean headsUpGoingAway)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      */
waitForStatusBarLayout()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 
setIsClickedNotification(View child, boolean clicked)582     public static void setIsClickedNotification(View child, boolean clicked) {
583         child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
584     }
585 
isClickedHeadsUpNotification(View child)586     public static boolean isClickedHeadsUpNotification(View child) {
587         Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
588         return clicked != null && clicked;
589     }
590 
setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive)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      */
setExpanded(NotificationData.Entry entry, boolean expanded)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
onReorderingAllowed()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 
setVisualStabilityManager(VisualStabilityManager visualStabilityManager)630     public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) {
631         mVisualStabilityManager = visualStabilityManager;
632     }
633 
setStatusBarState(int statusBarState)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 
setEntry(final NotificationData.Entry entry)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 
updateEntry()671         public void updateEntry() {
672             updateEntry(true);
673         }
674 
updateEntry(boolean updatePostTime)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 
isSticky()695         private boolean isSticky() {
696             return (entry.row.isPinned() && expanded)
697                     || remoteInputActive || hasFullScreenIntent(entry);
698         }
699 
700         @Override
compareTo(HeadsUpEntry o)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 
removeAutoRemovalCallbacks()728         public void removeAutoRemovalCallbacks() {
729             mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
730         }
731 
wasShownLongEnough()732         public boolean wasShownLongEnough() {
733             return earliestRemovaltime < mClock.currentTimeMillis();
734         }
735 
removeAsSoonAsPossible()736         public void removeAsSoonAsPossible() {
737             removeAutoRemovalCallbacks();
738             mHandler.postDelayed(mRemoveHeadsUpRunnable,
739                     earliestRemovaltime - mClock.currentTimeMillis());
740         }
741 
reset()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 {
currentTimeMillis()752         public long currentTimeMillis() {
753             return SystemClock.elapsedRealtime();
754         }
755     }
756 
757 }
758