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