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