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