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