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