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; 17 18 import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT; 19 import static com.android.systemui.statusbar.NotificationRemoteInputManager 20 .FORCE_REMOTE_INPUT_HISTORY; 21 22 import android.app.Notification; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.content.Context; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageManager; 28 import android.database.ContentObserver; 29 import android.os.Build; 30 import android.os.PowerManager; 31 import android.os.RemoteException; 32 import android.os.ServiceManager; 33 import android.os.SystemClock; 34 import android.os.UserHandle; 35 import android.provider.Settings; 36 import android.service.notification.NotificationListenerService; 37 import android.service.notification.NotificationStats; 38 import android.service.notification.StatusBarNotification; 39 import android.text.TextUtils; 40 import android.util.ArraySet; 41 import android.util.EventLog; 42 import android.util.Log; 43 import android.view.View; 44 import android.view.ViewGroup; 45 46 import com.android.internal.annotations.VisibleForTesting; 47 import com.android.internal.logging.MetricsLogger; 48 import com.android.internal.statusbar.IStatusBarService; 49 import com.android.internal.statusbar.NotificationVisibility; 50 import com.android.internal.util.NotificationMessagingUtil; 51 import com.android.systemui.DejankUtils; 52 import com.android.systemui.Dependency; 53 import com.android.systemui.Dumpable; 54 import com.android.systemui.EventLogTags; 55 import com.android.systemui.ForegroundServiceController; 56 import com.android.systemui.R; 57 import com.android.systemui.UiOffloadThread; 58 import com.android.systemui.recents.misc.SystemServicesProxy; 59 import com.android.systemui.statusbar.notification.InflationException; 60 import com.android.systemui.statusbar.notification.NotificationInflater; 61 import com.android.systemui.statusbar.notification.RowInflaterTask; 62 import com.android.systemui.statusbar.notification.VisualStabilityManager; 63 import com.android.systemui.statusbar.phone.NotificationGroupManager; 64 import com.android.systemui.statusbar.phone.StatusBar; 65 import com.android.systemui.statusbar.policy.DeviceProvisionedController; 66 import com.android.systemui.statusbar.policy.HeadsUpManager; 67 import com.android.systemui.util.leak.LeakDetector; 68 69 import java.io.FileDescriptor; 70 import java.io.PrintWriter; 71 import java.util.ArrayList; 72 import java.util.HashMap; 73 import java.util.List; 74 75 /** 76 * NotificationEntryManager is responsible for the adding, removing, and updating of notifications. 77 * It also handles tasks such as their inflation and their interaction with other 78 * Notification.*Manager objects. 79 */ 80 public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback, 81 ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler, 82 VisualStabilityManager.Callback { 83 private static final String TAG = "NotificationEntryMgr"; 84 protected static final boolean DEBUG = false; 85 protected static final boolean ENABLE_HEADS_UP = true; 86 protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up"; 87 88 protected final NotificationMessagingUtil mMessagingUtil; 89 protected final Context mContext; 90 protected final HashMap<String, NotificationData.Entry> mPendingNotifications = new HashMap<>(); 91 protected final NotificationClicker mNotificationClicker = new NotificationClicker(); 92 protected final ArraySet<NotificationData.Entry> mHeadsUpEntriesToRemoveOnSwitch = 93 new ArraySet<>(); 94 95 // Dependencies: 96 protected final NotificationLockscreenUserManager mLockscreenUserManager = 97 Dependency.get(NotificationLockscreenUserManager.class); 98 protected final NotificationGroupManager mGroupManager = 99 Dependency.get(NotificationGroupManager.class); 100 protected final NotificationGutsManager mGutsManager = 101 Dependency.get(NotificationGutsManager.class); 102 protected final NotificationRemoteInputManager mRemoteInputManager = 103 Dependency.get(NotificationRemoteInputManager.class); 104 protected final NotificationMediaManager mMediaManager = 105 Dependency.get(NotificationMediaManager.class); 106 protected final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); 107 protected final DeviceProvisionedController mDeviceProvisionedController = 108 Dependency.get(DeviceProvisionedController.class); 109 protected final VisualStabilityManager mVisualStabilityManager = 110 Dependency.get(VisualStabilityManager.class); 111 protected final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class); 112 protected final ForegroundServiceController mForegroundServiceController = 113 Dependency.get(ForegroundServiceController.class); 114 protected final NotificationListener mNotificationListener = 115 Dependency.get(NotificationListener.class); 116 private final SmartReplyController mSmartReplyController = 117 Dependency.get(SmartReplyController.class); 118 119 protected IStatusBarService mBarService; 120 protected NotificationPresenter mPresenter; 121 protected Callback mCallback; 122 protected PowerManager mPowerManager; 123 protected SystemServicesProxy mSystemServicesProxy; 124 protected NotificationListenerService.RankingMap mLatestRankingMap; 125 protected HeadsUpManager mHeadsUpManager; 126 protected NotificationData mNotificationData; 127 protected ContentObserver mHeadsUpObserver; 128 protected boolean mUseHeadsUp = false; 129 protected boolean mDisableNotificationAlerts; 130 protected NotificationListContainer mListContainer; 131 private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener; 132 /** 133 * Notifications with keys in this set are not actually around anymore. We kept them around 134 * when they were canceled in response to a remote input interaction. This allows us to show 135 * what you replied and allows you to continue typing into it. 136 */ 137 private final ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>(); 138 139 140 private final class NotificationClicker implements View.OnClickListener { 141 142 @Override onClick(final View v)143 public void onClick(final View v) { 144 if (!(v instanceof ExpandableNotificationRow)) { 145 Log.e(TAG, "NotificationClicker called on a view that is not a notification row."); 146 return; 147 } 148 149 mPresenter.wakeUpIfDozing(SystemClock.uptimeMillis(), v); 150 151 final ExpandableNotificationRow row = (ExpandableNotificationRow) v; 152 final StatusBarNotification sbn = row.getStatusBarNotification(); 153 if (sbn == null) { 154 Log.e(TAG, "NotificationClicker called on an unclickable notification,"); 155 return; 156 } 157 158 // Check if the notification is displaying the menu, if so slide notification back 159 if (row.getProvider() != null && row.getProvider().isMenuVisible()) { 160 row.animateTranslateNotification(0); 161 return; 162 } 163 164 // Mark notification for one frame. 165 row.setJustClicked(true); 166 DejankUtils.postAfterTraversal(() -> row.setJustClicked(false)); 167 168 mCallback.onNotificationClicked(sbn, row); 169 } 170 register(ExpandableNotificationRow row, StatusBarNotification sbn)171 public void register(ExpandableNotificationRow row, StatusBarNotification sbn) { 172 Notification notification = sbn.getNotification(); 173 if (notification.contentIntent != null || notification.fullScreenIntent != null) { 174 row.setOnClickListener(this); 175 } else { 176 row.setOnClickListener(null); 177 } 178 } 179 } 180 181 private final DeviceProvisionedController.DeviceProvisionedListener 182 mDeviceProvisionedListener = 183 new DeviceProvisionedController.DeviceProvisionedListener() { 184 @Override 185 public void onDeviceProvisionedChanged() { 186 updateNotifications(); 187 } 188 }; 189 getLatestRankingMap()190 public NotificationListenerService.RankingMap getLatestRankingMap() { 191 return mLatestRankingMap; 192 } 193 setLatestRankingMap(NotificationListenerService.RankingMap latestRankingMap)194 public void setLatestRankingMap(NotificationListenerService.RankingMap latestRankingMap) { 195 mLatestRankingMap = latestRankingMap; 196 } 197 setDisableNotificationAlerts(boolean disableNotificationAlerts)198 public void setDisableNotificationAlerts(boolean disableNotificationAlerts) { 199 mDisableNotificationAlerts = disableNotificationAlerts; 200 mHeadsUpObserver.onChange(true); 201 } 202 destroy()203 public void destroy() { 204 mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener); 205 } 206 onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp)207 public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) { 208 if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) { 209 removeNotification(entry.key, getLatestRankingMap()); 210 mHeadsUpEntriesToRemoveOnSwitch.remove(entry); 211 if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) { 212 setLatestRankingMap(null); 213 } 214 } else { 215 updateNotificationRanking(null); 216 } 217 } 218 219 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)220 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 221 pw.println("NotificationEntryManager state:"); 222 pw.print(" mPendingNotifications="); 223 if (mPendingNotifications.size() == 0) { 224 pw.println("null"); 225 } else { 226 for (NotificationData.Entry entry : mPendingNotifications.values()) { 227 pw.println(entry.notification); 228 } 229 } 230 pw.print(" mUseHeadsUp="); 231 pw.println(mUseHeadsUp); 232 pw.print(" mKeysKeptForRemoteInput: "); 233 pw.println(mKeysKeptForRemoteInput); 234 } 235 NotificationEntryManager(Context context)236 public NotificationEntryManager(Context context) { 237 mContext = context; 238 mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 239 mBarService = IStatusBarService.Stub.asInterface( 240 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 241 mMessagingUtil = new NotificationMessagingUtil(context); 242 mSystemServicesProxy = SystemServicesProxy.getInstance(mContext); 243 mGroupManager.setPendingEntries(mPendingNotifications); 244 } 245 setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer, Callback callback, HeadsUpManager headsUpManager)246 public void setUpWithPresenter(NotificationPresenter presenter, 247 NotificationListContainer listContainer, Callback callback, 248 HeadsUpManager headsUpManager) { 249 mPresenter = presenter; 250 mCallback = callback; 251 mNotificationData = new NotificationData(presenter); 252 mHeadsUpManager = headsUpManager; 253 mNotificationData.setHeadsUpManager(mHeadsUpManager); 254 mListContainer = listContainer; 255 256 mHeadsUpObserver = new ContentObserver(mPresenter.getHandler()) { 257 @Override 258 public void onChange(boolean selfChange) { 259 boolean wasUsing = mUseHeadsUp; 260 mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts 261 && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt( 262 mContext.getContentResolver(), 263 Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, 264 Settings.Global.HEADS_UP_OFF); 265 Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled")); 266 if (wasUsing != mUseHeadsUp) { 267 if (!mUseHeadsUp) { 268 Log.d(TAG, 269 "dismissing any existing heads up notification on disable event"); 270 mHeadsUpManager.releaseAllImmediately(); 271 } 272 } 273 } 274 }; 275 276 if (ENABLE_HEADS_UP) { 277 mContext.getContentResolver().registerContentObserver( 278 Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), 279 true, 280 mHeadsUpObserver); 281 mContext.getContentResolver().registerContentObserver( 282 Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true, 283 mHeadsUpObserver); 284 } 285 286 mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); 287 288 mHeadsUpObserver.onChange(true); // set up 289 mOnAppOpsClickListener = mGutsManager::openGuts; 290 } 291 getNotificationData()292 public NotificationData getNotificationData() { 293 return mNotificationData; 294 } 295 getNotificationLongClicker()296 public ExpandableNotificationRow.LongPressListener getNotificationLongClicker() { 297 return mGutsManager::openGuts; 298 } 299 300 @Override logNotificationExpansion(String key, boolean userAction, boolean expanded)301 public void logNotificationExpansion(String key, boolean userAction, boolean expanded) { 302 mUiOffloadThread.submit(() -> { 303 try { 304 mBarService.onNotificationExpansionChanged(key, userAction, expanded); 305 } catch (RemoteException e) { 306 // Ignore. 307 } 308 }); 309 } 310 311 @Override onReorderingAllowed()312 public void onReorderingAllowed() { 313 updateNotifications(); 314 } 315 shouldSuppressFullScreenIntent(NotificationData.Entry entry)316 private boolean shouldSuppressFullScreenIntent(NotificationData.Entry entry) { 317 if (mPresenter.isDeviceInVrMode()) { 318 return true; 319 } 320 321 return mNotificationData.shouldSuppressFullScreenIntent(entry); 322 } 323 inflateViews(NotificationData.Entry entry, ViewGroup parent)324 private void inflateViews(NotificationData.Entry entry, ViewGroup parent) { 325 PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, 326 entry.notification.getUser().getIdentifier()); 327 328 final StatusBarNotification sbn = entry.notification; 329 if (entry.row != null) { 330 entry.reset(); 331 updateNotification(entry, pmUser, sbn, entry.row); 332 } else { 333 new RowInflaterTask().inflate(mContext, parent, entry, 334 row -> { 335 bindRow(entry, pmUser, sbn, row); 336 updateNotification(entry, pmUser, sbn, row); 337 }); 338 } 339 } 340 bindRow(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row)341 private void bindRow(NotificationData.Entry entry, PackageManager pmUser, 342 StatusBarNotification sbn, ExpandableNotificationRow row) { 343 row.setExpansionLogger(this, entry.notification.getKey()); 344 row.setGroupManager(mGroupManager); 345 row.setHeadsUpManager(mHeadsUpManager); 346 row.setOnExpandClickListener(mPresenter); 347 row.setInflationCallback(this); 348 row.setLongPressListener(getNotificationLongClicker()); 349 mListContainer.bindRow(row); 350 mRemoteInputManager.bindRow(row); 351 352 // Get the app name. 353 // Note that Notification.Builder#bindHeaderAppName has similar logic 354 // but since this field is used in the guts, it must be accurate. 355 // Therefore we will only show the application label, or, failing that, the 356 // package name. No substitutions. 357 final String pkg = sbn.getPackageName(); 358 String appname = pkg; 359 try { 360 final ApplicationInfo info = pmUser.getApplicationInfo(pkg, 361 PackageManager.MATCH_UNINSTALLED_PACKAGES 362 | PackageManager.MATCH_DISABLED_COMPONENTS); 363 if (info != null) { 364 appname = String.valueOf(pmUser.getApplicationLabel(info)); 365 } 366 } catch (PackageManager.NameNotFoundException e) { 367 // Do nothing 368 } 369 row.setAppName(appname); 370 row.setOnDismissRunnable(() -> 371 performRemoveNotification(row.getStatusBarNotification())); 372 row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 373 if (ENABLE_REMOTE_INPUT) { 374 row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 375 } 376 377 row.setAppOpsOnClickListener(mOnAppOpsClickListener); 378 379 mCallback.onBindRow(entry, pmUser, sbn, row); 380 } 381 performRemoveNotification(StatusBarNotification n)382 public void performRemoveNotification(StatusBarNotification n) { 383 final int rank = mNotificationData.getRank(n.getKey()); 384 final int count = mNotificationData.getActiveNotifications().size(); 385 final NotificationVisibility nv = NotificationVisibility.obtain(n.getKey(), rank, count, 386 true); 387 NotificationData.Entry entry = mNotificationData.get(n.getKey()); 388 389 if (FORCE_REMOTE_INPUT_HISTORY 390 && mKeysKeptForRemoteInput.contains(n.getKey())) { 391 mKeysKeptForRemoteInput.remove(n.getKey()); 392 } 393 394 mRemoteInputManager.onPerformRemoveNotification(n, entry); 395 final String pkg = n.getPackageName(); 396 final String tag = n.getTag(); 397 final int id = n.getId(); 398 final int userId = n.getUserId(); 399 try { 400 int dismissalSurface = NotificationStats.DISMISSAL_SHADE; 401 if (isHeadsUp(n.getKey())) { 402 dismissalSurface = NotificationStats.DISMISSAL_PEEK; 403 } else if (mListContainer.hasPulsingNotifications()) { 404 dismissalSurface = NotificationStats.DISMISSAL_AOD; 405 } 406 mBarService.onNotificationClear(pkg, tag, id, userId, n.getKey(), dismissalSurface, nv); 407 removeNotification(n.getKey(), null); 408 409 } catch (RemoteException ex) { 410 // system process is dead if we're here. 411 } 412 413 mCallback.onPerformRemoveNotification(n); 414 } 415 416 /** 417 * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService 418 * about the failure. 419 * 420 * WARNING: this will call back into us. Don't hold any locks. 421 */ handleNotificationError(StatusBarNotification n, String message)422 void handleNotificationError(StatusBarNotification n, String message) { 423 removeNotification(n.getKey(), null); 424 try { 425 mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(), 426 n.getInitialPid(), message, n.getUserId()); 427 } catch (RemoteException ex) { 428 // The end is nigh. 429 } 430 } 431 abortExistingInflation(String key)432 private void abortExistingInflation(String key) { 433 if (mPendingNotifications.containsKey(key)) { 434 NotificationData.Entry entry = mPendingNotifications.get(key); 435 entry.abortTask(); 436 mPendingNotifications.remove(key); 437 } 438 NotificationData.Entry addedEntry = mNotificationData.get(key); 439 if (addedEntry != null) { 440 addedEntry.abortTask(); 441 } 442 } 443 444 @Override handleInflationException(StatusBarNotification notification, Exception e)445 public void handleInflationException(StatusBarNotification notification, Exception e) { 446 handleNotificationError(notification, e.getMessage()); 447 } 448 addEntry(NotificationData.Entry shadeEntry)449 private void addEntry(NotificationData.Entry shadeEntry) { 450 boolean isHeadsUped = shouldPeek(shadeEntry); 451 if (isHeadsUped) { 452 mHeadsUpManager.showNotification(shadeEntry); 453 // Mark as seen immediately 454 setNotificationShown(shadeEntry.notification); 455 } 456 addNotificationViews(shadeEntry); 457 mCallback.onNotificationAdded(shadeEntry); 458 } 459 460 @Override onAsyncInflationFinished(NotificationData.Entry entry)461 public void onAsyncInflationFinished(NotificationData.Entry entry) { 462 mPendingNotifications.remove(entry.key); 463 // If there was an async task started after the removal, we don't want to add it back to 464 // the list, otherwise we might get leaks. 465 boolean isNew = mNotificationData.get(entry.key) == null; 466 if (isNew && !entry.row.isRemoved()) { 467 addEntry(entry); 468 } else if (!isNew && entry.row.hasLowPriorityStateUpdated()) { 469 mVisualStabilityManager.onLowPriorityUpdated(entry); 470 mPresenter.updateNotificationViews(); 471 } 472 entry.row.setLowPriorityStateUpdated(false); 473 } 474 475 @Override removeNotification(String key, NotificationListenerService.RankingMap ranking)476 public void removeNotification(String key, NotificationListenerService.RankingMap ranking) { 477 boolean deferRemoval = false; 478 abortExistingInflation(key); 479 if (mHeadsUpManager.isHeadsUp(key)) { 480 // A cancel() in response to a remote input shouldn't be delayed, as it makes the 481 // sending look longer than it takes. 482 // Also we should not defer the removal if reordering isn't allowed since otherwise 483 // some notifications can't disappear before the panel is closed. 484 boolean ignoreEarliestRemovalTime = mRemoteInputManager.getController().isSpinning(key) 485 && !FORCE_REMOTE_INPUT_HISTORY 486 || !mVisualStabilityManager.isReorderingAllowed(); 487 deferRemoval = !mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime); 488 } 489 mMediaManager.onNotificationRemoved(key); 490 491 NotificationData.Entry entry = mNotificationData.get(key); 492 if (FORCE_REMOTE_INPUT_HISTORY 493 && shouldKeepForRemoteInput(entry) 494 && entry.row != null && !entry.row.isDismissed()) { 495 CharSequence remoteInputText = entry.remoteInputText; 496 if (TextUtils.isEmpty(remoteInputText)) { 497 remoteInputText = entry.remoteInputTextWhenReset; 498 } 499 StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry, 500 remoteInputText, false /* showSpinner */); 501 boolean updated = false; 502 entry.onRemoteInputInserted(); 503 try { 504 updateNotificationInternal(newSbn, null); 505 updated = true; 506 } catch (InflationException e) { 507 deferRemoval = false; 508 } 509 if (updated) { 510 Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key); 511 addKeyKeptForRemoteInput(entry.key); 512 return; 513 } 514 } 515 516 if (FORCE_REMOTE_INPUT_HISTORY 517 && shouldKeepForSmartReply(entry) 518 && entry.row != null && !entry.row.isDismissed()) { 519 // Turn off the spinner and hide buttons when an app cancels the notification. 520 StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry); 521 boolean updated = false; 522 try { 523 updateNotificationInternal(newSbn, null); 524 updated = true; 525 } catch (InflationException e) { 526 // Ignore just don't keep the notification around. 527 } 528 // Treat the reply as longer sending. 529 mSmartReplyController.stopSending(entry); 530 if (updated) { 531 Log.w(TAG, "Keeping notification around after sending smart reply " + entry.key); 532 addKeyKeptForRemoteInput(entry.key); 533 return; 534 } 535 } 536 537 // Actually removing notification so smart reply controller can forget about it. 538 mSmartReplyController.stopSending(entry); 539 540 if (deferRemoval) { 541 mLatestRankingMap = ranking; 542 mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key)); 543 return; 544 } 545 546 if (mRemoteInputManager.onRemoveNotification(entry)) { 547 mLatestRankingMap = ranking; 548 return; 549 } 550 551 if (entry != null && mGutsManager.getExposedGuts() != null 552 && mGutsManager.getExposedGuts() == entry.row.getGuts() 553 && entry.row.getGuts() != null && !entry.row.getGuts().isLeavebehind()) { 554 Log.w(TAG, "Keeping notification because it's showing guts. " + key); 555 mLatestRankingMap = ranking; 556 mGutsManager.setKeyToRemoveOnGutsClosed(key); 557 return; 558 } 559 560 if (entry != null) { 561 mForegroundServiceController.removeNotification(entry.notification); 562 } 563 564 if (entry != null && entry.row != null) { 565 entry.row.setRemoved(); 566 mListContainer.cleanUpViewState(entry.row); 567 } 568 // Let's remove the children if this was a summary 569 handleGroupSummaryRemoved(key); 570 StatusBarNotification old = removeNotificationViews(key, ranking); 571 572 mCallback.onNotificationRemoved(key, old); 573 } 574 rebuildNotificationWithRemoteInput(NotificationData.Entry entry, CharSequence remoteInputText, boolean showSpinner)575 public StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry, 576 CharSequence remoteInputText, boolean showSpinner) { 577 StatusBarNotification sbn = entry.notification; 578 579 Notification.Builder b = Notification.Builder 580 .recoverBuilder(mContext, sbn.getNotification().clone()); 581 if (remoteInputText != null) { 582 CharSequence[] oldHistory = sbn.getNotification().extras 583 .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY); 584 CharSequence[] newHistory; 585 if (oldHistory == null) { 586 newHistory = new CharSequence[1]; 587 } else { 588 newHistory = new CharSequence[oldHistory.length + 1]; 589 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length); 590 } 591 newHistory[0] = String.valueOf(remoteInputText); 592 b.setRemoteInputHistory(newHistory); 593 } 594 b.setShowRemoteInputSpinner(showSpinner); 595 b.setHideSmartReplies(true); 596 597 Notification newNotification = b.build(); 598 599 // Undo any compatibility view inflation 600 newNotification.contentView = sbn.getNotification().contentView; 601 newNotification.bigContentView = sbn.getNotification().bigContentView; 602 newNotification.headsUpContentView = sbn.getNotification().headsUpContentView; 603 604 StatusBarNotification newSbn = new StatusBarNotification(sbn.getPackageName(), 605 sbn.getOpPkg(), 606 sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(), 607 newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime()); 608 return newSbn; 609 } 610 611 @VisibleForTesting rebuildNotificationForCanceledSmartReplies( NotificationData.Entry entry)612 StatusBarNotification rebuildNotificationForCanceledSmartReplies( 613 NotificationData.Entry entry) { 614 return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */, 615 false /* showSpinner */); 616 } 617 shouldKeepForSmartReply(NotificationData.Entry entry)618 private boolean shouldKeepForSmartReply(NotificationData.Entry entry) { 619 return entry != null && mSmartReplyController.isSendingSmartReply(entry.key); 620 } 621 shouldKeepForRemoteInput(NotificationData.Entry entry)622 private boolean shouldKeepForRemoteInput(NotificationData.Entry entry) { 623 if (entry == null) { 624 return false; 625 } 626 if (mRemoteInputManager.getController().isSpinning(entry.key)) { 627 return true; 628 } 629 if (entry.hasJustSentRemoteInput()) { 630 return true; 631 } 632 return false; 633 } 634 removeNotificationViews(String key, NotificationListenerService.RankingMap ranking)635 private StatusBarNotification removeNotificationViews(String key, 636 NotificationListenerService.RankingMap ranking) { 637 NotificationData.Entry entry = mNotificationData.remove(key, ranking); 638 if (entry == null) { 639 Log.w(TAG, "removeNotification for unknown key: " + key); 640 return null; 641 } 642 updateNotifications(); 643 Dependency.get(LeakDetector.class).trackGarbage(entry); 644 return entry.notification; 645 } 646 647 /** 648 * Ensures that the group children are cancelled immediately when the group summary is cancelled 649 * instead of waiting for the notification manager to send all cancels. Otherwise this could 650 * lead to flickers. 651 * 652 * This also ensures that the animation looks nice and only consists of a single disappear 653 * animation instead of multiple. 654 * @param key the key of the notification was removed 655 * 656 */ handleGroupSummaryRemoved(String key)657 private void handleGroupSummaryRemoved(String key) { 658 NotificationData.Entry entry = mNotificationData.get(key); 659 if (entry != null && entry.row != null 660 && entry.row.isSummaryWithChildren()) { 661 if (entry.notification.getOverrideGroupKey() != null && !entry.row.isDismissed()) { 662 // We don't want to remove children for autobundled notifications as they are not 663 // always cancelled. We only remove them if they were dismissed by the user. 664 return; 665 } 666 List<ExpandableNotificationRow> notificationChildren = 667 entry.row.getNotificationChildren(); 668 for (int i = 0; i < notificationChildren.size(); i++) { 669 ExpandableNotificationRow row = notificationChildren.get(i); 670 if ((row.getStatusBarNotification().getNotification().flags 671 & Notification.FLAG_FOREGROUND_SERVICE) != 0) { 672 // the child is a foreground service notification which we can't remove! 673 continue; 674 } 675 row.setKeepInParent(true); 676 // we need to set this state earlier as otherwise we might generate some weird 677 // animations 678 row.setRemoved(); 679 } 680 } 681 } 682 updateNotificationsOnDensityOrFontScaleChanged()683 public void updateNotificationsOnDensityOrFontScaleChanged() { 684 ArrayList<NotificationData.Entry> userNotifications = 685 mNotificationData.getNotificationsForCurrentUser(); 686 for (int i = 0; i < userNotifications.size(); i++) { 687 NotificationData.Entry entry = userNotifications.get(i); 688 boolean exposedGuts = mGutsManager.getExposedGuts() != null 689 && entry.row.getGuts() == mGutsManager.getExposedGuts(); 690 entry.row.onDensityOrFontScaleChanged(); 691 if (exposedGuts) { 692 mGutsManager.onDensityOrFontScaleChanged(entry.row); 693 } 694 } 695 } 696 updateNotification(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row)697 protected void updateNotification(NotificationData.Entry entry, PackageManager pmUser, 698 StatusBarNotification sbn, ExpandableNotificationRow row) { 699 row.setNeedsRedaction(mLockscreenUserManager.needsRedaction(entry)); 700 boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey()); 701 boolean isUpdate = mNotificationData.get(entry.key) != null; 702 boolean wasLowPriority = row.isLowPriority(); 703 row.setIsLowPriority(isLowPriority); 704 row.setLowPriorityStateUpdated(isUpdate && (wasLowPriority != isLowPriority)); 705 // bind the click event to the content area 706 mNotificationClicker.register(row, sbn); 707 708 // Extract target SDK version. 709 try { 710 ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0); 711 entry.targetSdk = info.targetSdkVersion; 712 } catch (PackageManager.NameNotFoundException ex) { 713 Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex); 714 } 715 row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD 716 && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP); 717 entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP); 718 entry.autoRedacted = entry.notification.getNotification().publicVersion == null; 719 720 entry.row = row; 721 entry.row.setOnActivatedListener(mPresenter); 722 723 boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn, 724 mNotificationData.getImportance(sbn.getKey())); 725 boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight 726 && !mPresenter.isPresenterFullyCollapsed(); 727 row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); 728 row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp); 729 row.updateNotification(entry); 730 } 731 732 addNotificationViews(NotificationData.Entry entry)733 protected void addNotificationViews(NotificationData.Entry entry) { 734 if (entry == null) { 735 return; 736 } 737 // Add the expanded view and icon. 738 mNotificationData.add(entry); 739 tagForeground(entry.notification); 740 updateNotifications(); 741 } 742 createNotificationViews(StatusBarNotification sbn)743 protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn) 744 throws InflationException { 745 if (DEBUG) { 746 Log.d(TAG, "createNotificationViews(notification=" + sbn); 747 } 748 NotificationData.Entry entry = new NotificationData.Entry(sbn); 749 Dependency.get(LeakDetector.class).trackInstance(entry); 750 entry.createIcons(mContext, sbn); 751 // Construct the expanded view. 752 inflateViews(entry, mListContainer.getViewParentForNotification(entry)); 753 return entry; 754 } 755 addNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)756 private void addNotificationInternal(StatusBarNotification notification, 757 NotificationListenerService.RankingMap ranking) throws InflationException { 758 String key = notification.getKey(); 759 if (DEBUG) Log.d(TAG, "addNotification key=" + key); 760 761 mNotificationData.updateRanking(ranking); 762 NotificationData.Entry shadeEntry = createNotificationViews(notification); 763 boolean isHeadsUped = shouldPeek(shadeEntry); 764 if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) { 765 if (shouldSuppressFullScreenIntent(shadeEntry)) { 766 if (DEBUG) { 767 Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + key); 768 } 769 } else if (mNotificationData.getImportance(key) 770 < NotificationManager.IMPORTANCE_HIGH) { 771 if (DEBUG) { 772 Log.d(TAG, "No Fullscreen intent: not important enough: " 773 + key); 774 } 775 } else { 776 // Stop screensaver if the notification has a fullscreen intent. 777 // (like an incoming phone call) 778 SystemServicesProxy.getInstance(mContext).awakenDreamsAsync(); 779 780 // not immersive & a fullscreen alert should be shown 781 if (DEBUG) 782 Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); 783 try { 784 EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, 785 key); 786 notification.getNotification().fullScreenIntent.send(); 787 shadeEntry.notifyFullScreenIntentLaunched(); 788 mMetricsLogger.count("note_fullscreen", 1); 789 } catch (PendingIntent.CanceledException e) { 790 } 791 } 792 } 793 abortExistingInflation(key); 794 795 mForegroundServiceController.addNotification(notification, 796 mNotificationData.getImportance(key)); 797 798 mPendingNotifications.put(key, shadeEntry); 799 mGroupManager.onPendingEntryAdded(shadeEntry); 800 } 801 802 @VisibleForTesting tagForeground(StatusBarNotification notification)803 protected void tagForeground(StatusBarNotification notification) { 804 ArraySet<Integer> activeOps = mForegroundServiceController.getAppOps( 805 notification.getUserId(), notification.getPackageName()); 806 if (activeOps != null) { 807 int N = activeOps.size(); 808 for (int i = 0; i < N; i++) { 809 updateNotificationsForAppOp(activeOps.valueAt(i), notification.getUid(), 810 notification.getPackageName(), true); 811 } 812 } 813 } 814 815 @Override addNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)816 public void addNotification(StatusBarNotification notification, 817 NotificationListenerService.RankingMap ranking) { 818 try { 819 addNotificationInternal(notification, ranking); 820 } catch (InflationException e) { 821 handleInflationException(notification, e); 822 } 823 } 824 updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon)825 public void updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon) { 826 String foregroundKey = mForegroundServiceController.getStandardLayoutKey( 827 UserHandle.getUserId(uid), pkg); 828 if (foregroundKey != null) { 829 mNotificationData.updateAppOp(appOp, uid, pkg, foregroundKey, showIcon); 830 updateNotifications(); 831 } 832 } 833 alertAgain(NotificationData.Entry oldEntry, Notification newNotification)834 private boolean alertAgain(NotificationData.Entry oldEntry, Notification newNotification) { 835 return oldEntry == null || !oldEntry.hasInterrupted() 836 || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; 837 } 838 updateNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)839 private void updateNotificationInternal(StatusBarNotification notification, 840 NotificationListenerService.RankingMap ranking) throws InflationException { 841 if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")"); 842 843 final String key = notification.getKey(); 844 abortExistingInflation(key); 845 NotificationData.Entry entry = mNotificationData.get(key); 846 if (entry == null) { 847 return; 848 } 849 mHeadsUpEntriesToRemoveOnSwitch.remove(entry); 850 mRemoteInputManager.onUpdateNotification(entry); 851 mSmartReplyController.stopSending(entry); 852 853 if (key.equals(mGutsManager.getKeyToRemoveOnGutsClosed())) { 854 mGutsManager.setKeyToRemoveOnGutsClosed(null); 855 Log.w(TAG, "Notification that was kept for guts was updated. " + key); 856 } 857 858 Notification n = notification.getNotification(); 859 mNotificationData.updateRanking(ranking); 860 861 final StatusBarNotification oldNotification = entry.notification; 862 entry.notification = notification; 863 mGroupManager.onEntryUpdated(entry, oldNotification); 864 865 entry.updateIcons(mContext, notification); 866 inflateViews(entry, mListContainer.getViewParentForNotification(entry)); 867 868 mForegroundServiceController.updateNotification(notification, 869 mNotificationData.getImportance(key)); 870 871 boolean shouldPeek = shouldPeek(entry, notification); 872 boolean alertAgain = alertAgain(entry, n); 873 874 updateHeadsUp(key, entry, shouldPeek, alertAgain); 875 updateNotifications(); 876 877 if (!notification.isClearable()) { 878 // The user may have performed a dismiss action on the notification, since it's 879 // not clearable we should snap it back. 880 mListContainer.snapViewIfNeeded(entry.row); 881 } 882 883 if (DEBUG) { 884 // Is this for you? 885 boolean isForCurrentUser = mPresenter.isNotificationForCurrentProfiles(notification); 886 Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); 887 } 888 889 mCallback.onNotificationUpdated(notification); 890 } 891 892 @Override updateNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)893 public void updateNotification(StatusBarNotification notification, 894 NotificationListenerService.RankingMap ranking) { 895 try { 896 updateNotificationInternal(notification, ranking); 897 } catch (InflationException e) { 898 handleInflationException(notification, e); 899 } 900 } 901 updateNotifications()902 public void updateNotifications() { 903 mNotificationData.filterAndSort(); 904 905 mPresenter.updateNotificationViews(); 906 } 907 updateNotificationRanking(NotificationListenerService.RankingMap ranking)908 public void updateNotificationRanking(NotificationListenerService.RankingMap ranking) { 909 mNotificationData.updateRanking(ranking); 910 updateNotifications(); 911 } 912 shouldPeek(NotificationData.Entry entry)913 protected boolean shouldPeek(NotificationData.Entry entry) { 914 return shouldPeek(entry, entry.notification); 915 } 916 shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn)917 public boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) { 918 if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) { 919 if (DEBUG) Log.d(TAG, "No peeking: no huns or vr mode"); 920 return false; 921 } 922 923 if (mNotificationData.shouldFilterOut(entry)) { 924 if (DEBUG) Log.d(TAG, "No peeking: filtered notification: " + sbn.getKey()); 925 return false; 926 } 927 928 boolean inUse = mPowerManager.isScreenOn() && !mSystemServicesProxy.isDreaming(); 929 930 if (!inUse && !mPresenter.isDozing()) { 931 if (DEBUG) { 932 Log.d(TAG, "No peeking: not in use: " + sbn.getKey()); 933 } 934 return false; 935 } 936 937 if (!mPresenter.isDozing() && mNotificationData.shouldSuppressPeek(entry)) { 938 if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey()); 939 return false; 940 } 941 942 // Peeking triggers an ambient display pulse, so disable peek is ambient is active 943 if (mPresenter.isDozing() && mNotificationData.shouldSuppressAmbient(entry)) { 944 if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey()); 945 return false; 946 } 947 948 if (entry.hasJustLaunchedFullScreenIntent()) { 949 if (DEBUG) Log.d(TAG, "No peeking: recent fullscreen: " + sbn.getKey()); 950 return false; 951 } 952 953 if (isSnoozedPackage(sbn)) { 954 if (DEBUG) Log.d(TAG, "No peeking: snoozed package: " + sbn.getKey()); 955 return false; 956 } 957 958 // Allow peeking for DEFAULT notifications only if we're on Ambient Display. 959 int importanceLevel = mPresenter.isDozing() ? NotificationManager.IMPORTANCE_DEFAULT 960 : NotificationManager.IMPORTANCE_HIGH; 961 if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) { 962 if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey()); 963 return false; 964 } 965 966 // Don't peek notifications that are suppressed due to group alert behavior 967 if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) { 968 if (DEBUG) Log.d(TAG, "No peeking: suppressed due to group alert behavior"); 969 return false; 970 } 971 972 if (!mCallback.shouldPeek(entry, sbn)) { 973 return false; 974 } 975 976 return true; 977 } 978 setNotificationShown(StatusBarNotification n)979 protected void setNotificationShown(StatusBarNotification n) { 980 setNotificationsShown(new String[]{n.getKey()}); 981 } 982 setNotificationsShown(String[] keys)983 protected void setNotificationsShown(String[] keys) { 984 try { 985 mNotificationListener.setNotificationsShown(keys); 986 } catch (RuntimeException e) { 987 Log.d(TAG, "failed setNotificationsShown: ", e); 988 } 989 } 990 isSnoozedPackage(StatusBarNotification sbn)991 protected boolean isSnoozedPackage(StatusBarNotification sbn) { 992 return mHeadsUpManager.isSnoozed(sbn.getPackageName()); 993 } 994 updateHeadsUp(String key, NotificationData.Entry entry, boolean shouldPeek, boolean alertAgain)995 protected void updateHeadsUp(String key, NotificationData.Entry entry, boolean shouldPeek, 996 boolean alertAgain) { 997 final boolean wasHeadsUp = isHeadsUp(key); 998 if (wasHeadsUp) { 999 if (!shouldPeek) { 1000 // We don't want this to be interrupting anymore, lets remove it 1001 mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */); 1002 } else { 1003 mHeadsUpManager.updateNotification(entry, alertAgain); 1004 } 1005 } else if (shouldPeek && alertAgain) { 1006 // This notification was updated to be a heads-up, show it! 1007 mHeadsUpManager.showNotification(entry); 1008 } 1009 } 1010 isHeadsUp(String key)1011 protected boolean isHeadsUp(String key) { 1012 return mHeadsUpManager.isHeadsUp(key); 1013 } 1014 isNotificationKeptForRemoteInput(String key)1015 public boolean isNotificationKeptForRemoteInput(String key) { 1016 return mKeysKeptForRemoteInput.contains(key); 1017 } 1018 removeKeyKeptForRemoteInput(String key)1019 public void removeKeyKeptForRemoteInput(String key) { 1020 mKeysKeptForRemoteInput.remove(key); 1021 } 1022 addKeyKeptForRemoteInput(String key)1023 public void addKeyKeptForRemoteInput(String key) { 1024 if (FORCE_REMOTE_INPUT_HISTORY) { 1025 mKeysKeptForRemoteInput.add(key); 1026 } 1027 } 1028 1029 /** 1030 * Callback for NotificationEntryManager. 1031 */ 1032 public interface Callback { 1033 1034 /** 1035 * Called when a new entry is created. 1036 * 1037 * @param shadeEntry entry that was created 1038 */ onNotificationAdded(NotificationData.Entry shadeEntry)1039 void onNotificationAdded(NotificationData.Entry shadeEntry); 1040 1041 /** 1042 * Called when a notification was updated. 1043 * 1044 * @param notification notification that was updated 1045 */ onNotificationUpdated(StatusBarNotification notification)1046 void onNotificationUpdated(StatusBarNotification notification); 1047 1048 /** 1049 * Called when a notification was removed. 1050 * 1051 * @param key key of notification that was removed 1052 * @param old StatusBarNotification of the notification before it was removed 1053 */ onNotificationRemoved(String key, StatusBarNotification old)1054 void onNotificationRemoved(String key, StatusBarNotification old); 1055 1056 1057 /** 1058 * Called when a notification is clicked. 1059 * 1060 * @param sbn notification that was clicked 1061 * @param row row for that notification 1062 */ onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row)1063 void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row); 1064 1065 /** 1066 * Called when a new notification and row is created. 1067 * 1068 * @param entry entry for the notification 1069 * @param pmUser package manager for user 1070 * @param sbn notification 1071 * @param row row for the notification 1072 */ onBindRow(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row)1073 void onBindRow(NotificationData.Entry entry, PackageManager pmUser, 1074 StatusBarNotification sbn, ExpandableNotificationRow row); 1075 1076 /** 1077 * Removes a notification immediately. 1078 * 1079 * @param statusBarNotification notification that is being removed 1080 */ onPerformRemoveNotification(StatusBarNotification statusBarNotification)1081 void onPerformRemoveNotification(StatusBarNotification statusBarNotification); 1082 1083 /** 1084 * Returns true if NotificationEntryManager should peek this notification. 1085 * 1086 * @param entry entry of the notification that might be peeked 1087 * @param sbn notification that might be peeked 1088 * @return true if the notification should be peeked 1089 */ shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn)1090 boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn); 1091 } 1092 } 1093