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 android.annotation.Nullable; 22 import android.app.Notification; 23 import android.content.Context; 24 import android.service.notification.NotificationListenerService; 25 import android.service.notification.StatusBarNotification; 26 import android.util.ArrayMap; 27 import android.util.Log; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.internal.statusbar.NotificationVisibility; 31 import com.android.systemui.Dependency; 32 import com.android.systemui.Dumpable; 33 import com.android.systemui.statusbar.NotificationLifetimeExtender; 34 import com.android.systemui.statusbar.NotificationPresenter; 35 import com.android.systemui.statusbar.NotificationRemoteInputManager; 36 import com.android.systemui.statusbar.NotificationRemoveInterceptor; 37 import com.android.systemui.statusbar.NotificationUiAdjustment; 38 import com.android.systemui.statusbar.NotificationUpdateHandler; 39 import com.android.systemui.statusbar.notification.collection.NotificationData; 40 import com.android.systemui.statusbar.notification.collection.NotificationData.KeyguardEnvironment; 41 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 42 import com.android.systemui.statusbar.notification.collection.NotificationRowBinder; 43 import com.android.systemui.statusbar.notification.logging.NotificationLogger; 44 import com.android.systemui.statusbar.notification.row.NotificationContentInflater; 45 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; 46 import com.android.systemui.statusbar.notification.stack.NotificationListContainer; 47 import com.android.systemui.statusbar.policy.HeadsUpManager; 48 import com.android.systemui.util.leak.LeakDetector; 49 50 import java.io.FileDescriptor; 51 import java.io.PrintWriter; 52 import java.util.ArrayList; 53 import java.util.HashMap; 54 import java.util.List; 55 import java.util.Map; 56 57 /** 58 * NotificationEntryManager is responsible for the adding, removing, and updating of notifications. 59 * It also handles tasks such as their inflation and their interaction with other 60 * Notification.*Manager objects. 61 */ 62 public class NotificationEntryManager implements 63 Dumpable, 64 NotificationContentInflater.InflationCallback, 65 NotificationUpdateHandler, 66 VisualStabilityManager.Callback { 67 private static final String TAG = "NotificationEntryMgr"; 68 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 69 70 /** 71 * Used when a notification is removed and it doesn't have a reason that maps to one of the 72 * reasons defined in NotificationListenerService 73 * (e.g. {@link NotificationListenerService.REASON_CANCEL}) 74 */ 75 public static final int UNDEFINED_DISMISS_REASON = 0; 76 77 @VisibleForTesting 78 protected final HashMap<String, NotificationEntry> mPendingNotifications = new HashMap<>(); 79 80 private final Map<NotificationEntry, NotificationLifetimeExtender> mRetainedNotifications = 81 new ArrayMap<>(); 82 83 // Lazily retrieved dependencies 84 private NotificationRemoteInputManager mRemoteInputManager; 85 private NotificationRowBinder mNotificationRowBinder; 86 87 private NotificationPresenter mPresenter; 88 @VisibleForTesting 89 protected NotificationData mNotificationData; 90 91 @VisibleForTesting 92 final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders 93 = new ArrayList<>(); 94 private final List<NotificationEntryListener> mNotificationEntryListeners = new ArrayList<>(); 95 private NotificationRemoveInterceptor mRemoveInterceptor; 96 97 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)98 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 99 pw.println("NotificationEntryManager state:"); 100 pw.print(" mPendingNotifications="); 101 if (mPendingNotifications.size() == 0) { 102 pw.println("null"); 103 } else { 104 for (NotificationEntry entry : mPendingNotifications.values()) { 105 pw.println(entry.notification); 106 } 107 } 108 pw.println(" Lifetime-extended notifications:"); 109 if (mRetainedNotifications.isEmpty()) { 110 pw.println(" None"); 111 } else { 112 for (Map.Entry<NotificationEntry, NotificationLifetimeExtender> entry 113 : mRetainedNotifications.entrySet()) { 114 pw.println(" " + entry.getKey().notification + " retained by " 115 + entry.getValue().getClass().getName()); 116 } 117 } 118 } 119 NotificationEntryManager(Context context)120 public NotificationEntryManager(Context context) { 121 mNotificationData = new NotificationData(); 122 } 123 124 /** Adds a {@link NotificationEntryListener}. */ addNotificationEntryListener(NotificationEntryListener listener)125 public void addNotificationEntryListener(NotificationEntryListener listener) { 126 mNotificationEntryListeners.add(listener); 127 } 128 129 /** Sets the {@link NotificationRemoveInterceptor}. */ setNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor)130 public void setNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) { 131 mRemoveInterceptor = interceptor; 132 } 133 134 /** 135 * Our dependencies can have cyclic references, so some need to be lazy 136 */ getRemoteInputManager()137 private NotificationRemoteInputManager getRemoteInputManager() { 138 if (mRemoteInputManager == null) { 139 mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class); 140 } 141 return mRemoteInputManager; 142 } 143 setRowBinder(NotificationRowBinder notificationRowBinder)144 public void setRowBinder(NotificationRowBinder notificationRowBinder) { 145 mNotificationRowBinder = notificationRowBinder; 146 } 147 setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer, HeadsUpManager headsUpManager)148 public void setUpWithPresenter(NotificationPresenter presenter, 149 NotificationListContainer listContainer, 150 HeadsUpManager headsUpManager) { 151 mPresenter = presenter; 152 mNotificationData.setHeadsUpManager(headsUpManager); 153 } 154 155 /** Adds multiple {@link NotificationLifetimeExtender}s. */ addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders)156 public void addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders) { 157 for (NotificationLifetimeExtender extender : extenders) { 158 addNotificationLifetimeExtender(extender); 159 } 160 } 161 162 /** Adds a {@link NotificationLifetimeExtender}. */ addNotificationLifetimeExtender(NotificationLifetimeExtender extender)163 public void addNotificationLifetimeExtender(NotificationLifetimeExtender extender) { 164 mNotificationLifetimeExtenders.add(extender); 165 extender.setCallback(key -> removeNotification(key, null, UNDEFINED_DISMISS_REASON)); 166 } 167 getNotificationData()168 public NotificationData getNotificationData() { 169 return mNotificationData; 170 } 171 172 @Override onReorderingAllowed()173 public void onReorderingAllowed() { 174 updateNotifications(); 175 } 176 177 /** 178 * Requests a notification to be removed. 179 * 180 * @param n the notification to remove. 181 * @param reason why it is being removed e.g. {@link NotificationListenerService#REASON_CANCEL}, 182 * or 0 if unknown. 183 */ performRemoveNotification(StatusBarNotification n, int reason)184 public void performRemoveNotification(StatusBarNotification n, int reason) { 185 final NotificationVisibility nv = obtainVisibility(n.getKey()); 186 removeNotificationInternal( 187 n.getKey(), null, nv, false /* forceRemove */, true /* removedByUser */, 188 reason); 189 } 190 obtainVisibility(String key)191 private NotificationVisibility obtainVisibility(String key) { 192 final int rank = mNotificationData.getRank(key); 193 final int count = mNotificationData.getActiveNotifications().size(); 194 NotificationVisibility.NotificationLocation location = 195 NotificationLogger.getNotificationLocation(getNotificationData().get(key)); 196 return NotificationVisibility.obtain(key, rank, count, true, location); 197 } 198 abortExistingInflation(String key)199 private void abortExistingInflation(String key) { 200 if (mPendingNotifications.containsKey(key)) { 201 NotificationEntry entry = mPendingNotifications.get(key); 202 entry.abortTask(); 203 mPendingNotifications.remove(key); 204 } 205 NotificationEntry addedEntry = mNotificationData.get(key); 206 if (addedEntry != null) { 207 addedEntry.abortTask(); 208 } 209 } 210 211 /** 212 * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService 213 * about the failure. 214 * 215 * WARNING: this will call back into us. Don't hold any locks. 216 */ 217 @Override handleInflationException(StatusBarNotification n, Exception e)218 public void handleInflationException(StatusBarNotification n, Exception e) { 219 removeNotificationInternal( 220 n.getKey(), null, null, true /* forceRemove */, false /* removedByUser */, 221 REASON_ERROR); 222 for (NotificationEntryListener listener : mNotificationEntryListeners) { 223 listener.onInflationError(n, e); 224 } 225 } 226 227 @Override onAsyncInflationFinished(NotificationEntry entry, @InflationFlag int inflatedFlags)228 public void onAsyncInflationFinished(NotificationEntry entry, 229 @InflationFlag int inflatedFlags) { 230 mPendingNotifications.remove(entry.key); 231 // If there was an async task started after the removal, we don't want to add it back to 232 // the list, otherwise we might get leaks. 233 if (!entry.isRowRemoved()) { 234 boolean isNew = mNotificationData.get(entry.key) == null; 235 if (isNew) { 236 for (NotificationEntryListener listener : mNotificationEntryListeners) { 237 listener.onEntryInflated(entry, inflatedFlags); 238 } 239 mNotificationData.add(entry); 240 for (NotificationEntryListener listener : mNotificationEntryListeners) { 241 listener.onBeforeNotificationAdded(entry); 242 } 243 updateNotifications(); 244 for (NotificationEntryListener listener : mNotificationEntryListeners) { 245 listener.onNotificationAdded(entry); 246 } 247 } else { 248 for (NotificationEntryListener listener : mNotificationEntryListeners) { 249 listener.onEntryReinflated(entry); 250 } 251 } 252 } 253 } 254 255 @Override removeNotification(String key, NotificationListenerService.RankingMap ranking, int reason)256 public void removeNotification(String key, NotificationListenerService.RankingMap ranking, 257 int reason) { 258 removeNotificationInternal(key, ranking, obtainVisibility(key), false /* forceRemove */, 259 false /* removedByUser */, reason); 260 } 261 removeNotificationInternal( String key, @Nullable NotificationListenerService.RankingMap ranking, @Nullable NotificationVisibility visibility, boolean forceRemove, boolean removedByUser, int reason)262 private void removeNotificationInternal( 263 String key, 264 @Nullable NotificationListenerService.RankingMap ranking, 265 @Nullable NotificationVisibility visibility, 266 boolean forceRemove, 267 boolean removedByUser, 268 int reason) { 269 270 if (mRemoveInterceptor != null 271 && mRemoveInterceptor.onNotificationRemoveRequested(key, reason)) { 272 // Remove intercepted; skip 273 return; 274 } 275 276 final NotificationEntry entry = mNotificationData.get(key); 277 boolean lifetimeExtended = false; 278 279 // Notification was canceled before it got inflated 280 if (entry == null) { 281 NotificationEntry pendingEntry = mPendingNotifications.get(key); 282 if (pendingEntry != null) { 283 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) { 284 if (extender.shouldExtendLifetimeForPendingNotification(pendingEntry)) { 285 extendLifetime(pendingEntry, extender); 286 lifetimeExtended = true; 287 } 288 } 289 } 290 } 291 292 if (!lifetimeExtended) { 293 abortExistingInflation(key); 294 } 295 296 if (entry != null) { 297 // If a manager needs to keep the notification around for whatever reason, we 298 // keep the notification 299 boolean entryDismissed = entry.isRowDismissed(); 300 if (!forceRemove && !entryDismissed) { 301 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) { 302 if (extender.shouldExtendLifetime(entry)) { 303 extendLifetime(entry, extender); 304 lifetimeExtended = true; 305 break; 306 } 307 } 308 } 309 310 if (!lifetimeExtended) { 311 // At this point, we are guaranteed the notification will be removed 312 313 // Ensure any managers keeping the lifetime extended stop managing the entry 314 cancelLifetimeExtension(entry); 315 316 if (entry.rowExists()) { 317 entry.removeRow(); 318 } 319 320 // Let's remove the children if this was a summary 321 handleGroupSummaryRemoved(key); 322 323 mNotificationData.remove(key, ranking); 324 updateNotifications(); 325 Dependency.get(LeakDetector.class).trackGarbage(entry); 326 removedByUser |= entryDismissed; 327 328 for (NotificationEntryListener listener : mNotificationEntryListeners) { 329 listener.onEntryRemoved(entry, visibility, removedByUser); 330 } 331 } 332 } 333 } 334 335 /** 336 * Ensures that the group children are cancelled immediately when the group summary is cancelled 337 * instead of waiting for the notification manager to send all cancels. Otherwise this could 338 * lead to flickers. 339 * 340 * This also ensures that the animation looks nice and only consists of a single disappear 341 * animation instead of multiple. 342 * @param key the key of the notification was removed 343 * 344 */ handleGroupSummaryRemoved(String key)345 private void handleGroupSummaryRemoved(String key) { 346 NotificationEntry entry = mNotificationData.get(key); 347 if (entry != null && entry.rowExists() && entry.isSummaryWithChildren()) { 348 if (entry.notification.getOverrideGroupKey() != null && !entry.isRowDismissed()) { 349 // We don't want to remove children for autobundled notifications as they are not 350 // always cancelled. We only remove them if they were dismissed by the user. 351 return; 352 } 353 List<NotificationEntry> childEntries = entry.getChildren(); 354 if (childEntries == null) { 355 return; 356 } 357 for (int i = 0; i < childEntries.size(); i++) { 358 NotificationEntry childEntry = childEntries.get(i); 359 boolean isForeground = (entry.notification.getNotification().flags 360 & Notification.FLAG_FOREGROUND_SERVICE) != 0; 361 boolean keepForReply = 362 getRemoteInputManager().shouldKeepForRemoteInputHistory(childEntry) 363 || getRemoteInputManager().shouldKeepForSmartReplyHistory(childEntry); 364 if (isForeground || keepForReply) { 365 // the child is a foreground service notification which we can't remove or it's 366 // a child we're keeping around for reply! 367 continue; 368 } 369 childEntry.setKeepInParent(true); 370 // we need to set this state earlier as otherwise we might generate some weird 371 // animations 372 childEntry.removeRow(); 373 } 374 } 375 } 376 addNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap rankingMap)377 private void addNotificationInternal(StatusBarNotification notification, 378 NotificationListenerService.RankingMap rankingMap) throws InflationException { 379 String key = notification.getKey(); 380 if (DEBUG) { 381 Log.d(TAG, "addNotification key=" + key); 382 } 383 384 mNotificationData.updateRanking(rankingMap); 385 NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking(); 386 rankingMap.getRanking(key, ranking); 387 388 NotificationEntry entry = new NotificationEntry(notification, ranking); 389 390 Dependency.get(LeakDetector.class).trackInstance(entry); 391 // Construct the expanded view. 392 requireBinder().inflateViews(entry, () -> performRemoveNotification(notification, 393 REASON_CANCEL)); 394 395 abortExistingInflation(key); 396 397 mPendingNotifications.put(key, entry); 398 for (NotificationEntryListener listener : mNotificationEntryListeners) { 399 listener.onPendingEntryAdded(entry); 400 } 401 } 402 403 @Override addNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)404 public void addNotification(StatusBarNotification notification, 405 NotificationListenerService.RankingMap ranking) { 406 try { 407 addNotificationInternal(notification, ranking); 408 } catch (InflationException e) { 409 handleInflationException(notification, e); 410 } 411 } 412 updateNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)413 private void updateNotificationInternal(StatusBarNotification notification, 414 NotificationListenerService.RankingMap ranking) throws InflationException { 415 if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")"); 416 417 final String key = notification.getKey(); 418 abortExistingInflation(key); 419 NotificationEntry entry = mNotificationData.get(key); 420 if (entry == null) { 421 return; 422 } 423 424 // Notification is updated so it is essentially re-added and thus alive again. Don't need 425 // to keep its lifetime extended. 426 cancelLifetimeExtension(entry); 427 428 mNotificationData.update(entry, ranking, notification); 429 430 for (NotificationEntryListener listener : mNotificationEntryListeners) { 431 listener.onPreEntryUpdated(entry); 432 } 433 434 requireBinder().inflateViews(entry, () -> performRemoveNotification(notification, 435 REASON_CANCEL)); 436 updateNotifications(); 437 438 if (DEBUG) { 439 // Is this for you? 440 boolean isForCurrentUser = Dependency.get(KeyguardEnvironment.class) 441 .isNotificationForCurrentProfiles(notification); 442 Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); 443 } 444 445 for (NotificationEntryListener listener : mNotificationEntryListeners) { 446 listener.onPostEntryUpdated(entry); 447 } 448 } 449 450 @Override updateNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)451 public void updateNotification(StatusBarNotification notification, 452 NotificationListenerService.RankingMap ranking) { 453 try { 454 updateNotificationInternal(notification, ranking); 455 } catch (InflationException e) { 456 handleInflationException(notification, e); 457 } 458 } 459 updateNotifications()460 public void updateNotifications() { 461 mNotificationData.filterAndSort(); 462 if (mPresenter != null) { 463 mPresenter.updateNotificationViews(); 464 } 465 } 466 467 @Override updateNotificationRanking(NotificationListenerService.RankingMap rankingMap)468 public void updateNotificationRanking(NotificationListenerService.RankingMap rankingMap) { 469 List<NotificationEntry> entries = new ArrayList<>(); 470 entries.addAll(mNotificationData.getActiveNotifications()); 471 entries.addAll(mPendingNotifications.values()); 472 473 // Has a copy of the current UI adjustments. 474 ArrayMap<String, NotificationUiAdjustment> oldAdjustments = new ArrayMap<>(); 475 ArrayMap<String, Integer> oldImportances = new ArrayMap<>(); 476 for (NotificationEntry entry : entries) { 477 NotificationUiAdjustment adjustment = 478 NotificationUiAdjustment.extractFromNotificationEntry(entry); 479 oldAdjustments.put(entry.key, adjustment); 480 oldImportances.put(entry.key, entry.importance); 481 } 482 483 // Populate notification entries from the new rankings. 484 mNotificationData.updateRanking(rankingMap); 485 updateRankingOfPendingNotifications(rankingMap); 486 487 // By comparing the old and new UI adjustments, reinflate the view accordingly. 488 for (NotificationEntry entry : entries) { 489 requireBinder().onNotificationRankingUpdated( 490 entry, 491 oldImportances.get(entry.key), 492 oldAdjustments.get(entry.key), 493 NotificationUiAdjustment.extractFromNotificationEntry(entry)); 494 } 495 496 updateNotifications(); 497 498 for (NotificationEntryListener listener : mNotificationEntryListeners) { 499 listener.onNotificationRankingUpdated(rankingMap); 500 } 501 } 502 updateRankingOfPendingNotifications( @ullable NotificationListenerService.RankingMap rankingMap)503 private void updateRankingOfPendingNotifications( 504 @Nullable NotificationListenerService.RankingMap rankingMap) { 505 if (rankingMap == null) { 506 return; 507 } 508 NotificationListenerService.Ranking tmpRanking = new NotificationListenerService.Ranking(); 509 for (NotificationEntry pendingNotification : mPendingNotifications.values()) { 510 rankingMap.getRanking(pendingNotification.key, tmpRanking); 511 pendingNotification.populateFromRanking(tmpRanking); 512 } 513 } 514 515 /** 516 * @return An iterator for all "pending" notifications. Pending notifications are newly-posted 517 * notifications whose views have not yet been inflated. In general, the system pretends like 518 * these don't exist, although there are a couple exceptions. 519 */ getPendingNotificationsIterator()520 public Iterable<NotificationEntry> getPendingNotificationsIterator() { 521 return mPendingNotifications.values(); 522 } 523 extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender)524 private void extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender) { 525 NotificationLifetimeExtender activeExtender = mRetainedNotifications.get(entry); 526 if (activeExtender != null && activeExtender != extender) { 527 activeExtender.setShouldManageLifetime(entry, false); 528 } 529 mRetainedNotifications.put(entry, extender); 530 extender.setShouldManageLifetime(entry, true); 531 } 532 cancelLifetimeExtension(NotificationEntry entry)533 private void cancelLifetimeExtension(NotificationEntry entry) { 534 NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry); 535 if (activeExtender != null) { 536 activeExtender.setShouldManageLifetime(entry, false); 537 } 538 } 539 requireBinder()540 private NotificationRowBinder requireBinder() { 541 if (mNotificationRowBinder == null) { 542 throw new RuntimeException("You must initialize NotificationEntryManager by calling" 543 + "setRowBinder() before using."); 544 } 545 return mNotificationRowBinder; 546 } 547 } 548