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