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