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