All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.systemui.statusbar.NotificationViewHierarchyManager Maven / Gradle / Ivy

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.systemui.statusbar;

import static com.android.systemui.Dependency.MAIN_HANDLER_NAME;

import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Trace;
import android.os.UserHandle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import com.android.systemui.R;
import com.android.systemui.bubbles.BubbleData;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.ShadeController;
import com.android.systemui.util.Assert;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import dagger.Lazy;

/**
 * NotificationViewHierarchyManager manages updating the view hierarchy of notification views based
 * on their group structure. For example, if a notification becomes bundled with another,
 * NotificationViewHierarchyManager will update the view hierarchy to reflect that. It also will
 * tell NotificationListContainer which notifications to display, and inform it of changes to those
 * notifications that might affect their display.
 */
@Singleton
public class NotificationViewHierarchyManager implements DynamicPrivacyController.Listener {
    private static final String TAG = "NotificationViewHierarchyManager";

    private final Handler mHandler;

    //TODO: change this top >?
    private final HashMap>
            mTmpChildOrderMap = new HashMap<>();

    // Dependencies:
    protected final NotificationLockscreenUserManager mLockscreenUserManager;
    protected final NotificationGroupManager mGroupManager;
    protected final VisualStabilityManager mVisualStabilityManager;
    private final SysuiStatusBarStateController mStatusBarStateController;
    private final NotificationEntryManager mEntryManager;

    // Lazy
    private final Lazy mShadeController;

    /**
     * {@code true} if notifications not part of a group should by default be rendered in their
     * expanded state. If {@code false}, then only the first notification will be expanded if
     * possible.
     */
    private final boolean mAlwaysExpandNonGroupedNotification;
    private final BubbleData mBubbleData;
    private final DynamicPrivacyController mDynamicPrivacyController;

    private NotificationPresenter mPresenter;
    private NotificationListContainer mListContainer;

    // Used to help track down re-entrant calls to our update methods, which will cause bugs.
    private boolean mPerformingUpdate;
    // Hack to get around re-entrant call in onDynamicPrivacyChanged() until we can track down
    // the problem.
    private boolean mIsHandleDynamicPrivacyChangeScheduled;

    @Inject
    public NotificationViewHierarchyManager(Context context,
            @Named(MAIN_HANDLER_NAME) Handler mainHandler,
            NotificationLockscreenUserManager notificationLockscreenUserManager,
            NotificationGroupManager groupManager,
            VisualStabilityManager visualStabilityManager,
            StatusBarStateController statusBarStateController,
            NotificationEntryManager notificationEntryManager,
            Lazy shadeController,
            BubbleData bubbleData,
            DynamicPrivacyController privacyController) {
        mHandler = mainHandler;
        mLockscreenUserManager = notificationLockscreenUserManager;
        mGroupManager = groupManager;
        mVisualStabilityManager = visualStabilityManager;
        mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
        mEntryManager = notificationEntryManager;
        mShadeController = shadeController;
        Resources res = context.getResources();
        mAlwaysExpandNonGroupedNotification =
                res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
        mBubbleData = bubbleData;
        mDynamicPrivacyController = privacyController;
        privacyController.addListener(this);
    }

    public void setUpWithPresenter(NotificationPresenter presenter,
            NotificationListContainer listContainer) {
        mPresenter = presenter;
        mListContainer = listContainer;
    }

