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 17 package com.android.systemui.statusbar; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.os.Handler; 22 import android.os.Trace; 23 import android.os.UserHandle; 24 import android.util.Log; 25 import android.view.View; 26 import android.view.ViewGroup; 27 28 import com.android.systemui.R; 29 import com.android.systemui.dagger.qualifiers.Main; 30 import com.android.systemui.plugins.statusbar.StatusBarStateController; 31 import com.android.systemui.statusbar.dagger.StatusBarModule; 32 import com.android.systemui.statusbar.notification.AssistantFeedbackController; 33 import com.android.systemui.statusbar.notification.DynamicChildBindController; 34 import com.android.systemui.statusbar.notification.DynamicPrivacyController; 35 import com.android.systemui.statusbar.notification.NotificationEntryManager; 36 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 37 import com.android.systemui.statusbar.notification.collection.inflation.LowPriorityInflationHelper; 38 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; 39 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; 40 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 41 import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController; 42 import com.android.systemui.statusbar.notification.stack.NotificationListContainer; 43 import com.android.systemui.statusbar.phone.KeyguardBypassController; 44 import com.android.systemui.util.Assert; 45 import com.android.wm.shell.bubbles.Bubbles; 46 47 import java.util.ArrayList; 48 import java.util.HashMap; 49 import java.util.List; 50 import java.util.Optional; 51 import java.util.Stack; 52 53 /** 54 * NotificationViewHierarchyManager manages updating the view hierarchy of notification views based 55 * on their group structure. For example, if a notification becomes bundled with another, 56 * NotificationViewHierarchyManager will update the view hierarchy to reflect that. It also will 57 * tell NotificationListContainer which notifications to display, and inform it of changes to those 58 * notifications that might affect their display. 59 */ 60 public class NotificationViewHierarchyManager implements DynamicPrivacyController.Listener { 61 private static final String TAG = "NotificationViewHierarchyManager"; 62 63 private final Handler mHandler; 64 65 /** 66 * Re-usable map of top-level notifications to their sorted children if any. 67 * If the top-level notification doesn't have children, its key will still exist in this map 68 * with its value explicitly set to null. 69 */ 70 private final HashMap<NotificationEntry, List<NotificationEntry>> mTmpChildOrderMap = 71 new HashMap<>(); 72 73 // Dependencies: 74 private final DynamicChildBindController mDynamicChildBindController; 75 protected final NotificationLockscreenUserManager mLockscreenUserManager; 76 protected final NotificationGroupManagerLegacy mGroupManager; 77 protected final VisualStabilityManager mVisualStabilityManager; 78 private final SysuiStatusBarStateController mStatusBarStateController; 79 private final NotificationEntryManager mEntryManager; 80 private final LowPriorityInflationHelper mLowPriorityInflationHelper; 81 82 /** 83 * {@code true} if notifications not part of a group should by default be rendered in their 84 * expanded state. If {@code false}, then only the first notification will be expanded if 85 * possible. 86 */ 87 private final boolean mAlwaysExpandNonGroupedNotification; 88 private final Optional<Bubbles> mBubblesOptional; 89 private final DynamicPrivacyController mDynamicPrivacyController; 90 private final KeyguardBypassController mBypassController; 91 private final ForegroundServiceSectionController mFgsSectionController; 92 private AssistantFeedbackController mAssistantFeedbackController; 93 private final Context mContext; 94 95 private NotificationPresenter mPresenter; 96 private NotificationListContainer mListContainer; 97 98 // Used to help track down re-entrant calls to our update methods, which will cause bugs. 99 private boolean mPerformingUpdate; 100 // Hack to get around re-entrant call in onDynamicPrivacyChanged() until we can track down 101 // the problem. 102 private boolean mIsHandleDynamicPrivacyChangeScheduled; 103 104 /** 105 * Injected constructor. See {@link StatusBarModule}. 106 */ NotificationViewHierarchyManager( Context context, @Main Handler mainHandler, NotificationLockscreenUserManager notificationLockscreenUserManager, NotificationGroupManagerLegacy groupManager, VisualStabilityManager visualStabilityManager, StatusBarStateController statusBarStateController, NotificationEntryManager notificationEntryManager, KeyguardBypassController bypassController, Optional<Bubbles> bubblesOptional, DynamicPrivacyController privacyController, ForegroundServiceSectionController fgsSectionController, DynamicChildBindController dynamicChildBindController, LowPriorityInflationHelper lowPriorityInflationHelper, AssistantFeedbackController assistantFeedbackController)107 public NotificationViewHierarchyManager( 108 Context context, 109 @Main Handler mainHandler, 110 NotificationLockscreenUserManager notificationLockscreenUserManager, 111 NotificationGroupManagerLegacy groupManager, 112 VisualStabilityManager visualStabilityManager, 113 StatusBarStateController statusBarStateController, 114 NotificationEntryManager notificationEntryManager, 115 KeyguardBypassController bypassController, 116 Optional<Bubbles> bubblesOptional, 117 DynamicPrivacyController privacyController, 118 ForegroundServiceSectionController fgsSectionController, 119 DynamicChildBindController dynamicChildBindController, 120 LowPriorityInflationHelper lowPriorityInflationHelper, 121 AssistantFeedbackController assistantFeedbackController) { 122 mContext = context; 123 mHandler = mainHandler; 124 mLockscreenUserManager = notificationLockscreenUserManager; 125 mBypassController = bypassController; 126 mGroupManager = groupManager; 127 mVisualStabilityManager = visualStabilityManager; 128 mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController; 129 mEntryManager = notificationEntryManager; 130 mFgsSectionController = fgsSectionController; 131 Resources res = context.getResources(); 132 mAlwaysExpandNonGroupedNotification = 133 res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications); 134 mBubblesOptional = bubblesOptional; 135 mDynamicPrivacyController = privacyController; 136 mDynamicChildBindController = dynamicChildBindController; 137 mLowPriorityInflationHelper = lowPriorityInflationHelper; 138 mAssistantFeedbackController = assistantFeedbackController; 139 } 140 setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer)141 public void setUpWithPresenter(NotificationPresenter presenter, 142 NotificationListContainer listContainer) { 143 mPresenter = presenter; 144 mListContainer = listContainer; 145 mDynamicPrivacyController.addListener(this); 146 } 147 148 /** 149 * Updates the visual representation of the notifications. 150 */ 151 //TODO: Rewrite this to focus on Entries, or some other data object instead of views updateNotificationViews()152 public void updateNotificationViews() { 153 Assert.isMainThread(); 154 beginUpdate(); 155 156 List<NotificationEntry> activeNotifications = mEntryManager.getVisibleNotifications(); 157 ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size()); 158 final int N = activeNotifications.size(); 159 for (int i = 0; i < N; i++) { 160 NotificationEntry ent = activeNotifications.get(i); 161 if (shouldSuppressActiveNotification(ent)) { 162 continue; 163 } 164 165 int userId = ent.getSbn().getUserId(); 166 167 // Display public version of the notification if we need to redact. 168 // TODO: This area uses a lot of calls into NotificationLockscreenUserManager. 169 // We can probably move some of this code there. 170 int currentUserId = mLockscreenUserManager.getCurrentUserId(); 171 boolean devicePublic = mLockscreenUserManager.isLockscreenPublicMode(currentUserId); 172 boolean userPublic = devicePublic 173 || mLockscreenUserManager.isLockscreenPublicMode(userId); 174 if (userPublic && mDynamicPrivacyController.isDynamicallyUnlocked() 175 && (userId == currentUserId || userId == UserHandle.USER_ALL 176 || !mLockscreenUserManager.needsSeparateWorkChallenge(userId))) { 177 userPublic = false; 178 } 179 boolean needsRedaction = mLockscreenUserManager.needsRedaction(ent); 180 boolean sensitive = userPublic && needsRedaction; 181 boolean deviceSensitive = devicePublic 182 && !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic( 183 currentUserId); 184 ent.setSensitive(sensitive, deviceSensitive); 185 ent.getRow().setNeedsRedaction(needsRedaction); 186 mLowPriorityInflationHelper.recheckLowPriorityViewAndInflate(ent, ent.getRow()); 187 boolean isChildInGroup = mGroupManager.isChildInGroup(ent); 188 189 boolean groupChangesAllowed = 190 mVisualStabilityManager.areGroupChangesAllowed() // user isn't looking at notifs 191 || !ent.hasFinishedInitialization(); // notif recently added 192 193 NotificationEntry parent = mGroupManager.getGroupSummary(ent); 194 if (!groupChangesAllowed) { 195 // We don't to change groups while the user is looking at them 196 boolean wasChildInGroup = ent.isChildInGroup(); 197 if (isChildInGroup && !wasChildInGroup) { 198 isChildInGroup = wasChildInGroup; 199 mVisualStabilityManager.addGroupChangesAllowedCallback(mEntryManager, 200 false /* persistent */); 201 } else if (!isChildInGroup && wasChildInGroup) { 202 // We allow grouping changes if the group was collapsed 203 if (mGroupManager.isLogicalGroupExpanded(ent.getSbn())) { 204 isChildInGroup = wasChildInGroup; 205 parent = ent.getRow().getNotificationParent().getEntry(); 206 mVisualStabilityManager.addGroupChangesAllowedCallback(mEntryManager, 207 false /* persistent */); 208 } 209 } 210 } 211 212 if (isChildInGroup) { 213 List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent); 214 if (orderedChildren == null) { 215 orderedChildren = new ArrayList<>(); 216 mTmpChildOrderMap.put(parent, orderedChildren); 217 } 218 orderedChildren.add(ent); 219 } else { 220 // Top-level notif (either a summary or single notification) 221 222 // A child may have already added its summary to mTmpChildOrderMap with a 223 // list of children. This can happen since there's no guarantee summaries are 224 // sorted before its children. 225 if (!mTmpChildOrderMap.containsKey(ent)) { 226 // mTmpChildOrderMap's keyset is used to iterate through all entries, so it's 227 // necessary to add each top-level notif as a key 228 mTmpChildOrderMap.put(ent, null); 229 } 230 toShow.add(ent.getRow()); 231 } 232 233 } 234 235 ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>(); 236 for (int i=0; i< mListContainer.getContainerChildCount(); i++) { 237 View child = mListContainer.getContainerChildAt(i); 238 if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) { 239 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 240 241 // Blocking helper is effectively a detached view. Don't bother removing it from the 242 // layout. 243 if (!row.isBlockingHelperShowing()) { 244 viewsToRemove.add((ExpandableNotificationRow) child); 245 } 246 } 247 } 248 249 for (ExpandableNotificationRow viewToRemove : viewsToRemove) { 250 NotificationEntry entry = viewToRemove.getEntry(); 251 if (mEntryManager.getPendingOrActiveNotif(entry.getKey()) != null 252 && !shouldSuppressActiveNotification(entry)) { 253 // we are only transferring this notification to its parent, don't generate an 254 // animation. If the notification is suppressed, this isn't a transfer. 255 mListContainer.setChildTransferInProgress(true); 256 } 257 if (viewToRemove.isSummaryWithChildren()) { 258 viewToRemove.removeAllChildren(); 259 } 260 mListContainer.removeContainerView(viewToRemove); 261 mListContainer.setChildTransferInProgress(false); 262 } 263 264 removeNotificationChildren(); 265 266 for (int i = 0; i < toShow.size(); i++) { 267 View v = toShow.get(i); 268 if (v.getParent() == null) { 269 mVisualStabilityManager.notifyViewAddition(v); 270 mListContainer.addContainerView(v); 271 } else if (!mListContainer.containsView(v)) { 272 // the view is added somewhere else. Let's make sure 273 // the ordering works properly below, by excluding these 274 toShow.remove(v); 275 i--; 276 } 277 } 278 279 addNotificationChildrenAndSort(); 280 281 // So after all this work notifications still aren't sorted correctly. 282 // Let's do that now by advancing through toShow and mListContainer in 283 // lock-step, making sure mListContainer matches what we see in toShow. 284 int j = 0; 285 for (int i = 0; i < mListContainer.getContainerChildCount(); i++) { 286 View child = mListContainer.getContainerChildAt(i); 287 if (!(child instanceof ExpandableNotificationRow)) { 288 // We don't care about non-notification views. 289 continue; 290 } 291 if (((ExpandableNotificationRow) child).isBlockingHelperShowing()) { 292 // Don't count/reorder notifications that are showing the blocking helper! 293 continue; 294 } 295 296 ExpandableNotificationRow targetChild = toShow.get(j); 297 if (child != targetChild) { 298 // Oops, wrong notification at this position. Put the right one 299 // here and advance both lists. 300 if (mVisualStabilityManager.canReorderNotification(targetChild)) { 301 mListContainer.changeViewPosition(targetChild, i); 302 } else { 303 mVisualStabilityManager.addReorderingAllowedCallback(mEntryManager, 304 false /* persistent */); 305 } 306 } 307 j++; 308 309 } 310 311 mDynamicChildBindController.updateContentViews(mTmpChildOrderMap); 312 mVisualStabilityManager.onReorderingFinished(); 313 // clear the map again for the next usage 314 mTmpChildOrderMap.clear(); 315 316 updateRowStatesInternal(); 317 318 mListContainer.onNotificationViewUpdateFinished(); 319 320 endUpdate(); 321 } 322 323 /** 324 * Should a notification entry from the active list be suppressed and not show? 325 */ shouldSuppressActiveNotification(NotificationEntry ent)326 private boolean shouldSuppressActiveNotification(NotificationEntry ent) { 327 final boolean isBubbleNotificationSuppressedFromShade = mBubblesOptional.isPresent() 328 && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade( 329 ent.getKey(), ent.getSbn().getGroupKey()); 330 if (ent.isRowDismissed() || ent.isRowRemoved() 331 || isBubbleNotificationSuppressedFromShade 332 || mFgsSectionController.hasEntry(ent)) { 333 // we want to suppress removed notifications because they could 334 // temporarily become children if they were isolated before. 335 return true; 336 } 337 return false; 338 } 339 addNotificationChildrenAndSort()340 private void addNotificationChildrenAndSort() { 341 // Let's now add all notification children which are missing 342 boolean orderChanged = false; 343 ArrayList<ExpandableNotificationRow> orderedRows = new ArrayList<>(); 344 for (int i = 0; i < mListContainer.getContainerChildCount(); i++) { 345 View view = mListContainer.getContainerChildAt(i); 346 if (!(view instanceof ExpandableNotificationRow)) { 347 // We don't care about non-notification views. 348 continue; 349 } 350 351 ExpandableNotificationRow parent = (ExpandableNotificationRow) view; 352 List<ExpandableNotificationRow> children = parent.getAttachedChildren(); 353 List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent.getEntry()); 354 if (orderedChildren == null) { 355 // Not a group 356 continue; 357 } 358 parent.setUntruncatedChildCount(orderedChildren.size()); 359 for (int childIndex = 0; childIndex < orderedChildren.size(); childIndex++) { 360 ExpandableNotificationRow childView = orderedChildren.get(childIndex).getRow(); 361 if (children == null || !children.contains(childView)) { 362 if (childView.getParent() != null) { 363 Log.wtf(TAG, "trying to add a notification child that already has " 364 + "a parent. class:" + childView.getParent().getClass() 365 + "\n child: " + childView); 366 // This shouldn't happen. We can recover by removing it though. 367 ((ViewGroup) childView.getParent()).removeView(childView); 368 } 369 mVisualStabilityManager.notifyViewAddition(childView); 370 parent.addChildNotification(childView, childIndex); 371 mListContainer.notifyGroupChildAdded(childView); 372 } 373 orderedRows.add(childView); 374 } 375 376 // Finally after removing and adding has been performed we can apply the order. 377 orderChanged |= parent.applyChildOrder(orderedRows, mVisualStabilityManager, 378 mEntryManager); 379 orderedRows.clear(); 380 } 381 if (orderChanged) { 382 mListContainer.generateChildOrderChangedEvent(); 383 } 384 } 385 removeNotificationChildren()386 private void removeNotificationChildren() { 387 // First let's remove all children which don't belong in the parents 388 ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>(); 389 for (int i = 0; i < mListContainer.getContainerChildCount(); i++) { 390 View view = mListContainer.getContainerChildAt(i); 391 if (!(view instanceof ExpandableNotificationRow)) { 392 // We don't care about non-notification views. 393 continue; 394 } 395 396 ExpandableNotificationRow parent = (ExpandableNotificationRow) view; 397 List<ExpandableNotificationRow> children = parent.getAttachedChildren(); 398 List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent.getEntry()); 399 400 if (children != null) { 401 toRemove.clear(); 402 for (ExpandableNotificationRow childRow : children) { 403 if ((orderedChildren == null 404 || !orderedChildren.contains(childRow.getEntry())) 405 && !childRow.keepInParent()) { 406 toRemove.add(childRow); 407 } 408 } 409 for (ExpandableNotificationRow remove : toRemove) { 410 parent.removeChildNotification(remove); 411 if (mEntryManager.getActiveNotificationUnfiltered( 412 remove.getEntry().getSbn().getKey()) == null) { 413 // We only want to add an animation if the view is completely removed 414 // otherwise it's just a transfer 415 mListContainer.notifyGroupChildRemoved(remove, 416 parent.getChildrenContainer()); 417 } 418 } 419 } 420 } 421 } 422 423 /** 424 * Updates expanded, dimmed and locked states of notification rows. 425 */ updateRowStates()426 public void updateRowStates() { 427 Assert.isMainThread(); 428 beginUpdate(); 429 updateRowStatesInternal(); 430 endUpdate(); 431 } 432 updateRowStatesInternal()433 private void updateRowStatesInternal() { 434 Trace.beginSection("NotificationViewHierarchyManager#updateRowStates"); 435 final int N = mListContainer.getContainerChildCount(); 436 437 int visibleNotifications = 0; 438 boolean onKeyguard = 439 mStatusBarStateController.getCurrentOrUpcomingState() == StatusBarState.KEYGUARD; 440 Stack<ExpandableNotificationRow> stack = new Stack<>(); 441 for (int i = N - 1; i >= 0; i--) { 442 View child = mListContainer.getContainerChildAt(i); 443 if (!(child instanceof ExpandableNotificationRow)) { 444 continue; 445 } 446 stack.push((ExpandableNotificationRow) child); 447 } 448 while(!stack.isEmpty()) { 449 ExpandableNotificationRow row = stack.pop(); 450 NotificationEntry entry = row.getEntry(); 451 boolean isChildNotification = mGroupManager.isChildInGroup(entry); 452 453 if (!onKeyguard) { 454 // If mAlwaysExpandNonGroupedNotification is false, then only expand the 455 // very first notification and if it's not a child of grouped notifications. 456 row.setSystemExpanded(mAlwaysExpandNonGroupedNotification 457 || (visibleNotifications == 0 && !isChildNotification 458 && !row.isLowPriority())); 459 } 460 461 int userId = entry.getSbn().getUserId(); 462 boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup( 463 entry.getSbn()) && !entry.isRowRemoved(); 464 boolean showOnKeyguard = mLockscreenUserManager.shouldShowOnKeyguard(entry); 465 if (!showOnKeyguard) { 466 // min priority notifications should show if their summary is showing 467 if (mGroupManager.isChildInGroup(entry)) { 468 NotificationEntry summary = mGroupManager.getLogicalGroupSummary(entry); 469 if (summary != null && mLockscreenUserManager.shouldShowOnKeyguard(summary)) { 470 showOnKeyguard = true; 471 } 472 } 473 } 474 if (suppressedSummary 475 || mLockscreenUserManager.shouldHideNotifications(userId) 476 || (onKeyguard && !showOnKeyguard)) { 477 entry.getRow().setVisibility(View.GONE); 478 } else { 479 boolean wasGone = entry.getRow().getVisibility() == View.GONE; 480 if (wasGone) { 481 entry.getRow().setVisibility(View.VISIBLE); 482 } 483 if (!isChildNotification && !entry.getRow().isRemoved()) { 484 if (wasGone) { 485 // notify the scroller of a child addition 486 mListContainer.generateAddAnimation(entry.getRow(), 487 !showOnKeyguard /* fromMoreCard */); 488 } 489 visibleNotifications++; 490 } 491 } 492 if (row.isSummaryWithChildren()) { 493 List<ExpandableNotificationRow> notificationChildren = 494 row.getAttachedChildren(); 495 int size = notificationChildren.size(); 496 for (int i = size - 1; i >= 0; i--) { 497 stack.push(notificationChildren.get(i)); 498 } 499 } 500 row.showFeedbackIcon(mAssistantFeedbackController.showFeedbackIndicator(entry), 501 mAssistantFeedbackController.getFeedbackResources(entry)); 502 row.setLastAudiblyAlertedMs(entry.getLastAudiblyAlertedMs()); 503 } 504 505 Trace.beginSection("NotificationPresenter#onUpdateRowStates"); 506 mPresenter.onUpdateRowStates(); 507 Trace.endSection(); 508 Trace.endSection(); 509 } 510 511 @Override onDynamicPrivacyChanged()512 public void onDynamicPrivacyChanged() { 513 if (mPerformingUpdate) { 514 Log.w(TAG, "onDynamicPrivacyChanged made a re-entrant call"); 515 } 516 // This listener can be called from updateNotificationViews() via a convoluted listener 517 // chain, so we post here to prevent a re-entrant call. See b/136186188 518 // TODO: Refactor away the need for this 519 if (!mIsHandleDynamicPrivacyChangeScheduled) { 520 mIsHandleDynamicPrivacyChangeScheduled = true; 521 mHandler.post(this::onHandleDynamicPrivacyChanged); 522 } 523 } 524 onHandleDynamicPrivacyChanged()525 private void onHandleDynamicPrivacyChanged() { 526 mIsHandleDynamicPrivacyChangeScheduled = false; 527 updateNotificationViews(); 528 } 529 beginUpdate()530 private void beginUpdate() { 531 if (mPerformingUpdate) { 532 Log.wtf(TAG, "Re-entrant code during update", new Exception()); 533 } 534 mPerformingUpdate = true; 535 } 536 endUpdate()537 private void endUpdate() { 538 if (!mPerformingUpdate) { 539 Log.wtf(TAG, "Manager state has become desynced", new Exception()); 540 } 541 mPerformingUpdate = false; 542 } 543 } 544