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