    /**
     * Updates the visual representation of the notifications.
     */
    //TODO: Rewrite this to focus on Entries, or some other data object instead of views
    public void updateNotificationViews() {
        Assert.isMainThread();
        beginUpdate();

        ArrayList activeNotifications = mEntryManager.getNotificationData()
                .getActiveNotifications();
        ArrayList toShow = new ArrayList<>(activeNotifications.size());
        final int N = activeNotifications.size();
        for (int i = 0; i < N; i++) {
            NotificationEntry ent = activeNotifications.get(i);
            if (ent.isRowDismissed() || ent.isRowRemoved()
                    || (mBubbleData.hasBubbleWithKey(ent.key) && !ent.showInShadeWhenBubble())) {
                // we don't want to update removed notifications because they could
                // temporarily become children if they were isolated before.
                continue;
            }

            int userId = ent.notification.getUserId();

            // Display public version of the notification if we need to redact.
            // TODO: This area uses a lot of calls into NotificationLockscreenUserManager.
            // We can probably move some of this code there.
            int currentUserId = mLockscreenUserManager.getCurrentUserId();
            boolean devicePublic = mLockscreenUserManager.isLockscreenPublicMode(currentUserId);
            boolean userPublic = devicePublic
                    || mLockscreenUserManager.isLockscreenPublicMode(userId);
            if (userPublic && mDynamicPrivacyController.isDynamicallyUnlocked()
                    && (userId == currentUserId || userId == UserHandle.USER_ALL
                    || !mLockscreenUserManager.needsSeparateWorkChallenge(userId))) {
                userPublic = false;
            }
            boolean needsRedaction = mLockscreenUserManager.needsRedaction(ent);
            boolean sensitive = userPublic && needsRedaction;
            boolean deviceSensitive = devicePublic
                    && !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
                    currentUserId);
            ent.getRow().setSensitive(sensitive, deviceSensitive);
            ent.getRow().setNeedsRedaction(needsRedaction);
            if (mGroupManager.isChildInGroupWithSummary(ent.notification)) {
                NotificationEntry summary = mGroupManager.getGroupSummary(ent.notification);
                List orderedChildren =
                        mTmpChildOrderMap.get(summary.getRow());
                if (orderedChildren == null) {
                    orderedChildren = new ArrayList<>();
                    mTmpChildOrderMap.put(summary.getRow(), orderedChildren);
                }
                orderedChildren.add(ent.getRow());
            } else {
                toShow.add(ent.getRow());
            }
        }

