• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.NotificationRowContentBinder.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.getKey(), 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.getKey(), alertEntry);
186         onAlertEntryAdded(alertEntry);
187         entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
188         entry.setIsAlerting(true);
189     }
190 
191     /**
192      * Manager-specific logic that should occur when an entry is added.
193      * @param alertEntry alert entry added
194      */
onAlertEntryAdded(@onNull AlertEntry alertEntry)195     protected abstract void onAlertEntryAdded(@NonNull AlertEntry alertEntry);
196 
197     /**
198      * Remove a notification and reset the alert entry.
199      * @param key key of notification to remove
200      */
removeAlertEntry(@onNull String key)201     protected final void removeAlertEntry(@NonNull String key) {
202         AlertEntry alertEntry = mAlertEntries.get(key);
203         if (alertEntry == null) {
204             return;
205         }
206         NotificationEntry entry = alertEntry.mEntry;
207 
208         // If the notification is animating, we will remove it at the end of the animation.
209         if (entry != null && entry.isExpandAnimationRunning()) {
210             return;
211         }
212 
213         mAlertEntries.remove(key);
214         onAlertEntryRemoved(alertEntry);
215         entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
216         alertEntry.reset();
217         if (mExtendedLifetimeAlertEntries.contains(entry)) {
218             if (mNotificationLifetimeFinishedCallback != null) {
219                 mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
220             }
221             mExtendedLifetimeAlertEntries.remove(entry);
222         }
223     }
224 
225     /**
226      * Manager-specific logic that should occur when an alert entry is removed.
227      * @param alertEntry alert entry removed
228      */
onAlertEntryRemoved(@onNull AlertEntry alertEntry)229     protected abstract void onAlertEntryRemoved(@NonNull AlertEntry alertEntry);
230 
231     /**
232      * Returns a new alert entry instance.
233      * @return a new AlertEntry
234      */
createAlertEntry()235     protected AlertEntry createAlertEntry() {
236         return new AlertEntry();
237     }
238 
239     /**
240      * Whether or not the alert can be removed currently.  If it hasn't been on screen long enough
241      * it should not be removed unless forced
242      * @param key the key to check if removable
243      * @return true if the alert entry can be removed
244      */
canRemoveImmediately(String key)245     protected boolean canRemoveImmediately(String key) {
246         AlertEntry alertEntry = mAlertEntries.get(key);
247         return alertEntry == null || alertEntry.wasShownLongEnough()
248                 || alertEntry.mEntry.isRowDismissed();
249     }
250 
251     ///////////////////////////////////////////////////////////////////////////////////////////////
252     // NotificationLifetimeExtender Methods
253 
254     @Override
setCallback(NotificationSafeToRemoveCallback callback)255     public void setCallback(NotificationSafeToRemoveCallback callback) {
256         mNotificationLifetimeFinishedCallback = callback;
257     }
258 
259     @Override
shouldExtendLifetime(NotificationEntry entry)260     public boolean shouldExtendLifetime(NotificationEntry entry) {
261         return !canRemoveImmediately(entry.getKey());
262     }
263 
264     @Override
setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)265     public void setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend) {
266         if (shouldExtend) {
267             mExtendedLifetimeAlertEntries.add(entry);
268             // We need to make sure that entries are stopping to alert eventually, let's remove
269             // this as soon as possible.
270             AlertEntry alertEntry = mAlertEntries.get(entry.getKey());
271             alertEntry.removeAsSoonAsPossible();
272         } else {
273             mExtendedLifetimeAlertEntries.remove(entry);
274         }
275     }
276     ///////////////////////////////////////////////////////////////////////////////////////////////
277 
278     protected class AlertEntry implements Comparable<AlertEntry> {
279         @Nullable public NotificationEntry mEntry;
280         public long mPostTime;
281         public long mEarliestRemovaltime;
282 
283         @Nullable protected Runnable mRemoveAlertRunnable;
284 
setEntry(@onNull final NotificationEntry entry)285         public void setEntry(@NonNull final NotificationEntry entry) {
286             setEntry(entry, () -> removeAlertEntry(entry.getKey()));
287         }
288 
setEntry(@onNull final NotificationEntry entry, @Nullable Runnable removeAlertRunnable)289         public void setEntry(@NonNull final NotificationEntry entry,
290                 @Nullable Runnable removeAlertRunnable) {
291             mEntry = entry;
292             mRemoveAlertRunnable = removeAlertRunnable;
293 
294             mPostTime = calculatePostTime();
295             updateEntry(true /* updatePostTime */);
296         }
297 
298         /**
299          * Updates an entry's removal time.
300          * @param updatePostTime whether or not to refresh the post time
301          */
updateEntry(boolean updatePostTime)302         public void updateEntry(boolean updatePostTime) {
303             if (Log.isLoggable(TAG, Log.VERBOSE)) {
304                 Log.v(TAG, "updateEntry");
305             }
306 
307             long currentTime = mClock.currentTimeMillis();
308             mEarliestRemovaltime = currentTime + mMinimumDisplayTime;
309             if (updatePostTime) {
310                 mPostTime = Math.max(mPostTime, currentTime);
311             }
312             removeAutoRemovalCallbacks();
313 
314             if (!isSticky()) {
315                 long finishTime = calculateFinishTime();
316                 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
317                 mHandler.postDelayed(mRemoveAlertRunnable, removeDelay);
318             }
319         }
320 
321         /**
322          * Whether or not the notification is "sticky" i.e. should stay on screen regardless
323          * of the timer and should be removed externally.
324          * @return true if the notification is sticky
325          */
isSticky()326         public boolean isSticky() {
327             return false;
328         }
329 
330         /**
331          * Whether the notification has been on screen long enough and can be removed.
332          * @return true if the notification has been on screen long enough
333          */
wasShownLongEnough()334         public boolean wasShownLongEnough() {
335             return mEarliestRemovaltime < mClock.currentTimeMillis();
336         }
337 
338         @Override
compareTo(@onNull AlertEntry alertEntry)339         public int compareTo(@NonNull AlertEntry alertEntry) {
340             return (mPostTime < alertEntry.mPostTime)
341                     ? 1 : ((mPostTime == alertEntry.mPostTime)
342                             ? mEntry.getKey().compareTo(alertEntry.mEntry.getKey()) : -1);
343         }
344 
reset()345         public void reset() {
346             mEntry = null;
347             removeAutoRemovalCallbacks();
348             mRemoveAlertRunnable = null;
349         }
350 
351         /**
352          * Clear any pending removal runnables.
353          */
removeAutoRemovalCallbacks()354         public void removeAutoRemovalCallbacks() {
355             if (mRemoveAlertRunnable != null) {
356                 mHandler.removeCallbacks(mRemoveAlertRunnable);
357             }
358         }
359 
360         /**
361          * Remove the alert at the earliest allowed removal time.
362          */
removeAsSoonAsPossible()363         public void removeAsSoonAsPossible() {
364             if (mRemoveAlertRunnable != null) {
365                 removeAutoRemovalCallbacks();
366                 mHandler.postDelayed(mRemoveAlertRunnable,
367                         mEarliestRemovaltime - mClock.currentTimeMillis());
368             }
369         }
370 
371         /**
372          * Calculate what the post time of a notification is at some current time.
373          * @return the post time
374          */
calculatePostTime()375         protected long calculatePostTime() {
376             return mClock.currentTimeMillis();
377         }
378 
379         /**
380          * Calculate when the notification should auto-dismiss itself.
381          * @return the finish time
382          */
calculateFinishTime()383         protected long calculateFinishTime() {
384             return mPostTime + mAutoDismissNotificationDecay;
385         }
386     }
387 
388     protected final static class Clock {
currentTimeMillis()389         public long currentTimeMillis() {
390             return SystemClock.elapsedRealtime();
391         }
392     }
393 }
394