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 static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.Notification;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.database.ContentObserver;
27 import android.os.Handler;
28 import android.util.ArrayMap;
29 import android.util.ArraySet;
30 import android.util.Log;
31 import android.view.accessibility.AccessibilityEvent;
32 import android.view.accessibility.AccessibilityManager;
33 
34 import com.android.internal.logging.MetricsLogger;
35 import com.android.internal.logging.UiEvent;
36 import com.android.internal.logging.UiEventLogger;
37 import com.android.systemui.EventLogTags;
38 import com.android.systemui.dagger.qualifiers.Main;
39 import com.android.systemui.res.R;
40 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
41 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
42 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
43 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
44 import com.android.systemui.util.ListenerSet;
45 import com.android.systemui.util.concurrency.DelayableExecutor;
46 import com.android.systemui.util.settings.GlobalSettings;
47 import com.android.systemui.util.time.SystemClock;
48 
49 import java.io.PrintWriter;
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.stream.Stream;
53 
54 /**
55  * A manager which handles heads up notifications which is a special mode where
56  * they simply peek from the top of the screen.
57  */
58 public abstract class BaseHeadsUpManager implements HeadsUpManager {
59     private static final String TAG = "BaseHeadsUpManager";
60     private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
61 
62     protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>();
63 
64     protected final Context mContext;
65 
66     protected int mTouchAcceptanceDelay;
67     protected int mSnoozeLengthMs;
68     protected boolean mHasPinnedNotification;
69     protected int mUser;
70 
71     private final ArrayMap<String, Long> mSnoozedPackages;
72     private final AccessibilityManagerWrapper mAccessibilityMgr;
73 
74     private final UiEventLogger mUiEventLogger;
75     private final AvalancheController mAvalancheController;
76 
77     protected final SystemClock mSystemClock;
78     protected final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>();
79     protected final HeadsUpManagerLogger mLogger;
80     protected int mMinimumDisplayTime;
81     protected int mStickyForSomeTimeAutoDismissTime;
82     protected int mAutoDismissTime;
83     protected DelayableExecutor mExecutor;
84 
85     /**
86      * Enum entry for notification peek logged from this class.
87      */
88     enum NotificationPeekEvent implements UiEventLogger.UiEventEnum {
89         @UiEvent(doc = "Heads-up notification peeked on screen.")
90         NOTIFICATION_PEEK(801);
91 
92         private final int mId;
NotificationPeekEvent(int id)93         NotificationPeekEvent(int id) {
94             mId = id;
95         }
getId()96         @Override public int getId() {
97             return mId;
98         }
99     }
100 
BaseHeadsUpManager(@onNull final Context context, HeadsUpManagerLogger logger, @Main Handler handler, GlobalSettings globalSettings, SystemClock systemClock, @Main DelayableExecutor executor, AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger, AvalancheController avalancheController)101     public BaseHeadsUpManager(@NonNull final Context context,
102             HeadsUpManagerLogger logger,
103             @Main Handler handler,
104             GlobalSettings globalSettings,
105             SystemClock systemClock,
106             @Main DelayableExecutor executor,
107             AccessibilityManagerWrapper accessibilityManagerWrapper,
108             UiEventLogger uiEventLogger,
109             AvalancheController avalancheController) {
110         mLogger = logger;
111         mExecutor = executor;
112         mSystemClock = systemClock;
113         mContext = context;
114         mAccessibilityMgr = accessibilityManagerWrapper;
115         mUiEventLogger = uiEventLogger;
116         mAvalancheController = avalancheController;
117         Resources resources = context.getResources();
118         mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
119                 ? 500 : resources.getInteger(R.integer.heads_up_notification_minimum_time);
120         mStickyForSomeTimeAutoDismissTime = resources.getInteger(
121                 R.integer.sticky_heads_up_notification_time);
122         mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay);
123         mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay);
124         mSnoozedPackages = new ArrayMap<>();
125         int defaultSnoozeLengthMs =
126                 resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
127 
128         mSnoozeLengthMs = globalSettings.getInt(SETTING_HEADS_UP_SNOOZE_LENGTH_MS,
129                 defaultSnoozeLengthMs);
130         ContentObserver settingsObserver = new ContentObserver(handler) {
131             @Override
132             public void onChange(boolean selfChange) {
133                 final int packageSnoozeLengthMs = globalSettings.getInt(
134                         SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
135                 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
136                     mSnoozeLengthMs = packageSnoozeLengthMs;
137                     mLogger.logSnoozeLengthChange(packageSnoozeLengthMs);
138                 }
139             }
140         };
141         globalSettings.registerContentObserverSync(
142                 globalSettings.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS),
143                 /* notifyForDescendants = */ false,
144                 settingsObserver);
145     }
146 
147     /**
148      * Adds an OnHeadUpChangedListener to observe events.
149      */
addListener(@onNull OnHeadsUpChangedListener listener)150     public void addListener(@NonNull OnHeadsUpChangedListener listener) {
151         mListeners.addIfAbsent(listener);
152     }
153 
154     /**
155      * Removes the OnHeadUpChangedListener from the observer list.
156      */
removeListener(@onNull OnHeadsUpChangedListener listener)157     public void removeListener(@NonNull OnHeadsUpChangedListener listener) {
158         mListeners.remove(listener);
159     }
160 
161     /**
162      * Called when posting a new notification that should appear on screen.
163      * Adds the notification to be managed.
164      * @param entry entry to show
165      */
166     @Override
showNotification(@onNull NotificationEntry entry)167     public void showNotification(@NonNull NotificationEntry entry) {
168         HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry);
169 
170         mLogger.logShowNotificationRequest(entry);
171 
172         Runnable runnable = () -> {
173             // TODO(b/315362456) log outside runnable too
174             mLogger.logShowNotification(entry);
175 
176             // Add new entry and begin managing it
177             mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry);
178             onEntryAdded(headsUpEntry);
179             // TODO(b/328390331) move accessibility events to the view layer
180             entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
181             entry.setIsHeadsUpEntry(true);
182 
183             updateNotificationInternal(entry.getKey(), true /* shouldHeadsUpAgain */);
184             entry.setInterruption();
185         };
186         mAvalancheController.update(headsUpEntry, runnable, "showNotification");
187     }
188 
189     /**
190      * Try to remove the notification.  May not succeed if the notification has not been shown long
191      * enough and needs to be kept around.
192      * @param key the key of the notification to remove
193      * @param releaseImmediately force a remove regardless of earliest removal time
194      * @return true if notification is removed, false otherwise
195      */
196     @Override
removeNotification(@onNull String key, boolean releaseImmediately)197     public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
198         final boolean isWaiting = mAvalancheController.isWaiting(key);
199         mLogger.logRemoveNotification(key, releaseImmediately, isWaiting);
200 
201         if (mAvalancheController.isWaiting(key)) {
202             removeEntry(key, "removeNotification (isWaiting)");
203             return true;
204         }
205         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
206         if (headsUpEntry == null) {
207             return true;
208         }
209         if (releaseImmediately) {
210             removeEntry(key, "removeNotification (releaseImmediately)");
211             return true;
212         }
213         if (canRemoveImmediately(key)) {
214             removeEntry(key, "removeNotification (canRemoveImmediately)");
215             return true;
216         }
217         headsUpEntry.removeAsSoonAsPossible();
218         return false;
219     }
220 
221     /**
222      * Called when the notification state has been updated.
223      * @param key the key of the entry that was updated
224      * @param shouldHeadsUpAgain whether the notification should show again and force reevaluation
225      *                           of removal time
226      */
updateNotification(@onNull String key, boolean shouldHeadsUpAgain)227     public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) {
228         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
229         mLogger.logUpdateNotificationRequest(key, shouldHeadsUpAgain, headsUpEntry != null);
230 
231         Runnable runnable = () -> {
232             updateNotificationInternal(key, shouldHeadsUpAgain);
233         };
234         mAvalancheController.update(headsUpEntry, runnable, "updateNotification");
235     }
236 
updateNotificationInternal(@onNull String key, boolean shouldHeadsUpAgain)237     private void updateNotificationInternal(@NonNull String key, boolean shouldHeadsUpAgain) {
238         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
239         mLogger.logUpdateNotification(key, shouldHeadsUpAgain, headsUpEntry != null);
240         if (headsUpEntry == null) {
241             // the entry was released before this update (i.e by a listener) This can happen
242             // with the groupmanager
243             return;
244         }
245         // TODO(b/328390331) move accessibility events to the view layer
246         headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
247 
248         if (shouldHeadsUpAgain) {
249             headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification");
250             if (headsUpEntry != null) {
251                 setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry),
252                         "updateNotificationInternal");
253             }
254         }
255     }
256 
257     /**
258      * Clears all managed notifications.
259      */
releaseAllImmediately()260     public void releaseAllImmediately() {
261         mLogger.logReleaseAllImmediately();
262         // A copy is necessary here as we are changing the underlying map.  This would cause
263         // undefined behavior if we iterated over the key set directly.
264         ArraySet<String> keysToRemove = new ArraySet<>(mHeadsUpEntryMap.keySet());
265 
266         // Must get waiting keys before calling removeEntry, which clears waiting entries in
267         // AvalancheController
268         List<String> waitingKeysToRemove = mAvalancheController.getWaitingKeys();
269 
270         for (String key : keysToRemove) {
271             removeEntry(key, "releaseAllImmediately (keysToRemove)");
272         }
273         for (String key : waitingKeysToRemove) {
274             removeEntry(key, "releaseAllImmediately (waitingKeysToRemove)");
275         }
276     }
277 
278     /**
279      * Returns the entry if it is managed by this manager.
280      * @param key key of notification
281      * @return the entry
282      */
283     @Nullable
getEntry(@onNull String key)284     public NotificationEntry getEntry(@NonNull String key) {
285         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
286         return headsUpEntry != null ? headsUpEntry.mEntry : null;
287     }
288 
289     /**
290      * Returns the stream of all current notifications managed by this manager.
291      * @return all entries
292      */
293     @NonNull
294     @Override
getAllEntries()295     public Stream<NotificationEntry> getAllEntries() {
296         return getHeadsUpEntryList().stream().map(headsUpEntry -> headsUpEntry.mEntry);
297     }
298 
getHeadsUpEntryList()299     public List<HeadsUpEntry> getHeadsUpEntryList() {
300         List<HeadsUpEntry> entryList = new ArrayList<>(mHeadsUpEntryMap.values());
301         entryList.addAll(mAvalancheController.getWaitingEntryList());
302         return entryList;
303     }
304 
305     /**
306      * Whether or not there are any active notifications.
307      * @return true if there is an entry, false otherwise
308      */
309     @Override
hasNotifications()310     public boolean hasNotifications() {
311         return !mHeadsUpEntryMap.isEmpty()
312                 || !mAvalancheController.getWaitingEntryList().isEmpty();
313     }
314 
315     /**
316      * @return true if the notification is managed by this manager
317      */
isHeadsUpEntry(@onNull String key)318     public boolean isHeadsUpEntry(@NonNull String key) {
319         return mHeadsUpEntryMap.containsKey(key) || mAvalancheController.isWaiting(key);
320     }
321 
322     /**
323      * @param key
324      * @return When a HUN entry should be removed in milliseconds from now
325      */
326     @Override
getEarliestRemovalTime(String key)327     public long getEarliestRemovalTime(String key) {
328         HeadsUpEntry entry = mHeadsUpEntryMap.get(key);
329         if (entry != null) {
330             return Math.max(0, entry.mEarliestRemovalTime - mSystemClock.elapsedRealtime());
331         }
332         return 0;
333     }
334 
shouldHeadsUpBecomePinned(@onNull NotificationEntry entry)335     protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) {
336         final HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
337         if (headsUpEntry == null) {
338             // This should not happen since shouldHeadsUpBecomePinned is always called after adding
339             // the NotificationEntry into mHeadsUpEntryMap.
340             return hasFullScreenIntent(entry);
341         }
342         return hasFullScreenIntent(entry) && !headsUpEntry.mWasUnpinned;
343     }
344 
hasFullScreenIntent(@onNull NotificationEntry entry)345     protected boolean hasFullScreenIntent(@NonNull NotificationEntry entry) {
346         return entry.getSbn().getNotification().fullScreenIntent != null;
347     }
348 
setEntryPinned( @onNull BaseHeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned, String reason)349     protected void setEntryPinned(
350             @NonNull BaseHeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned,
351             String reason) {
352         mLogger.logSetEntryPinned(headsUpEntry.mEntry, isPinned, reason);
353         NotificationEntry entry = headsUpEntry.mEntry;
354         if (!isPinned) {
355             headsUpEntry.mWasUnpinned = true;
356         }
357         if (headsUpEntry.isRowPinned() != isPinned) {
358             headsUpEntry.setRowPinned(isPinned);
359             updatePinnedMode();
360             if (isPinned && entry.getSbn() != null) {
361                mUiEventLogger.logWithInstanceId(
362                         NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(),
363                         entry.getSbn().getPackageName(), entry.getSbn().getInstanceId());
364             }
365         // TODO(b/325936094) use the isPinned Flow instead
366             for (OnHeadsUpChangedListener listener : mListeners) {
367                 if (isPinned) {
368                     listener.onHeadsUpPinned(entry);
369                 } else {
370                     listener.onHeadsUpUnPinned(entry);
371                 }
372             }
373         }
374     }
375 
getContentFlag()376     public @InflationFlag int getContentFlag() {
377         return FLAG_CONTENT_VIEW_HEADS_UP;
378     }
379 
380     /**
381      * Manager-specific logic that should occur when an entry is added.
382      * @param headsUpEntry entry added
383      */
onEntryAdded(HeadsUpEntry headsUpEntry)384     protected void onEntryAdded(HeadsUpEntry headsUpEntry) {
385         NotificationEntry entry = headsUpEntry.mEntry;
386         entry.setHeadsUp(true);
387 
388         final boolean shouldPin = shouldHeadsUpBecomePinned(entry);
389         setEntryPinned(headsUpEntry, shouldPin, "onEntryAdded");
390         EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */);
391         for (OnHeadsUpChangedListener listener : mListeners) {
392             listener.onHeadsUpStateChanged(entry, true);
393         }
394     }
395 
396     /**
397      * Remove a notification from the alerting entries.
398      * @param key key of notification to remove
399      */
removeEntry(@onNull String key, String reason)400     protected final void removeEntry(@NonNull String key, String reason) {
401         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
402         boolean isWaiting;
403         if (headsUpEntry == null) {
404             headsUpEntry = mAvalancheController.getWaitingEntry(key);
405             isWaiting = true;
406         } else {
407             isWaiting = false;
408         }
409         mLogger.logRemoveEntryRequest(key, reason, isWaiting);
410         HeadsUpEntry finalHeadsUpEntry = headsUpEntry;
411         Runnable runnable = () -> {
412             mLogger.logRemoveEntry(key, reason, isWaiting);
413 
414             if (finalHeadsUpEntry == null) {
415                 return;
416             }
417             NotificationEntry entry = finalHeadsUpEntry.mEntry;
418 
419             // If the notification is animating, we will remove it at the end of the animation.
420             if (entry != null && entry.isExpandAnimationRunning()) {
421                 return;
422             }
423             entry.demoteStickyHun();
424             mHeadsUpEntryMap.remove(key);
425             onEntryRemoved(finalHeadsUpEntry);
426             // TODO(b/328390331) move accessibility events to the view layer
427             entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
428             if (NotificationsHeadsUpRefactor.isEnabled()) {
429                 finalHeadsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
430             } else {
431                 finalHeadsUpEntry.reset();
432             }
433         };
434         mAvalancheController.delete(headsUpEntry, runnable, "removeEntry");
435     }
436 
437     /**
438      * Manager-specific logic that should occur when an entry is removed.
439      * @param headsUpEntry entry removed
440      */
onEntryRemoved(HeadsUpEntry headsUpEntry)441     protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
442         NotificationEntry entry = headsUpEntry.mEntry;
443         entry.setHeadsUp(false);
444         setEntryPinned(headsUpEntry, false /* isPinned */, "onEntryRemoved");
445         EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */);
446         mLogger.logNotificationActuallyRemoved(entry);
447         for (OnHeadsUpChangedListener listener : mListeners) {
448             listener.onHeadsUpStateChanged(entry, false);
449         }
450     }
451 
452     /**
453      * Manager-specific logic, that should occur, when the entry is updated, and its posted time has
454      * changed.
455      *
456      * @param headsUpEntry entry updated
457      */
onEntryUpdated(HeadsUpEntry headsUpEntry)458     protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
459     }
460 
updatePinnedMode()461     protected void updatePinnedMode() {
462         boolean hasPinnedNotification = hasPinnedNotificationInternal();
463         if (hasPinnedNotification == mHasPinnedNotification) {
464             return;
465         }
466         mLogger.logUpdatePinnedMode(hasPinnedNotification);
467         mHasPinnedNotification = hasPinnedNotification;
468         if (mHasPinnedNotification) {
469             MetricsLogger.count(mContext, "note_peek", 1);
470         }
471         for (OnHeadsUpChangedListener listener : mListeners) {
472             listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
473         }
474     }
475 
476     /**
477      * Returns if the given notification is snoozed or not.
478      */
isSnoozed(@onNull String packageName)479     public boolean isSnoozed(@NonNull String packageName) {
480         final String key = snoozeKey(packageName, mUser);
481         Long snoozedUntil = mSnoozedPackages.get(key);
482         if (snoozedUntil != null) {
483             if (snoozedUntil > mSystemClock.elapsedRealtime()) {
484                 mLogger.logIsSnoozedReturned(key);
485                 return true;
486             }
487             mLogger.logPackageUnsnoozed(key);
488             mSnoozedPackages.remove(key);
489         }
490         return false;
491     }
492 
493     /**
494      * Snoozes all current Heads Up Notifications.
495      */
snooze()496     public void snooze() {
497         List<String> keySet = new ArrayList<>(mHeadsUpEntryMap.keySet());
498         keySet.addAll(mAvalancheController.getWaitingKeys());
499         for (String key : keySet) {
500             HeadsUpEntry entry = getHeadsUpEntry(key);
501             String packageName = entry.mEntry.getSbn().getPackageName();
502             String snoozeKey = snoozeKey(packageName, mUser);
503             mLogger.logPackageSnoozed(snoozeKey);
504             mSnoozedPackages.put(snoozeKey, mSystemClock.elapsedRealtime() + mSnoozeLengthMs);
505         }
506     }
507 
508     @NonNull
snoozeKey(@onNull String packageName, int user)509     private static String snoozeKey(@NonNull String packageName, int user) {
510         return user + "," + packageName;
511     }
512 
513     @Nullable
getHeadsUpEntry(@onNull String key)514     protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
515         if (mHeadsUpEntryMap.containsKey(key)) {
516             return mHeadsUpEntryMap.get(key);
517         }
518         return mAvalancheController.getWaitingEntry(key);
519     }
520 
521     /**
522      * Returns the top Heads Up Notification, which appears to show at first.
523      */
524     @Nullable
getTopEntry()525     public NotificationEntry getTopEntry() {
526         HeadsUpEntry topEntry = getTopHeadsUpEntry();
527         return (topEntry != null) ? topEntry.mEntry : null;
528     }
529 
530     @Nullable
getTopHeadsUpEntry()531     protected HeadsUpEntry getTopHeadsUpEntry() {
532         if (mHeadsUpEntryMap.isEmpty()) {
533             return null;
534         }
535         HeadsUpEntry topEntry = null;
536         for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
537             if (topEntry == null || entry.compareTo(topEntry) < 0) {
538                 topEntry = entry;
539             }
540         }
541         return topEntry;
542     }
543 
544     /**
545      * Sets the current user.
546      */
setUser(int user)547     public void setUser(int user) {
548         mUser = user;
549     }
550 
551     /** Returns the ID of the current user. */
getUser()552     public int getUser() {
553         return  mUser;
554     }
555 
556     @Override
dump(@onNull PrintWriter pw, @NonNull String[] args)557     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
558         pw.println("HeadsUpManager state:");
559         dumpInternal(pw, args);
560     }
561 
dumpInternal(@onNull PrintWriter pw, @NonNull String[] args)562     protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
563         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
564         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
565         pw.print("  now="); pw.println(mSystemClock.elapsedRealtime());
566         pw.print("  mUser="); pw.println(mUser);
567         for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
568             pw.print("  HeadsUpEntry="); pw.println(entry.mEntry);
569         }
570         int n = mSnoozedPackages.size();
571         pw.println("  snoozed packages: " + n);
572         for (int i = 0; i < n; i++) {
573             pw.print("    "); pw.print(mSnoozedPackages.valueAt(i));
574             pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
575         }
576     }
577 
578     /**
579      * Returns if there are any pinned Heads Up Notifications or not.
580      */
hasPinnedHeadsUp()581     public boolean hasPinnedHeadsUp() {
582         return mHasPinnedNotification;
583     }
584 
hasPinnedNotificationInternal()585     private boolean hasPinnedNotificationInternal() {
586         for (String key : mHeadsUpEntryMap.keySet()) {
587             HeadsUpEntry entry = getHeadsUpEntry(key);
588             if (entry.mEntry.isRowPinned()) {
589                 return true;
590             }
591         }
592         return false;
593     }
594 
595     /**
596      * Unpins all pinned Heads Up Notifications.
597      * @param userUnPinned The unpinned action is trigger by user real operation.
598      */
unpinAll(boolean userUnPinned)599     public void unpinAll(boolean userUnPinned) {
600         for (String key : mHeadsUpEntryMap.keySet()) {
601             HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
602             mLogger.logUnpinEntryRequest(key);
603             Runnable runnable = () -> {
604                 mLogger.logUnpinEntry(key);
605 
606                 setEntryPinned(headsUpEntry, false /* isPinned */, "unpinAll");
607                 // maybe it got un sticky
608                 headsUpEntry.updateEntry(false /* updatePostTime */, "unpinAll");
609 
610                 // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay
611                 // on the screen.
612                 if (userUnPinned && headsUpEntry.mEntry != null) {
613                     if (headsUpEntry.mEntry.mustStayOnScreen()) {
614                         headsUpEntry.mEntry.setHeadsUpIsVisible();
615                     }
616                 }
617             };
618             mAvalancheController.delete(headsUpEntry, runnable, "unpinAll");
619         }
620     }
621 
622     /**
623      * Returns the value of the tracking-heads-up flag. See the doc of {@code setTrackingHeadsUp} as
624      * well.
625      */
isTrackingHeadsUp()626     public boolean isTrackingHeadsUp() {
627         // Might be implemented in subclass.
628         return false;
629     }
630 
631     /**
632      * Compare two entries and decide how they should be ranked.
633      *
634      * @return -1 if the first argument should be ranked higher than the second, 1 if the second
635      * one should be ranked higher and 0 if they are equal.
636      */
compare(@ullable NotificationEntry a, @Nullable NotificationEntry b)637     public int compare(@Nullable NotificationEntry a, @Nullable NotificationEntry b) {
638         if (a == null || b == null) {
639             return Boolean.compare(a == null, b == null);
640         }
641         HeadsUpEntry aEntry = getHeadsUpEntry(a.getKey());
642         HeadsUpEntry bEntry = getHeadsUpEntry(b.getKey());
643         if (aEntry == null || bEntry == null) {
644             return Boolean.compare(aEntry == null, bEntry == null);
645         }
646         return aEntry.compareTo(bEntry);
647     }
648 
649     /**
650      * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
651      * until it's collapsed again.
652      */
setExpanded(@onNull NotificationEntry entry, boolean expanded)653     public void setExpanded(@NonNull NotificationEntry entry, boolean expanded) {
654         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
655         if (headsUpEntry != null && entry.isRowPinned()) {
656             headsUpEntry.setExpanded(expanded);
657         }
658     }
659 
660     /**
661      * Notes that the user took an action on an entry that might indirectly cause the system or the
662      * app to remove the notification.
663      *
664      * @param entry the entry that might be indirectly removed by the user's action
665      *
666      * @see HeadsUpCoordinator#mActionPressListener
667      * @see #canRemoveImmediately(String)
668      */
setUserActionMayIndirectlyRemove(@onNull NotificationEntry entry)669     public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) {
670         HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
671         if (headsUpEntry != null) {
672             headsUpEntry.mUserActionMayIndirectlyRemove = true;
673         }
674     }
675 
676     /**
677      * Whether or not the entry can be removed currently.  If it hasn't been on screen long enough
678      * it should not be removed unless forced
679      * @param key the key to check if removable
680      * @return true if the entry can be removed
681      */
682     @Override
canRemoveImmediately(@onNull String key)683     public boolean canRemoveImmediately(@NonNull String key) {
684         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
685         if (headsUpEntry != null && headsUpEntry.mUserActionMayIndirectlyRemove) {
686             return true;
687         }
688         return headsUpEntry == null || headsUpEntry.wasShownLongEnough()
689                 || headsUpEntry.mEntry.isRowDismissed();
690     }
691 
692     /**
693      * @param key
694      * @return true if the entry is (pinned and expanded) or (has an active remote input)
695      */
696     @Override
isSticky(String key)697     public boolean isSticky(String key) {
698         HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
699         if (headsUpEntry != null) {
700             return headsUpEntry.isSticky();
701         }
702         return false;
703     }
704 
705     @NonNull
createHeadsUpEntry(NotificationEntry entry)706     protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
707         return new HeadsUpEntry(entry);
708     }
709 
710     /**
711      * Determines if the notification is for a critical call that must display on top of an active
712      * input notification.
713      * The call isOngoing check is for a special case of incoming calls (see b/164291424).
714      */
isCriticalCallNotif(NotificationEntry entry)715     private static boolean isCriticalCallNotif(NotificationEntry entry) {
716         Notification n = entry.getSbn().getNotification();
717         boolean isIncomingCall = n.isStyle(Notification.CallStyle.class) && n.extras.getInt(
718                 Notification.EXTRA_CALL_TYPE) == Notification.CallStyle.CALL_TYPE_INCOMING;
719         return isIncomingCall || (entry.getSbn().isOngoing()
720                 && Notification.CATEGORY_CALL.equals(n.category));
721     }
722 
723     /**
724      * This represents a notification and how long it is in a heads up mode. It also manages its
725      * lifecycle automatically when created. This class is public because it is exposed by methods
726      * of AvalancheController that take it as param.
727      */
728     public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
729         public boolean mRemoteInputActive;
730         public boolean mUserActionMayIndirectlyRemove;
731 
732         protected boolean mExpanded;
733         protected boolean mWasUnpinned;
734 
735         @Nullable public NotificationEntry mEntry;
736         public long mPostTime;
737         public long mEarliestRemovalTime;
738 
739         @Nullable protected Runnable mRemoveRunnable;
740 
741         @Nullable private Runnable mCancelRemoveRunnable;
742 
HeadsUpEntry()743         public HeadsUpEntry() {
744             NotificationsHeadsUpRefactor.assertInLegacyMode();
745         }
746 
HeadsUpEntry(NotificationEntry entry)747         public HeadsUpEntry(NotificationEntry entry) {
748             // Attach NotificationEntry for AvalancheController to log key and
749             // record mPostTime for AvalancheController sorting
750             setEntry(entry, createRemoveRunnable(entry));
751         }
752 
753         /** Attach a NotificationEntry. */
setEntry(@onNull final NotificationEntry entry)754         public void setEntry(@NonNull final NotificationEntry entry) {
755             NotificationsHeadsUpRefactor.assertInLegacyMode();
756             setEntry(entry, createRemoveRunnable(entry));
757         }
758 
setEntry(@onNull final NotificationEntry entry, @Nullable Runnable removeRunnable)759         private void setEntry(@NonNull final NotificationEntry entry,
760                 @Nullable Runnable removeRunnable) {
761             mEntry = entry;
762             mRemoveRunnable = removeRunnable;
763 
764             mPostTime = calculatePostTime();
765             updateEntry(true /* updatePostTime */, "setEntry");
766         }
767 
isRowPinned()768         protected boolean isRowPinned() {
769             return mEntry != null && mEntry.isRowPinned();
770         }
771 
setRowPinned(boolean pinned)772         protected void setRowPinned(boolean pinned) {
773             if (mEntry != null) mEntry.setRowPinned(pinned);
774         }
775 
776         /**
777          * An interface that returns the amount of time left this HUN should show.
778          */
779         interface FinishTimeUpdater {
updateAndGetTimeRemaining()780             long updateAndGetTimeRemaining();
781         }
782 
783         /**
784          * Updates an entry's removal time.
785          * @param updatePostTime whether or not to refresh the post time
786          */
updateEntry(boolean updatePostTime, @Nullable String reason)787         public void updateEntry(boolean updatePostTime, @Nullable String reason) {
788             updateEntry(updatePostTime, /* updateEarliestRemovalTime= */ true, reason);
789         }
790 
791         /**
792          * Updates an entry's removal time.
793          * @param updatePostTime whether or not to refresh the post time
794          * @param updateEarliestRemovalTime whether this update should further delay removal
795          */
updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime, @Nullable String reason)796         public void updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime,
797                 @Nullable String reason) {
798             Runnable runnable = () -> {
799                 mLogger.logUpdateEntry(mEntry, updatePostTime, reason);
800 
801                 final long now = mSystemClock.elapsedRealtime();
802                 if (updateEarliestRemovalTime) {
803                     mEarliestRemovalTime = now + mMinimumDisplayTime;
804                 }
805 
806                 if (updatePostTime) {
807                     mPostTime = Math.max(mPostTime, now);
808                 }
809             };
810             mAvalancheController.update(this, runnable, "updateEntry (updatePostTime)");
811 
812             if (isSticky()) {
813                 cancelAutoRemovalCallbacks("updateEntry (sticky)");
814                 return;
815             }
816 
817             FinishTimeUpdater finishTimeCalculator = () -> {
818                 final long finishTime = calculateFinishTime();
819                 final long now = mSystemClock.elapsedRealtime();
820                 final long timeLeft = NotificationThrottleHun.isEnabled()
821                         ? Math.max(finishTime, mEarliestRemovalTime) - now
822                         : Math.max(finishTime - now, mMinimumDisplayTime);
823                 return timeLeft;
824             };
825             scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)");
826 
827             // Notify the manager, that the posted time has changed.
828             onEntryUpdated(this);
829         }
830 
831         /**
832          * Whether or not the notification is "sticky" i.e. should stay on screen regardless
833          * of the timer (forever) and should be removed externally.
834          * @return true if the notification is sticky
835          */
isSticky()836         public boolean isSticky() {
837             return (mEntry.isRowPinned() && mExpanded)
838                     || mRemoteInputActive
839                     || hasFullScreenIntent(mEntry);
840         }
841 
isStickyForSomeTime()842         public boolean isStickyForSomeTime() {
843             return mEntry.isStickyAndNotDemoted();
844         }
845 
846         /**
847          * Whether the notification has been on screen long enough and can be removed.
848          * @return true if the notification has been on screen long enough
849          */
wasShownLongEnough()850         public boolean wasShownLongEnough() {
851             return mEarliestRemovalTime < mSystemClock.elapsedRealtime();
852         }
853 
compareNonTimeFields(HeadsUpEntry headsUpEntry)854         public int compareNonTimeFields(HeadsUpEntry headsUpEntry) {
855             boolean selfFullscreen = hasFullScreenIntent(mEntry);
856             boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry);
857             if (selfFullscreen && !otherFullscreen) {
858                 return -1;
859             } else if (!selfFullscreen && otherFullscreen) {
860                 return 1;
861             }
862 
863             boolean selfCall = isCriticalCallNotif(mEntry);
864             boolean otherCall = isCriticalCallNotif(headsUpEntry.mEntry);
865 
866             if (selfCall && !otherCall) {
867                 return -1;
868             } else if (!selfCall && otherCall) {
869                 return 1;
870             }
871 
872             if (mRemoteInputActive && !headsUpEntry.mRemoteInputActive) {
873                 return -1;
874             } else if (!mRemoteInputActive && headsUpEntry.mRemoteInputActive) {
875                 return 1;
876             }
877             return 0;
878         }
879 
compareTo(@onNull HeadsUpEntry headsUpEntry)880         public int compareTo(@NonNull HeadsUpEntry headsUpEntry) {
881             boolean isPinned = mEntry.isRowPinned();
882             boolean otherPinned = headsUpEntry.mEntry.isRowPinned();
883             if (isPinned && !otherPinned) {
884                 return -1;
885             } else if (!isPinned && otherPinned) {
886                 return 1;
887             }
888             int nonTimeCompareResult = compareNonTimeFields(headsUpEntry);
889             if (nonTimeCompareResult != 0) {
890                 return nonTimeCompareResult;
891             }
892             if (mPostTime > headsUpEntry.mPostTime) {
893                 return -1;
894             } else if (mPostTime == headsUpEntry.mPostTime) {
895                 return mEntry.getKey().compareTo(headsUpEntry.mEntry.getKey());
896             } else {
897                 return 1;
898             }
899         }
900 
901         @Override
hashCode()902         public int hashCode() {
903             if (mEntry == null) return super.hashCode();
904             int result = mEntry.getKey().hashCode();
905             result = 31 * result;
906             return result;
907         }
908 
909         @Override
equals(@ullable Object o)910         public boolean equals(@Nullable Object o) {
911             if (this == o) return true;
912             if (o == null || !(o instanceof HeadsUpEntry)) return false;
913             HeadsUpEntry otherHeadsUpEntry = (HeadsUpEntry) o;
914             if (mEntry != null && otherHeadsUpEntry.mEntry != null) {
915                 return mEntry.getKey().equals(otherHeadsUpEntry.mEntry.getKey());
916             }
917             return false;
918         }
919 
setExpanded(boolean expanded)920         public void setExpanded(boolean expanded) {
921             this.mExpanded = expanded;
922         }
923 
reset()924         public void reset() {
925             NotificationsHeadsUpRefactor.assertInLegacyMode();
926             cancelAutoRemovalCallbacks("reset()");
927             mEntry = null;
928             mRemoveRunnable = null;
929             mExpanded = false;
930             mRemoteInputActive = false;
931         }
932 
933         /**
934          * Clear any pending removal runnables.
935          */
cancelAutoRemovalCallbacks(@ullable String reason)936         public void cancelAutoRemovalCallbacks(@Nullable String reason) {
937             mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
938             Runnable runnable = () -> {
939                 final boolean removed = cancelAutoRemovalCallbackInternal();
940 
941                 if (removed) {
942                     mLogger.logAutoRemoveCanceled(mEntry, reason);
943                 }
944             };
945             if (isHeadsUpEntry(this.mEntry.getKey())) {
946                 mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
947             } else {
948                 // Just removed
949                 runnable.run();
950             }
951         }
952 
scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, @NonNull String reason)953         public void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator,
954                 @NonNull String reason) {
955 
956             mLogger.logAutoRemoveRequest(this.mEntry, reason);
957             Runnable runnable = () -> {
958                 long delayMs = finishTimeCalculator.updateAndGetTimeRemaining();
959 
960                 if (mRemoveRunnable == null) {
961                     Log.wtf(TAG, "scheduleAutoRemovalCallback with no callback set");
962                     return;
963                 }
964 
965                 final boolean deletedExistingRemovalRunnable = cancelAutoRemovalCallbackInternal();
966                 mCancelRemoveRunnable = mExecutor.executeDelayed(mRemoveRunnable,
967                         delayMs);
968 
969                 if (deletedExistingRemovalRunnable) {
970                     mLogger.logAutoRemoveRescheduled(mEntry, delayMs, reason);
971                 } else {
972                     mLogger.logAutoRemoveScheduled(mEntry, delayMs, reason);
973                 }
974             };
975             mAvalancheController.update(this, runnable,
976                     reason + " scheduleAutoRemovalCallback");
977         }
978 
cancelAutoRemovalCallbackInternal()979         public boolean cancelAutoRemovalCallbackInternal() {
980             final boolean scheduled = (mCancelRemoveRunnable != null);
981 
982             if (scheduled) {
983                 mCancelRemoveRunnable.run();  // Delete removal runnable from Executor queue
984                 mCancelRemoveRunnable = null;
985             }
986 
987             return scheduled;
988         }
989 
990         /**
991          * Remove the entry at the earliest allowed removal time.
992          */
removeAsSoonAsPossible()993         public void removeAsSoonAsPossible() {
994             if (mRemoveRunnable != null) {
995 
996                 FinishTimeUpdater finishTimeCalculator = () -> {
997                     final long timeLeft = mEarliestRemovalTime - mSystemClock.elapsedRealtime();
998                     return timeLeft;
999                 };
1000                 scheduleAutoRemovalCallback(finishTimeCalculator, "removeAsSoonAsPossible");
1001             }
1002         }
1003 
1004         /** Creates a runnable to remove this notification from the alerting entries. */
createRemoveRunnable(NotificationEntry entry)1005         protected Runnable createRemoveRunnable(NotificationEntry entry) {
1006             return () -> removeEntry(entry.getKey(), "createRemoveRunnable");
1007         }
1008 
1009         /**
1010          * Calculate what the post time of a notification is at some current time.
1011          * @return the post time
1012          */
calculatePostTime()1013         protected long calculatePostTime() {
1014             // The actual post time will be just after the heads-up really slided in
1015             return mSystemClock.elapsedRealtime() + mTouchAcceptanceDelay;
1016         }
1017 
1018         /**
1019          * @return When the notification should auto-dismiss itself, based on
1020          * {@link SystemClock#elapsedRealtime()}
1021          */
calculateFinishTime()1022         protected long calculateFinishTime() {
1023             int requestedTimeOutMs;
1024             if (isStickyForSomeTime()) {
1025                 requestedTimeOutMs = mStickyForSomeTimeAutoDismissTime;
1026             } else {
1027                 requestedTimeOutMs = mAvalancheController.getDurationMs(this, mAutoDismissTime);
1028             }
1029             final long duration = getRecommendedHeadsUpTimeoutMs(requestedTimeOutMs);
1030             return mPostTime + duration;
1031         }
1032 
1033         /**
1034          * Get user-preferred or default timeout duration. The larger one will be returned.
1035          * @return milliseconds before auto-dismiss
1036          * @param requestedTimeout
1037          */
getRecommendedHeadsUpTimeoutMs(int requestedTimeout)1038         protected int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) {
1039             return mAccessibilityMgr.getRecommendedTimeoutMillis(
1040                     requestedTimeout,
1041                     AccessibilityManager.FLAG_CONTENT_CONTROLS
1042                             | AccessibilityManager.FLAG_CONTENT_ICONS
1043                             | AccessibilityManager.FLAG_CONTENT_TEXT);
1044         }
1045     }
1046 }
1047