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