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