1 /* 2 * Copyright (C) 2017 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 package com.android.systemui.statusbar.notification; 17 18 import static android.service.notification.NotificationListenerService.REASON_ERROR; 19 20 import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_UNKNOWN; 21 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.app.Notification; 26 import android.os.RemoteException; 27 import android.os.SystemClock; 28 import android.service.notification.NotificationListenerService; 29 import android.service.notification.NotificationListenerService.Ranking; 30 import android.service.notification.NotificationListenerService.RankingMap; 31 import android.service.notification.StatusBarNotification; 32 import android.util.ArrayMap; 33 import android.util.ArraySet; 34 import android.util.Log; 35 36 import com.android.internal.annotations.VisibleForTesting; 37 import com.android.internal.statusbar.IStatusBarService; 38 import com.android.internal.statusbar.NotificationVisibility; 39 import com.android.systemui.Dumpable; 40 import com.android.systemui.dump.DumpManager; 41 import com.android.systemui.flags.FeatureFlags; 42 import com.android.systemui.statusbar.NotificationLifetimeExtender; 43 import com.android.systemui.statusbar.NotificationListener; 44 import com.android.systemui.statusbar.NotificationListener.NotificationHandler; 45 import com.android.systemui.statusbar.NotificationPresenter; 46 import com.android.systemui.statusbar.NotificationRemoteInputManager; 47 import com.android.systemui.statusbar.NotificationRemoveInterceptor; 48 import com.android.systemui.statusbar.NotificationUiAdjustment; 49 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 50 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; 51 import com.android.systemui.statusbar.notification.collection.legacy.LegacyNotificationRanker; 52 import com.android.systemui.statusbar.notification.collection.legacy.LegacyNotificationRankerStub; 53 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; 54 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; 55 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; 56 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; 57 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 58 import com.android.systemui.statusbar.notification.dagger.NotificationsModule; 59 import com.android.systemui.statusbar.notification.logging.NotificationLogger; 60 import com.android.systemui.util.Assert; 61 import com.android.systemui.util.leak.LeakDetector; 62 63 import java.io.FileDescriptor; 64 import java.io.PrintWriter; 65 import java.util.ArrayList; 66 import java.util.Collection; 67 import java.util.Collections; 68 import java.util.HashMap; 69 import java.util.List; 70 import java.util.Map; 71 import java.util.Set; 72 73 import dagger.Lazy; 74 75 /** 76 * NotificationEntryManager is responsible for the adding, removing, and updating of 77 * {@link NotificationEntry}s. It also handles tasks such as their inflation and their interaction 78 * with other Notification.*Manager objects. 79 * 80 * We track notification entries through this lifecycle: 81 * 1. Pending 82 * 2. Active 83 * 3. Sorted / filtered (visible) 84 * 85 * Every entry spends some amount of time in the pending state, while it is being inflated. Once 86 * inflated, an entry moves into the active state, where it _could_ potentially be shown to the 87 * user. After an entry makes its way into the active state, we sort and filter the entire set to 88 * repopulate the visible set. 89 * 90 * There are a few different things that other classes may be interested in, and most of them 91 * involve the current set of notifications. Here's a brief overview of things you may want to know: 92 * @see #getVisibleNotifications() for the visible set 93 * @see #getActiveNotificationUnfiltered(String) to check if a key exists 94 * @see #getPendingNotificationsIterator() for an iterator over the pending notifications 95 * @see #getPendingOrActiveNotif(String) to find a notification exists for that key in any list 96 * @see #getActiveNotificationsForCurrentUser() to see every notification that the current user owns 97 */ 98 public class NotificationEntryManager implements 99 CommonNotifCollection, 100 Dumpable, 101 VisualStabilityManager.Callback { 102 103 private final NotificationEntryManagerLogger mLogger; 104 private final NotificationGroupManagerLegacy mGroupManager; 105 private final FeatureFlags mFeatureFlags; 106 private final Lazy<NotificationRowBinder> mNotificationRowBinderLazy; 107 private final Lazy<NotificationRemoteInputManager> mRemoteInputManagerLazy; 108 private final LeakDetector mLeakDetector; 109 private final ForegroundServiceDismissalFeatureController mFgsFeatureController; 110 private final IStatusBarService mStatusBarService; 111 private final DumpManager mDumpManager; 112 113 private final Set<NotificationEntry> mAllNotifications = new ArraySet<>(); 114 private final Set<NotificationEntry> mReadOnlyAllNotifications = 115 Collections.unmodifiableSet(mAllNotifications); 116 117 /** Pending notifications are ones awaiting inflation */ 118 @VisibleForTesting 119 protected final HashMap<String, NotificationEntry> mPendingNotifications = new HashMap<>(); 120 /** 121 * Active notifications have been inflated / prepared and could become visible, but may get 122 * filtered out if for instance they are not for the current user 123 */ 124 private final ArrayMap<String, NotificationEntry> mActiveNotifications = new ArrayMap<>(); 125 @VisibleForTesting 126 /** This is the list of "active notifications for this user in this context" */ 127 protected final ArrayList<NotificationEntry> mSortedAndFiltered = new ArrayList<>(); 128 private final List<NotificationEntry> mReadOnlyNotifications = 129 Collections.unmodifiableList(mSortedAndFiltered); 130 131 private final Map<NotificationEntry, NotificationLifetimeExtender> mRetainedNotifications = 132 new ArrayMap<>(); 133 134 private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>(); 135 136 private LegacyNotificationRanker mRanker = new LegacyNotificationRankerStub(); 137 private NotificationPresenter mPresenter; 138 private RankingMap mLatestRankingMap; 139 140 @VisibleForTesting 141 final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders 142 = new ArrayList<>(); 143 private final List<NotificationEntryListener> mNotificationEntryListeners = new ArrayList<>(); 144 private final List<NotificationRemoveInterceptor> mRemoveInterceptors = new ArrayList<>(); 145 146 /** 147 * Injected constructor. See {@link NotificationsModule}. 148 */ NotificationEntryManager( NotificationEntryManagerLogger logger, NotificationGroupManagerLegacy groupManager, FeatureFlags featureFlags, Lazy<NotificationRowBinder> notificationRowBinderLazy, Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy, LeakDetector leakDetector, ForegroundServiceDismissalFeatureController fgsFeatureController, IStatusBarService statusBarService, DumpManager dumpManager )149 public NotificationEntryManager( 150 NotificationEntryManagerLogger logger, 151 NotificationGroupManagerLegacy groupManager, 152 FeatureFlags featureFlags, 153 Lazy<NotificationRowBinder> notificationRowBinderLazy, 154 Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy, 155 LeakDetector leakDetector, 156 ForegroundServiceDismissalFeatureController fgsFeatureController, 157 IStatusBarService statusBarService, 158 DumpManager dumpManager 159 ) { 160 mLogger = logger; 161 mGroupManager = groupManager; 162 mFeatureFlags = featureFlags; 163 mNotificationRowBinderLazy = notificationRowBinderLazy; 164 mRemoteInputManagerLazy = notificationRemoteInputManagerLazy; 165 mLeakDetector = leakDetector; 166 mFgsFeatureController = fgsFeatureController; 167 mStatusBarService = statusBarService; 168 mDumpManager = dumpManager; 169 } 170 171 /** Once called, the NEM will start processing notification events from system server. */ initialize( NotificationListener notificationListener, LegacyNotificationRanker ranker)172 public void initialize( 173 NotificationListener notificationListener, 174 LegacyNotificationRanker ranker) { 175 mRanker = ranker; 176 notificationListener.addNotificationHandler(mNotifListener); 177 mDumpManager.registerDumpable(this); 178 } 179 180 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)181 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 182 pw.println("NotificationEntryManager state:"); 183 pw.println(" mAllNotifications="); 184 if (mAllNotifications.size() == 0) { 185 pw.println("null"); 186 } else { 187 int i = 0; 188 for (NotificationEntry entry : mAllNotifications) { 189 dumpEntry(pw, " ", i, entry); 190 i++; 191 } 192 } 193 pw.print(" mPendingNotifications="); 194 if (mPendingNotifications.size() == 0) { 195 pw.println("null"); 196 } else { 197 for (NotificationEntry entry : mPendingNotifications.values()) { 198 pw.println(entry.getSbn()); 199 } 200 } 201 pw.println(" Remove interceptors registered:"); 202 for (NotificationRemoveInterceptor interceptor : mRemoveInterceptors) { 203 pw.println(" " + interceptor.getClass().getSimpleName()); 204 } 205 pw.println(" Lifetime extenders registered:"); 206 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) { 207 pw.println(" " + extender.getClass().getSimpleName()); 208 } 209 pw.println(" Lifetime-extended notifications:"); 210 if (mRetainedNotifications.isEmpty()) { 211 pw.println(" None"); 212 } else { 213 for (Map.Entry<NotificationEntry, NotificationLifetimeExtender> entry 214 : mRetainedNotifications.entrySet()) { 215 pw.println(" " + entry.getKey().getSbn() + " retained by " 216 + entry.getValue().getClass().getName()); 217 } 218 } 219 } 220 221 /** Adds a {@link NotificationEntryListener}. */ addNotificationEntryListener(NotificationEntryListener listener)222 public void addNotificationEntryListener(NotificationEntryListener listener) { 223 mNotificationEntryListeners.add(listener); 224 } 225 226 /** 227 * Removes a {@link NotificationEntryListener} previously registered via 228 * {@link #addNotificationEntryListener(NotificationEntryListener)}. 229 */ removeNotificationEntryListener(NotificationEntryListener listener)230 public void removeNotificationEntryListener(NotificationEntryListener listener) { 231 mNotificationEntryListeners.remove(listener); 232 } 233 234 /** Add a {@link NotificationRemoveInterceptor}. */ addNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor)235 public void addNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) { 236 mRemoveInterceptors.add(interceptor); 237 } 238 239 /** Remove a {@link NotificationRemoveInterceptor} */ removeNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor)240 public void removeNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) { 241 mRemoveInterceptors.remove(interceptor); 242 } 243 setUpWithPresenter(NotificationPresenter presenter)244 public void setUpWithPresenter(NotificationPresenter presenter) { 245 mPresenter = presenter; 246 } 247 248 /** Adds multiple {@link NotificationLifetimeExtender}s. */ addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders)249 public void addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders) { 250 for (NotificationLifetimeExtender extender : extenders) { 251 addNotificationLifetimeExtender(extender); 252 } 253 } 254 255 /** Adds a {@link NotificationLifetimeExtender}. */ addNotificationLifetimeExtender(NotificationLifetimeExtender extender)256 public void addNotificationLifetimeExtender(NotificationLifetimeExtender extender) { 257 mNotificationLifetimeExtenders.add(extender); 258 extender.setCallback(key -> removeNotification(key, mLatestRankingMap, 259 UNDEFINED_DISMISS_REASON)); 260 } 261 262 @Override onChangeAllowed()263 public void onChangeAllowed() { 264 updateNotifications("reordering is now allowed"); 265 } 266 267 /** 268 * User requests a notification to be removed. 269 * 270 * @param n the notification to remove. 271 * @param reason why it is being removed e.g. {@link NotificationListenerService#REASON_CANCEL}, 272 * or 0 if unknown. 273 */ performRemoveNotification( StatusBarNotification n, @NonNull DismissedByUserStats stats, int reason )274 public void performRemoveNotification( 275 StatusBarNotification n, 276 @NonNull DismissedByUserStats stats, 277 int reason 278 ) { 279 removeNotificationInternal( 280 n.getKey(), 281 null, 282 stats.notificationVisibility, 283 false /* forceRemove */, 284 stats, 285 reason); 286 } 287 obtainVisibility(String key)288 private NotificationVisibility obtainVisibility(String key) { 289 NotificationEntry e = mActiveNotifications.get(key); 290 final int rank; 291 if (e != null) { 292 rank = e.getRanking().getRank(); 293 } else { 294 rank = 0; 295 } 296 297 final int count = mActiveNotifications.size(); 298 NotificationVisibility.NotificationLocation location = 299 NotificationLogger.getNotificationLocation(getActiveNotificationUnfiltered(key)); 300 return NotificationVisibility.obtain(key, rank, count, true, location); 301 } 302 abortExistingInflation(String key, String reason)303 private void abortExistingInflation(String key, String reason) { 304 if (mPendingNotifications.containsKey(key)) { 305 NotificationEntry entry = mPendingNotifications.get(key); 306 entry.abortTask(); 307 mPendingNotifications.remove(key); 308 mLogger.logInflationAborted(key, "pending", reason); 309 } 310 NotificationEntry addedEntry = getActiveNotificationUnfiltered(key); 311 if (addedEntry != null) { 312 addedEntry.abortTask(); 313 mLogger.logInflationAborted(key, "active", reason); 314 } 315 } 316 317 /** 318 * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService 319 * about the failure. 320 * 321 * WARNING: this will call back into us. Don't hold any locks. 322 */ handleInflationException(StatusBarNotification n, Exception e)323 private void handleInflationException(StatusBarNotification n, Exception e) { 324 removeNotificationInternal( 325 n.getKey(), 326 null, 327 null, 328 true /* forceRemove */, 329 null /* dismissedByUserStats */, 330 REASON_ERROR); 331 for (NotificationEntryListener listener : mNotificationEntryListeners) { 332 listener.onInflationError(n, e); 333 } 334 } 335 336 private final InflationCallback mInflationCallback = new InflationCallback() { 337 @Override 338 public void handleInflationException(NotificationEntry entry, Exception e) { 339 NotificationEntryManager.this.handleInflationException(entry.getSbn(), e); 340 } 341 342 @Override 343 public void onAsyncInflationFinished(NotificationEntry entry) { 344 mPendingNotifications.remove(entry.getKey()); 345 // If there was an async task started after the removal, we don't want to add it back to 346 // the list, otherwise we might get leaks. 347 if (!entry.isRowRemoved()) { 348 boolean isNew = getActiveNotificationUnfiltered(entry.getKey()) == null; 349 mLogger.logNotifInflated(entry.getKey(), isNew); 350 if (isNew) { 351 for (NotificationEntryListener listener : mNotificationEntryListeners) { 352 listener.onEntryInflated(entry); 353 } 354 addActiveNotification(entry); 355 updateNotifications("onAsyncInflationFinished"); 356 for (NotificationEntryListener listener : mNotificationEntryListeners) { 357 listener.onNotificationAdded(entry); 358 } 359 } else { 360 for (NotificationEntryListener listener : mNotificationEntryListeners) { 361 listener.onEntryReinflated(entry); 362 } 363 } 364 } 365 } 366 }; 367 368 private final NotificationHandler mNotifListener = new NotificationHandler() { 369 @Override 370 public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 371 final boolean isUpdateToInflatedNotif = mActiveNotifications.containsKey(sbn.getKey()); 372 if (isUpdateToInflatedNotif) { 373 updateNotification(sbn, rankingMap); 374 } else { 375 addNotification(sbn, rankingMap); 376 } 377 } 378 379 @Override 380 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { 381 removeNotification(sbn.getKey(), rankingMap, UNDEFINED_DISMISS_REASON); 382 } 383 384 @Override 385 public void onNotificationRemoved( 386 StatusBarNotification sbn, 387 RankingMap rankingMap, 388 int reason) { 389 removeNotification(sbn.getKey(), rankingMap, reason); 390 } 391 392 @Override 393 public void onNotificationRankingUpdate(RankingMap rankingMap) { 394 updateNotificationRanking(rankingMap); 395 } 396 397 @Override 398 public void onNotificationsInitialized() { 399 } 400 }; 401 402 /** 403 * Equivalent to the old NotificationData#add 404 * @param entry - an entry which is prepared for display 405 */ addActiveNotification(NotificationEntry entry)406 private void addActiveNotification(NotificationEntry entry) { 407 Assert.isMainThread(); 408 409 mActiveNotifications.put(entry.getKey(), entry); 410 mGroupManager.onEntryAdded(entry); 411 updateRankingAndSort(mRanker.getRankingMap(), "addEntryInternalInternal"); 412 } 413 414 /** 415 * Available so that tests can directly manipulate the list of active notifications easily 416 * 417 * @param entry the entry to add directly to the visible notification map 418 */ 419 @VisibleForTesting addActiveNotificationForTest(NotificationEntry entry)420 public void addActiveNotificationForTest(NotificationEntry entry) { 421 mActiveNotifications.put(entry.getKey(), entry); 422 mGroupManager.onEntryAdded(entry); 423 424 reapplyFilterAndSort("addVisibleNotification"); 425 } 426 427 @VisibleForTesting removeNotification(String key, RankingMap ranking, int reason)428 protected void removeNotification(String key, RankingMap ranking, int reason) { 429 removeNotificationInternal( 430 key, 431 ranking, 432 obtainVisibility(key), 433 false /* forceRemove */, 434 null /* dismissedByUserStats */, 435 reason); 436 } 437 438 /** 439 * Internally remove a notification because system server has reported the notification 440 * should be removed OR the user has manually dismissed the notification 441 * @param dismissedByUserStats non-null if the user manually dismissed the notification 442 */ removeNotificationInternal( String key, @Nullable RankingMap ranking, @Nullable NotificationVisibility visibility, boolean forceRemove, DismissedByUserStats dismissedByUserStats, int reason)443 private void removeNotificationInternal( 444 String key, 445 @Nullable RankingMap ranking, 446 @Nullable NotificationVisibility visibility, 447 boolean forceRemove, 448 DismissedByUserStats dismissedByUserStats, 449 int reason) { 450 451 final NotificationEntry entry = getActiveNotificationUnfiltered(key); 452 453 for (NotificationRemoveInterceptor interceptor : mRemoveInterceptors) { 454 if (interceptor.onNotificationRemoveRequested(key, entry, reason)) { 455 // Remove intercepted; log and skip 456 mLogger.logRemovalIntercepted(key); 457 return; 458 } 459 } 460 461 boolean lifetimeExtended = false; 462 463 // Notification was canceled before it got inflated 464 if (entry == null) { 465 NotificationEntry pendingEntry = mPendingNotifications.get(key); 466 if (pendingEntry != null) { 467 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) { 468 if (extender.shouldExtendLifetimeForPendingNotification(pendingEntry)) { 469 extendLifetime(pendingEntry, extender); 470 lifetimeExtended = true; 471 mLogger.logLifetimeExtended(key, extender.getClass().getName(), "pending"); 472 } 473 } 474 if (!lifetimeExtended) { 475 // At this point, we are guaranteed the notification will be removed 476 abortExistingInflation(key, "removeNotification"); 477 // Fix for b/201097913: NotifCollectionListener#onEntryRemoved specifies that 478 // #onEntryRemoved should be called when a notification is cancelled, 479 // regardless of whether the notification was pending or active. 480 // Note that mNotificationEntryListeners are NOT notified of #onEntryRemoved 481 // because for that interface, #onEntryRemoved should only be called for 482 // active entries, NOT pending ones. 483 for (NotifCollectionListener listener : mNotifCollectionListeners) { 484 listener.onEntryRemoved(pendingEntry, REASON_UNKNOWN); 485 } 486 for (NotifCollectionListener listener : mNotifCollectionListeners) { 487 listener.onEntryCleanUp(pendingEntry); 488 } 489 mAllNotifications.remove(pendingEntry); 490 mLeakDetector.trackGarbage(pendingEntry); 491 } 492 } 493 } else { 494 // If a manager needs to keep the notification around for whatever reason, we 495 // keep the notification 496 boolean entryDismissed = entry.isRowDismissed(); 497 if (!forceRemove && !entryDismissed) { 498 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) { 499 if (extender.shouldExtendLifetime(entry)) { 500 mLatestRankingMap = ranking; 501 extendLifetime(entry, extender); 502 lifetimeExtended = true; 503 mLogger.logLifetimeExtended(key, extender.getClass().getName(), "active"); 504 break; 505 } 506 } 507 } 508 509 if (!lifetimeExtended) { 510 // At this point, we are guaranteed the notification will be removed 511 abortExistingInflation(key, "removeNotification"); 512 mAllNotifications.remove(entry); 513 514 // Ensure any managers keeping the lifetime extended stop managing the entry 515 cancelLifetimeExtension(entry); 516 517 if (entry.rowExists()) { 518 entry.removeRow(); 519 } 520 521 // Let's remove the children if this was a summary 522 handleGroupSummaryRemoved(key); 523 removeVisibleNotification(key); 524 updateNotifications("removeNotificationInternal"); 525 final boolean removedByUser = dismissedByUserStats != null; 526 527 mLogger.logNotifRemoved(entry.getKey(), removedByUser); 528 if (removedByUser && visibility != null) { 529 sendNotificationRemovalToServer(entry.getSbn(), dismissedByUserStats); 530 } 531 for (NotificationEntryListener listener : mNotificationEntryListeners) { 532 listener.onEntryRemoved(entry, visibility, removedByUser, reason); 533 } 534 for (NotifCollectionListener listener : mNotifCollectionListeners) { 535 // NEM doesn't have a good knowledge of reasons so defaulting to unknown. 536 listener.onEntryRemoved(entry, REASON_UNKNOWN); 537 } 538 for (NotifCollectionListener listener : mNotifCollectionListeners) { 539 listener.onEntryCleanUp(entry); 540 } 541 mLeakDetector.trackGarbage(entry); 542 } 543 } 544 } 545 sendNotificationRemovalToServer( StatusBarNotification notification, DismissedByUserStats dismissedByUserStats)546 private void sendNotificationRemovalToServer( 547 StatusBarNotification notification, 548 DismissedByUserStats dismissedByUserStats) { 549 try { 550 mStatusBarService.onNotificationClear( 551 notification.getPackageName(), 552 notification.getUser().getIdentifier(), 553 notification.getKey(), 554 dismissedByUserStats.dismissalSurface, 555 dismissedByUserStats.dismissalSentiment, 556 dismissedByUserStats.notificationVisibility); 557 } catch (RemoteException ex) { 558 // system process is dead if we're here. 559 } 560 } 561 562 /** 563 * Ensures that the group children are cancelled immediately when the group summary is cancelled 564 * instead of waiting for the notification manager to send all cancels. Otherwise this could 565 * lead to flickers. 566 * 567 * This also ensures that the animation looks nice and only consists of a single disappear 568 * animation instead of multiple. 569 * @param key the key of the notification was removed 570 * 571 */ handleGroupSummaryRemoved(String key)572 private void handleGroupSummaryRemoved(String key) { 573 NotificationEntry entry = getActiveNotificationUnfiltered(key); 574 if (entry != null && entry.rowExists() && entry.isSummaryWithChildren()) { 575 if (entry.getSbn().getOverrideGroupKey() != null && !entry.isRowDismissed()) { 576 // We don't want to remove children for autobundled notifications as they are not 577 // always cancelled. We only remove them if they were dismissed by the user. 578 return; 579 } 580 List<NotificationEntry> childEntries = entry.getAttachedNotifChildren(); 581 if (childEntries == null) { 582 return; 583 } 584 for (int i = 0; i < childEntries.size(); i++) { 585 NotificationEntry childEntry = childEntries.get(i); 586 boolean isForeground = (entry.getSbn().getNotification().flags 587 & Notification.FLAG_FOREGROUND_SERVICE) != 0; 588 boolean keepForReply = 589 mRemoteInputManagerLazy.get().shouldKeepForRemoteInputHistory(childEntry) 590 || mRemoteInputManagerLazy.get().shouldKeepForSmartReplyHistory(childEntry); 591 if (isForeground || keepForReply) { 592 // the child is a foreground service notification which we can't remove or it's 593 // a child we're keeping around for reply! 594 continue; 595 } 596 childEntry.setKeepInParent(true); 597 // we need to set this state earlier as otherwise we might generate some weird 598 // animations 599 childEntry.removeRow(); 600 } 601 } 602 } 603 addNotificationInternal( StatusBarNotification notification, RankingMap rankingMap)604 private void addNotificationInternal( 605 StatusBarNotification notification, 606 RankingMap rankingMap) throws InflationException { 607 String key = notification.getKey(); 608 if (DEBUG) { 609 Log.d(TAG, "addNotification key=" + key); 610 } 611 612 updateRankingAndSort(rankingMap, "addNotificationInternal"); 613 614 Ranking ranking = new Ranking(); 615 rankingMap.getRanking(key, ranking); 616 617 NotificationEntry entry = mPendingNotifications.get(key); 618 if (entry != null) { 619 entry.setSbn(notification); 620 entry.setRanking(ranking); 621 } else { 622 entry = new NotificationEntry( 623 notification, 624 ranking, 625 mFgsFeatureController.isForegroundServiceDismissalEnabled(), 626 SystemClock.uptimeMillis()); 627 mAllNotifications.add(entry); 628 mLeakDetector.trackInstance(entry); 629 630 for (NotifCollectionListener listener : mNotifCollectionListeners) { 631 listener.onEntryInit(entry); 632 } 633 } 634 635 for (NotifCollectionListener listener : mNotifCollectionListeners) { 636 listener.onEntryBind(entry, notification); 637 } 638 639 // Construct the expanded view. 640 if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { 641 mNotificationRowBinderLazy.get().inflateViews(entry, mInflationCallback); 642 } 643 644 mPendingNotifications.put(key, entry); 645 mLogger.logNotifAdded(entry.getKey()); 646 for (NotificationEntryListener listener : mNotificationEntryListeners) { 647 listener.onPendingEntryAdded(entry); 648 } 649 for (NotifCollectionListener listener : mNotifCollectionListeners) { 650 listener.onEntryAdded(entry); 651 } 652 for (NotifCollectionListener listener : mNotifCollectionListeners) { 653 listener.onRankingApplied(); 654 } 655 } 656 addNotification(StatusBarNotification notification, RankingMap ranking)657 public void addNotification(StatusBarNotification notification, RankingMap ranking) { 658 try { 659 addNotificationInternal(notification, ranking); 660 } catch (InflationException e) { 661 handleInflationException(notification, e); 662 } 663 } 664 updateNotificationInternal(StatusBarNotification notification, RankingMap ranking)665 private void updateNotificationInternal(StatusBarNotification notification, 666 RankingMap ranking) throws InflationException { 667 if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")"); 668 669 final String key = notification.getKey(); 670 abortExistingInflation(key, "updateNotification"); 671 final NotificationEntry entry = getActiveNotificationUnfiltered(key); 672 if (entry == null) { 673 return; 674 } 675 676 // Notification is updated so it is essentially re-added and thus alive again. Don't need 677 // to keep its lifetime extended. 678 cancelLifetimeExtension(entry); 679 680 updateRankingAndSort(ranking, "updateNotificationInternal"); 681 StatusBarNotification oldSbn = entry.getSbn(); 682 entry.setSbn(notification); 683 for (NotifCollectionListener listener : mNotifCollectionListeners) { 684 listener.onEntryBind(entry, notification); 685 } 686 mGroupManager.onEntryUpdated(entry, oldSbn); 687 688 mLogger.logNotifUpdated(entry.getKey()); 689 for (NotificationEntryListener listener : mNotificationEntryListeners) { 690 listener.onPreEntryUpdated(entry); 691 } 692 final boolean fromSystem = ranking != null; 693 for (NotifCollectionListener listener : mNotifCollectionListeners) { 694 listener.onEntryUpdated(entry, fromSystem); 695 } 696 697 if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { 698 mNotificationRowBinderLazy.get().inflateViews(entry, mInflationCallback); 699 } 700 701 updateNotifications("updateNotificationInternal"); 702 703 for (NotificationEntryListener listener : mNotificationEntryListeners) { 704 listener.onPostEntryUpdated(entry); 705 } 706 for (NotifCollectionListener listener : mNotifCollectionListeners) { 707 listener.onRankingApplied(); 708 } 709 } 710 updateNotification(StatusBarNotification notification, RankingMap ranking)711 public void updateNotification(StatusBarNotification notification, RankingMap ranking) { 712 try { 713 updateNotificationInternal(notification, ranking); 714 } catch (InflationException e) { 715 handleInflationException(notification, e); 716 } 717 } 718 719 /** 720 * Update the notifications 721 * @param reason why the notifications are updating 722 */ updateNotifications(String reason)723 public void updateNotifications(String reason) { 724 reapplyFilterAndSort(reason); 725 if (mPresenter != null && !mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { 726 mPresenter.updateNotificationViews(reason); 727 } 728 } 729 updateNotificationRanking(RankingMap rankingMap)730 public void updateNotificationRanking(RankingMap rankingMap) { 731 List<NotificationEntry> entries = new ArrayList<>(); 732 entries.addAll(getVisibleNotifications()); 733 entries.addAll(mPendingNotifications.values()); 734 735 // Has a copy of the current UI adjustments. 736 ArrayMap<String, NotificationUiAdjustment> oldAdjustments = new ArrayMap<>(); 737 ArrayMap<String, Integer> oldImportances = new ArrayMap<>(); 738 for (NotificationEntry entry : entries) { 739 NotificationUiAdjustment adjustment = 740 NotificationUiAdjustment.extractFromNotificationEntry(entry); 741 oldAdjustments.put(entry.getKey(), adjustment); 742 oldImportances.put(entry.getKey(), entry.getImportance()); 743 } 744 745 // Populate notification entries from the new rankings. 746 updateRankingAndSort(rankingMap, "updateNotificationRanking"); 747 updateRankingOfPendingNotifications(rankingMap); 748 749 // By comparing the old and new UI adjustments, reinflate the view accordingly. 750 for (NotificationEntry entry : entries) { 751 mNotificationRowBinderLazy.get() 752 .onNotificationRankingUpdated( 753 entry, 754 oldImportances.get(entry.getKey()), 755 oldAdjustments.get(entry.getKey()), 756 NotificationUiAdjustment.extractFromNotificationEntry(entry), 757 mInflationCallback); 758 } 759 760 updateNotifications("updateNotificationRanking"); 761 762 for (NotificationEntryListener listener : mNotificationEntryListeners) { 763 listener.onNotificationRankingUpdated(rankingMap); 764 } 765 for (NotifCollectionListener listener : mNotifCollectionListeners) { 766 listener.onRankingUpdate(rankingMap); 767 } 768 for (NotifCollectionListener listener : mNotifCollectionListeners) { 769 listener.onRankingApplied(); 770 } 771 } 772 updateRankingOfPendingNotifications(@ullable RankingMap rankingMap)773 private void updateRankingOfPendingNotifications(@Nullable RankingMap rankingMap) { 774 if (rankingMap == null) { 775 return; 776 } 777 for (NotificationEntry pendingNotification : mPendingNotifications.values()) { 778 Ranking ranking = new Ranking(); 779 if (rankingMap.getRanking(pendingNotification.getKey(), ranking)) { 780 pendingNotification.setRanking(ranking); 781 } 782 } 783 } 784 785 /** 786 * @return An iterator for all "pending" notifications. Pending notifications are newly-posted 787 * notifications whose views have not yet been inflated. In general, the system pretends like 788 * these don't exist, although there are a couple exceptions. 789 */ getPendingNotificationsIterator()790 public Iterable<NotificationEntry> getPendingNotificationsIterator() { 791 return mPendingNotifications.values(); 792 } 793 794 /** 795 * Use this method to retrieve a notification entry that has been prepared for presentation. 796 * Note that the notification may be filtered out and never shown to the user. 797 * 798 * @see #getVisibleNotifications() for the currently sorted and filtered list 799 * 800 * @return a {@link NotificationEntry} if it has been prepared, else null 801 */ getActiveNotificationUnfiltered(String key)802 public NotificationEntry getActiveNotificationUnfiltered(String key) { 803 return mActiveNotifications.get(key); 804 } 805 806 /** 807 * Gets the pending or visible notification entry with the given key. Returns null if 808 * notification doesn't exist. 809 */ getPendingOrActiveNotif(String key)810 public NotificationEntry getPendingOrActiveNotif(String key) { 811 if (mPendingNotifications.containsKey(key)) { 812 return mPendingNotifications.get(key); 813 } else { 814 return mActiveNotifications.get(key); 815 } 816 } 817 extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender)818 private void extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender) { 819 NotificationLifetimeExtender activeExtender = mRetainedNotifications.get(entry); 820 if (activeExtender != null && activeExtender != extender) { 821 activeExtender.setShouldManageLifetime(entry, false); 822 } 823 mRetainedNotifications.put(entry, extender); 824 extender.setShouldManageLifetime(entry, true); 825 } 826 cancelLifetimeExtension(NotificationEntry entry)827 private void cancelLifetimeExtension(NotificationEntry entry) { 828 NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry); 829 if (activeExtender != null) { 830 activeExtender.setShouldManageLifetime(entry, false); 831 } 832 } 833 834 /* 835 * ----- 836 * Annexed from NotificationData below: 837 * Some of these methods may be redundant but require some reworking to remove. For now 838 * we'll try to keep the behavior the same and can simplify these interfaces in another pass 839 */ 840 841 /** Internalization of NotificationData#remove */ removeVisibleNotification(String key)842 private void removeVisibleNotification(String key) { 843 // no need to synchronize if we're on the main thread dawg 844 Assert.isMainThread(); 845 846 NotificationEntry removed = mActiveNotifications.remove(key); 847 848 if (removed == null) return; 849 mGroupManager.onEntryRemoved(removed); 850 } 851 852 /** @return list of active notifications filtered for the current user */ getActiveNotificationsForCurrentUser()853 public List<NotificationEntry> getActiveNotificationsForCurrentUser() { 854 Assert.isMainThread(); 855 ArrayList<NotificationEntry> filtered = new ArrayList<>(); 856 857 final int len = mActiveNotifications.size(); 858 for (int i = 0; i < len; i++) { 859 NotificationEntry entry = mActiveNotifications.valueAt(i); 860 if (!mRanker.isNotificationForCurrentProfiles(entry)) { 861 continue; 862 } 863 filtered.add(entry); 864 } 865 866 return filtered; 867 } 868 869 //TODO: Get rid of this in favor of NotificationUpdateHandler#updateNotificationRanking 870 /** 871 * @param rankingMap the {@link RankingMap} to apply to the current notification list 872 * @param reason the reason for calling this method, which will be logged 873 */ updateRanking(RankingMap rankingMap, String reason)874 public void updateRanking(RankingMap rankingMap, String reason) { 875 updateRankingAndSort(rankingMap, reason); 876 for (NotifCollectionListener listener : mNotifCollectionListeners) { 877 listener.onRankingApplied(); 878 } 879 } 880 881 /** Resorts / filters the current notification set with the current RankingMap */ reapplyFilterAndSort(String reason)882 public void reapplyFilterAndSort(String reason) { 883 updateRankingAndSort(mRanker.getRankingMap(), reason); 884 } 885 886 /** Calls to NotificationRankingManager and updates mSortedAndFiltered */ updateRankingAndSort(@onNull RankingMap rankingMap, String reason)887 private void updateRankingAndSort(@NonNull RankingMap rankingMap, String reason) { 888 mSortedAndFiltered.clear(); 889 mSortedAndFiltered.addAll(mRanker.updateRanking( 890 rankingMap, mActiveNotifications.values(), reason)); 891 } 892 893 /** dump the current active notification list. Called from StatusBar */ dump(PrintWriter pw, String indent)894 public void dump(PrintWriter pw, String indent) { 895 pw.println("NotificationEntryManager"); 896 int filteredLen = mSortedAndFiltered.size(); 897 pw.print(indent); 898 pw.println("active notifications: " + filteredLen); 899 int active; 900 for (active = 0; active < filteredLen; active++) { 901 NotificationEntry e = mSortedAndFiltered.get(active); 902 dumpEntry(pw, indent, active, e); 903 } 904 synchronized (mActiveNotifications) { 905 int totalLen = mActiveNotifications.size(); 906 pw.print(indent); 907 pw.println("inactive notifications: " + (totalLen - active)); 908 int inactiveCount = 0; 909 for (int i = 0; i < totalLen; i++) { 910 NotificationEntry entry = mActiveNotifications.valueAt(i); 911 if (!mSortedAndFiltered.contains(entry)) { 912 dumpEntry(pw, indent, inactiveCount, entry); 913 inactiveCount++; 914 } 915 } 916 } 917 } 918 dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e)919 private void dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e) { 920 pw.print(indent); 921 pw.println(" [" + i + "] key=" + e.getKey() + " icon=" + e.getIcons().getStatusBarIcon()); 922 StatusBarNotification n = e.getSbn(); 923 pw.print(indent); 924 pw.println(" pkg=" + n.getPackageName() + " id=" + n.getId() + " importance=" 925 + e.getRanking().getImportance()); 926 pw.print(indent); 927 pw.println(" notification=" + n.getNotification()); 928 } 929 930 /** 931 * This is the answer to the question "what notifications should the user be seeing right now?" 932 * These are sorted and filtered, and directly inform the notification shade what to show 933 * 934 * @return A read-only list of the currently active notifications 935 */ getVisibleNotifications()936 public List<NotificationEntry> getVisibleNotifications() { 937 return mReadOnlyNotifications; 938 } 939 940 /** 941 * Returns a collections containing ALL notifications we know about, including ones that are 942 * hidden or for other users. See {@link CommonNotifCollection#getAllNotifs()}. 943 */ 944 @Override getAllNotifs()945 public Collection<NotificationEntry> getAllNotifs() { 946 return mReadOnlyAllNotifications; 947 } 948 949 /** @return A count of the active notifications */ getActiveNotificationsCount()950 public int getActiveNotificationsCount() { 951 return mReadOnlyNotifications.size(); 952 } 953 954 /** 955 * @return {@code true} if there is at least one notification that should be visible right now 956 */ hasActiveNotifications()957 public boolean hasActiveNotifications() { 958 return mReadOnlyNotifications.size() != 0; 959 } 960 961 @Override addCollectionListener(NotifCollectionListener listener)962 public void addCollectionListener(NotifCollectionListener listener) { 963 mNotifCollectionListeners.add(listener); 964 } 965 966 /* 967 * End annexation 968 * ----- 969 */ 970 971 972 /** 973 * Provides access to keyguard state and user settings dependent data. 974 */ 975 public interface KeyguardEnvironment { 976 /** true if the device is provisioned (should always be true in practice) */ isDeviceProvisioned()977 boolean isDeviceProvisioned(); 978 /** true if the notification is for the current profiles */ isNotificationForCurrentProfiles(StatusBarNotification sbn)979 boolean isNotificationForCurrentProfiles(StatusBarNotification sbn); 980 } 981 982 private static final String TAG = "NotificationEntryMgr"; 983 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 984 985 /** 986 * Used when a notification is removed and it doesn't have a reason that maps to one of the 987 * reasons defined in NotificationListenerService 988 * (e.g. {@link NotificationListenerService#REASON_CANCEL}) 989 */ 990 public static final int UNDEFINED_DISMISS_REASON = 0; 991 } 992