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_CANCEL; 22 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; 23 import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED; 24 import static android.service.notification.NotificationListenerService.REASON_CLICK; 25 import static android.service.notification.NotificationListenerService.REASON_ERROR; 26 import static android.service.notification.NotificationListenerService.REASON_GROUP_OPTIMIZATION; 27 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; 28 import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL; 29 import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL_ALL; 30 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED; 31 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_CHANGED; 32 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_SUSPENDED; 33 import static android.service.notification.NotificationListenerService.REASON_PROFILE_TURNED_OFF; 34 import static android.service.notification.NotificationListenerService.REASON_SNOOZED; 35 import static android.service.notification.NotificationListenerService.REASON_TIMEOUT; 36 import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED; 37 import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED; 38 39 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED; 40 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED; 41 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; 42 43 import static java.util.Objects.requireNonNull; 44 45 import android.annotation.IntDef; 46 import android.annotation.MainThread; 47 import android.annotation.Nullable; 48 import android.annotation.UserIdInt; 49 import android.app.Notification; 50 import android.os.Handler; 51 import android.os.RemoteException; 52 import android.os.Trace; 53 import android.os.UserHandle; 54 import android.service.notification.NotificationListenerService; 55 import android.service.notification.NotificationListenerService.Ranking; 56 import android.service.notification.NotificationListenerService.RankingMap; 57 import android.service.notification.StatusBarNotification; 58 import android.util.ArrayMap; 59 import android.util.Pair; 60 61 import androidx.annotation.NonNull; 62 63 import com.android.internal.statusbar.IStatusBarService; 64 import com.android.systemui.Dumpable; 65 import com.android.systemui.dagger.SysUISingleton; 66 import com.android.systemui.dagger.qualifiers.Main; 67 import com.android.systemui.dump.DumpManager; 68 import com.android.systemui.dump.LogBufferEulogizer; 69 import com.android.systemui.flags.FeatureFlags; 70 import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent; 71 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer; 72 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler; 73 import com.android.systemui.statusbar.notification.collection.notifcollection.BindEntryEvent; 74 import com.android.systemui.statusbar.notification.collection.notifcollection.CleanUpEntryEvent; 75 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; 76 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; 77 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryAddedEvent; 78 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryRemovedEvent; 79 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent; 80 import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent; 81 import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater; 82 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 83 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger; 84 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; 85 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent; 86 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; 87 import com.android.systemui.statusbar.notification.collection.notifcollection.RankingAppliedEvent; 88 import com.android.systemui.statusbar.notification.collection.notifcollection.RankingUpdatedEvent; 89 import com.android.systemui.util.Assert; 90 import com.android.systemui.util.time.SystemClock; 91 92 import java.io.FileDescriptor; 93 import java.io.PrintWriter; 94 import java.lang.annotation.Retention; 95 import java.lang.annotation.RetentionPolicy; 96 import java.util.ArrayDeque; 97 import java.util.ArrayList; 98 import java.util.Collection; 99 import java.util.Collections; 100 import java.util.List; 101 import java.util.Map; 102 import java.util.Objects; 103 import java.util.Queue; 104 import java.util.concurrent.TimeUnit; 105 106 import javax.inject.Inject; 107 108 /** 109 * Keeps a record of all of the "active" notifications, i.e. the notifications that are currently 110 * posted to the phone. This collection is unsorted, ungrouped, and unfiltered. Just because a 111 * notification appears in this collection doesn't mean that it's currently present in the shade 112 * (notifications can be hidden for a variety of reasons). Code that cares about what notifications 113 * are *visible* right now should register listeners later in the pipeline. 114 * 115 * Each notification is represented by a {@link NotificationEntry}, which is itself made up of two 116 * parts: a {@link StatusBarNotification} and a {@link Ranking}. When notifications are updated, 117 * their underlying SBNs and Rankings are swapped out, but the enclosing NotificationEntry (and its 118 * associated key) remain the same. In general, an SBN can only be updated when the notification is 119 * reposted by the source app; Rankings are updated much more often, usually every time there is an 120 * update from any kind from NotificationManager. 121 * 122 * In general, this collection closely mirrors the list maintained by NotificationManager, but it 123 * can occasionally diverge due to lifetime extenders (see 124 * {@link #addNotificationLifetimeExtender(NotifLifetimeExtender)}). 125 * 126 * Interested parties can register listeners 127 * ({@link #addCollectionListener(NotifCollectionListener)}) to be informed when notifications 128 * events occur. 129 */ 130 @MainThread 131 @SysUISingleton 132 public class NotifCollection implements Dumpable { 133 private final IStatusBarService mStatusBarService; 134 private final SystemClock mClock; 135 private final FeatureFlags mFeatureFlags; 136 private final NotifCollectionLogger mLogger; 137 private final Handler mMainHandler; 138 private final LogBufferEulogizer mEulogizer; 139 140 private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>(); 141 private final Collection<NotificationEntry> mReadOnlyNotificationSet = 142 Collections.unmodifiableCollection(mNotificationSet.values()); 143 144 @Nullable private CollectionReadyForBuildListener mBuildListener; 145 private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>(); 146 private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>(); 147 private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>(); 148 149 private Queue<NotifEvent> mEventQueue = new ArrayDeque<>(); 150 151 private boolean mAttached = false; 152 private boolean mAmDispatchingToOtherCode; 153 private long mInitializedTimestamp = 0; 154 155 @Inject NotifCollection( IStatusBarService statusBarService, SystemClock clock, FeatureFlags featureFlags, NotifCollectionLogger logger, @Main Handler mainHandler, LogBufferEulogizer logBufferEulogizer, DumpManager dumpManager)156 public NotifCollection( 157 IStatusBarService statusBarService, 158 SystemClock clock, 159 FeatureFlags featureFlags, 160 NotifCollectionLogger logger, 161 @Main Handler mainHandler, 162 LogBufferEulogizer logBufferEulogizer, 163 DumpManager dumpManager) { 164 Assert.isMainThread(); 165 mStatusBarService = statusBarService; 166 mClock = clock; 167 mFeatureFlags = featureFlags; 168 mLogger = logger; 169 mMainHandler = mainHandler; 170 mEulogizer = logBufferEulogizer; 171 172 dumpManager.registerDumpable(TAG, this); 173 } 174 175 /** Initializes the NotifCollection and registers it to receive notification events. */ attach(GroupCoalescer groupCoalescer)176 public void attach(GroupCoalescer groupCoalescer) { 177 Assert.isMainThread(); 178 if (mAttached) { 179 throw new RuntimeException("attach() called twice"); 180 } 181 mAttached = true; 182 183 groupCoalescer.setNotificationHandler(mNotifHandler); 184 } 185 186 /** 187 * Sets the class responsible for converting the collection into the list of currently-visible 188 * notifications. 189 */ setBuildListener(CollectionReadyForBuildListener buildListener)190 void setBuildListener(CollectionReadyForBuildListener buildListener) { 191 Assert.isMainThread(); 192 mBuildListener = buildListener; 193 } 194 195 /** @see NotifPipeline#getEntry(String) () */ getEntry(String key)196 NotificationEntry getEntry(String key) { 197 return mNotificationSet.get(key); 198 } 199 200 /** @see NotifPipeline#getAllNotifs() */ getAllNotifs()201 Collection<NotificationEntry> getAllNotifs() { 202 Assert.isMainThread(); 203 return mReadOnlyNotificationSet; 204 } 205 206 /** @see NotifPipeline#addCollectionListener(NotifCollectionListener) */ addCollectionListener(NotifCollectionListener listener)207 void addCollectionListener(NotifCollectionListener listener) { 208 Assert.isMainThread(); 209 mNotifCollectionListeners.add(listener); 210 } 211 212 /** @see NotifPipeline#addNotificationLifetimeExtender(NotifLifetimeExtender) */ addNotificationLifetimeExtender(NotifLifetimeExtender extender)213 void addNotificationLifetimeExtender(NotifLifetimeExtender extender) { 214 Assert.isMainThread(); 215 checkForReentrantCall(); 216 if (mLifetimeExtenders.contains(extender)) { 217 throw new IllegalArgumentException("Extender " + extender + " already added."); 218 } 219 mLifetimeExtenders.add(extender); 220 extender.setCallback(this::onEndLifetimeExtension); 221 } 222 223 /** @see NotifPipeline#addNotificationDismissInterceptor(NotifDismissInterceptor) */ addNotificationDismissInterceptor(NotifDismissInterceptor interceptor)224 void addNotificationDismissInterceptor(NotifDismissInterceptor interceptor) { 225 Assert.isMainThread(); 226 checkForReentrantCall(); 227 if (mDismissInterceptors.contains(interceptor)) { 228 throw new IllegalArgumentException("Interceptor " + interceptor + " already added."); 229 } 230 mDismissInterceptors.add(interceptor); 231 interceptor.setCallback(this::onEndDismissInterception); 232 } 233 234 /** 235 * Dismisses multiple notifications on behalf of the user. 236 */ dismissNotifications( List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss)237 public void dismissNotifications( 238 List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss) { 239 Assert.isMainThread(); 240 checkForReentrantCall(); 241 242 final List<NotificationEntry> entriesToLocallyDismiss = new ArrayList<>(); 243 for (int i = 0; i < entriesToDismiss.size(); i++) { 244 NotificationEntry entry = entriesToDismiss.get(i).first; 245 DismissedByUserStats stats = entriesToDismiss.get(i).second; 246 247 requireNonNull(stats); 248 if (entry != mNotificationSet.get(entry.getKey())) { 249 throw mEulogizer.record( 250 new IllegalStateException("Invalid entry: " + entry.getKey())); 251 } 252 253 if (entry.getDismissState() == DISMISSED) { 254 continue; 255 } 256 257 updateDismissInterceptors(entry); 258 if (isDismissIntercepted(entry)) { 259 mLogger.logNotifDismissedIntercepted(entry.getKey()); 260 continue; 261 } 262 263 entriesToLocallyDismiss.add(entry); 264 if (!isCanceled(entry)) { 265 // send message to system server if this notification hasn't already been cancelled 266 try { 267 mStatusBarService.onNotificationClear( 268 entry.getSbn().getPackageName(), 269 entry.getSbn().getUser().getIdentifier(), 270 entry.getSbn().getKey(), 271 stats.dismissalSurface, 272 stats.dismissalSentiment, 273 stats.notificationVisibility); 274 } catch (RemoteException e) { 275 // system process is dead if we're here. 276 mLogger.logRemoteExceptionOnNotificationClear(entry.getKey(), e); 277 } 278 } 279 } 280 281 locallyDismissNotifications(entriesToLocallyDismiss); 282 dispatchEventsAndRebuildList(); 283 } 284 285 /** 286 * Dismisses a single notification on behalf of the user. 287 */ dismissNotification( NotificationEntry entry, @NonNull DismissedByUserStats stats)288 public void dismissNotification( 289 NotificationEntry entry, 290 @NonNull DismissedByUserStats stats) { 291 dismissNotifications(List.of(new Pair<>(entry, stats))); 292 } 293 294 /** 295 * Dismisses all clearable notifications for a given userid on behalf of the user. 296 */ dismissAllNotifications(@serIdInt int userId)297 public void dismissAllNotifications(@UserIdInt int userId) { 298 Assert.isMainThread(); 299 checkForReentrantCall(); 300 301 mLogger.logDismissAll(userId); 302 303 try { 304 // TODO(b/169585328): Do not clear media player notifications 305 mStatusBarService.onClearAllNotifications(userId); 306 } catch (RemoteException e) { 307 // system process is dead if we're here. 308 mLogger.logRemoteExceptionOnClearAllNotifications(e); 309 } 310 311 final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs()); 312 for (int i = entries.size() - 1; i >= 0; i--) { 313 NotificationEntry entry = entries.get(i); 314 if (!shouldDismissOnClearAll(entry, userId)) { 315 // system server won't be removing these notifications, but we still give dismiss 316 // interceptors the chance to filter the notification 317 updateDismissInterceptors(entry); 318 if (isDismissIntercepted(entry)) { 319 mLogger.logNotifClearAllDismissalIntercepted(entry.getKey()); 320 } 321 entries.remove(i); 322 } 323 } 324 325 locallyDismissNotifications(entries); 326 dispatchEventsAndRebuildList(); 327 } 328 329 /** 330 * Optimistically marks the given notifications as dismissed -- we'll wait for the signal 331 * from system server before removing it from our notification set. 332 */ locallyDismissNotifications(List<NotificationEntry> entries)333 private void locallyDismissNotifications(List<NotificationEntry> entries) { 334 final List<NotificationEntry> canceledEntries = new ArrayList<>(); 335 336 for (int i = 0; i < entries.size(); i++) { 337 NotificationEntry entry = entries.get(i); 338 339 entry.setDismissState(DISMISSED); 340 mLogger.logNotifDismissed(entry.getKey()); 341 342 if (isCanceled(entry)) { 343 canceledEntries.add(entry); 344 } else { 345 // Mark any children as dismissed as system server will auto-dismiss them as well 346 if (entry.getSbn().getNotification().isGroupSummary()) { 347 for (NotificationEntry otherEntry : mNotificationSet.values()) { 348 if (shouldAutoDismissChildren(otherEntry, entry.getSbn().getGroupKey())) { 349 otherEntry.setDismissState(PARENT_DISMISSED); 350 mLogger.logChildDismissed(otherEntry); 351 if (isCanceled(otherEntry)) { 352 canceledEntries.add(otherEntry); 353 } 354 } 355 } 356 } 357 } 358 } 359 360 // Immediately remove any dismissed notifs that have already been canceled by system server 361 // (probably due to being lifetime-extended up until this point). 362 for (NotificationEntry canceledEntry : canceledEntries) { 363 mLogger.logDismissOnAlreadyCanceledEntry(canceledEntry); 364 tryRemoveNotification(canceledEntry); 365 } 366 } 367 onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)368 private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 369 Assert.isMainThread(); 370 371 postNotification(sbn, requireRanking(rankingMap, sbn.getKey())); 372 applyRanking(rankingMap); 373 dispatchEventsAndRebuildList(); 374 } 375 onNotificationGroupPosted(List<CoalescedEvent> batch)376 private void onNotificationGroupPosted(List<CoalescedEvent> batch) { 377 Assert.isMainThread(); 378 379 mLogger.logNotifGroupPosted(batch.get(0).getSbn().getGroupKey(), batch.size()); 380 381 for (CoalescedEvent event : batch) { 382 postNotification(event.getSbn(), event.getRanking()); 383 } 384 dispatchEventsAndRebuildList(); 385 } 386 onNotificationRemoved( StatusBarNotification sbn, RankingMap rankingMap, int reason)387 private void onNotificationRemoved( 388 StatusBarNotification sbn, 389 RankingMap rankingMap, 390 int reason) { 391 Assert.isMainThread(); 392 393 mLogger.logNotifRemoved(sbn.getKey(), reason); 394 395 final NotificationEntry entry = mNotificationSet.get(sbn.getKey()); 396 if (entry == null) { 397 // TODO (b/160008901): Throw an exception here 398 mLogger.logNoNotificationToRemoveWithKey(sbn.getKey()); 399 return; 400 } 401 402 entry.mCancellationReason = reason; 403 tryRemoveNotification(entry); 404 applyRanking(rankingMap); 405 dispatchEventsAndRebuildList(); 406 } 407 onNotificationRankingUpdate(RankingMap rankingMap)408 private void onNotificationRankingUpdate(RankingMap rankingMap) { 409 Assert.isMainThread(); 410 mEventQueue.add(new RankingUpdatedEvent(rankingMap)); 411 applyRanking(rankingMap); 412 dispatchEventsAndRebuildList(); 413 } 414 onNotificationsInitialized()415 private void onNotificationsInitialized() { 416 mInitializedTimestamp = mClock.uptimeMillis(); 417 } 418 postNotification( StatusBarNotification sbn, Ranking ranking)419 private void postNotification( 420 StatusBarNotification sbn, 421 Ranking ranking) { 422 NotificationEntry entry = mNotificationSet.get(sbn.getKey()); 423 424 if (entry == null) { 425 // A new notification! 426 entry = new NotificationEntry(sbn, ranking, mClock.uptimeMillis()); 427 mEventQueue.add(new InitEntryEvent(entry)); 428 mEventQueue.add(new BindEntryEvent(entry, sbn)); 429 mNotificationSet.put(sbn.getKey(), entry); 430 431 mLogger.logNotifPosted(sbn.getKey()); 432 mEventQueue.add(new EntryAddedEvent(entry)); 433 434 } else { 435 // Update to an existing entry 436 437 // Notification is updated so it is essentially re-added and thus alive again, so we 438 // can reset its state. 439 // TODO: If a coalesced event ever gets here, it's possible to lose track of children, 440 // since their rankings might have been updated earlier (and thus we may no longer 441 // think a child is associated with this locally-dismissed entry). 442 cancelLocalDismissal(entry); 443 cancelLifetimeExtension(entry); 444 cancelDismissInterception(entry); 445 entry.mCancellationReason = REASON_NOT_CANCELED; 446 447 entry.setSbn(sbn); 448 mEventQueue.add(new BindEntryEvent(entry, sbn)); 449 450 mLogger.logNotifUpdated(sbn.getKey()); 451 mEventQueue.add(new EntryUpdatedEvent(entry, true /* fromSystem */)); 452 } 453 } 454 455 /** 456 * Tries to remove a notification from the notification set. This removal may be blocked by 457 * lifetime extenders. Does not trigger a rebuild of the list; caller must do that manually. 458 * 459 * @return True if the notification was removed, false otherwise. 460 */ tryRemoveNotification(NotificationEntry entry)461 private boolean tryRemoveNotification(NotificationEntry entry) { 462 if (mNotificationSet.get(entry.getKey()) != entry) { 463 throw mEulogizer.record( 464 new IllegalStateException("No notification to remove with key " 465 + entry.getKey())); 466 } 467 468 if (!isCanceled(entry)) { 469 throw mEulogizer.record( 470 new IllegalStateException("Cannot remove notification " + entry.getKey() 471 + ": has not been marked for removal")); 472 } 473 474 if (cannotBeLifetimeExtended(entry)) { 475 cancelLifetimeExtension(entry); 476 } else { 477 updateLifetimeExtension(entry); 478 } 479 480 if (!isLifetimeExtended(entry)) { 481 mLogger.logNotifReleased(entry.getKey()); 482 mNotificationSet.remove(entry.getKey()); 483 cancelDismissInterception(entry); 484 mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason)); 485 mEventQueue.add(new CleanUpEntryEvent(entry)); 486 return true; 487 } else { 488 return false; 489 } 490 } 491 applyRanking(@onNull RankingMap rankingMap)492 private void applyRanking(@NonNull RankingMap rankingMap) { 493 for (NotificationEntry entry : mNotificationSet.values()) { 494 if (!isCanceled(entry)) { 495 496 // TODO: (b/148791039) We should crash if we are ever handed a ranking with 497 // incomplete entries. Right now, there's a race condition in NotificationListener 498 // that means this might occur when SystemUI is starting up. 499 Ranking ranking = new Ranking(); 500 if (rankingMap.getRanking(entry.getKey(), ranking)) { 501 entry.setRanking(ranking); 502 503 // TODO: (b/145659174) update the sbn's overrideGroupKey in 504 // NotificationEntry.setRanking instead of here once we fully migrate to the 505 // NewNotifPipeline 506 if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { 507 final String newOverrideGroupKey = ranking.getOverrideGroupKey(); 508 if (!Objects.equals(entry.getSbn().getOverrideGroupKey(), 509 newOverrideGroupKey)) { 510 entry.getSbn().setOverrideGroupKey(newOverrideGroupKey); 511 } 512 } 513 } else { 514 mLogger.logRankingMissing(entry.getKey(), rankingMap); 515 } 516 } 517 } 518 mEventQueue.add(new RankingAppliedEvent()); 519 } 520 dispatchEventsAndRebuildList()521 private void dispatchEventsAndRebuildList() { 522 Trace.beginSection("NotifCollection.dispatchEventsAndRebuildList"); 523 mAmDispatchingToOtherCode = true; 524 while (!mEventQueue.isEmpty()) { 525 mEventQueue.remove().dispatchTo(mNotifCollectionListeners); 526 } 527 mAmDispatchingToOtherCode = false; 528 529 if (mBuildListener != null) { 530 mBuildListener.onBuildList(mReadOnlyNotificationSet); 531 } 532 Trace.endSection(); 533 } 534 onEndLifetimeExtension( @onNull NotifLifetimeExtender extender, @NonNull NotificationEntry entry)535 private void onEndLifetimeExtension( 536 @NonNull NotifLifetimeExtender extender, 537 @NonNull NotificationEntry entry) { 538 Assert.isMainThread(); 539 if (!mAttached) { 540 return; 541 } 542 checkForReentrantCall(); 543 544 if (!entry.mLifetimeExtenders.remove(extender)) { 545 throw mEulogizer.record(new IllegalStateException( 546 String.format( 547 "Cannot end lifetime extension for extender \"%s\" (%s)", 548 extender.getName(), 549 extender))); 550 } 551 552 mLogger.logLifetimeExtensionEnded( 553 entry.getKey(), 554 extender, 555 entry.mLifetimeExtenders.size()); 556 557 if (!isLifetimeExtended(entry)) { 558 if (tryRemoveNotification(entry)) { 559 dispatchEventsAndRebuildList(); 560 } 561 } 562 } 563 cancelLifetimeExtension(NotificationEntry entry)564 private void cancelLifetimeExtension(NotificationEntry entry) { 565 mAmDispatchingToOtherCode = true; 566 for (NotifLifetimeExtender extender : entry.mLifetimeExtenders) { 567 extender.cancelLifetimeExtension(entry); 568 } 569 mAmDispatchingToOtherCode = false; 570 entry.mLifetimeExtenders.clear(); 571 } 572 isLifetimeExtended(NotificationEntry entry)573 private boolean isLifetimeExtended(NotificationEntry entry) { 574 return entry.mLifetimeExtenders.size() > 0; 575 } 576 updateLifetimeExtension(NotificationEntry entry)577 private void updateLifetimeExtension(NotificationEntry entry) { 578 entry.mLifetimeExtenders.clear(); 579 mAmDispatchingToOtherCode = true; 580 for (NotifLifetimeExtender extender : mLifetimeExtenders) { 581 if (extender.shouldExtendLifetime(entry, entry.mCancellationReason)) { 582 mLogger.logLifetimeExtended(entry.getKey(), extender); 583 entry.mLifetimeExtenders.add(extender); 584 } 585 } 586 mAmDispatchingToOtherCode = false; 587 } 588 updateDismissInterceptors(@onNull NotificationEntry entry)589 private void updateDismissInterceptors(@NonNull NotificationEntry entry) { 590 entry.mDismissInterceptors.clear(); 591 mAmDispatchingToOtherCode = true; 592 for (NotifDismissInterceptor interceptor : mDismissInterceptors) { 593 if (interceptor.shouldInterceptDismissal(entry)) { 594 entry.mDismissInterceptors.add(interceptor); 595 } 596 } 597 mAmDispatchingToOtherCode = false; 598 } 599 cancelLocalDismissal(NotificationEntry entry)600 private void cancelLocalDismissal(NotificationEntry entry) { 601 if (entry.getDismissState() != NOT_DISMISSED) { 602 entry.setDismissState(NOT_DISMISSED); 603 if (entry.getSbn().getNotification().isGroupSummary()) { 604 for (NotificationEntry otherEntry : mNotificationSet.values()) { 605 if (otherEntry.getSbn().getGroupKey().equals(entry.getSbn().getGroupKey()) 606 && otherEntry.getDismissState() == PARENT_DISMISSED) { 607 otherEntry.setDismissState(NOT_DISMISSED); 608 } 609 } 610 } 611 } 612 } 613 onEndDismissInterception( NotifDismissInterceptor interceptor, NotificationEntry entry, @NonNull DismissedByUserStats stats)614 private void onEndDismissInterception( 615 NotifDismissInterceptor interceptor, 616 NotificationEntry entry, 617 @NonNull DismissedByUserStats stats) { 618 Assert.isMainThread(); 619 if (!mAttached) { 620 return; 621 } 622 checkForReentrantCall(); 623 624 if (!entry.mDismissInterceptors.remove(interceptor)) { 625 throw mEulogizer.record(new IllegalStateException( 626 String.format( 627 "Cannot end dismiss interceptor for interceptor \"%s\" (%s)", 628 interceptor.getName(), 629 interceptor))); 630 } 631 632 if (!isDismissIntercepted(entry)) { 633 dismissNotification(entry, stats); 634 } 635 } 636 cancelDismissInterception(NotificationEntry entry)637 private void cancelDismissInterception(NotificationEntry entry) { 638 mAmDispatchingToOtherCode = true; 639 for (NotifDismissInterceptor interceptor : entry.mDismissInterceptors) { 640 interceptor.cancelDismissInterception(entry); 641 } 642 mAmDispatchingToOtherCode = false; 643 entry.mDismissInterceptors.clear(); 644 } 645 isDismissIntercepted(NotificationEntry entry)646 private boolean isDismissIntercepted(NotificationEntry entry) { 647 return entry.mDismissInterceptors.size() > 0; 648 } 649 checkForReentrantCall()650 private void checkForReentrantCall() { 651 if (mAmDispatchingToOtherCode) { 652 throw mEulogizer.record(new IllegalStateException("Reentrant call detected")); 653 } 654 } 655 656 // While the NotificationListener is connecting to NotificationManager, there is a short period 657 // during which it's possible for us to receive events about notifications we don't yet know 658 // about (or that otherwise don't make sense). Until that race condition is fixed, we create a 659 // "forgiveness window" of five seconds during which we won't crash if we receive nonsensical 660 // messages from system server. crashIfNotInitializing(RuntimeException exception)661 private void crashIfNotInitializing(RuntimeException exception) { 662 final boolean isRecentlyInitialized = mInitializedTimestamp == 0 663 || mClock.uptimeMillis() - mInitializedTimestamp 664 < INITIALIZATION_FORGIVENESS_WINDOW; 665 666 if (isRecentlyInitialized) { 667 mLogger.logIgnoredError(exception.getMessage()); 668 } else { 669 throw mEulogizer.record(exception); 670 } 671 } 672 673 private static Ranking requireRanking(RankingMap rankingMap, String key) { 674 // TODO: Modify RankingMap so that we don't have to make a copy here 675 Ranking ranking = new Ranking(); 676 if (!rankingMap.getRanking(key, ranking)) { 677 throw new IllegalArgumentException("Ranking map doesn't contain key: " + key); 678 } 679 return ranking; 680 } 681 682 /** 683 * True if the notification has been canceled by system server. Usually, such notifications are 684 * immediately removed from the collection, but can sometimes stick around due to lifetime 685 * extenders. 686 */ 687 private boolean isCanceled(NotificationEntry entry) { 688 return entry.mCancellationReason != REASON_NOT_CANCELED; 689 } 690 691 private boolean cannotBeLifetimeExtended(NotificationEntry entry) { 692 final boolean locallyDismissedByUser = entry.getDismissState() != NOT_DISMISSED; 693 final boolean systemServerReportedUserCancel = 694 entry.mCancellationReason == REASON_CLICK 695 || entry.mCancellationReason == REASON_CANCEL; 696 return locallyDismissedByUser || systemServerReportedUserCancel; 697 } 698 699 /** 700 * When a group summary is dismissed, NotificationManager will also try to dismiss its children. 701 * Returns true if we think dismissing the group summary with group key 702 * <code>dismissedGroupKey</code> will cause NotificationManager to also dismiss 703 * <code>entry</code>. 704 * 705 * See NotificationManager.cancelGroupChildrenByListLocked() for corresponding code. 706 */ 707 private static boolean shouldAutoDismissChildren( 708 NotificationEntry entry, 709 String dismissedGroupKey) { 710 return entry.getSbn().getGroupKey().equals(dismissedGroupKey) 711 && !entry.getSbn().getNotification().isGroupSummary() 712 && !hasFlag(entry, Notification.FLAG_FOREGROUND_SERVICE) 713 && !hasFlag(entry, Notification.FLAG_BUBBLE) 714 && entry.getDismissState() != DISMISSED; 715 } 716 717 /** 718 * When the user 'clears all notifications' through SystemUI, NotificationManager will not 719 * dismiss unclearable notifications. 720 * @return true if we think NotificationManager will dismiss the entry when asked to 721 * cancel this notification with {@link NotificationListenerService#REASON_CANCEL_ALL} 722 * 723 * See NotificationManager.cancelAllLocked for corresponding code. 724 */ 725 private static boolean shouldDismissOnClearAll( 726 NotificationEntry entry, 727 @UserIdInt int userId) { 728 return userIdMatches(entry, userId) 729 && entry.isClearable() 730 && !hasFlag(entry, Notification.FLAG_BUBBLE) 731 && entry.getDismissState() != DISMISSED; 732 } 733 734 private static boolean hasFlag(NotificationEntry entry, int flag) { 735 return (entry.getSbn().getNotification().flags & flag) != 0; 736 } 737 738 /** 739 * Determine whether the userId applies to the notification in question, either because 740 * they match exactly, or one of them is USER_ALL (which is treated as a wildcard). 741 * 742 * See NotificationManager#notificationMatchesUserId 743 */ 744 private static boolean userIdMatches(NotificationEntry entry, int userId) { 745 return userId == UserHandle.USER_ALL 746 || entry.getSbn().getUser().getIdentifier() == UserHandle.USER_ALL 747 || entry.getSbn().getUser().getIdentifier() == userId; 748 } 749 750 @Override 751 public void dump(@NonNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args) { 752 final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs()); 753 754 pw.println("\t" + TAG + " unsorted/unfiltered notifications:"); 755 if (entries.size() == 0) { 756 pw.println("\t\t None"); 757 } 758 pw.println( 759 ListDumper.dumpList( 760 entries, 761 true, 762 "\t\t")); 763 } 764 765 private final BatchableNotificationHandler mNotifHandler = new BatchableNotificationHandler() { 766 @Override 767 public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 768 NotifCollection.this.onNotificationPosted(sbn, rankingMap); 769 } 770 771 @Override 772 public void onNotificationBatchPosted(List<CoalescedEvent> events) { 773 NotifCollection.this.onNotificationGroupPosted(events); 774 } 775 776 @Override 777 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { 778 NotifCollection.this.onNotificationRemoved(sbn, rankingMap, REASON_UNKNOWN); 779 } 780 781 @Override 782 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, 783 int reason) { 784 NotifCollection.this.onNotificationRemoved(sbn, rankingMap, reason); 785 } 786 787 @Override 788 public void onNotificationRankingUpdate(RankingMap rankingMap) { 789 NotifCollection.this.onNotificationRankingUpdate(rankingMap); 790 } 791 792 @Override 793 public void onNotificationsInitialized() { 794 NotifCollection.this.onNotificationsInitialized(); 795 } 796 }; 797 798 private static final String TAG = "NotifCollection"; 799 800 /** 801 * Get an object which can be used to update a notification (internally to the pipeline) 802 * in response to a user action. 803 * 804 * @param name the name of the component that will update notifiations 805 * @return an updater 806 */ 807 public InternalNotifUpdater getInternalNotifUpdater(String name) { 808 return (sbn, reason) -> mMainHandler.post( 809 () -> updateNotificationInternally(sbn, name, reason)); 810 } 811 812 /** 813 * Provide an updated StatusBarNotification for an existing entry. If no entry exists for the 814 * given notification key, this method does nothing. 815 * 816 * @param sbn the updated notification 817 * @param name the component which is updating the notification 818 * @param reason the reason the notification is being updated 819 */ updateNotificationInternally(StatusBarNotification sbn, String name, String reason)820 private void updateNotificationInternally(StatusBarNotification sbn, String name, 821 String reason) { 822 Assert.isMainThread(); 823 checkForReentrantCall(); 824 825 // Make sure we have the notification to update 826 NotificationEntry entry = mNotificationSet.get(sbn.getKey()); 827 if (entry == null) { 828 mLogger.logNotifInternalUpdateFailed(sbn.getKey(), name, reason); 829 return; 830 } 831 mLogger.logNotifInternalUpdate(sbn.getKey(), name, reason); 832 833 // First do the pieces of postNotification which are not about assuming the notification 834 // was sent by the app 835 entry.setSbn(sbn); 836 mEventQueue.add(new BindEntryEvent(entry, sbn)); 837 838 mLogger.logNotifUpdated(sbn.getKey()); 839 mEventQueue.add(new EntryUpdatedEvent(entry, false /* fromSystem */)); 840 841 // Skip the applyRanking step and go straight to dispatching the events 842 dispatchEventsAndRebuildList(); 843 } 844 845 @IntDef(prefix = { "REASON_" }, value = { 846 REASON_NOT_CANCELED, 847 REASON_UNKNOWN, 848 REASON_CLICK, 849 REASON_CANCEL_ALL, 850 REASON_ERROR, 851 REASON_PACKAGE_CHANGED, 852 REASON_USER_STOPPED, 853 REASON_PACKAGE_BANNED, 854 REASON_APP_CANCEL, 855 REASON_APP_CANCEL_ALL, 856 REASON_LISTENER_CANCEL, 857 REASON_LISTENER_CANCEL_ALL, 858 REASON_GROUP_SUMMARY_CANCELED, 859 REASON_GROUP_OPTIMIZATION, 860 REASON_PACKAGE_SUSPENDED, 861 REASON_PROFILE_TURNED_OFF, 862 REASON_UNAUTOBUNDLED, 863 REASON_CHANNEL_BANNED, 864 REASON_SNOOZED, 865 REASON_TIMEOUT, 866 }) 867 @Retention(RetentionPolicy.SOURCE) 868 public @interface CancellationReason {} 869 870 static final int REASON_NOT_CANCELED = -1; 871 public static final int REASON_UNKNOWN = 0; 872 873 private static final long INITIALIZATION_FORGIVENESS_WINDOW = TimeUnit.SECONDS.toMillis(5); 874 } 875