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