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