        ArrayList viewsToRemove = new ArrayList<>();
        for (int i=0; i< mListContainer.getContainerChildCount(); i++) {
            View child = mListContainer.getContainerChildAt(i);
            if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
                ExpandableNotificationRow row = (ExpandableNotificationRow) child;

                // Blocking helper is effectively a detached view. Don't bother removing it from the
                // layout.
                if (!row.isBlockingHelperShowing()) {
                    viewsToRemove.add((ExpandableNotificationRow) child);
                }
            }
        }

        for (ExpandableNotificationRow viewToRemove : viewsToRemove) {
            if (mGroupManager.isChildInGroupWithSummary(viewToRemove.getStatusBarNotification())) {
                // we are only transferring this notification to its parent, don't generate an
                // animation
                mListContainer.setChildTransferInProgress(true);
            }
            if (viewToRemove.isSummaryWithChildren()) {
                viewToRemove.removeAllChildren();
            }
            mListContainer.removeContainerView(viewToRemove);
            mListContainer.setChildTransferInProgress(false);
        }

        removeNotificationChildren();

        for (int i = 0; i < toShow.size(); i++) {
            View v = toShow.get(i);
            if (v.getParent() == null) {
                mVisualStabilityManager.notifyViewAddition(v);
                mListContainer.addContainerView(v);
            } else if (!mListContainer.containsView(v)) {
                // the view is added somewhere else. Let's make sure
                // the ordering works properly below, by excluding these
                toShow.remove(v);
                i--;
            }
        }

        addNotificationChildrenAndSort();

        // So after all this work notifications still aren't sorted correctly.
        // Let's do that now by advancing through toShow and mListContainer in
        // lock-step, making sure mListContainer matches what we see in toShow.
        int j = 0;
        for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
            View child = mListContainer.getContainerChildAt(i);
            if (!(child instanceof ExpandableNotificationRow)) {
                // We don't care about non-notification views.
                continue;
            }
            if (((ExpandableNotificationRow) child).isBlockingHelperShowing()) {
                // Don't count/reorder notifications that are showing the blocking helper!
                continue;
            }

            ExpandableNotificationRow targetChild = toShow.get(j);
            if (child != targetChild) {
                // Oops, wrong notification at this position. Put the right one
                // here and advance both lists.
                if (mVisualStabilityManager.canReorderNotification(targetChild)) {
                    mListContainer.changeViewPosition(targetChild, i);
                } else {
                    mVisualStabilityManager.addReorderingAllowedCallback(mEntryManager);
                }
            }
            j++;

        }

        mVisualStabilityManager.onReorderingFinished();
        // clear the map again for the next usage
        mTmpChildOrderMap.clear();

        updateRowStatesInternal();

        mListContainer.onNotificationViewUpdateFinished();

        endUpdate();
    }

    private void addNotificationChildrenAndSort() {
        // Let's now add all notification children which are missing
        boolean orderChanged = false;
        for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
            View view = mListContainer.getContainerChildAt(i);
            if (!(view instanceof ExpandableNotificationRow)) {
                // We don't care about non-notification views.
                continue;
            }

            ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
            List children = parent.getNotificationChildren();
            List orderedChildren = mTmpChildOrderMap.get(parent);

            for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size();
                    childIndex++) {
                ExpandableNotificationRow childView = orderedChildren.get(childIndex);
                if (children == null || !children.contains(childView)) {
                    if (childView.getParent() != null) {
                        Log.wtf(TAG, "trying to add a notification child that already has " +
                                "a parent. class:" + childView.getParent().getClass() +
                                "\n child: " + childView);
                        // This shouldn't happen. We can recover by removing it though.
                        ((ViewGroup) childView.getParent()).removeView(childView);
                    }
                    mVisualStabilityManager.notifyViewAddition(childView);
                    parent.addChildNotification(childView, childIndex);
                    mListContainer.notifyGroupChildAdded(childView);
                }
            }

            // Finally after removing and adding has been performed we can apply the order.
            orderChanged |= parent.applyChildOrder(orderedChildren, mVisualStabilityManager,
                    mEntryManager);
        }
        if (orderChanged) {
            mListContainer.generateChildOrderChangedEvent();
        }
    }

    private void removeNotificationChildren() {
        // First let's remove all children which don't belong in the parents
        ArrayList toRemove = new ArrayList<>();
        for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
            View view = mListContainer.getContainerChildAt(i);
            if (!(view instanceof ExpandableNotificationRow)) {
                // We don't care about non-notification views.
                continue;
            }

            ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
            List children = parent.getNotificationChildren();
            List orderedChildren = mTmpChildOrderMap.get(parent);

            if (children != null) {
                toRemove.clear();
                for (ExpandableNotificationRow childRow : children) {
                    if ((orderedChildren == null
                            || !orderedChildren.contains(childRow))
                            && !childRow.keepInParent()) {
                        toRemove.add(childRow);
                    }
                }
                for (ExpandableNotificationRow remove : toRemove) {
                    parent.removeChildNotification(remove);
                    if (mEntryManager.getNotificationData().get(
                            remove.getStatusBarNotification().getKey()) == null) {
                        // We only want to add an animation if the view is completely removed
                        // otherwise it's just a transfer
                        mListContainer.notifyGroupChildRemoved(remove,
                                parent.getChildrenContainer());
                    }
                }
            }
        }
    }

    /**
     * Updates expanded, dimmed and locked states of notification rows.
     */
    public void updateRowStates() {
        Assert.isMainThread();
        beginUpdate();
        updateRowStatesInternal();
        endUpdate();
    }

    private void updateRowStatesInternal() {
        Trace.beginSection("NotificationViewHierarchyManager#updateRowStates");
        final int N = mListContainer.getContainerChildCount();

        int visibleNotifications = 0;
        boolean onKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD;
        int maxNotifications = -1;
        if (onKeyguard) {
            maxNotifications = mPresenter.getMaxNotificationsWhileLocked(true /* recompute */);
        }
        mListContainer.setMaxDisplayedNotifications(maxNotifications);
        Stack stack = new Stack<>();
        for (int i = N - 1; i >= 0; i--) {
            View child = mListContainer.getContainerChildAt(i);
            if (!(child instanceof ExpandableNotificationRow)) {
                continue;
            }
            stack.push((ExpandableNotificationRow) child);
        }
        while(!stack.isEmpty()) {
            ExpandableNotificationRow row = stack.pop();
            NotificationEntry entry = row.getEntry();
            boolean isChildNotification =
                    mGroupManager.isChildInGroupWithSummary(entry.notification);

            row.setOnKeyguard(onKeyguard);

            if (!onKeyguard) {
                // If mAlwaysExpandNonGroupedNotification is false, then only expand the
                // very first notification and if it's not a child of grouped notifications.
                row.setSystemExpanded(mAlwaysExpandNonGroupedNotification
                        || (visibleNotifications == 0 && !isChildNotification
                        && !row.isLowPriority()));
            }

            entry.getRow().setOnAmbient(mShadeController.get().isDozing());
            int userId = entry.notification.getUserId();
            boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
                    entry.notification) && !entry.isRowRemoved();
            boolean showOnKeyguard = mLockscreenUserManager.shouldShowOnKeyguard(entry);
            if (!showOnKeyguard) {
                // min priority notifications should show if their summary is showing
                if (mGroupManager.isChildInGroupWithSummary(entry.notification)) {
                    NotificationEntry summary = mGroupManager.getLogicalGroupSummary(
                            entry.notification);
                    if (summary != null && mLockscreenUserManager.shouldShowOnKeyguard(summary)) {
                        showOnKeyguard = true;
                    }
                }
            }
            if (suppressedSummary
                    || mLockscreenUserManager.shouldHideNotifications(userId)
                    || (onKeyguard && !showOnKeyguard)) {
                entry.getRow().setVisibility(View.GONE);
            } else {
                boolean wasGone = entry.getRow().getVisibility() == View.GONE;
                if (wasGone) {
                    entry.getRow().setVisibility(View.VISIBLE);
                }
                if (!isChildNotification && !entry.getRow().isRemoved()) {
                    if (wasGone) {
                        // notify the scroller of a child addition
                        mListContainer.generateAddAnimation(entry.getRow(),
                                !showOnKeyguard /* fromMoreCard */);
                    }
                    visibleNotifications++;
                }
            }
            if (row.isSummaryWithChildren()) {
                List notificationChildren =
                        row.getNotificationChildren();
                int size = notificationChildren.size();
                for (int i = size - 1; i >= 0; i--) {
                    stack.push(notificationChildren.get(i));
                }
            }

            row.showAppOpsIcons(entry.mActiveAppOps);
            row.setLastAudiblyAlertedMs(entry.lastAudiblyAlertedMs);
        }

        Trace.beginSection("NotificationPresenter#onUpdateRowStates");
        mPresenter.onUpdateRowStates();
        Trace.endSection();
        Trace.endSection();
    }

    @Override
    public void onDynamicPrivacyChanged() {
        if (mPerformingUpdate) {
            Log.w(TAG, "onDynamicPrivacyChanged made a re-entrant call");
        }
        // This listener can be called from updateNotificationViews() via a convoluted listener
        // chain, so we post here to prevent a re-entrant call. See b/136186188
        // TODO: Refactor away the need for this
        if (!mIsHandleDynamicPrivacyChangeScheduled) {
            mIsHandleDynamicPrivacyChangeScheduled = true;
            mHandler.post(this::onHandleDynamicPrivacyChanged);
        }
    }

    private void onHandleDynamicPrivacyChanged() {
        mIsHandleDynamicPrivacyChangeScheduled = false;
        updateNotificationViews();
    }

    private void beginUpdate() {
        if (mPerformingUpdate) {
            Log.wtf(TAG, "Re-entrant code during update", new Exception());
        }
        mPerformingUpdate = true;
    }

    private void endUpdate() {
        if (!mPerformingUpdate) {
            Log.wtf(TAG, "Manager state has become desynced", new Exception());
        }
        mPerformingUpdate = false;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy