Home | History | Annotate | Download | only in statusbar
      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
    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 
    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
    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 
    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 
    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      */
    351     public void updateRowStates() {
    352         Assert.isMainThread();
    353         beginUpdate();
    354         updateRowStatesInternal();
    355         endUpdate();
    356     }
    357 
    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
    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 
    459     private void onHandleDynamicPrivacyChanged() {
    460         mIsHandleDynamicPrivacyChangeScheduled = false;
    461         updateNotificationViews();
    462     }
    463 
    464     private void beginUpdate() {
    465         if (mPerformingUpdate) {
    466             Log.wtf(TAG, "Re-entrant code during update", new Exception());
    467         }
    468         mPerformingUpdate = true;
    469     }
    470 
    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