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