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