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

src.com.android.systemui.statusbar.phone.NotificationGroupAlertTransferHelper Maven / Gradle / Ivy

/*
 * Copyright (C) 2018 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.phone;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.os.SystemClock;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;

import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.Dependency;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.statusbar.AlertingNotificationManager;
import com.android.systemui.statusbar.AmbientPulseManager;
import com.android.systemui.statusbar.AmbientPulseManager.OnAmbientChangedListener;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.NotificationContentInflater.AsyncInflationTask;
import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
import com.android.systemui.statusbar.phone.NotificationGroupManager.NotificationGroup;
import com.android.systemui.statusbar.phone.NotificationGroupManager.OnGroupChangeListener;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;

import java.util.ArrayList;
import java.util.Objects;

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

/**
 * A helper class dealing with the alert interactions between {@link NotificationGroupManager},
 * {@link HeadsUpManager}, {@link AmbientPulseManager}. In particular, this class deals with keeping
 * the correct notification in a group alerting based off the group suppression.
 */
@Singleton
public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedListener,
        OnAmbientChangedListener, StateListener {

    private static final long ALERT_TRANSFER_TIMEOUT = 300;

    /**
     * The list of entries containing group alert metadata for each group. Keyed by group key.
     */
    private final ArrayMap mGroupAlertEntries = new ArrayMap<>();

    /**
     * The list of entries currently inflating that should alert after inflation. Keyed by
     * notification key.
     */
    private final ArrayMap mPendingAlerts = new ArrayMap<>();

    private HeadsUpManager mHeadsUpManager;
    private final AmbientPulseManager mAmbientPulseManager =
            Dependency.get(AmbientPulseManager.class);
    private final NotificationGroupManager mGroupManager =
            Dependency.get(NotificationGroupManager.class);

    private NotificationEntryManager mEntryManager;

    private boolean mIsDozing;

    @Inject
    public NotificationGroupAlertTransferHelper() {
        Dependency.get(StatusBarStateController.class).addCallback(this);
    }

    /** Causes the TransferHelper to register itself as a listener to the appropriate classes. */
    public void bind(NotificationEntryManager entryManager,
            NotificationGroupManager groupManager) {
        if (mEntryManager != null) {
            throw new IllegalStateException("Already bound.");
        }

        // TODO(b/119637830): It would be good if GroupManager already had all pending notifications
        // as normal children (i.e. add notifications to GroupManager before inflation) so that we
        // don't have to have this dependency. We'd also have to worry less about the suppression
        // not being up to date.
        mEntryManager = entryManager;

        mEntryManager.addNotificationEntryListener(mNotificationEntryListener);
        groupManager.addOnGroupChangeListener(mOnGroupChangeListener);
    }

    /**
     * Whether or not a notification has transferred its alert state to the notification and
     * the notification should alert after inflating.
     *
     * @param entry notification to check
     * @return true if the entry was transferred to and should inflate + alert
     */
    public boolean isAlertTransferPending(@NonNull NotificationEntry entry) {
        PendingAlertInfo alertInfo = mPendingAlerts.get(entry.key);
        return alertInfo != null && alertInfo.isStillValid();
    }

    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
        mHeadsUpManager = headsUpManager;
    }

    @Override
    public void onStateChanged(int newState) {}

    @Override
    public void onDozingChanged(boolean isDozing) {
        if (mIsDozing != isDozing) {
            for (GroupAlertEntry groupAlertEntry : mGroupAlertEntries.values()) {
                groupAlertEntry.mLastAlertTransferTime = 0;
                groupAlertEntry.mAlertSummaryOnNextAddition = false;
            }
        }
        mIsDozing = isDozing;
    }

    private final OnGroupChangeListener mOnGroupChangeListener = new OnGroupChangeListener() {
        @Override
        public void onGroupCreated(NotificationGroup group, String groupKey) {
            mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group));
        }

        @Override
        public void onGroupRemoved(NotificationGroup group, String groupKey) {
            mGroupAlertEntries.remove(groupKey);
        }

        @Override
        public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {
            AlertingNotificationManager alertManager = getActiveAlertManager();
            if (suppressed) {
                if (alertManager.isAlerting(group.summary.key)) {
                    handleSuppressedSummaryAlerted(group.summary, alertManager);
                }
            } else {
                // Group summary can be null if we are no longer suppressed because the summary was
                // removed. In that case, we don't need to alert the summary.
                if (group.summary == null) {
                    return;
                }
                GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey(
                        group.summary.notification));
                // Group is no longer suppressed. We should check if we need to transfer the alert
                // back to the summary now that it's no longer suppressed.
                if (groupAlertEntry.mAlertSummaryOnNextAddition) {
                    if (!alertManager.isAlerting(group.summary.key)) {
                        alertNotificationWhenPossible(group.summary, alertManager);
                    }
                    groupAlertEntry.mAlertSummaryOnNextAddition = false;
                } else {
                    checkShouldTransferBack(groupAlertEntry);
                }
            }
        }
    };

    @Override
    public void onAmbientStateChanged(NotificationEntry entry, boolean isAmbient) {
        onAlertStateChanged(entry, isAmbient, mAmbientPulseManager);
    }

    @Override
    public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
        onAlertStateChanged(entry, isHeadsUp, mHeadsUpManager);
    }

    private void onAlertStateChanged(NotificationEntry entry, boolean isAlerting,
            AlertingNotificationManager alertManager) {
        if (isAlerting && mGroupManager.isSummaryOfSuppressedGroup(entry.notification)) {
            handleSuppressedSummaryAlerted(entry, alertManager);
        }
    }

    private final NotificationEntryListener mNotificationEntryListener =
            new NotificationEntryListener() {
        // Called when a new notification has been posted but is not inflated yet. We use this to
        // see as early as we can if we need to abort a transfer.
        @Override
        public void onPendingEntryAdded(NotificationEntry entry) {
            String groupKey = mGroupManager.getGroupKey(entry.notification);
            GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey);
            if (groupAlertEntry != null) {
                checkShouldTransferBack(groupAlertEntry);
            }
        }

        // Called when the entry's reinflation has finished. If there is an alert pending, we
        // then show the alert.
        @Override
        public void onEntryReinflated(NotificationEntry entry) {
            PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key);
            if (alertInfo != null) {
                if (alertInfo.isStillValid()) {
                    alertNotificationWhenPossible(entry, getActiveAlertManager());
                } else {
                    // The transfer is no longer valid. Free the content.
                    entry.getRow().freeContentViewWhenSafe(
                            alertInfo.mAlertManager.getContentFlag());
                }
            }
        }

        @Override
        public void onEntryRemoved(
                @Nullable NotificationEntry entry,
                NotificationVisibility visibility,
                boolean removedByUser) {
            // Removes any alerts pending on this entry. Note that this will not stop any inflation
            // tasks started by a transfer, so this should only be used as clean-up for when
            // inflation is stopped and the pending alert no longer needs to happen.
            mPendingAlerts.remove(entry.key);
        }
    };

    /**
     * Gets the number of new notifications pending inflation that will be added to the group
     * but currently aren't and should not alert.
     *
     * @param group group to check
     * @return the number of new notifications that will be added to the group
     */
    private int getPendingChildrenNotAlerting(@NonNull NotificationGroup group) {
        if (mEntryManager == null) {
            return 0;
        }
        int number = 0;
        Iterable values = mEntryManager.getPendingNotificationsIterator();
        for (NotificationEntry entry : values) {
            if (isPendingNotificationInGroup(entry, group) && onlySummaryAlerts(entry)) {
                number++;
            }
        }
        return number;
    }

    /**
     * Checks if the pending inflations will add children to this group.
     *
     * @param group group to check
     * @return true if a pending notification will add to this group
     */
    private boolean pendingInflationsWillAddChildren(@NonNull NotificationGroup group) {
        if (mEntryManager == null) {
            return false;
        }
        Iterable values = mEntryManager.getPendingNotificationsIterator();
        for (NotificationEntry entry : values) {
            if (isPendingNotificationInGroup(entry, group)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if a new pending notification will be added to the group.
     *
     * @param entry pending notification
     * @param group group to check
     * @return true if the notification will add to the group, false o/w
     */
    private boolean isPendingNotificationInGroup(@NonNull NotificationEntry entry,
            @NonNull NotificationGroup group) {
        String groupKey = mGroupManager.getGroupKey(group.summary.notification);
        return mGroupManager.isGroupChild(entry.notification)
                && Objects.equals(mGroupManager.getGroupKey(entry.notification), groupKey)
                && !group.children.containsKey(entry.key);
    }

    /**
     * Handles the scenario where a summary that has been suppressed is alerted.  A suppressed
     * summary should for all intents and purposes be invisible to the user and as a result should
     * not alert.  When this is the case, it is our responsibility to pass the alert to the
     * appropriate child which will be the representative notification alerting for the group.
     *
     * @param summary the summary that is suppressed and alerting
     * @param alertManager the alert manager that manages the alerting summary
     */
    private void handleSuppressedSummaryAlerted(@NonNull NotificationEntry summary,
            @NonNull AlertingNotificationManager alertManager) {
        StatusBarNotification sbn = summary.notification;
        GroupAlertEntry groupAlertEntry =
                mGroupAlertEntries.get(mGroupManager.getGroupKey(sbn));
        if (!mGroupManager.isSummaryOfSuppressedGroup(summary.notification)
                || !alertManager.isAlerting(sbn.getKey())
                || groupAlertEntry == null) {
            return;
        }

        if (pendingInflationsWillAddChildren(groupAlertEntry.mGroup)) {
            // New children will actually be added to this group, let's not transfer the alert.
            return;
        }

        NotificationEntry child = mGroupManager.getLogicalChildren(summary.notification).iterator().next();
        if (child != null) {
            if (child.getRow().keepInParent()
                    || child.isRowRemoved()
                    || child.isRowDismissed()) {
                // The notification is actually already removed. No need to alert it.
                return;
            }
            if (!alertManager.isAlerting(child.key) && onlySummaryAlerts(summary)) {
                groupAlertEntry.mLastAlertTransferTime = SystemClock.elapsedRealtime();
            }
            transferAlertState(summary, child, alertManager);
        }
    }

    /**
     * Transfers the alert state one entry to another. We remove the alert from the first entry
     * immediately to have the incorrect one up as short as possible. The second should alert
     * when possible.
     *
     * @param fromEntry entry to transfer alert from
     * @param toEntry entry to transfer to
     * @param alertManager alert manager for the alert type
     */
    private void transferAlertState(@NonNull NotificationEntry fromEntry, @NonNull NotificationEntry toEntry,
            @NonNull AlertingNotificationManager alertManager) {
        alertManager.removeNotification(fromEntry.key, true /* releaseImmediately */);
        alertNotificationWhenPossible(toEntry, alertManager);
    }

    /**
     * Determines if we need to transfer the alert back to the summary from the child and does
     * so if needed.
     *
     * This can happen since notification groups are not delivered as a whole unit and it is
     * possible we erroneously transfer the alert from the summary to the child even though
     * more children are coming. Thus, if a child is added within a certain timeframe after we
     * transfer, we back out and alert the summary again.
     *
     * @param groupAlertEntry group alert entry to check
     */
    private void checkShouldTransferBack(@NonNull GroupAlertEntry groupAlertEntry) {
        if (SystemClock.elapsedRealtime() - groupAlertEntry.mLastAlertTransferTime
                < ALERT_TRANSFER_TIMEOUT) {
            NotificationEntry summary = groupAlertEntry.mGroup.summary;
            AlertingNotificationManager alertManager = getActiveAlertManager();

            if (!onlySummaryAlerts(summary)) {
                return;
            }
            ArrayList children = mGroupManager.getLogicalChildren(summary.notification);
            int numChildren = children.size();
            int numPendingChildren = getPendingChildrenNotAlerting(groupAlertEntry.mGroup);
            numChildren += numPendingChildren;
            if (numChildren <= 1) {
                return;
            }
            boolean releasedChild = false;
            for (int i = 0; i < children.size(); i++) {
                NotificationEntry entry = children.get(i);
                if (onlySummaryAlerts(entry) && alertManager.isAlerting(entry.key)) {
                    releasedChild = true;
                    alertManager.removeNotification(entry.key, true /* releaseImmediately */);
                }
                if (mPendingAlerts.containsKey(entry.key)) {
                    // This is the child that would've been removed if it was inflated.
                    releasedChild = true;
                    mPendingAlerts.get(entry.key).mAbortOnInflation = true;
                }
            }
            if (releasedChild && !alertManager.isAlerting(summary.key)) {
                boolean notifyImmediately = (numChildren - numPendingChildren) > 1;
                if (notifyImmediately) {
                    alertNotificationWhenPossible(summary, alertManager);
                } else {
                    // Should wait until the pending child inflates before alerting.
                    groupAlertEntry.mAlertSummaryOnNextAddition = true;
                }
                groupAlertEntry.mLastAlertTransferTime = 0;
            }
        }
    }

    /**
     * Tries to alert the notification. If its content view is not inflated, we inflate and continue
     * when the entry finishes inflating the view.
     *
     * @param entry entry to show
     * @param alertManager alert manager for the alert type
     */
    private void alertNotificationWhenPossible(@NonNull NotificationEntry entry,
            @NonNull AlertingNotificationManager alertManager) {
        @InflationFlag int contentFlag = alertManager.getContentFlag();
        if (!entry.getRow().isInflationFlagSet(contentFlag)) {
            mPendingAlerts.put(entry.key, new PendingAlertInfo(entry, alertManager));
            entry.getRow().updateInflationFlag(contentFlag, true /* shouldInflate */);
            entry.getRow().inflateViews();
            return;
        }
        if (alertManager.isAlerting(entry.key)) {
            alertManager.updateNotification(entry.key, true /* alert */);
        } else {
            alertManager.showNotification(entry);
        }
    }

    private AlertingNotificationManager getActiveAlertManager() {
        return mIsDozing ? mAmbientPulseManager : mHeadsUpManager;
    }

    private boolean onlySummaryAlerts(NotificationEntry entry) {
        return entry.notification.getNotification().getGroupAlertBehavior()
                == Notification.GROUP_ALERT_SUMMARY;
    }

    /**
     * Information about a pending alert used to determine if the alert is still needed when
     * inflation completes.
     */
    private class PendingAlertInfo {
        /**
         * The alert manager when the transfer is initiated.
         */
        final AlertingNotificationManager mAlertManager;

        /**
         * The original notification when the transfer is initiated. This is used to determine if
         * the transfer is still valid if the notification is updated.
         */
        final StatusBarNotification mOriginalNotification;
        final NotificationEntry mEntry;

        /**
         * The notification is still pending inflation but we've decided that we no longer need
         * the content view (e.g. suppression might have changed and we decided we need to transfer
         * back). However, there is no way to abort just this inflation if other inflation requests
         * have started (see {@link AsyncInflationTask#supersedeTask(InflationTask)}). So instead
         * we just flag it as aborted and free when it's inflated.
         */
        boolean mAbortOnInflation;

        PendingAlertInfo(NotificationEntry entry, AlertingNotificationManager alertManager) {
            mOriginalNotification = entry.notification;
            mEntry = entry;
            mAlertManager = alertManager;
        }

        /**
         * Whether or not the pending alert is still valid and should still alert after inflation.
         *
         * @return true if the pending alert should still occur, false o/w
         */
        private boolean isStillValid() {
            if (mAbortOnInflation) {
                // Notification is aborted due to the transfer being explicitly cancelled
                return false;
            }
            if (mAlertManager != getActiveAlertManager()) {
                // Alert manager has changed
                return false;
            }
            if (mEntry.notification.getGroupKey() != mOriginalNotification.getGroupKey()) {
                // Groups have changed
                return false;
            }
            if (mEntry.notification.getNotification().isGroupSummary()
                    != mOriginalNotification.getNotification().isGroupSummary()) {
                // Notification has changed from group summary to not or vice versa
                return false;
            }
            return true;
        }
    }

    /**
     * Contains alert metadata for the notification group used to determine when/how the alert
     * should be transferred.
     */
    private static class GroupAlertEntry {
        /**
         * The time when the last alert transfer from summary to child happened.
         */
        long mLastAlertTransferTime;
        boolean mAlertSummaryOnNextAddition;
        final NotificationGroup mGroup;

        GroupAlertEntry(NotificationGroup group) {
            this.mGroup = group;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy