• 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 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