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 
HeadsUpManager(final Context context, View statusBarWindowView, NotificationGroupManager groupManager)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 
updateTouchableRegionListener()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 
setBar(PhoneStatusBar bar)156     public void setBar(PhoneStatusBar bar) {
157         mBar = bar;
158     }
159 
addListener(OnHeadsUpChangedListener listener)160     public void addListener(OnHeadsUpChangedListener listener) {
161         mListeners.add(listener);
162     }
163 
getBar()164     public PhoneStatusBar getBar() {
165         return mBar;
166     }
167 
168     /**
169      * Called when posting a new notification to the heads up.
170      */
showNotification(NotificationData.Entry headsUp)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      */
updateNotification(NotificationData.Entry headsUp, boolean alert)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 
addHeadsUpEntry(NotificationData.Entry entry)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 
shouldHeadsUpBecomePinned(NotificationData.Entry entry)212     private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
213         return !mIsExpanded || hasFullScreenIntent(entry);
214     }
215 
hasFullScreenIntent(NotificationData.Entry entry)216     private boolean hasFullScreenIntent(NotificationData.Entry entry) {
217         return entry.notification.getNotification().fullScreenIntent != null;
218     }
219 
setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned)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 
removeHeadsUpEntry(NotificationData.Entry entry)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 
updatePinnedMode()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      */
removeNotification(String key, boolean ignoreEarliestRemovalTime)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 
wasShownLongEnough(String key)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 
isHeadsUp(String key)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      */
releaseAllImmediately()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 
releaseImmediately(String key)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 
isSnoozed(String packageName)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 
snooze()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 
snoozeKey(String packageName, int user)339     private static String snoozeKey(String packageName, int user) {
340         return user + "," + packageName;
341     }
342 
getHeadsUpEntry(String key)343     private HeadsUpEntry getHeadsUpEntry(String key) {
344         return mHeadsUpEntries.get(key);
345     }
346 
getEntry(String key)347     public NotificationData.Entry getEntry(String key) {
348         return mHeadsUpEntries.get(key).entry;
349     }
350 
getAllEntries()351     public Collection<HeadsUpEntry> getAllEntries() {
352         return mHeadsUpEntries.values();
353     }
354 
getTopEntry()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      */
shouldSwallowClick(String key)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 
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info)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 
setUser(int user)410     public void setUser(int user) {
411         mUser = user;
412     }
413 
dump(FileDescriptor fd, PrintWriter pw, String[] args)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 
hasPinnedHeadsUp()431     public boolean hasPinnedHeadsUp() {
432         return mHasPinnedNotification;
433     }
434 
hasPinnedNotificationInternal()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      */
addSwipedOutNotification(String key)450     public void addSwipedOutNotification(String key) {
451         mSwipedOutKeys.add(key);
452     }
453 
unpinAll()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 
onExpandingFinished()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 
setTrackingHeadsUp(boolean trackingHeadsUp)478     public void setTrackingHeadsUp(boolean trackingHeadsUp) {
479         mTrackingHeadsUp = trackingHeadsUp;
480     }
481 
isTrackingHeadsUp()482     public boolean isTrackingHeadsUp() {
483         return mTrackingHeadsUp;
484     }
485 
setIsExpanded(boolean isExpanded)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      */
getTopHeadsUpPinnedHeight()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      */
compare(NotificationData.Entry a, NotificationData.Entry b)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      */
setHeadsUpGoingAway(boolean headsUpGoingAway)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      */
waitForStatusBarLayout()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 
setIsClickedNotification(View child, boolean clicked)568     public static void setIsClickedNotification(View child, boolean clicked) {
569         child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null);
570     }
571 
isClickedHeadsUpNotification(View child)572     public static boolean isClickedHeadsUpNotification(View child) {
573         Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION);
574         return clicked != null && clicked;
575     }
576 
setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive)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      */
setExpanded(NotificationData.Entry entry, boolean expanded)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 
setEntry(final NotificationData.Entry entry)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 
updateEntry()635         public void updateEntry() {
636             updateEntry(true);
637         }
638 
updateEntry(boolean updatePostTime)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 
isSticky()656         private boolean isSticky() {
657             return (entry.row.isPinned() && expanded)
658                     || remoteInputActive || hasFullScreenIntent(entry);
659         }
660 
661         @Override
compareTo(HeadsUpEntry o)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 
removeAutoRemovalCallbacks()689         public void removeAutoRemovalCallbacks() {
690             mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
691         }
692 
wasShownLongEnough()693         public boolean wasShownLongEnough() {
694             return earliestRemovaltime < mClock.currentTimeMillis();
695         }
696 
removeAsSoonAsPossible()697         public void removeAsSoonAsPossible() {
698             removeAutoRemovalCallbacks();
699             mHandler.postDelayed(mRemoveHeadsUpRunnable,
700                     earliestRemovaltime - mClock.currentTimeMillis());
701         }
702 
reset()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 {
currentTimeMillis()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          */
onHeadsUpPinnedModeChanged(boolean inPinnedMode)724         void onHeadsUpPinnedModeChanged(boolean inPinnedMode);
725 
726         /**
727          * A notification was just pinned to the top.
728          */
onHeadsUpPinned(ExpandableNotificationRow headsUp)729         void onHeadsUpPinned(ExpandableNotificationRow headsUp);
730 
731         /**
732          * A notification was just unpinned from the top.
733          */
onHeadsUpUnPinned(ExpandableNotificationRow headsUp)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          */
onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp)742         void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
743     }
744 }
745