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