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