blob: 33bcefb323f8ae309c0a99bfeb41ffd7e57a6736 [file] [log] [blame]
Kevind4f66a42018-08-03 13:12:51 -07001/*
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
17package com.android.systemui.statusbar;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.os.Handler;
22import android.os.Looper;
23import android.os.SystemClock;
24import android.util.ArrayMap;
25import android.util.ArraySet;
26import android.util.Log;
27import android.view.accessibility.AccessibilityEvent;
28
29import com.android.internal.annotations.VisibleForTesting;
Ned Burnsf81c4c42019-01-07 14:10:43 -050030import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Ned Burns1a5e22f2019-02-14 15:11:52 -050031import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
Kevind4f66a42018-08-03 13:12:51 -070032
33import 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 */
Kevina5ff1fa2018-08-21 16:35:48 -070040public abstract class AlertingNotificationManager implements NotificationLifetimeExtender {
Kevind4f66a42018-08-03 13:12:51 -070041 private static final String TAG = "AlertNotifManager";
42 protected final Clock mClock = new Clock();
43 protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>();
Kevina5ff1fa2018-08-21 16:35:48 -070044
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 */
Ned Burnsf81c4c42019-01-07 14:10:43 -050050 protected final ArraySet<NotificationEntry> mExtendedLifetimeAlertEntries = new ArraySet<>();
Kevina5ff1fa2018-08-21 16:35:48 -070051
52 protected NotificationSafeToRemoveCallback mNotificationLifetimeFinishedCallback;
Kevind4f66a42018-08-03 13:12:51 -070053 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 */
Ned Burnsf81c4c42019-01-07 14:10:43 -050063 public void showNotification(@NonNull NotificationEntry entry) {
Kevind4f66a42018-08-03 13:12:51 -070064 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 */
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 }
Kevina5ff1fa2018-08-21 16:35:48 -070087 if (releaseImmediately || canRemoveImmediately(key)) {
Kevind4f66a42018-08-03 13:12:51 -070088 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 */
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
Evan Laird94492852018-10-25 13:43:01 -0400114 alertEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
Kevind4f66a42018-08-03 13:12:51 -0700115 if (alert) {
116 alertEntry.updateEntry(true /* updatePostTime */);
117 }
118 }
119
120 /**
121 * Clears all managed notifications.
122 */
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
Ned Burnsf81c4c42019-01-07 14:10:43 -0500141 public NotificationEntry getEntry(@NonNull String key) {
Kevind4f66a42018-08-03 13:12:51 -0700142 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
Ned Burnsf81c4c42019-01-07 14:10:43 -0500151 public Stream<NotificationEntry> getAllEntries() {
Kevind4f66a42018-08-03 13:12:51 -0700152 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 */
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 */
Kevina97ea052018-09-11 13:53:18 -0700167 public boolean isAlerting(@NonNull String key) {
Kevind4f66a42018-08-03 13:12:51 -0700168 return mAlertEntries.containsKey(key);
169 }
170
171 /**
Kevin01a53cb2018-11-09 18:19:54 -0800172 * Gets the flag corresponding to the notification content view this alert manager will show.
173 *
174 * @return flag corresponding to the content view
175 */
176 public abstract @InflationFlag int getContentFlag();
177
178 /**
Kevind4f66a42018-08-03 13:12:51 -0700179 * Add a new entry and begin managing it.
180 * @param entry the entry to add
181 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500182 protected final void addAlertEntry(@NonNull NotificationEntry entry) {
Kevind4f66a42018-08-03 13:12:51 -0700183 AlertEntry alertEntry = createAlertEntry();
184 alertEntry.setEntry(entry);
185 mAlertEntries.put(entry.key, alertEntry);
186 onAlertEntryAdded(alertEntry);
Evan Laird94492852018-10-25 13:43:01 -0400187 entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
Kevind4f66a42018-08-03 13:12:51 -0700188 }
189
190 /**
191 * Manager-specific logic that should occur when an entry is added.
192 * @param alertEntry alert entry added
193 */
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 */
200 protected final void removeAlertEntry(@NonNull String key) {
201 AlertEntry alertEntry = mAlertEntries.get(key);
202 if (alertEntry == null) {
203 return;
204 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500205 NotificationEntry entry = alertEntry.mEntry;
Kevind4f66a42018-08-03 13:12:51 -0700206 mAlertEntries.remove(key);
207 onAlertEntryRemoved(alertEntry);
Evan Laird94492852018-10-25 13:43:01 -0400208 entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
Kevind4f66a42018-08-03 13:12:51 -0700209 alertEntry.reset();
Kevina5ff1fa2018-08-21 16:35:48 -0700210 if (mExtendedLifetimeAlertEntries.contains(entry)) {
211 if (mNotificationLifetimeFinishedCallback != null) {
212 mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
213 }
214 mExtendedLifetimeAlertEntries.remove(entry);
215 }
Kevind4f66a42018-08-03 13:12:51 -0700216 }
217
218 /**
219 * Manager-specific logic that should occur when an alert entry is removed.
220 * @param alertEntry alert entry removed
221 */
222 protected abstract void onAlertEntryRemoved(@NonNull AlertEntry alertEntry);
223
224 /**
225 * Returns a new alert entry instance.
226 * @return a new AlertEntry
227 */
228 protected AlertEntry createAlertEntry() {
229 return new AlertEntry();
230 }
231
Kevina5ff1fa2018-08-21 16:35:48 -0700232 /**
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 */
238 protected boolean canRemoveImmediately(String key) {
239 AlertEntry alertEntry = mAlertEntries.get(key);
Selim Cinekae55d832019-02-22 17:43:43 -0800240 return alertEntry == null || alertEntry.wasShownLongEnough()
241 || alertEntry.mEntry.isRowDismissed();
Kevina5ff1fa2018-08-21 16:35:48 -0700242 }
243
244 ///////////////////////////////////////////////////////////////////////////////////////////////
245 // NotificationLifetimeExtender Methods
246
247 @Override
248 public void setCallback(NotificationSafeToRemoveCallback callback) {
249 mNotificationLifetimeFinishedCallback = callback;
250 }
251
252 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500253 public boolean shouldExtendLifetime(NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700254 return !canRemoveImmediately(entry.key);
255 }
256
257 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500258 public void setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend) {
Kevina5ff1fa2018-08-21 16:35:48 -0700259 if (shouldExtend) {
260 mExtendedLifetimeAlertEntries.add(entry);
Selim Cinekbd559092019-01-28 19:38:58 -0800261 // 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();
Kevina5ff1fa2018-08-21 16:35:48 -0700265 } else {
266 mExtendedLifetimeAlertEntries.remove(entry);
267 }
268 }
269 ///////////////////////////////////////////////////////////////////////////////////////////////
270
Kevind4f66a42018-08-03 13:12:51 -0700271 protected class AlertEntry implements Comparable<AlertEntry> {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500272 @Nullable public NotificationEntry mEntry;
Kevind4f66a42018-08-03 13:12:51 -0700273 public long mPostTime;
274 public long mEarliestRemovaltime;
275
276 @Nullable protected Runnable mRemoveAlertRunnable;
277
Ned Burnsf81c4c42019-01-07 14:10:43 -0500278 public void setEntry(@NonNull final NotificationEntry entry) {
Kevind4f66a42018-08-03 13:12:51 -0700279 setEntry(entry, () -> removeAlertEntry(entry.key));
280 }
281
Ned Burnsf81c4c42019-01-07 14:10:43 -0500282 public void setEntry(@NonNull final NotificationEntry entry,
Kevind4f66a42018-08-03 13:12:51 -0700283 @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 */
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()) {
Kevina97ea052018-09-11 13:53:18 -0700308 long finishTime = calculateFinishTime();
Kevind4f66a42018-08-03 13:12:51 -0700309 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 */
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 */
327 public boolean wasShownLongEnough() {
328 return mEarliestRemovaltime < mClock.currentTimeMillis();
329 }
330
331 @Override
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
338 public void reset() {
339 mEntry = null;
340 removeAutoRemovalCallbacks();
341 mRemoveAlertRunnable = null;
342 }
343
344 /**
345 * Clear any pending removal runnables.
346 */
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 */
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 */
368 protected long calculatePostTime() {
369 return mClock.currentTimeMillis();
370 }
Kevina97ea052018-09-11 13:53:18 -0700371
372 /**
373 * Calculate when the notification should auto-dismiss itself.
374 * @return the finish time
375 */
376 protected long calculateFinishTime() {
377 return mPostTime + mAutoDismissNotificationDecay;
378 }
Kevind4f66a42018-08-03 13:12:51 -0700379 }
380
381 protected final static class Clock {
382 public long currentTimeMillis() {
383 return SystemClock.elapsedRealtime();
384 }
385 }
386}