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