/*
|
* 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<String, GroupAlertEntry> mGroupAlertEntries = new ArrayMap<>();
|
|
/**
|
* The list of entries currently inflating that should alert after inflation. Keyed by
|
* notification key.
|
*/
|
private final ArrayMap<String, PendingAlertInfo> 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<NotificationEntry> 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<NotificationEntry> 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<NotificationEntry> 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;
|
}
|
}
|
}
|