1 /* 2 * Copyright (C) 2015 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.policy; 18 19 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.Notification; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.database.ContentObserver; 27 import android.os.Handler; 28 import android.util.ArrayMap; 29 import android.util.ArraySet; 30 import android.util.Log; 31 import android.view.accessibility.AccessibilityEvent; 32 import android.view.accessibility.AccessibilityManager; 33 34 import com.android.internal.logging.MetricsLogger; 35 import com.android.internal.logging.UiEvent; 36 import com.android.internal.logging.UiEventLogger; 37 import com.android.systemui.EventLogTags; 38 import com.android.systemui.dagger.qualifiers.Main; 39 import com.android.systemui.res.R; 40 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 41 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; 42 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; 43 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; 44 import com.android.systemui.util.ListenerSet; 45 import com.android.systemui.util.concurrency.DelayableExecutor; 46 import com.android.systemui.util.settings.GlobalSettings; 47 import com.android.systemui.util.time.SystemClock; 48 49 import java.io.PrintWriter; 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.stream.Stream; 53 54 /** 55 * A manager which handles heads up notifications which is a special mode where 56 * they simply peek from the top of the screen. 57 */ 58 public abstract class BaseHeadsUpManager implements HeadsUpManager { 59 private static final String TAG = "BaseHeadsUpManager"; 60 private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; 61 62 protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>(); 63 64 protected final Context mContext; 65 66 protected int mTouchAcceptanceDelay; 67 protected int mSnoozeLengthMs; 68 protected boolean mHasPinnedNotification; 69 protected int mUser; 70 71 private final ArrayMap<String, Long> mSnoozedPackages; 72 private final AccessibilityManagerWrapper mAccessibilityMgr; 73 74 private final UiEventLogger mUiEventLogger; 75 private final AvalancheController mAvalancheController; 76 77 protected final SystemClock mSystemClock; 78 protected final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); 79 protected final HeadsUpManagerLogger mLogger; 80 protected int mMinimumDisplayTime; 81 protected int mStickyForSomeTimeAutoDismissTime; 82 protected int mAutoDismissTime; 83 protected DelayableExecutor mExecutor; 84 85 /** 86 * Enum entry for notification peek logged from this class. 87 */ 88 enum NotificationPeekEvent implements UiEventLogger.UiEventEnum { 89 @UiEvent(doc = "Heads-up notification peeked on screen.") 90 NOTIFICATION_PEEK(801); 91 92 private final int mId; NotificationPeekEvent(int id)93 NotificationPeekEvent(int id) { 94 mId = id; 95 } getId()96 @Override public int getId() { 97 return mId; 98 } 99 } 100 BaseHeadsUpManager(@onNull final Context context, HeadsUpManagerLogger logger, @Main Handler handler, GlobalSettings globalSettings, SystemClock systemClock, @Main DelayableExecutor executor, AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger, AvalancheController avalancheController)101 public BaseHeadsUpManager(@NonNull final Context context, 102 HeadsUpManagerLogger logger, 103 @Main Handler handler, 104 GlobalSettings globalSettings, 105 SystemClock systemClock, 106 @Main DelayableExecutor executor, 107 AccessibilityManagerWrapper accessibilityManagerWrapper, 108 UiEventLogger uiEventLogger, 109 AvalancheController avalancheController) { 110 mLogger = logger; 111 mExecutor = executor; 112 mSystemClock = systemClock; 113 mContext = context; 114 mAccessibilityMgr = accessibilityManagerWrapper; 115 mUiEventLogger = uiEventLogger; 116 mAvalancheController = avalancheController; 117 Resources resources = context.getResources(); 118 mMinimumDisplayTime = NotificationThrottleHun.isEnabled() 119 ? 500 : resources.getInteger(R.integer.heads_up_notification_minimum_time); 120 mStickyForSomeTimeAutoDismissTime = resources.getInteger( 121 R.integer.sticky_heads_up_notification_time); 122 mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay); 123 mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); 124 mSnoozedPackages = new ArrayMap<>(); 125 int defaultSnoozeLengthMs = 126 resources.getInteger(R.integer.heads_up_default_snooze_length_ms); 127 128 mSnoozeLengthMs = globalSettings.getInt(SETTING_HEADS_UP_SNOOZE_LENGTH_MS, 129 defaultSnoozeLengthMs); 130 ContentObserver settingsObserver = new ContentObserver(handler) { 131 @Override 132 public void onChange(boolean selfChange) { 133 final int packageSnoozeLengthMs = globalSettings.getInt( 134 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); 135 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { 136 mSnoozeLengthMs = packageSnoozeLengthMs; 137 mLogger.logSnoozeLengthChange(packageSnoozeLengthMs); 138 } 139 } 140 }; 141 globalSettings.registerContentObserverSync( 142 globalSettings.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), 143 /* notifyForDescendants = */ false, 144 settingsObserver); 145 } 146 147 /** 148 * Adds an OnHeadUpChangedListener to observe events. 149 */ addListener(@onNull OnHeadsUpChangedListener listener)150 public void addListener(@NonNull OnHeadsUpChangedListener listener) { 151 mListeners.addIfAbsent(listener); 152 } 153 154 /** 155 * Removes the OnHeadUpChangedListener from the observer list. 156 */ removeListener(@onNull OnHeadsUpChangedListener listener)157 public void removeListener(@NonNull OnHeadsUpChangedListener listener) { 158 mListeners.remove(listener); 159 } 160 161 /** 162 * Called when posting a new notification that should appear on screen. 163 * Adds the notification to be managed. 164 * @param entry entry to show 165 */ 166 @Override showNotification(@onNull NotificationEntry entry)167 public void showNotification(@NonNull NotificationEntry entry) { 168 HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry); 169 170 mLogger.logShowNotificationRequest(entry); 171 172 Runnable runnable = () -> { 173 // TODO(b/315362456) log outside runnable too 174 mLogger.logShowNotification(entry); 175 176 // Add new entry and begin managing it 177 mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry); 178 onEntryAdded(headsUpEntry); 179 // TODO(b/328390331) move accessibility events to the view layer 180 entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 181 entry.setIsHeadsUpEntry(true); 182 183 updateNotificationInternal(entry.getKey(), true /* shouldHeadsUpAgain */); 184 entry.setInterruption(); 185 }; 186 mAvalancheController.update(headsUpEntry, runnable, "showNotification"); 187 } 188 189 /** 190 * Try to remove the notification. May not succeed if the notification has not been shown long 191 * enough and needs to be kept around. 192 * @param key the key of the notification to remove 193 * @param releaseImmediately force a remove regardless of earliest removal time 194 * @return true if notification is removed, false otherwise 195 */ 196 @Override removeNotification(@onNull String key, boolean releaseImmediately)197 public boolean removeNotification(@NonNull String key, boolean releaseImmediately) { 198 final boolean isWaiting = mAvalancheController.isWaiting(key); 199 mLogger.logRemoveNotification(key, releaseImmediately, isWaiting); 200 201 if (mAvalancheController.isWaiting(key)) { 202 removeEntry(key, "removeNotification (isWaiting)"); 203 return true; 204 } 205 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 206 if (headsUpEntry == null) { 207 return true; 208 } 209 if (releaseImmediately) { 210 removeEntry(key, "removeNotification (releaseImmediately)"); 211 return true; 212 } 213 if (canRemoveImmediately(key)) { 214 removeEntry(key, "removeNotification (canRemoveImmediately)"); 215 return true; 216 } 217 headsUpEntry.removeAsSoonAsPossible(); 218 return false; 219 } 220 221 /** 222 * Called when the notification state has been updated. 223 * @param key the key of the entry that was updated 224 * @param shouldHeadsUpAgain whether the notification should show again and force reevaluation 225 * of removal time 226 */ updateNotification(@onNull String key, boolean shouldHeadsUpAgain)227 public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) { 228 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 229 mLogger.logUpdateNotificationRequest(key, shouldHeadsUpAgain, headsUpEntry != null); 230 231 Runnable runnable = () -> { 232 updateNotificationInternal(key, shouldHeadsUpAgain); 233 }; 234 mAvalancheController.update(headsUpEntry, runnable, "updateNotification"); 235 } 236 updateNotificationInternal(@onNull String key, boolean shouldHeadsUpAgain)237 private void updateNotificationInternal(@NonNull String key, boolean shouldHeadsUpAgain) { 238 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 239 mLogger.logUpdateNotification(key, shouldHeadsUpAgain, headsUpEntry != null); 240 if (headsUpEntry == null) { 241 // the entry was released before this update (i.e by a listener) This can happen 242 // with the groupmanager 243 return; 244 } 245 // TODO(b/328390331) move accessibility events to the view layer 246 headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 247 248 if (shouldHeadsUpAgain) { 249 headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification"); 250 if (headsUpEntry != null) { 251 setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry), 252 "updateNotificationInternal"); 253 } 254 } 255 } 256 257 /** 258 * Clears all managed notifications. 259 */ releaseAllImmediately()260 public void releaseAllImmediately() { 261 mLogger.logReleaseAllImmediately(); 262 // A copy is necessary here as we are changing the underlying map. This would cause 263 // undefined behavior if we iterated over the key set directly. 264 ArraySet<String> keysToRemove = new ArraySet<>(mHeadsUpEntryMap.keySet()); 265 266 // Must get waiting keys before calling removeEntry, which clears waiting entries in 267 // AvalancheController 268 List<String> waitingKeysToRemove = mAvalancheController.getWaitingKeys(); 269 270 for (String key : keysToRemove) { 271 removeEntry(key, "releaseAllImmediately (keysToRemove)"); 272 } 273 for (String key : waitingKeysToRemove) { 274 removeEntry(key, "releaseAllImmediately (waitingKeysToRemove)"); 275 } 276 } 277 278 /** 279 * Returns the entry if it is managed by this manager. 280 * @param key key of notification 281 * @return the entry 282 */ 283 @Nullable getEntry(@onNull String key)284 public NotificationEntry getEntry(@NonNull String key) { 285 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 286 return headsUpEntry != null ? headsUpEntry.mEntry : null; 287 } 288 289 /** 290 * Returns the stream of all current notifications managed by this manager. 291 * @return all entries 292 */ 293 @NonNull 294 @Override getAllEntries()295 public Stream<NotificationEntry> getAllEntries() { 296 return getHeadsUpEntryList().stream().map(headsUpEntry -> headsUpEntry.mEntry); 297 } 298 getHeadsUpEntryList()299 public List<HeadsUpEntry> getHeadsUpEntryList() { 300 List<HeadsUpEntry> entryList = new ArrayList<>(mHeadsUpEntryMap.values()); 301 entryList.addAll(mAvalancheController.getWaitingEntryList()); 302 return entryList; 303 } 304 305 /** 306 * Whether or not there are any active notifications. 307 * @return true if there is an entry, false otherwise 308 */ 309 @Override hasNotifications()310 public boolean hasNotifications() { 311 return !mHeadsUpEntryMap.isEmpty() 312 || !mAvalancheController.getWaitingEntryList().isEmpty(); 313 } 314 315 /** 316 * @return true if the notification is managed by this manager 317 */ isHeadsUpEntry(@onNull String key)318 public boolean isHeadsUpEntry(@NonNull String key) { 319 return mHeadsUpEntryMap.containsKey(key) || mAvalancheController.isWaiting(key); 320 } 321 322 /** 323 * @param key 324 * @return When a HUN entry should be removed in milliseconds from now 325 */ 326 @Override getEarliestRemovalTime(String key)327 public long getEarliestRemovalTime(String key) { 328 HeadsUpEntry entry = mHeadsUpEntryMap.get(key); 329 if (entry != null) { 330 return Math.max(0, entry.mEarliestRemovalTime - mSystemClock.elapsedRealtime()); 331 } 332 return 0; 333 } 334 shouldHeadsUpBecomePinned(@onNull NotificationEntry entry)335 protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) { 336 final HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 337 if (headsUpEntry == null) { 338 // This should not happen since shouldHeadsUpBecomePinned is always called after adding 339 // the NotificationEntry into mHeadsUpEntryMap. 340 return hasFullScreenIntent(entry); 341 } 342 return hasFullScreenIntent(entry) && !headsUpEntry.mWasUnpinned; 343 } 344 hasFullScreenIntent(@onNull NotificationEntry entry)345 protected boolean hasFullScreenIntent(@NonNull NotificationEntry entry) { 346 return entry.getSbn().getNotification().fullScreenIntent != null; 347 } 348 setEntryPinned( @onNull BaseHeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned, String reason)349 protected void setEntryPinned( 350 @NonNull BaseHeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned, 351 String reason) { 352 mLogger.logSetEntryPinned(headsUpEntry.mEntry, isPinned, reason); 353 NotificationEntry entry = headsUpEntry.mEntry; 354 if (!isPinned) { 355 headsUpEntry.mWasUnpinned = true; 356 } 357 if (headsUpEntry.isRowPinned() != isPinned) { 358 headsUpEntry.setRowPinned(isPinned); 359 updatePinnedMode(); 360 if (isPinned && entry.getSbn() != null) { 361 mUiEventLogger.logWithInstanceId( 362 NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(), 363 entry.getSbn().getPackageName(), entry.getSbn().getInstanceId()); 364 } 365 // TODO(b/325936094) use the isPinned Flow instead 366 for (OnHeadsUpChangedListener listener : mListeners) { 367 if (isPinned) { 368 listener.onHeadsUpPinned(entry); 369 } else { 370 listener.onHeadsUpUnPinned(entry); 371 } 372 } 373 } 374 } 375 getContentFlag()376 public @InflationFlag int getContentFlag() { 377 return FLAG_CONTENT_VIEW_HEADS_UP; 378 } 379 380 /** 381 * Manager-specific logic that should occur when an entry is added. 382 * @param headsUpEntry entry added 383 */ onEntryAdded(HeadsUpEntry headsUpEntry)384 protected void onEntryAdded(HeadsUpEntry headsUpEntry) { 385 NotificationEntry entry = headsUpEntry.mEntry; 386 entry.setHeadsUp(true); 387 388 final boolean shouldPin = shouldHeadsUpBecomePinned(entry); 389 setEntryPinned(headsUpEntry, shouldPin, "onEntryAdded"); 390 EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */); 391 for (OnHeadsUpChangedListener listener : mListeners) { 392 listener.onHeadsUpStateChanged(entry, true); 393 } 394 } 395 396 /** 397 * Remove a notification from the alerting entries. 398 * @param key key of notification to remove 399 */ removeEntry(@onNull String key, String reason)400 protected final void removeEntry(@NonNull String key, String reason) { 401 HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); 402 boolean isWaiting; 403 if (headsUpEntry == null) { 404 headsUpEntry = mAvalancheController.getWaitingEntry(key); 405 isWaiting = true; 406 } else { 407 isWaiting = false; 408 } 409 mLogger.logRemoveEntryRequest(key, reason, isWaiting); 410 HeadsUpEntry finalHeadsUpEntry = headsUpEntry; 411 Runnable runnable = () -> { 412 mLogger.logRemoveEntry(key, reason, isWaiting); 413 414 if (finalHeadsUpEntry == null) { 415 return; 416 } 417 NotificationEntry entry = finalHeadsUpEntry.mEntry; 418 419 // If the notification is animating, we will remove it at the end of the animation. 420 if (entry != null && entry.isExpandAnimationRunning()) { 421 return; 422 } 423 entry.demoteStickyHun(); 424 mHeadsUpEntryMap.remove(key); 425 onEntryRemoved(finalHeadsUpEntry); 426 // TODO(b/328390331) move accessibility events to the view layer 427 entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 428 if (NotificationsHeadsUpRefactor.isEnabled()) { 429 finalHeadsUpEntry.cancelAutoRemovalCallbacks("removeEntry"); 430 } else { 431 finalHeadsUpEntry.reset(); 432 } 433 }; 434 mAvalancheController.delete(headsUpEntry, runnable, "removeEntry"); 435 } 436 437 /** 438 * Manager-specific logic that should occur when an entry is removed. 439 * @param headsUpEntry entry removed 440 */ onEntryRemoved(HeadsUpEntry headsUpEntry)441 protected void onEntryRemoved(HeadsUpEntry headsUpEntry) { 442 NotificationEntry entry = headsUpEntry.mEntry; 443 entry.setHeadsUp(false); 444 setEntryPinned(headsUpEntry, false /* isPinned */, "onEntryRemoved"); 445 EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */); 446 mLogger.logNotificationActuallyRemoved(entry); 447 for (OnHeadsUpChangedListener listener : mListeners) { 448 listener.onHeadsUpStateChanged(entry, false); 449 } 450 } 451 452 /** 453 * Manager-specific logic, that should occur, when the entry is updated, and its posted time has 454 * changed. 455 * 456 * @param headsUpEntry entry updated 457 */ onEntryUpdated(HeadsUpEntry headsUpEntry)458 protected void onEntryUpdated(HeadsUpEntry headsUpEntry) { 459 } 460 updatePinnedMode()461 protected void updatePinnedMode() { 462 boolean hasPinnedNotification = hasPinnedNotificationInternal(); 463 if (hasPinnedNotification == mHasPinnedNotification) { 464 return; 465 } 466 mLogger.logUpdatePinnedMode(hasPinnedNotification); 467 mHasPinnedNotification = hasPinnedNotification; 468 if (mHasPinnedNotification) { 469 MetricsLogger.count(mContext, "note_peek", 1); 470 } 471 for (OnHeadsUpChangedListener listener : mListeners) { 472 listener.onHeadsUpPinnedModeChanged(hasPinnedNotification); 473 } 474 } 475 476 /** 477 * Returns if the given notification is snoozed or not. 478 */ isSnoozed(@onNull String packageName)479 public boolean isSnoozed(@NonNull String packageName) { 480 final String key = snoozeKey(packageName, mUser); 481 Long snoozedUntil = mSnoozedPackages.get(key); 482 if (snoozedUntil != null) { 483 if (snoozedUntil > mSystemClock.elapsedRealtime()) { 484 mLogger.logIsSnoozedReturned(key); 485 return true; 486 } 487 mLogger.logPackageUnsnoozed(key); 488 mSnoozedPackages.remove(key); 489 } 490 return false; 491 } 492 493 /** 494 * Snoozes all current Heads Up Notifications. 495 */ snooze()496 public void snooze() { 497 List<String> keySet = new ArrayList<>(mHeadsUpEntryMap.keySet()); 498 keySet.addAll(mAvalancheController.getWaitingKeys()); 499 for (String key : keySet) { 500 HeadsUpEntry entry = getHeadsUpEntry(key); 501 String packageName = entry.mEntry.getSbn().getPackageName(); 502 String snoozeKey = snoozeKey(packageName, mUser); 503 mLogger.logPackageSnoozed(snoozeKey); 504 mSnoozedPackages.put(snoozeKey, mSystemClock.elapsedRealtime() + mSnoozeLengthMs); 505 } 506 } 507 508 @NonNull snoozeKey(@onNull String packageName, int user)509 private static String snoozeKey(@NonNull String packageName, int user) { 510 return user + "," + packageName; 511 } 512 513 @Nullable getHeadsUpEntry(@onNull String key)514 protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) { 515 if (mHeadsUpEntryMap.containsKey(key)) { 516 return mHeadsUpEntryMap.get(key); 517 } 518 return mAvalancheController.getWaitingEntry(key); 519 } 520 521 /** 522 * Returns the top Heads Up Notification, which appears to show at first. 523 */ 524 @Nullable getTopEntry()525 public NotificationEntry getTopEntry() { 526 HeadsUpEntry topEntry = getTopHeadsUpEntry(); 527 return (topEntry != null) ? topEntry.mEntry : null; 528 } 529 530 @Nullable getTopHeadsUpEntry()531 protected HeadsUpEntry getTopHeadsUpEntry() { 532 if (mHeadsUpEntryMap.isEmpty()) { 533 return null; 534 } 535 HeadsUpEntry topEntry = null; 536 for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { 537 if (topEntry == null || entry.compareTo(topEntry) < 0) { 538 topEntry = entry; 539 } 540 } 541 return topEntry; 542 } 543 544 /** 545 * Sets the current user. 546 */ setUser(int user)547 public void setUser(int user) { 548 mUser = user; 549 } 550 551 /** Returns the ID of the current user. */ getUser()552 public int getUser() { 553 return mUser; 554 } 555 556 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)557 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 558 pw.println("HeadsUpManager state:"); 559 dumpInternal(pw, args); 560 } 561 dumpInternal(@onNull PrintWriter pw, @NonNull String[] args)562 protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) { 563 pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); 564 pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); 565 pw.print(" now="); pw.println(mSystemClock.elapsedRealtime()); 566 pw.print(" mUser="); pw.println(mUser); 567 for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { 568 pw.print(" HeadsUpEntry="); pw.println(entry.mEntry); 569 } 570 int n = mSnoozedPackages.size(); 571 pw.println(" snoozed packages: " + n); 572 for (int i = 0; i < n; i++) { 573 pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); 574 pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); 575 } 576 } 577 578 /** 579 * Returns if there are any pinned Heads Up Notifications or not. 580 */ hasPinnedHeadsUp()581 public boolean hasPinnedHeadsUp() { 582 return mHasPinnedNotification; 583 } 584 hasPinnedNotificationInternal()585 private boolean hasPinnedNotificationInternal() { 586 for (String key : mHeadsUpEntryMap.keySet()) { 587 HeadsUpEntry entry = getHeadsUpEntry(key); 588 if (entry.mEntry.isRowPinned()) { 589 return true; 590 } 591 } 592 return false; 593 } 594 595 /** 596 * Unpins all pinned Heads Up Notifications. 597 * @param userUnPinned The unpinned action is trigger by user real operation. 598 */ unpinAll(boolean userUnPinned)599 public void unpinAll(boolean userUnPinned) { 600 for (String key : mHeadsUpEntryMap.keySet()) { 601 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 602 mLogger.logUnpinEntryRequest(key); 603 Runnable runnable = () -> { 604 mLogger.logUnpinEntry(key); 605 606 setEntryPinned(headsUpEntry, false /* isPinned */, "unpinAll"); 607 // maybe it got un sticky 608 headsUpEntry.updateEntry(false /* updatePostTime */, "unpinAll"); 609 610 // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay 611 // on the screen. 612 if (userUnPinned && headsUpEntry.mEntry != null) { 613 if (headsUpEntry.mEntry.mustStayOnScreen()) { 614 headsUpEntry.mEntry.setHeadsUpIsVisible(); 615 } 616 } 617 }; 618 mAvalancheController.delete(headsUpEntry, runnable, "unpinAll"); 619 } 620 } 621 622 /** 623 * Returns the value of the tracking-heads-up flag. See the doc of {@code setTrackingHeadsUp} as 624 * well. 625 */ isTrackingHeadsUp()626 public boolean isTrackingHeadsUp() { 627 // Might be implemented in subclass. 628 return false; 629 } 630 631 /** 632 * Compare two entries and decide how they should be ranked. 633 * 634 * @return -1 if the first argument should be ranked higher than the second, 1 if the second 635 * one should be ranked higher and 0 if they are equal. 636 */ compare(@ullable NotificationEntry a, @Nullable NotificationEntry b)637 public int compare(@Nullable NotificationEntry a, @Nullable NotificationEntry b) { 638 if (a == null || b == null) { 639 return Boolean.compare(a == null, b == null); 640 } 641 HeadsUpEntry aEntry = getHeadsUpEntry(a.getKey()); 642 HeadsUpEntry bEntry = getHeadsUpEntry(b.getKey()); 643 if (aEntry == null || bEntry == null) { 644 return Boolean.compare(aEntry == null, bEntry == null); 645 } 646 return aEntry.compareTo(bEntry); 647 } 648 649 /** 650 * Set an entry to be expanded and therefore stick in the heads up area if it's pinned 651 * until it's collapsed again. 652 */ setExpanded(@onNull NotificationEntry entry, boolean expanded)653 public void setExpanded(@NonNull NotificationEntry entry, boolean expanded) { 654 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 655 if (headsUpEntry != null && entry.isRowPinned()) { 656 headsUpEntry.setExpanded(expanded); 657 } 658 } 659 660 /** 661 * Notes that the user took an action on an entry that might indirectly cause the system or the 662 * app to remove the notification. 663 * 664 * @param entry the entry that might be indirectly removed by the user's action 665 * 666 * @see HeadsUpCoordinator#mActionPressListener 667 * @see #canRemoveImmediately(String) 668 */ setUserActionMayIndirectlyRemove(@onNull NotificationEntry entry)669 public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) { 670 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 671 if (headsUpEntry != null) { 672 headsUpEntry.mUserActionMayIndirectlyRemove = true; 673 } 674 } 675 676 /** 677 * Whether or not the entry can be removed currently. If it hasn't been on screen long enough 678 * it should not be removed unless forced 679 * @param key the key to check if removable 680 * @return true if the entry can be removed 681 */ 682 @Override canRemoveImmediately(@onNull String key)683 public boolean canRemoveImmediately(@NonNull String key) { 684 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 685 if (headsUpEntry != null && headsUpEntry.mUserActionMayIndirectlyRemove) { 686 return true; 687 } 688 return headsUpEntry == null || headsUpEntry.wasShownLongEnough() 689 || headsUpEntry.mEntry.isRowDismissed(); 690 } 691 692 /** 693 * @param key 694 * @return true if the entry is (pinned and expanded) or (has an active remote input) 695 */ 696 @Override isSticky(String key)697 public boolean isSticky(String key) { 698 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 699 if (headsUpEntry != null) { 700 return headsUpEntry.isSticky(); 701 } 702 return false; 703 } 704 705 @NonNull createHeadsUpEntry(NotificationEntry entry)706 protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { 707 return new HeadsUpEntry(entry); 708 } 709 710 /** 711 * Determines if the notification is for a critical call that must display on top of an active 712 * input notification. 713 * The call isOngoing check is for a special case of incoming calls (see b/164291424). 714 */ isCriticalCallNotif(NotificationEntry entry)715 private static boolean isCriticalCallNotif(NotificationEntry entry) { 716 Notification n = entry.getSbn().getNotification(); 717 boolean isIncomingCall = n.isStyle(Notification.CallStyle.class) && n.extras.getInt( 718 Notification.EXTRA_CALL_TYPE) == Notification.CallStyle.CALL_TYPE_INCOMING; 719 return isIncomingCall || (entry.getSbn().isOngoing() 720 && Notification.CATEGORY_CALL.equals(n.category)); 721 } 722 723 /** 724 * This represents a notification and how long it is in a heads up mode. It also manages its 725 * lifecycle automatically when created. This class is public because it is exposed by methods 726 * of AvalancheController that take it as param. 727 */ 728 public class HeadsUpEntry implements Comparable<HeadsUpEntry> { 729 public boolean mRemoteInputActive; 730 public boolean mUserActionMayIndirectlyRemove; 731 732 protected boolean mExpanded; 733 protected boolean mWasUnpinned; 734 735 @Nullable public NotificationEntry mEntry; 736 public long mPostTime; 737 public long mEarliestRemovalTime; 738 739 @Nullable protected Runnable mRemoveRunnable; 740 741 @Nullable private Runnable mCancelRemoveRunnable; 742 HeadsUpEntry()743 public HeadsUpEntry() { 744 NotificationsHeadsUpRefactor.assertInLegacyMode(); 745 } 746 HeadsUpEntry(NotificationEntry entry)747 public HeadsUpEntry(NotificationEntry entry) { 748 // Attach NotificationEntry for AvalancheController to log key and 749 // record mPostTime for AvalancheController sorting 750 setEntry(entry, createRemoveRunnable(entry)); 751 } 752 753 /** Attach a NotificationEntry. */ setEntry(@onNull final NotificationEntry entry)754 public void setEntry(@NonNull final NotificationEntry entry) { 755 NotificationsHeadsUpRefactor.assertInLegacyMode(); 756 setEntry(entry, createRemoveRunnable(entry)); 757 } 758 setEntry(@onNull final NotificationEntry entry, @Nullable Runnable removeRunnable)759 private void setEntry(@NonNull final NotificationEntry entry, 760 @Nullable Runnable removeRunnable) { 761 mEntry = entry; 762 mRemoveRunnable = removeRunnable; 763 764 mPostTime = calculatePostTime(); 765 updateEntry(true /* updatePostTime */, "setEntry"); 766 } 767 isRowPinned()768 protected boolean isRowPinned() { 769 return mEntry != null && mEntry.isRowPinned(); 770 } 771 setRowPinned(boolean pinned)772 protected void setRowPinned(boolean pinned) { 773 if (mEntry != null) mEntry.setRowPinned(pinned); 774 } 775 776 /** 777 * An interface that returns the amount of time left this HUN should show. 778 */ 779 interface FinishTimeUpdater { updateAndGetTimeRemaining()780 long updateAndGetTimeRemaining(); 781 } 782 783 /** 784 * Updates an entry's removal time. 785 * @param updatePostTime whether or not to refresh the post time 786 */ updateEntry(boolean updatePostTime, @Nullable String reason)787 public void updateEntry(boolean updatePostTime, @Nullable String reason) { 788 updateEntry(updatePostTime, /* updateEarliestRemovalTime= */ true, reason); 789 } 790 791 /** 792 * Updates an entry's removal time. 793 * @param updatePostTime whether or not to refresh the post time 794 * @param updateEarliestRemovalTime whether this update should further delay removal 795 */ updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime, @Nullable String reason)796 public void updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime, 797 @Nullable String reason) { 798 Runnable runnable = () -> { 799 mLogger.logUpdateEntry(mEntry, updatePostTime, reason); 800 801 final long now = mSystemClock.elapsedRealtime(); 802 if (updateEarliestRemovalTime) { 803 mEarliestRemovalTime = now + mMinimumDisplayTime; 804 } 805 806 if (updatePostTime) { 807 mPostTime = Math.max(mPostTime, now); 808 } 809 }; 810 mAvalancheController.update(this, runnable, "updateEntry (updatePostTime)"); 811 812 if (isSticky()) { 813 cancelAutoRemovalCallbacks("updateEntry (sticky)"); 814 return; 815 } 816 817 FinishTimeUpdater finishTimeCalculator = () -> { 818 final long finishTime = calculateFinishTime(); 819 final long now = mSystemClock.elapsedRealtime(); 820 final long timeLeft = NotificationThrottleHun.isEnabled() 821 ? Math.max(finishTime, mEarliestRemovalTime) - now 822 : Math.max(finishTime - now, mMinimumDisplayTime); 823 return timeLeft; 824 }; 825 scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); 826 827 // Notify the manager, that the posted time has changed. 828 onEntryUpdated(this); 829 } 830 831 /** 832 * Whether or not the notification is "sticky" i.e. should stay on screen regardless 833 * of the timer (forever) and should be removed externally. 834 * @return true if the notification is sticky 835 */ isSticky()836 public boolean isSticky() { 837 return (mEntry.isRowPinned() && mExpanded) 838 || mRemoteInputActive 839 || hasFullScreenIntent(mEntry); 840 } 841 isStickyForSomeTime()842 public boolean isStickyForSomeTime() { 843 return mEntry.isStickyAndNotDemoted(); 844 } 845 846 /** 847 * Whether the notification has been on screen long enough and can be removed. 848 * @return true if the notification has been on screen long enough 849 */ wasShownLongEnough()850 public boolean wasShownLongEnough() { 851 return mEarliestRemovalTime < mSystemClock.elapsedRealtime(); 852 } 853 compareNonTimeFields(HeadsUpEntry headsUpEntry)854 public int compareNonTimeFields(HeadsUpEntry headsUpEntry) { 855 boolean selfFullscreen = hasFullScreenIntent(mEntry); 856 boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry); 857 if (selfFullscreen && !otherFullscreen) { 858 return -1; 859 } else if (!selfFullscreen && otherFullscreen) { 860 return 1; 861 } 862 863 boolean selfCall = isCriticalCallNotif(mEntry); 864 boolean otherCall = isCriticalCallNotif(headsUpEntry.mEntry); 865 866 if (selfCall && !otherCall) { 867 return -1; 868 } else if (!selfCall && otherCall) { 869 return 1; 870 } 871 872 if (mRemoteInputActive && !headsUpEntry.mRemoteInputActive) { 873 return -1; 874 } else if (!mRemoteInputActive && headsUpEntry.mRemoteInputActive) { 875 return 1; 876 } 877 return 0; 878 } 879 compareTo(@onNull HeadsUpEntry headsUpEntry)880 public int compareTo(@NonNull HeadsUpEntry headsUpEntry) { 881 boolean isPinned = mEntry.isRowPinned(); 882 boolean otherPinned = headsUpEntry.mEntry.isRowPinned(); 883 if (isPinned && !otherPinned) { 884 return -1; 885 } else if (!isPinned && otherPinned) { 886 return 1; 887 } 888 int nonTimeCompareResult = compareNonTimeFields(headsUpEntry); 889 if (nonTimeCompareResult != 0) { 890 return nonTimeCompareResult; 891 } 892 if (mPostTime > headsUpEntry.mPostTime) { 893 return -1; 894 } else if (mPostTime == headsUpEntry.mPostTime) { 895 return mEntry.getKey().compareTo(headsUpEntry.mEntry.getKey()); 896 } else { 897 return 1; 898 } 899 } 900 901 @Override hashCode()902 public int hashCode() { 903 if (mEntry == null) return super.hashCode(); 904 int result = mEntry.getKey().hashCode(); 905 result = 31 * result; 906 return result; 907 } 908 909 @Override equals(@ullable Object o)910 public boolean equals(@Nullable Object o) { 911 if (this == o) return true; 912 if (o == null || !(o instanceof HeadsUpEntry)) return false; 913 HeadsUpEntry otherHeadsUpEntry = (HeadsUpEntry) o; 914 if (mEntry != null && otherHeadsUpEntry.mEntry != null) { 915 return mEntry.getKey().equals(otherHeadsUpEntry.mEntry.getKey()); 916 } 917 return false; 918 } 919 setExpanded(boolean expanded)920 public void setExpanded(boolean expanded) { 921 this.mExpanded = expanded; 922 } 923 reset()924 public void reset() { 925 NotificationsHeadsUpRefactor.assertInLegacyMode(); 926 cancelAutoRemovalCallbacks("reset()"); 927 mEntry = null; 928 mRemoveRunnable = null; 929 mExpanded = false; 930 mRemoteInputActive = false; 931 } 932 933 /** 934 * Clear any pending removal runnables. 935 */ cancelAutoRemovalCallbacks(@ullable String reason)936 public void cancelAutoRemovalCallbacks(@Nullable String reason) { 937 mLogger.logAutoRemoveCancelRequest(this.mEntry, reason); 938 Runnable runnable = () -> { 939 final boolean removed = cancelAutoRemovalCallbackInternal(); 940 941 if (removed) { 942 mLogger.logAutoRemoveCanceled(mEntry, reason); 943 } 944 }; 945 if (isHeadsUpEntry(this.mEntry.getKey())) { 946 mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks"); 947 } else { 948 // Just removed 949 runnable.run(); 950 } 951 } 952 scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, @NonNull String reason)953 public void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, 954 @NonNull String reason) { 955 956 mLogger.logAutoRemoveRequest(this.mEntry, reason); 957 Runnable runnable = () -> { 958 long delayMs = finishTimeCalculator.updateAndGetTimeRemaining(); 959 960 if (mRemoveRunnable == null) { 961 Log.wtf(TAG, "scheduleAutoRemovalCallback with no callback set"); 962 return; 963 } 964 965 final boolean deletedExistingRemovalRunnable = cancelAutoRemovalCallbackInternal(); 966 mCancelRemoveRunnable = mExecutor.executeDelayed(mRemoveRunnable, 967 delayMs); 968 969 if (deletedExistingRemovalRunnable) { 970 mLogger.logAutoRemoveRescheduled(mEntry, delayMs, reason); 971 } else { 972 mLogger.logAutoRemoveScheduled(mEntry, delayMs, reason); 973 } 974 }; 975 mAvalancheController.update(this, runnable, 976 reason + " scheduleAutoRemovalCallback"); 977 } 978 cancelAutoRemovalCallbackInternal()979 public boolean cancelAutoRemovalCallbackInternal() { 980 final boolean scheduled = (mCancelRemoveRunnable != null); 981 982 if (scheduled) { 983 mCancelRemoveRunnable.run(); // Delete removal runnable from Executor queue 984 mCancelRemoveRunnable = null; 985 } 986 987 return scheduled; 988 } 989 990 /** 991 * Remove the entry at the earliest allowed removal time. 992 */ removeAsSoonAsPossible()993 public void removeAsSoonAsPossible() { 994 if (mRemoveRunnable != null) { 995 996 FinishTimeUpdater finishTimeCalculator = () -> { 997 final long timeLeft = mEarliestRemovalTime - mSystemClock.elapsedRealtime(); 998 return timeLeft; 999 }; 1000 scheduleAutoRemovalCallback(finishTimeCalculator, "removeAsSoonAsPossible"); 1001 } 1002 } 1003 1004 /** Creates a runnable to remove this notification from the alerting entries. */ createRemoveRunnable(NotificationEntry entry)1005 protected Runnable createRemoveRunnable(NotificationEntry entry) { 1006 return () -> removeEntry(entry.getKey(), "createRemoveRunnable"); 1007 } 1008 1009 /** 1010 * Calculate what the post time of a notification is at some current time. 1011 * @return the post time 1012 */ calculatePostTime()1013 protected long calculatePostTime() { 1014 // The actual post time will be just after the heads-up really slided in 1015 return mSystemClock.elapsedRealtime() + mTouchAcceptanceDelay; 1016 } 1017 1018 /** 1019 * @return When the notification should auto-dismiss itself, based on 1020 * {@link SystemClock#elapsedRealtime()} 1021 */ calculateFinishTime()1022 protected long calculateFinishTime() { 1023 int requestedTimeOutMs; 1024 if (isStickyForSomeTime()) { 1025 requestedTimeOutMs = mStickyForSomeTimeAutoDismissTime; 1026 } else { 1027 requestedTimeOutMs = mAvalancheController.getDurationMs(this, mAutoDismissTime); 1028 } 1029 final long duration = getRecommendedHeadsUpTimeoutMs(requestedTimeOutMs); 1030 return mPostTime + duration; 1031 } 1032 1033 /** 1034 * Get user-preferred or default timeout duration. The larger one will be returned. 1035 * @return milliseconds before auto-dismiss 1036 * @param requestedTimeout 1037 */ getRecommendedHeadsUpTimeoutMs(int requestedTimeout)1038 protected int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) { 1039 return mAccessibilityMgr.getRecommendedTimeoutMillis( 1040 requestedTimeout, 1041 AccessibilityManager.FLAG_CONTENT_CONTROLS 1042 | AccessibilityManager.FLAG_CONTENT_ICONS 1043 | AccessibilityManager.FLAG_CONTENT_TEXT); 1044 } 1045 } 1046 } 1047