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