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