• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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