1 /*
2  * Copyright (C) 2018 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;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.Handler;
22 import android.os.SystemClock;
23 import android.util.ArrayMap;
24 import android.util.ArraySet;
25 import android.view.accessibility.AccessibilityEvent;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.systemui.dagger.qualifiers.Main;
29 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
30 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
31 import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
32 
33 import java.util.stream.Stream;
34 
35 /**
36  * A manager which contains notification alerting functionality, providing methods to add and
37  * remove notifications that appear on screen for a period of time and dismiss themselves at the
38  * appropriate time.  These include heads up notifications and ambient pulses.
39  */
40 public abstract class AlertingNotificationManager {
41     private static final String TAG = "AlertNotifManager";
42     protected final Clock mClock = new Clock();
43     protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>();
44     protected final HeadsUpManagerLogger mLogger;
45 
AlertingNotificationManager(HeadsUpManagerLogger logger, @Main Handler handler)46     public AlertingNotificationManager(HeadsUpManagerLogger logger, @Main Handler handler) {
47         mLogger = logger;
48         mHandler = handler;
49     }
50 
51     protected int mMinimumDisplayTime;
52     protected int mStickyDisplayTime;
53     protected int mAutoDismissNotificationDecay;
54     @VisibleForTesting
55     public Handler mHandler;
56 
57     /**
58      * Called when posting a new notification that should alert the user and appear on screen.
59      * Adds the notification to be managed.
60      * @param entry entry to show
61      */
showNotification(@onNull NotificationEntry entry)62     public void showNotification(@NonNull NotificationEntry entry) {
63         mLogger.logShowNotification(entry);
64         addAlertEntry(entry);
65         updateNotification(entry.getKey(), true /* alert */);
66         entry.setInterruption();
67     }
68 
69     /**
70      * Try to remove the notification.  May not succeed if the notification has not been shown long
71      * enough and needs to be kept around.
72      * @param key the key of the notification to remove
73      * @param releaseImmediately force a remove regardless of earliest removal time
74      * @return true if notification is removed, false otherwise
75      */
removeNotification(@onNull String key, boolean releaseImmediately)76     public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
77         mLogger.logRemoveNotification(key, releaseImmediately);
78         AlertEntry alertEntry = mAlertEntries.get(key);
79         if (alertEntry == null) {
80             return true;
81         }
82         if (releaseImmediately || canRemoveImmediately(key)) {
83             removeAlertEntry(key);
84         } else {
85             alertEntry.removeAsSoonAsPossible();
86             return false;
87         }
88         return true;
89     }
90 
91     /**
92      * Called when the notification state has been updated.
93      * @param key the key of the entry that was updated
94      * @param alert whether the notification should alert again and force reevaluation of
95      *              removal time
96      */
updateNotification(@onNull String key, boolean alert)97     public void updateNotification(@NonNull String key, boolean alert) {
98         AlertEntry alertEntry = mAlertEntries.get(key);
99         mLogger.logUpdateNotification(key, alert, alertEntry != null);
100         if (alertEntry == null) {
101             // the entry was released before this update (i.e by a listener) This can happen
102             // with the groupmanager
103             return;
104         }
105 
106         alertEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
107         if (alert) {
108             alertEntry.updateEntry(true /* updatePostTime */);
109         }
110     }
111 
112     /**
113      * Clears all managed notifications.
114      */
releaseAllImmediately()115     public void releaseAllImmediately() {
116         mLogger.logReleaseAllImmediately();
117         // A copy is necessary here as we are changing the underlying map.  This would cause
118         // undefined behavior if we iterated over the key set directly.
119         ArraySet<String> keysToRemove = new ArraySet<>(mAlertEntries.keySet());
120         for (String key : keysToRemove) {
121             removeAlertEntry(key);
122         }
123     }
124 
125     /**
126      * Returns the entry if it is managed by this manager.
127      * @param key key of notification
128      * @return the entry
129      */
130     @Nullable
getEntry(@onNull String key)131     public NotificationEntry getEntry(@NonNull String key) {
132         AlertEntry entry = mAlertEntries.get(key);
133         return entry != null ? entry.mEntry : null;
134     }
135 
136     /**
137      * Returns the stream of all current notifications managed by this manager.
138      * @return all entries
139      */
140     @NonNull
getAllEntries()141     public Stream<NotificationEntry> getAllEntries() {
142         return mAlertEntries.values().stream().map(headsUpEntry -> headsUpEntry.mEntry);
143     }
144 
145     /**
146      * Whether or not there are any active alerting notifications.
147      * @return true if there is an alert, false otherwise
148      */
hasNotifications()149     public boolean hasNotifications() {
150         return !mAlertEntries.isEmpty();
151     }
152 
153     /**
154      * Whether or not the given notification is alerting and managed by this manager.
155      * @return true if the notification is alerting
156      */
isAlerting(@onNull String key)157     public boolean isAlerting(@NonNull String key) {
158         return mAlertEntries.containsKey(key);
159     }
160 
161     /**
162      * Gets the flag corresponding to the notification content view this alert manager will show.
163      *
164      * @return flag corresponding to the content view
165      */
getContentFlag()166     public abstract @InflationFlag int getContentFlag();
167 
168     /**
169      * Add a new entry and begin managing it.
170      * @param entry the entry to add
171      */
addAlertEntry(@onNull NotificationEntry entry)172     protected final void addAlertEntry(@NonNull NotificationEntry entry) {
173         AlertEntry alertEntry = createAlertEntry();
174         alertEntry.setEntry(entry);
175         mAlertEntries.put(entry.getKey(), alertEntry);
176         onAlertEntryAdded(alertEntry);
177         entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
178         entry.setIsAlerting(true);
179     }
180 
181     /**
182      * Manager-specific logic that should occur when an entry is added.
183      * @param alertEntry alert entry added
184      */
onAlertEntryAdded(@onNull AlertEntry alertEntry)185     protected abstract void onAlertEntryAdded(@NonNull AlertEntry alertEntry);
186 
187     /**
188      * Remove a notification and reset the alert entry.
189      * @param key key of notification to remove
190      */
removeAlertEntry(@onNull String key)191     protected final void removeAlertEntry(@NonNull String key) {
192         AlertEntry alertEntry = mAlertEntries.get(key);
193         if (alertEntry == null) {
194             return;
195         }
196         NotificationEntry entry = alertEntry.mEntry;
197 
198         // If the notification is animating, we will remove it at the end of the animation.
199         if (entry != null && entry.isExpandAnimationRunning()) {
200             return;
201         }
202         entry.demoteStickyHun();
203         mAlertEntries.remove(key);
204         onAlertEntryRemoved(alertEntry);
205         entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
206         alertEntry.reset();
207     }
208 
209     /**
210      * Manager-specific logic that should occur when an alert entry is removed.
211      * @param alertEntry alert entry removed
212      */
onAlertEntryRemoved(@onNull AlertEntry alertEntry)213     protected abstract void onAlertEntryRemoved(@NonNull AlertEntry alertEntry);
214 
215     /**
216      * Returns a new alert entry instance.
217      * @return a new AlertEntry
218      */
createAlertEntry()219     protected AlertEntry createAlertEntry() {
220         return new AlertEntry();
221     }
222 
223     /**
224      * Whether or not the alert can be removed currently.  If it hasn't been on screen long enough
225      * it should not be removed unless forced
226      * @param key the key to check if removable
227      * @return true if the alert entry can be removed
228      */
canRemoveImmediately(String key)229     public boolean canRemoveImmediately(String key) {
230         AlertEntry alertEntry = mAlertEntries.get(key);
231         return alertEntry == null || alertEntry.wasShownLongEnough()
232                 || alertEntry.mEntry.isRowDismissed();
233     }
234 
235     /**
236      * @param key
237      * @return true if the entry is (pinned and expanded) or (has an active remote input)
238      */
isSticky(String key)239     public boolean isSticky(String key) {
240         AlertEntry alerting = mAlertEntries.get(key);
241         if (alerting != null) {
242             return alerting.isSticky();
243         }
244         return false;
245     }
246 
247     /**
248      * @param key
249      * @return When a HUN entry should be removed in milliseconds from now
250      */
getEarliestRemovalTime(String key)251     public long getEarliestRemovalTime(String key) {
252         AlertEntry alerting = mAlertEntries.get(key);
253         if (alerting != null) {
254             return Math.max(0, alerting.mEarliestRemovaltime - mClock.currentTimeMillis());
255         }
256         return 0;
257     }
258 
259     protected class AlertEntry implements Comparable<AlertEntry> {
260         @Nullable public NotificationEntry mEntry;
261         public long mPostTime;
262         public long mEarliestRemovaltime;
263 
264         @Nullable protected Runnable mRemoveAlertRunnable;
265 
setEntry(@onNull final NotificationEntry entry)266         public void setEntry(@NonNull final NotificationEntry entry) {
267             setEntry(entry, () -> removeAlertEntry(entry.getKey()));
268         }
269 
setEntry(@onNull final NotificationEntry entry, @Nullable Runnable removeAlertRunnable)270         public void setEntry(@NonNull final NotificationEntry entry,
271                 @Nullable Runnable removeAlertRunnable) {
272             mEntry = entry;
273             mRemoveAlertRunnable = removeAlertRunnable;
274 
275             mPostTime = calculatePostTime();
276             updateEntry(true /* updatePostTime */);
277         }
278 
279         /**
280          * Updates an entry's removal time.
281          * @param updatePostTime whether or not to refresh the post time
282          */
updateEntry(boolean updatePostTime)283         public void updateEntry(boolean updatePostTime) {
284             mLogger.logUpdateEntry(mEntry, updatePostTime);
285 
286             final long now = mClock.currentTimeMillis();
287             mEarliestRemovaltime = now + mMinimumDisplayTime;
288 
289             if (updatePostTime) {
290                 mPostTime = Math.max(mPostTime, now);
291             }
292             removeAutoRemovalCallbacks();
293 
294             if (!isSticky()) {
295                 final long finishTime = calculateFinishTime();
296                 final long timeLeft = Math.max(finishTime - now, mMinimumDisplayTime);
297                 mHandler.postDelayed(mRemoveAlertRunnable, timeLeft);
298             }
299         }
300 
301         /**
302          * Whether or not the notification is "sticky" i.e. should stay on screen regardless
303          * of the timer (forever) and should be removed externally.
304          * @return true if the notification is sticky
305          */
isSticky()306         public boolean isSticky() {
307             // This implementation is overridden by HeadsUpManager HeadsUpEntry #isSticky
308             return false;
309         }
310 
isStickyForSomeTime()311         public boolean isStickyForSomeTime() {
312             // This implementation is overridden by HeadsUpManager HeadsUpEntry #isStickyForSomeTime
313             return false;
314         }
315 
316         /**
317          * Whether the notification has befen on screen long enough and can be removed.
318          * @return true if the notification has been on screen long enough
319          */
wasShownLongEnough()320         public boolean wasShownLongEnough() {
321             return mEarliestRemovaltime < mClock.currentTimeMillis();
322         }
323 
324         @Override
compareTo(@onNull AlertEntry alertEntry)325         public int compareTo(@NonNull AlertEntry alertEntry) {
326             return (mPostTime < alertEntry.mPostTime)
327                     ? 1 : ((mPostTime == alertEntry.mPostTime)
328                             ? mEntry.getKey().compareTo(alertEntry.mEntry.getKey()) : -1);
329         }
330 
reset()331         public void reset() {
332             mEntry = null;
333             removeAutoRemovalCallbacks();
334             mRemoveAlertRunnable = null;
335         }
336 
337         /**
338          * Clear any pending removal runnables.
339          */
removeAutoRemovalCallbacks()340         public void removeAutoRemovalCallbacks() {
341             if (mRemoveAlertRunnable != null) {
342                 mHandler.removeCallbacks(mRemoveAlertRunnable);
343             }
344         }
345 
346         /**
347          * Remove the alert at the earliest allowed removal time.
348          */
removeAsSoonAsPossible()349         public void removeAsSoonAsPossible() {
350             if (mRemoveAlertRunnable != null) {
351                 removeAutoRemovalCallbacks();
352 
353                 final long timeLeft = mEarliestRemovaltime - mClock.currentTimeMillis();
354                 mHandler.postDelayed(mRemoveAlertRunnable, timeLeft);
355             }
356         }
357 
358         /**
359          * Calculate what the post time of a notification is at some current time.
360          * @return the post time
361          */
calculatePostTime()362         protected long calculatePostTime() {
363             return mClock.currentTimeMillis();
364         }
365 
366         /**
367          * @return When the notification should auto-dismiss itself, based on
368          * {@link SystemClock#elapsedRealTime()}
369          */
calculateFinishTime()370         protected long calculateFinishTime() {
371             // Overridden by HeadsUpManager HeadsUpEntry #calculateFinishTime
372             return 0;
373         }
374     }
375 
376     protected final static class Clock {
currentTimeMillis()377         public long currentTimeMillis() {
378             return SystemClock.elapsedRealtime();
379         }
380     }
381 }
382