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