1 /* 2 * Copyright (C) 2019 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.notification.collection; 18 19 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; 20 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; 21 import static android.service.notification.NotificationListenerService.REASON_ASSISTANT_CANCEL; 22 import static android.service.notification.NotificationListenerService.REASON_CANCEL; 23 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; 24 import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED; 25 import static android.service.notification.NotificationListenerService.REASON_CHANNEL_REMOVED; 26 import static android.service.notification.NotificationListenerService.REASON_CLEAR_DATA; 27 import static android.service.notification.NotificationListenerService.REASON_CLICK; 28 import static android.service.notification.NotificationListenerService.REASON_ERROR; 29 import static android.service.notification.NotificationListenerService.REASON_GROUP_OPTIMIZATION; 30 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; 31 import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL; 32 import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL_ALL; 33 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED; 34 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_CHANGED; 35 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_SUSPENDED; 36 import static android.service.notification.NotificationListenerService.REASON_PROFILE_TURNED_OFF; 37 import static android.service.notification.NotificationListenerService.REASON_SNOOZED; 38 import static android.service.notification.NotificationListenerService.REASON_TIMEOUT; 39 import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED; 40 import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED; 41 42 import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; 43 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED; 44 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED; 45 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; 46 import static com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt.cancellationReasonDebugString; 47 48 import static java.util.Objects.requireNonNull; 49 50 import android.annotation.IntDef; 51 import android.annotation.MainThread; 52 import android.annotation.UserIdInt; 53 import android.app.Notification; 54 import android.app.NotificationChannel; 55 import android.os.Handler; 56 import android.os.RemoteException; 57 import android.os.Trace; 58 import android.os.UserHandle; 59 import android.service.notification.NotificationListenerService; 60 import android.service.notification.NotificationListenerService.Ranking; 61 import android.service.notification.NotificationListenerService.RankingMap; 62 import android.service.notification.StatusBarNotification; 63 import android.util.ArrayMap; 64 import android.util.Pair; 65 66 import androidx.annotation.NonNull; 67 import androidx.annotation.Nullable; 68 69 import com.android.internal.annotations.VisibleForTesting; 70 import com.android.internal.statusbar.IStatusBarService; 71 import com.android.systemui.Dumpable; 72 import com.android.systemui.dagger.SysUISingleton; 73 import com.android.systemui.dagger.qualifiers.Background; 74 import com.android.systemui.dagger.qualifiers.Main; 75 import com.android.systemui.dump.DumpManager; 76 import com.android.systemui.dump.LogBufferEulogizer; 77 import com.android.systemui.statusbar.notification.NotifPipelineFlags; 78 import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent; 79 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer; 80 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler; 81 import com.android.systemui.statusbar.notification.collection.notifcollection.BindEntryEvent; 82 import com.android.systemui.statusbar.notification.collection.notifcollection.ChannelChangedEvent; 83 import com.android.systemui.statusbar.notification.collection.notifcollection.CleanUpEntryEvent; 84 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; 85 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; 86 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryAddedEvent; 87 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryRemovedEvent; 88 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent; 89 import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent; 90 import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater; 91 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionInconsistencyTracker; 92 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 93 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger; 94 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; 95 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent; 96 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; 97 import com.android.systemui.statusbar.notification.collection.notifcollection.RankingAppliedEvent; 98 import com.android.systemui.statusbar.notification.collection.notifcollection.RankingUpdatedEvent; 99 import com.android.systemui.util.Assert; 100 import com.android.systemui.util.time.SystemClock; 101 102 import java.io.PrintWriter; 103 import java.lang.annotation.Retention; 104 import java.lang.annotation.RetentionPolicy; 105 import java.util.ArrayDeque; 106 import java.util.ArrayList; 107 import java.util.Collection; 108 import java.util.Collections; 109 import java.util.Comparator; 110 import java.util.HashMap; 111 import java.util.List; 112 import java.util.Map; 113 import java.util.Objects; 114 import java.util.Queue; 115 import java.util.concurrent.Executor; 116 import java.util.concurrent.TimeUnit; 117 118 import javax.inject.Inject; 119 120 /** 121 * Keeps a record of all of the "active" notifications, i.e. the notifications that are currently 122 * posted to the phone. This collection is unsorted, ungrouped, and unfiltered. Just because a 123 * notification appears in this collection doesn't mean that it's currently present in the shade 124 * (notifications can be hidden for a variety of reasons). Code that cares about what notifications 125 * are *visible* right now should register listeners later in the pipeline. 126 * 127 * Each notification is represented by a {@link NotificationEntry}, which is itself made up of two 128 * parts: a {@link StatusBarNotification} and a {@link Ranking}. When notifications are updated, 129 * their underlying SBNs and Rankings are swapped out, but the enclosing NotificationEntry (and its 130 * associated key) remain the same. In general, an SBN can only be updated when the notification is 131 * reposted by the source app; Rankings are updated much more often, usually every time there is an 132 * update from any kind from NotificationManager. 133 * 134 * In general, this collection closely mirrors the list maintained by NotificationManager, but it 135 * can occasionally diverge due to lifetime extenders (see 136 * {@link #addNotificationLifetimeExtender(NotifLifetimeExtender)}). 137 * 138 * Interested parties can register listeners 139 * ({@link #addCollectionListener(NotifCollectionListener)}) to be informed when notifications 140 * events occur. 141 */ 142 @MainThread 143 @SysUISingleton 144 public class NotifCollection implements Dumpable, PipelineDumpable { 145 private final IStatusBarService mStatusBarService; 146 private final SystemClock mClock; 147 private final NotifPipelineFlags mNotifPipelineFlags; 148 private final NotifCollectionLogger mLogger; 149 private final Handler mMainHandler; 150 private final Executor mBgExecutor; 151 private final LogBufferEulogizer mEulogizer; 152 private final DumpManager mDumpManager; 153 private final NotifCollectionInconsistencyTracker mInconsistencyTracker; 154 155 private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>(); 156 private final Collection<NotificationEntry> mReadOnlyNotificationSet = 157 Collections.unmodifiableCollection(mNotificationSet.values()); 158 private final HashMap<String, FutureDismissal> mFutureDismissals = new HashMap<>(); 159 160 @Nullable private CollectionReadyForBuildListener mBuildListener; 161 private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>(); 162 private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>(); 163 private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>(); 164 165 166 private Queue<NotifEvent> mEventQueue = new ArrayDeque<>(); 167 private final Runnable mRebuildListRunnable = () -> { 168 if (mBuildListener != null) { 169 mBuildListener.onBuildList(mReadOnlyNotificationSet, "asynchronousUpdate"); 170 } 171 }; 172 173 private boolean mAttached = false; 174 private boolean mAmDispatchingToOtherCode; 175 private long mInitializedTimestamp = 0; 176 177 @Inject NotifCollection( IStatusBarService statusBarService, SystemClock clock, NotifPipelineFlags notifPipelineFlags, NotifCollectionLogger logger, @Main Handler mainHandler, @Background Executor bgExecutor, LogBufferEulogizer logBufferEulogizer, DumpManager dumpManager)178 public NotifCollection( 179 IStatusBarService statusBarService, 180 SystemClock clock, 181 NotifPipelineFlags notifPipelineFlags, 182 NotifCollectionLogger logger, 183 @Main Handler mainHandler, 184 @Background Executor bgExecutor, 185 LogBufferEulogizer logBufferEulogizer, 186 DumpManager dumpManager) { 187 mStatusBarService = statusBarService; 188 mClock = clock; 189 mNotifPipelineFlags = notifPipelineFlags; 190 mLogger = logger; 191 mMainHandler = mainHandler; 192 mBgExecutor = bgExecutor; 193 mEulogizer = logBufferEulogizer; 194 mDumpManager = dumpManager; 195 mInconsistencyTracker = new NotifCollectionInconsistencyTracker(mLogger); 196 } 197 198 /** Initializes the NotifCollection and registers it to receive notification events. */ attach(GroupCoalescer groupCoalescer)199 public void attach(GroupCoalescer groupCoalescer) { 200 Assert.isMainThread(); 201 if (mAttached) { 202 throw new RuntimeException("attach() called twice"); 203 } 204 mAttached = true; 205 mDumpManager.registerDumpable(TAG, this); 206 groupCoalescer.setNotificationHandler(mNotifHandler); 207 mInconsistencyTracker.attach(mNotificationSet::keySet, groupCoalescer::getCoalescedKeySet); 208 } 209 210 /** 211 * Sets the class responsible for converting the collection into the list of currently-visible 212 * notifications. 213 */ setBuildListener(CollectionReadyForBuildListener buildListener)214 void setBuildListener(CollectionReadyForBuildListener buildListener) { 215 Assert.isMainThread(); 216 mBuildListener = buildListener; 217 } 218 219 /** @see NotifPipeline#getEntry(String) () */ 220 @Nullable getEntry(@onNull String key)221 NotificationEntry getEntry(@NonNull String key) { 222 return mNotificationSet.get(key); 223 } 224 225 /** @see NotifPipeline#getAllNotifs() */ getAllNotifs()226 Collection<NotificationEntry> getAllNotifs() { 227 Assert.isMainThread(); 228 return mReadOnlyNotificationSet; 229 } 230 231 /** @see NotifPipeline#addCollectionListener(NotifCollectionListener) */ addCollectionListener(NotifCollectionListener listener)232 void addCollectionListener(NotifCollectionListener listener) { 233 Assert.isMainThread(); 234 mNotifCollectionListeners.add(listener); 235 } 236 237 /** @see NotifPipeline#removeCollectionListener(NotifCollectionListener) */ removeCollectionListener(NotifCollectionListener listener)238 void removeCollectionListener(NotifCollectionListener listener) { 239 Assert.isMainThread(); 240 mNotifCollectionListeners.remove(listener); 241 } 242 243 /** @see NotifPipeline#addNotificationLifetimeExtender(NotifLifetimeExtender) */ addNotificationLifetimeExtender(NotifLifetimeExtender extender)244 void addNotificationLifetimeExtender(NotifLifetimeExtender extender) { 245 Assert.isMainThread(); 246 checkForReentrantCall(); 247 if (mLifetimeExtenders.contains(extender)) { 248 throw new IllegalArgumentException("Extender " + extender + " already added."); 249 } 250 mLifetimeExtenders.add(extender); 251 extender.setCallback(this::onEndLifetimeExtension); 252 } 253 254 /** @see NotifPipeline#addNotificationDismissInterceptor(NotifDismissInterceptor) */ addNotificationDismissInterceptor(NotifDismissInterceptor interceptor)255 void addNotificationDismissInterceptor(NotifDismissInterceptor interceptor) { 256 Assert.isMainThread(); 257 checkForReentrantCall(); 258 if (mDismissInterceptors.contains(interceptor)) { 259 throw new IllegalArgumentException("Interceptor " + interceptor + " already added."); 260 } 261 mDismissInterceptors.add(interceptor); 262 interceptor.setCallback(this::onEndDismissInterception); 263 } 264 265 /** 266 * Dismisses multiple notifications on behalf of the user. 267 */ dismissNotifications( List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss)268 public void dismissNotifications( 269 List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss) { 270 Assert.isMainThread(); 271 checkForReentrantCall(); 272 273 // TODO (b/206842750): This method is called from (silent) clear all and non-clear all 274 // contexts and should be checking the NO_CLEAR flag, rather than depending on NSSL 275 // to pass in a properly filtered list of notifications 276 277 final List<NotificationEntry> entriesToLocallyDismiss = new ArrayList<>(); 278 for (int i = 0; i < entriesToDismiss.size(); i++) { 279 NotificationEntry entry = entriesToDismiss.get(i).first; 280 DismissedByUserStats stats = entriesToDismiss.get(i).second; 281 282 requireNonNull(stats); 283 NotificationEntry storedEntry = mNotificationSet.get(entry.getKey()); 284 if (storedEntry == null) { 285 mLogger.logNonExistentNotifDismissed(entry); 286 continue; 287 } 288 if (entry != storedEntry) { 289 throw mEulogizer.record( 290 new IllegalStateException("Invalid entry: " 291 + "different stored and dismissed entries for " + logKey(entry) 292 + " stored=@" + Integer.toHexString(storedEntry.hashCode()))); 293 } 294 295 if (entry.getDismissState() == DISMISSED) { 296 continue; 297 } 298 299 updateDismissInterceptors(entry); 300 if (isDismissIntercepted(entry)) { 301 mLogger.logNotifDismissedIntercepted(entry); 302 continue; 303 } 304 305 entriesToLocallyDismiss.add(entry); 306 if (!isCanceled(entry)) { 307 // send message to system server if this notification hasn't already been cancelled 308 mBgExecutor.execute(() -> { 309 try { 310 mStatusBarService.onNotificationClear( 311 entry.getSbn().getPackageName(), 312 entry.getSbn().getUser().getIdentifier(), 313 entry.getSbn().getKey(), 314 stats.dismissalSurface, 315 stats.dismissalSentiment, 316 stats.notificationVisibility); 317 } catch (RemoteException e) { 318 // system process is dead if we're here. 319 mLogger.logRemoteExceptionOnNotificationClear(entry, e); 320 } 321 }); 322 } 323 } 324 325 locallyDismissNotifications(entriesToLocallyDismiss); 326 dispatchEventsAndRebuildList("dismissNotifications"); 327 } 328 329 /** 330 * Dismisses a single notification on behalf of the user. 331 */ dismissNotification( NotificationEntry entry, @NonNull DismissedByUserStats stats)332 public void dismissNotification( 333 NotificationEntry entry, 334 @NonNull DismissedByUserStats stats) { 335 dismissNotifications(List.of(new Pair<>(entry, stats))); 336 } 337 338 /** 339 * Dismisses all clearable notifications for a given userid on behalf of the user. 340 */ dismissAllNotifications(@serIdInt int userId)341 public void dismissAllNotifications(@UserIdInt int userId) { 342 Assert.isMainThread(); 343 checkForReentrantCall(); 344 345 mLogger.logDismissAll(userId); 346 347 try { 348 // TODO(b/169585328): Do not clear media player notifications 349 mStatusBarService.onClearAllNotifications(userId); 350 } catch (RemoteException e) { 351 // system process is dead if we're here. 352 mLogger.logRemoteExceptionOnClearAllNotifications(e); 353 } 354 355 final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs()); 356 for (int i = entries.size() - 1; i >= 0; i--) { 357 NotificationEntry entry = entries.get(i); 358 if (!shouldDismissOnClearAll(entry, userId)) { 359 // system server won't be removing these notifications, but we still give dismiss 360 // interceptors the chance to filter the notification 361 updateDismissInterceptors(entry); 362 if (isDismissIntercepted(entry)) { 363 mLogger.logNotifClearAllDismissalIntercepted(entry); 364 } 365 entries.remove(i); 366 } 367 } 368 369 locallyDismissNotifications(entries); 370 dispatchEventsAndRebuildList("dismissAllNotifications"); 371 } 372 373 /** 374 * Optimistically marks the given notifications as dismissed -- we'll wait for the signal 375 * from system server before removing it from our notification set. 376 */ locallyDismissNotifications(List<NotificationEntry> entries)377 private void locallyDismissNotifications(List<NotificationEntry> entries) { 378 final List<NotificationEntry> canceledEntries = new ArrayList<>(); 379 380 for (int i = 0; i < entries.size(); i++) { 381 NotificationEntry entry = entries.get(i); 382 383 entry.setDismissState(DISMISSED); 384 mLogger.logNotifDismissed(entry); 385 386 if (isCanceled(entry)) { 387 canceledEntries.add(entry); 388 } else { 389 // Mark any children as dismissed as system server will auto-dismiss them as well 390 if (entry.getSbn().getNotification().isGroupSummary()) { 391 for (NotificationEntry otherEntry : mNotificationSet.values()) { 392 if (shouldAutoDismissChildren(otherEntry, entry.getSbn().getGroupKey())) { 393 otherEntry.setDismissState(PARENT_DISMISSED); 394 mLogger.logChildDismissed(otherEntry); 395 if (isCanceled(otherEntry)) { 396 canceledEntries.add(otherEntry); 397 } 398 } 399 } 400 } 401 } 402 } 403 404 // Immediately remove any dismissed notifs that have already been canceled by system server 405 // (probably due to being lifetime-extended up until this point). 406 for (NotificationEntry canceledEntry : canceledEntries) { 407 mLogger.logDismissOnAlreadyCanceledEntry(canceledEntry); 408 tryRemoveNotification(canceledEntry); 409 } 410 } 411 onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)412 private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 413 Assert.isMainThread(); 414 415 postNotification(sbn, requireRanking(rankingMap, sbn.getKey())); 416 applyRanking(rankingMap); 417 dispatchEventsAndRebuildList("onNotificationPosted"); 418 } 419 onNotificationGroupPosted(List<CoalescedEvent> batch)420 private void onNotificationGroupPosted(List<CoalescedEvent> batch) { 421 Assert.isMainThread(); 422 423 mLogger.logNotifGroupPosted(batch.get(0).getSbn().getGroupKey(), batch.size()); 424 425 for (CoalescedEvent event : batch) { 426 postNotification(event.getSbn(), event.getRanking()); 427 } 428 dispatchEventsAndRebuildList("onNotificationGroupPosted"); 429 } 430 onNotificationRemoved( StatusBarNotification sbn, RankingMap rankingMap, int reason)431 private void onNotificationRemoved( 432 StatusBarNotification sbn, 433 RankingMap rankingMap, 434 int reason) { 435 Assert.isMainThread(); 436 437 mLogger.logNotifRemoved(sbn, reason); 438 439 final NotificationEntry entry = mNotificationSet.get(sbn.getKey()); 440 if (entry == null) { 441 // TODO (b/160008901): Throw an exception here 442 mLogger.logNoNotificationToRemoveWithKey(sbn, reason); 443 return; 444 } 445 446 entry.mCancellationReason = reason; 447 tryRemoveNotification(entry); 448 applyRanking(rankingMap); 449 dispatchEventsAndRebuildList("onNotificationRemoved"); 450 } 451 onNotificationRankingUpdate(RankingMap rankingMap)452 private void onNotificationRankingUpdate(RankingMap rankingMap) { 453 Assert.isMainThread(); 454 mEventQueue.add(new RankingUpdatedEvent(rankingMap)); 455 applyRanking(rankingMap); 456 dispatchEventsAndRebuildList("onNotificationRankingUpdate"); 457 } 458 onNotificationChannelModified( String pkgName, UserHandle user, NotificationChannel channel, int modificationType)459 private void onNotificationChannelModified( 460 String pkgName, 461 UserHandle user, 462 NotificationChannel channel, 463 int modificationType) { 464 Assert.isMainThread(); 465 mEventQueue.add(new ChannelChangedEvent(pkgName, user, channel, modificationType)); 466 dispatchEventsAndAsynchronouslyRebuildList(); 467 } 468 onNotificationsInitialized()469 private void onNotificationsInitialized() { 470 mInitializedTimestamp = mClock.uptimeMillis(); 471 } 472 postNotification( StatusBarNotification sbn, Ranking ranking)473 private void postNotification( 474 StatusBarNotification sbn, 475 Ranking ranking) { 476 NotificationEntry entry = mNotificationSet.get(sbn.getKey()); 477 478 if (entry == null) { 479 // A new notification! 480 entry = new NotificationEntry(sbn, ranking, mClock.uptimeMillis()); 481 mEventQueue.add(new InitEntryEvent(entry)); 482 mEventQueue.add(new BindEntryEvent(entry, sbn)); 483 mNotificationSet.put(sbn.getKey(), entry); 484 485 mLogger.logNotifPosted(entry); 486 mEventQueue.add(new EntryAddedEvent(entry)); 487 488 } else { 489 // Update to an existing entry 490 491 // Notification is updated so it is essentially re-added and thus alive again, so we 492 // can reset its state. 493 // TODO: If a coalesced event ever gets here, it's possible to lose track of children, 494 // since their rankings might have been updated earlier (and thus we may no longer 495 // think a child is associated with this locally-dismissed entry). 496 cancelLocalDismissal(entry); 497 cancelLifetimeExtension(entry); 498 cancelDismissInterception(entry); 499 entry.mCancellationReason = REASON_NOT_CANCELED; 500 501 entry.setSbn(sbn); 502 mEventQueue.add(new BindEntryEvent(entry, sbn)); 503 504 mLogger.logNotifUpdated(entry); 505 mEventQueue.add(new EntryUpdatedEvent(entry, true /* fromSystem */)); 506 } 507 } 508 509 /** 510 * Tries to remove a notification from the notification set. This removal may be blocked by 511 * lifetime extenders. Does not trigger a rebuild of the list; caller must do that manually. 512 * 513 * @return True if the notification was removed, false otherwise. 514 */ tryRemoveNotification(NotificationEntry entry)515 private boolean tryRemoveNotification(NotificationEntry entry) { 516 if (mNotificationSet.get(entry.getKey()) != entry) { 517 throw mEulogizer.record( 518 new IllegalStateException("No notification to remove with key " 519 + logKey(entry))); 520 } 521 522 if (!isCanceled(entry)) { 523 throw mEulogizer.record( 524 new IllegalStateException("Cannot remove notification " + logKey(entry) 525 + ": has not been marked for removal")); 526 } 527 528 if (cannotBeLifetimeExtended(entry)) { 529 cancelLifetimeExtension(entry); 530 } else { 531 updateLifetimeExtension(entry); 532 } 533 534 if (!isLifetimeExtended(entry)) { 535 mLogger.logNotifReleased(entry); 536 mNotificationSet.remove(entry.getKey()); 537 cancelDismissInterception(entry); 538 mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason)); 539 mEventQueue.add(new CleanUpEntryEvent(entry)); 540 handleFutureDismissal(entry); 541 return true; 542 } else { 543 return false; 544 } 545 } 546 547 /** 548 * Get the group summary entry 549 * @param groupKey 550 * @return 551 */ 552 @Nullable getGroupSummary(String groupKey)553 public NotificationEntry getGroupSummary(String groupKey) { 554 return mNotificationSet 555 .values() 556 .stream() 557 .filter(it -> Objects.equals(it.getSbn().getGroupKey(), groupKey)) 558 .filter(it -> it.getSbn().getNotification().isGroupSummary()) 559 .findFirst().orElse(null); 560 } 561 562 /** 563 * Checks if the entry is the only child in the logical group; 564 * it need not have a summary to qualify 565 * 566 * @param entry the entry to check 567 */ isOnlyChildInGroup(NotificationEntry entry)568 public boolean isOnlyChildInGroup(NotificationEntry entry) { 569 String groupKey = entry.getSbn().getGroupKey(); 570 return mNotificationSet.get(entry.getKey()) == entry 571 && mNotificationSet 572 .values() 573 .stream() 574 .filter(it -> Objects.equals(it.getSbn().getGroupKey(), groupKey)) 575 .filter(it -> !it.getSbn().getNotification().isGroupSummary()) 576 .count() == 1; 577 } 578 applyRanking(@onNull RankingMap rankingMap)579 private void applyRanking(@NonNull RankingMap rankingMap) { 580 ArrayMap<String, NotificationEntry> currentEntriesWithoutRankings = null; 581 for (NotificationEntry entry : mNotificationSet.values()) { 582 if (!isCanceled(entry)) { 583 584 // TODO: (b/148791039) We should crash if we are ever handed a ranking with 585 // incomplete entries. Right now, there's a race condition in NotificationListener 586 // that means this might occur when SystemUI is starting up. 587 Ranking ranking = new Ranking(); 588 if (rankingMap.getRanking(entry.getKey(), ranking)) { 589 entry.setRanking(ranking); 590 591 // TODO: (b/145659174) update the sbn's overrideGroupKey in 592 // NotificationEntry.setRanking instead of here once we fully migrate to the 593 // NewNotifPipeline 594 final String newOverrideGroupKey = ranking.getOverrideGroupKey(); 595 if (!Objects.equals(entry.getSbn().getOverrideGroupKey(), 596 newOverrideGroupKey)) { 597 entry.getSbn().setOverrideGroupKey(newOverrideGroupKey); 598 } 599 } else { 600 if (currentEntriesWithoutRankings == null) { 601 currentEntriesWithoutRankings = new ArrayMap<>(); 602 } 603 currentEntriesWithoutRankings.put(entry.getKey(), entry); 604 } 605 } 606 } 607 608 mInconsistencyTracker.logNewMissingNotifications(rankingMap); 609 mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap); 610 if (currentEntriesWithoutRankings != null) { 611 for (NotificationEntry entry : currentEntriesWithoutRankings.values()) { 612 entry.mCancellationReason = REASON_UNKNOWN; 613 tryRemoveNotification(entry); 614 } 615 } 616 mEventQueue.add(new RankingAppliedEvent()); 617 } 618 dispatchEventsAndRebuildList(String reason)619 private void dispatchEventsAndRebuildList(String reason) { 620 Trace.beginSection("NotifCollection.dispatchEventsAndRebuildList"); 621 if (mMainHandler.hasCallbacks(mRebuildListRunnable)) { 622 mMainHandler.removeCallbacks(mRebuildListRunnable); 623 } 624 625 dispatchEvents(); 626 627 if (mBuildListener != null) { 628 mBuildListener.onBuildList(mReadOnlyNotificationSet, reason); 629 } 630 Trace.endSection(); 631 } 632 dispatchEventsAndAsynchronouslyRebuildList()633 private void dispatchEventsAndAsynchronouslyRebuildList() { 634 Trace.beginSection("NotifCollection.dispatchEventsAndAsynchronouslyRebuildList"); 635 636 dispatchEvents(); 637 638 if (!mMainHandler.hasCallbacks(mRebuildListRunnable)) { 639 mMainHandler.postDelayed(mRebuildListRunnable, 1000L); 640 } 641 642 Trace.endSection(); 643 } 644 dispatchEvents()645 private void dispatchEvents() { 646 Trace.beginSection("NotifCollection.dispatchEvents"); 647 648 mAmDispatchingToOtherCode = true; 649 while (!mEventQueue.isEmpty()) { 650 mEventQueue.remove().dispatchTo(mNotifCollectionListeners); 651 } 652 mAmDispatchingToOtherCode = false; 653 654 Trace.endSection(); 655 } 656 onEndLifetimeExtension( @onNull NotifLifetimeExtender extender, @NonNull NotificationEntry entry)657 private void onEndLifetimeExtension( 658 @NonNull NotifLifetimeExtender extender, 659 @NonNull NotificationEntry entry) { 660 Assert.isMainThread(); 661 if (!mAttached) { 662 return; 663 } 664 checkForReentrantCall(); 665 666 NotificationEntry collectionEntry = getEntry(entry.getKey()); 667 String logKey = logKey(entry); 668 String collectionEntryIs = collectionEntry == null ? "null" 669 : entry == collectionEntry ? "same" : "different"; 670 671 if (entry != collectionEntry) { 672 // TODO: We should probably make this throw, but that's too risky right now 673 mLogger.logEntryBeingExtendedNotInCollection(entry, extender, collectionEntryIs); 674 } 675 676 if (!entry.mLifetimeExtenders.remove(extender)) { 677 throw mEulogizer.record(new IllegalStateException( 678 String.format("Cannot end lifetime extension for extender \"%s\"" 679 + " of entry %s (collection entry is %s)", 680 extender.getName(), logKey, collectionEntryIs))); 681 } 682 683 mLogger.logLifetimeExtensionEnded(entry, extender, entry.mLifetimeExtenders.size()); 684 685 if (!isLifetimeExtended(entry)) { 686 if (tryRemoveNotification(entry)) { 687 dispatchEventsAndRebuildList("onEndLifetimeExtension"); 688 } 689 } 690 } 691 cancelLifetimeExtension(NotificationEntry entry)692 private void cancelLifetimeExtension(NotificationEntry entry) { 693 mAmDispatchingToOtherCode = true; 694 for (NotifLifetimeExtender extender : entry.mLifetimeExtenders) { 695 extender.cancelLifetimeExtension(entry); 696 } 697 mAmDispatchingToOtherCode = false; 698 entry.mLifetimeExtenders.clear(); 699 } 700 isLifetimeExtended(NotificationEntry entry)701 private boolean isLifetimeExtended(NotificationEntry entry) { 702 return entry.mLifetimeExtenders.size() > 0; 703 } 704 updateLifetimeExtension(NotificationEntry entry)705 private void updateLifetimeExtension(NotificationEntry entry) { 706 entry.mLifetimeExtenders.clear(); 707 mAmDispatchingToOtherCode = true; 708 for (NotifLifetimeExtender extender : mLifetimeExtenders) { 709 if (extender.maybeExtendLifetime(entry, entry.mCancellationReason)) { 710 mLogger.logLifetimeExtended(entry, extender); 711 entry.mLifetimeExtenders.add(extender); 712 } 713 } 714 mAmDispatchingToOtherCode = false; 715 } 716 updateDismissInterceptors(@onNull NotificationEntry entry)717 private void updateDismissInterceptors(@NonNull NotificationEntry entry) { 718 entry.mDismissInterceptors.clear(); 719 mAmDispatchingToOtherCode = true; 720 for (NotifDismissInterceptor interceptor : mDismissInterceptors) { 721 if (interceptor.shouldInterceptDismissal(entry)) { 722 entry.mDismissInterceptors.add(interceptor); 723 } 724 } 725 mAmDispatchingToOtherCode = false; 726 } 727 cancelLocalDismissal(NotificationEntry entry)728 private void cancelLocalDismissal(NotificationEntry entry) { 729 if (entry.getDismissState() != NOT_DISMISSED) { 730 entry.setDismissState(NOT_DISMISSED); 731 if (entry.getSbn().getNotification().isGroupSummary()) { 732 for (NotificationEntry otherEntry : mNotificationSet.values()) { 733 if (otherEntry.getSbn().getGroupKey().equals(entry.getSbn().getGroupKey()) 734 && otherEntry.getDismissState() == PARENT_DISMISSED) { 735 otherEntry.setDismissState(NOT_DISMISSED); 736 } 737 } 738 } 739 } 740 } 741 onEndDismissInterception( NotifDismissInterceptor interceptor, NotificationEntry entry, @NonNull DismissedByUserStats stats)742 private void onEndDismissInterception( 743 NotifDismissInterceptor interceptor, 744 NotificationEntry entry, 745 @NonNull DismissedByUserStats stats) { 746 Assert.isMainThread(); 747 if (!mAttached) { 748 return; 749 } 750 checkForReentrantCall(); 751 752 if (!entry.mDismissInterceptors.remove(interceptor)) { 753 throw mEulogizer.record(new IllegalStateException( 754 String.format( 755 "Cannot end dismiss interceptor for interceptor \"%s\" (%s)", 756 interceptor.getName(), 757 interceptor))); 758 } 759 760 if (!isDismissIntercepted(entry)) { 761 dismissNotification(entry, stats); 762 } 763 } 764 cancelDismissInterception(NotificationEntry entry)765 private void cancelDismissInterception(NotificationEntry entry) { 766 mAmDispatchingToOtherCode = true; 767 for (NotifDismissInterceptor interceptor : entry.mDismissInterceptors) { 768 interceptor.cancelDismissInterception(entry); 769 } 770 mAmDispatchingToOtherCode = false; 771 entry.mDismissInterceptors.clear(); 772 } 773 isDismissIntercepted(NotificationEntry entry)774 private boolean isDismissIntercepted(NotificationEntry entry) { 775 return entry.mDismissInterceptors.size() > 0; 776 } 777 checkForReentrantCall()778 private void checkForReentrantCall() { 779 if (mAmDispatchingToOtherCode) { 780 throw mEulogizer.record(new IllegalStateException("Reentrant call detected")); 781 } 782 } 783 784 // While the NotificationListener is connecting to NotificationManager, there is a short period 785 // during which it's possible for us to receive events about notifications we don't yet know 786 // about (or that otherwise don't make sense). Until that race condition is fixed, we create a 787 // "forgiveness window" of five seconds during which we won't crash if we receive nonsensical 788 // messages from system server. crashIfNotInitializing(RuntimeException exception)789 private void crashIfNotInitializing(RuntimeException exception) { 790 final boolean isRecentlyInitialized = mInitializedTimestamp == 0 791 || mClock.uptimeMillis() - mInitializedTimestamp 792 < INITIALIZATION_FORGIVENESS_WINDOW; 793 794 if (isRecentlyInitialized) { 795 mLogger.logIgnoredError(exception.getMessage()); 796 } else { 797 throw mEulogizer.record(exception); 798 } 799 } 800 801 private static Ranking requireRanking(RankingMap rankingMap, String key) { 802 // TODO: Modify RankingMap so that we don't have to make a copy here 803 Ranking ranking = new Ranking(); 804 if (!rankingMap.getRanking(key, ranking)) { 805 throw new IllegalArgumentException("Ranking map doesn't contain key: " + key); 806 } 807 return ranking; 808 } 809 810 /** 811 * True if the notification has been canceled by system server. Usually, such notifications are 812 * immediately removed from the collection, but can sometimes stick around due to lifetime 813 * extenders. 814 */ 815 private boolean isCanceled(NotificationEntry entry) { 816 return entry.mCancellationReason != REASON_NOT_CANCELED; 817 } 818 819 private boolean cannotBeLifetimeExtended(NotificationEntry entry) { 820 final boolean locallyDismissedByUser = entry.getDismissState() != NOT_DISMISSED; 821 final boolean systemServerReportedUserCancel = 822 entry.mCancellationReason == REASON_CLICK 823 || entry.mCancellationReason == REASON_CANCEL; 824 return locallyDismissedByUser || systemServerReportedUserCancel; 825 } 826 827 /** 828 * When a group summary is dismissed, NotificationManager will also try to dismiss its children. 829 * Returns true if we think dismissing the group summary with group key 830 * <code>dismissedGroupKey</code> will cause NotificationManager to also dismiss 831 * <code>entry</code>. 832 * 833 * See NotificationManager.cancelGroupChildrenByListLocked() for corresponding code. 834 */ 835 @VisibleForTesting 836 static boolean shouldAutoDismissChildren( 837 NotificationEntry entry, 838 String dismissedGroupKey) { 839 return entry.getSbn().getGroupKey().equals(dismissedGroupKey) 840 && !entry.getSbn().getNotification().isGroupSummary() 841 && !hasFlag(entry, Notification.FLAG_ONGOING_EVENT) 842 && !hasFlag(entry, Notification.FLAG_BUBBLE) 843 && !hasFlag(entry, Notification.FLAG_NO_CLEAR) 844 && entry.getDismissState() != DISMISSED; 845 } 846 847 /** 848 * When the user 'clears all notifications' through SystemUI, NotificationManager will not 849 * dismiss unclearable notifications. 850 * @return true if we think NotificationManager will dismiss the entry when asked to 851 * cancel this notification with {@link NotificationListenerService#REASON_CANCEL_ALL} 852 * 853 * See NotificationManager.cancelAllLocked for corresponding code. 854 */ 855 private static boolean shouldDismissOnClearAll( 856 NotificationEntry entry, 857 @UserIdInt int userId) { 858 return userIdMatches(entry, userId) 859 && entry.isClearable() 860 && !hasFlag(entry, Notification.FLAG_BUBBLE) 861 && entry.getDismissState() != DISMISSED; 862 } 863 864 private static boolean hasFlag(NotificationEntry entry, int flag) { 865 return (entry.getSbn().getNotification().flags & flag) != 0; 866 } 867 868 /** 869 * Determine whether the userId applies to the notification in question, either because 870 * they match exactly, or one of them is USER_ALL (which is treated as a wildcard). 871 * 872 * See NotificationManager#notificationMatchesUserId 873 */ 874 private static boolean userIdMatches(NotificationEntry entry, int userId) { 875 return userId == UserHandle.USER_ALL 876 || entry.getSbn().getUser().getIdentifier() == UserHandle.USER_ALL 877 || entry.getSbn().getUser().getIdentifier() == userId; 878 } 879 880 @Override 881 public void dump(PrintWriter pw, @NonNull String[] args) { 882 final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs()); 883 entries.sort(Comparator.comparing(NotificationEntry::getKey)); 884 885 pw.println("\t" + TAG + " unsorted/unfiltered notifications: " + entries.size()); 886 pw.println( 887 ListDumper.dumpList( 888 entries, 889 true, 890 "\t\t")); 891 892 mInconsistencyTracker.dump(pw); 893 } 894 895 @Override 896 public void dumpPipeline(@NonNull PipelineDumper d) { 897 d.dump("notifCollectionListeners", mNotifCollectionListeners); 898 d.dump("lifetimeExtenders", mLifetimeExtenders); 899 d.dump("dismissInterceptors", mDismissInterceptors); 900 d.dump("buildListener", mBuildListener); 901 } 902 903 private final BatchableNotificationHandler mNotifHandler = new BatchableNotificationHandler() { 904 @Override 905 public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 906 NotifCollection.this.onNotificationPosted(sbn, rankingMap); 907 } 908 909 @Override 910 public void onNotificationBatchPosted(List<CoalescedEvent> events) { 911 NotifCollection.this.onNotificationGroupPosted(events); 912 } 913 914 @Override 915 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { 916 NotifCollection.this.onNotificationRemoved(sbn, rankingMap, REASON_UNKNOWN); 917 } 918 919 @Override 920 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, 921 int reason) { 922 NotifCollection.this.onNotificationRemoved(sbn, rankingMap, reason); 923 } 924 925 @Override 926 public void onNotificationRankingUpdate(RankingMap rankingMap) { 927 NotifCollection.this.onNotificationRankingUpdate(rankingMap); 928 } 929 930 @Override 931 public void onNotificationChannelModified( 932 String pkgName, 933 UserHandle user, 934 NotificationChannel channel, 935 int modificationType) { 936 NotifCollection.this.onNotificationChannelModified( 937 pkgName, 938 user, 939 channel, 940 modificationType); 941 } 942 943 @Override 944 public void onNotificationsInitialized() { 945 NotifCollection.this.onNotificationsInitialized(); 946 } 947 }; 948 949 private static final String TAG = "NotifCollection"; 950 951 /** 952 * Get an object which can be used to update a notification (internally to the pipeline) 953 * in response to a user action. 954 * 955 * @param name the name of the component that will update notifiations 956 * @return an updater 957 */ 958 public InternalNotifUpdater getInternalNotifUpdater(String name) { 959 return (sbn, reason) -> mMainHandler.post( 960 () -> updateNotificationInternally(sbn, name, reason)); 961 } 962 963 /** 964 * Provide an updated StatusBarNotification for an existing entry. If no entry exists for the 965 * given notification key, this method does nothing. 966 * 967 * @param sbn the updated notification 968 * @param name the component which is updating the notification 969 * @param reason the reason the notification is being updated 970 */ updateNotificationInternally(StatusBarNotification sbn, String name, String reason)971 private void updateNotificationInternally(StatusBarNotification sbn, String name, 972 String reason) { 973 Assert.isMainThread(); 974 checkForReentrantCall(); 975 976 // Make sure we have the notification to update 977 NotificationEntry entry = mNotificationSet.get(sbn.getKey()); 978 if (entry == null) { 979 mLogger.logNotifInternalUpdateFailed(sbn, name, reason); 980 return; 981 } 982 mLogger.logNotifInternalUpdate(entry, name, reason); 983 984 // First do the pieces of postNotification which are not about assuming the notification 985 // was sent by the app 986 entry.setSbn(sbn); 987 mEventQueue.add(new BindEntryEvent(entry, sbn)); 988 989 mLogger.logNotifUpdated(entry); 990 mEventQueue.add(new EntryUpdatedEvent(entry, false /* fromSystem */)); 991 992 // Skip the applyRanking step and go straight to dispatching the events 993 dispatchEventsAndRebuildList("updateNotificationInternally"); 994 } 995 996 /** 997 * A method to alert the collection that an async operation is happening, at the end of which a 998 * dismissal request will be made. This method has the additional guarantee that if a parent 999 * notification exists for a single child, then that notification will also be dismissed. 1000 * 1001 * The runnable returned must be run at the end of the async operation to enact the cancellation 1002 * 1003 * @param entry the notification we want to dismiss 1004 * @param cancellationReason the reason for the cancellation 1005 * @param statsCreator the callback for generating the stats for an entry 1006 * @return the runnable to be run when the dismissal is ready to happen 1007 */ registerFutureDismissal(NotificationEntry entry, int cancellationReason, DismissedByUserStatsCreator statsCreator)1008 public Runnable registerFutureDismissal(NotificationEntry entry, int cancellationReason, 1009 DismissedByUserStatsCreator statsCreator) { 1010 FutureDismissal dismissal = mFutureDismissals.get(entry.getKey()); 1011 if (dismissal != null) { 1012 mLogger.logFutureDismissalReused(dismissal); 1013 return dismissal; 1014 } 1015 dismissal = new FutureDismissal(entry, cancellationReason, statsCreator); 1016 mFutureDismissals.put(entry.getKey(), dismissal); 1017 mLogger.logFutureDismissalRegistered(dismissal); 1018 return dismissal; 1019 } 1020 handleFutureDismissal(NotificationEntry entry)1021 private void handleFutureDismissal(NotificationEntry entry) { 1022 final FutureDismissal futureDismissal = mFutureDismissals.remove(entry.getKey()); 1023 if (futureDismissal != null) { 1024 futureDismissal.onSystemServerCancel(entry.mCancellationReason); 1025 } 1026 } 1027 1028 /** A single method interface that callers can pass in when registering future dismissals */ 1029 public interface DismissedByUserStatsCreator { createDismissedByUserStats(NotificationEntry entry)1030 DismissedByUserStats createDismissedByUserStats(NotificationEntry entry); 1031 } 1032 1033 /** A class which tracks the double dismissal events coming in from both the system server and 1034 * the ui */ 1035 public class FutureDismissal implements Runnable { 1036 private final NotificationEntry mEntry; 1037 private final DismissedByUserStatsCreator mStatsCreator; 1038 @Nullable 1039 private final NotificationEntry mSummaryToDismiss; 1040 private final String mLabel; 1041 1042 private boolean mDidRun; 1043 private boolean mDidSystemServerCancel; 1044 FutureDismissal(NotificationEntry entry, @CancellationReason int cancellationReason, DismissedByUserStatsCreator statsCreator)1045 private FutureDismissal(NotificationEntry entry, @CancellationReason int cancellationReason, 1046 DismissedByUserStatsCreator statsCreator) { 1047 mEntry = entry; 1048 mStatsCreator = statsCreator; 1049 mSummaryToDismiss = fetchSummaryToDismiss(entry); 1050 mLabel = "<FutureDismissal@" + Integer.toHexString(hashCode()) 1051 + " entry=" + logKey(mEntry) 1052 + " reason=" + cancellationReasonDebugString(cancellationReason) 1053 + " summary=" + logKey(mSummaryToDismiss) 1054 + ">"; 1055 } 1056 1057 @Nullable fetchSummaryToDismiss(NotificationEntry entry)1058 private NotificationEntry fetchSummaryToDismiss(NotificationEntry entry) { 1059 if (isOnlyChildInGroup(entry)) { 1060 String group = entry.getSbn().getGroupKey(); 1061 NotificationEntry summary = getGroupSummary(group); 1062 if (summary != null && summary.isDismissable()) return summary; 1063 } 1064 return null; 1065 } 1066 1067 /** called when the entry has been removed from the collection */ onSystemServerCancel(@ancellationReason int cancellationReason)1068 public void onSystemServerCancel(@CancellationReason int cancellationReason) { 1069 Assert.isMainThread(); 1070 if (mDidSystemServerCancel) { 1071 mLogger.logFutureDismissalDoubleCancelledByServer(this); 1072 return; 1073 } 1074 mLogger.logFutureDismissalGotSystemServerCancel(this, cancellationReason); 1075 mDidSystemServerCancel = true; 1076 // TODO: Internally dismiss the summary now instead of waiting for onUiCancel 1077 } 1078 onUiCancel()1079 private void onUiCancel() { 1080 mFutureDismissals.remove(mEntry.getKey()); 1081 final NotificationEntry currentEntry = getEntry(mEntry.getKey()); 1082 // generate stats for the entry before dismissing summary, which could affect state 1083 final DismissedByUserStats stats = mStatsCreator.createDismissedByUserStats(mEntry); 1084 // dismiss the summary (if it exists) 1085 if (mSummaryToDismiss != null) { 1086 final NotificationEntry currentSummary = getEntry(mSummaryToDismiss.getKey()); 1087 if (currentSummary == mSummaryToDismiss) { 1088 mLogger.logFutureDismissalDismissing(this, "summary"); 1089 dismissNotification(mSummaryToDismiss, 1090 mStatsCreator.createDismissedByUserStats(mSummaryToDismiss)); 1091 } else { 1092 mLogger.logFutureDismissalMismatchedEntry(this, "summary", currentSummary); 1093 } 1094 } 1095 // dismiss this entry (if it is still around) 1096 if (mDidSystemServerCancel) { 1097 mLogger.logFutureDismissalAlreadyCancelledByServer(this); 1098 } else if (currentEntry == mEntry) { 1099 mLogger.logFutureDismissalDismissing(this, "entry"); 1100 dismissNotification(mEntry, stats); 1101 } else { 1102 mLogger.logFutureDismissalMismatchedEntry(this, "entry", currentEntry); 1103 } 1104 } 1105 1106 /** called when the dismissal should be completed */ 1107 @Override run()1108 public void run() { 1109 Assert.isMainThread(); 1110 if (mDidRun) { 1111 mLogger.logFutureDismissalDoubleRun(this); 1112 return; 1113 } 1114 mDidRun = true; 1115 onUiCancel(); 1116 } 1117 1118 /** provides a debug label for this instance */ getLabel()1119 public String getLabel() { 1120 return mLabel; 1121 } 1122 } 1123 1124 @IntDef(prefix = { "REASON_" }, value = { 1125 REASON_NOT_CANCELED, 1126 REASON_UNKNOWN, 1127 REASON_CLICK, 1128 REASON_CANCEL, 1129 REASON_CANCEL_ALL, 1130 REASON_ERROR, 1131 REASON_PACKAGE_CHANGED, 1132 REASON_USER_STOPPED, 1133 REASON_PACKAGE_BANNED, 1134 REASON_APP_CANCEL, 1135 REASON_APP_CANCEL_ALL, 1136 REASON_LISTENER_CANCEL, 1137 REASON_LISTENER_CANCEL_ALL, 1138 REASON_GROUP_SUMMARY_CANCELED, 1139 REASON_GROUP_OPTIMIZATION, 1140 REASON_PACKAGE_SUSPENDED, 1141 REASON_PROFILE_TURNED_OFF, 1142 REASON_UNAUTOBUNDLED, 1143 REASON_CHANNEL_BANNED, 1144 REASON_SNOOZED, 1145 REASON_TIMEOUT, 1146 REASON_CHANNEL_REMOVED, 1147 REASON_CLEAR_DATA, 1148 REASON_ASSISTANT_CANCEL, 1149 }) 1150 @Retention(RetentionPolicy.SOURCE) 1151 public @interface CancellationReason {} 1152 1153 static final int REASON_NOT_CANCELED = -1; 1154 public static final int REASON_UNKNOWN = 0; 1155 1156 private static final long INITIALIZATION_FORGIVENESS_WINDOW = TimeUnit.SECONDS.toMillis(5); 1157 } 1158