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