blob: b9d23b4a344a23fd7b94e1694404805fe17afc4a [file] [log] [blame]
/*
* Copyright (C) 2020 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.notification.collection.coordinator;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.phone.NotifPanelEvents;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.Compile;
import com.android.systemui.util.concurrency.DelayableExecutor;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
/**
* Ensures that notifications are visually stable if the user is looking at the notifications.
* Group and section changes are re-allowed when the notification entries are no longer being
* viewed.
*/
// TODO(b/204468557): Move to @CoordinatorScope
@SysUISingleton
public class VisualStabilityCoordinator implements Coordinator, Dumpable,
NotifPanelEvents.Listener {
public static final String TAG = "VisualStability";
public static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
private final DelayableExecutor mDelayableExecutor;
private final HeadsUpManager mHeadsUpManager;
private final NotifPanelEvents mNotifPanelEvents;
private final StatusBarStateController mStatusBarStateController;
private final VisualStabilityProvider mVisualStabilityProvider;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private boolean mSleepy = true;
private boolean mFullyDozed;
private boolean mPanelExpanded;
private boolean mPulsing;
private boolean mNotifPanelCollapsing;
private boolean mNotifPanelLaunchingActivity;
private boolean mPipelineRunAllowed;
private boolean mReorderingAllowed;
private boolean mIsSuppressingPipelineRun = false;
private boolean mIsSuppressingGroupChange = false;
private final Set<String> mEntriesWithSuppressedSectionChange = new HashSet<>();
private boolean mIsSuppressingEntryReorder = false;
// key: notification key that can temporarily change its section
// value: runnable that when run removes its associated RemoveOverrideSuppressionRunnable
// from the DelayableExecutor's queue
private Map<String, Runnable> mEntriesThatCanChangeSection = new HashMap<>();
@VisibleForTesting
protected static final long ALLOW_SECTION_CHANGE_TIMEOUT = 500;
@Inject
public VisualStabilityCoordinator(
DelayableExecutor delayableExecutor,
DumpManager dumpManager,
HeadsUpManager headsUpManager,
NotifPanelEvents notifPanelEvents,
StatusBarStateController statusBarStateController,
VisualStabilityProvider visualStabilityProvider,
WakefulnessLifecycle wakefulnessLifecycle) {
mHeadsUpManager = headsUpManager;
mVisualStabilityProvider = visualStabilityProvider;
mWakefulnessLifecycle = wakefulnessLifecycle;
mStatusBarStateController = statusBarStateController;
mDelayableExecutor = delayableExecutor;
mNotifPanelEvents = notifPanelEvents;
dumpManager.registerDumpable(this);
}
@Override
public void attach(NotifPipeline pipeline) {
mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
mSleepy = mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP;
mFullyDozed = mStatusBarStateController.getDozeAmount() == 1f;
mStatusBarStateController.addCallback(mStatusBarStateControllerListener);
mPulsing = mStatusBarStateController.isPulsing();
mNotifPanelEvents.registerListener(this);
pipeline.setVisualStabilityManager(mNotifStabilityManager);
}
// TODO(b/203826051): Ensure stability manager can allow reordering off-screen
// HUNs to the top of the shade
private final NotifStabilityManager mNotifStabilityManager =
new NotifStabilityManager("VisualStabilityCoordinator") {
@Override
public void onBeginRun() {
mIsSuppressingPipelineRun = false;
mIsSuppressingGroupChange = false;
mEntriesWithSuppressedSectionChange.clear();
mIsSuppressingEntryReorder = false;
}
@Override
public boolean isPipelineRunAllowed() {
mIsSuppressingPipelineRun |= !mPipelineRunAllowed;
return mPipelineRunAllowed;
}
@Override
public boolean isGroupChangeAllowed(@NonNull NotificationEntry entry) {
final boolean isGroupChangeAllowedForEntry =
mReorderingAllowed || mHeadsUpManager.isAlerting(entry.getKey());
mIsSuppressingGroupChange |= !isGroupChangeAllowedForEntry;
return isGroupChangeAllowedForEntry;
}
@Override
public boolean isGroupPruneAllowed(@NonNull GroupEntry entry) {
final boolean isGroupPruneAllowedForEntry = mReorderingAllowed;
mIsSuppressingGroupChange |= !isGroupPruneAllowedForEntry;
return isGroupPruneAllowedForEntry;
}
@Override
public boolean isSectionChangeAllowed(@NonNull NotificationEntry entry) {
final boolean isSectionChangeAllowedForEntry =
mReorderingAllowed
|| mHeadsUpManager.isAlerting(entry.getKey())
|| mEntriesThatCanChangeSection.containsKey(entry.getKey());
if (!isSectionChangeAllowedForEntry) {
mEntriesWithSuppressedSectionChange.add(entry.getKey());
}
return isSectionChangeAllowedForEntry;
}
@Override
public boolean isEntryReorderingAllowed(@NonNull ListEntry section) {
return mReorderingAllowed;
}
@Override
public boolean isEveryChangeAllowed() {
return mReorderingAllowed;
}
@Override
public void onEntryReorderSuppressed() {
mIsSuppressingEntryReorder = true;
}
};
private void updateAllowedStates(String field, boolean value) {
boolean wasPipelineRunAllowed = mPipelineRunAllowed;
boolean wasReorderingAllowed = mReorderingAllowed;
mPipelineRunAllowed = !isPanelCollapsingOrLaunchingActivity();
mReorderingAllowed = isReorderingAllowed();
if (DEBUG && (wasPipelineRunAllowed != mPipelineRunAllowed
|| wasReorderingAllowed != mReorderingAllowed)) {
Log.d(TAG, "Stability allowances changed:"
+ " pipelineRunAllowed " + wasPipelineRunAllowed + "->" + mPipelineRunAllowed
+ " reorderingAllowed " + wasReorderingAllowed + "->" + mReorderingAllowed
+ " when setting " + field + "=" + value);
}
if ((mPipelineRunAllowed && mIsSuppressingPipelineRun)
|| (mReorderingAllowed && (mIsSuppressingGroupChange
|| isSuppressingSectionChange()
|| mIsSuppressingEntryReorder))) {
mNotifStabilityManager.invalidateList();
}
mVisualStabilityProvider.setReorderingAllowed(mReorderingAllowed);
}
private boolean isSuppressingSectionChange() {
return !mEntriesWithSuppressedSectionChange.isEmpty();
}
private boolean isPanelCollapsingOrLaunchingActivity() {
return mNotifPanelCollapsing || mNotifPanelLaunchingActivity;
}
private boolean isReorderingAllowed() {
return ((mFullyDozed && mSleepy) || !mPanelExpanded) && !mPulsing;
}
/**
* Allows this notification entry to be re-ordered in the notification list temporarily until
* the timeout has passed.
*
* Typically this is allowed because the user has directly changed something about the
* notification and we are reordering based on the user's change.
*
* @param entry notification entry that can change sections even if isReorderingAllowed is false
* @param now current time SystemClock.uptimeMillis
*/
public void temporarilyAllowSectionChanges(@NonNull NotificationEntry entry, long now) {
final String entryKey = entry.getKey();
final boolean wasSectionChangeAllowed =
mNotifStabilityManager.isSectionChangeAllowed(entry);
// If it exists, cancel previous timeout
if (mEntriesThatCanChangeSection.containsKey(entryKey)) {
mEntriesThatCanChangeSection.get(entryKey).run();
}
// Schedule & store new timeout cancellable
mEntriesThatCanChangeSection.put(
entryKey,
mDelayableExecutor.executeAtTime(
() -> mEntriesThatCanChangeSection.remove(entryKey),
now + ALLOW_SECTION_CHANGE_TIMEOUT));
if (!wasSectionChangeAllowed) {
mNotifStabilityManager.invalidateList();
}
}
final StatusBarStateController.StateListener mStatusBarStateControllerListener =
new StatusBarStateController.StateListener() {
@Override
public void onPulsingChanged(boolean pulsing) {
mPulsing = pulsing;
updateAllowedStates("pulsing", pulsing);
}
@Override
public void onExpandedChanged(boolean expanded) {
mPanelExpanded = expanded;
updateAllowedStates("panelExpanded", expanded);
}
@Override
public void onDozeAmountChanged(float linear, float eased) {
final boolean fullyDozed = linear == 1f;
mFullyDozed = fullyDozed;
updateAllowedStates("fullyDozed", fullyDozed);
}
};
final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
@Override
public void onFinishedGoingToSleep() {
// NOTE: this method is called much earlier than what we consider "finished" going to
// sleep (the animation isn't done), so we also need to check the doze amount is not 1
// and use the combo to determine that the locked shade is not visible.
mSleepy = true;
updateAllowedStates("sleepy", true);
}
@Override
public void onStartedWakingUp() {
mSleepy = false;
updateAllowedStates("sleepy", false);
}
};
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("pipelineRunAllowed: " + mPipelineRunAllowed);
pw.println(" notifPanelCollapsing: " + mNotifPanelCollapsing);
pw.println(" launchingNotifActivity: " + mNotifPanelLaunchingActivity);
pw.println("reorderingAllowed: " + mReorderingAllowed);
pw.println(" sleepy: " + mSleepy);
pw.println(" fullyDozed: " + mFullyDozed);
pw.println(" panelExpanded: " + mPanelExpanded);
pw.println(" pulsing: " + mPulsing);
pw.println("isSuppressingPipelineRun: " + mIsSuppressingPipelineRun);
pw.println("isSuppressingGroupChange: " + mIsSuppressingGroupChange);
pw.println("isSuppressingEntryReorder: " + mIsSuppressingEntryReorder);
pw.println("entriesWithSuppressedSectionChange: "
+ mEntriesWithSuppressedSectionChange.size());
for (String key : mEntriesWithSuppressedSectionChange) {
pw.println(" " + key);
}
pw.println("entriesThatCanChangeSection: " + mEntriesThatCanChangeSection.size());
for (String key : mEntriesThatCanChangeSection.keySet()) {
pw.println(" " + key);
}
}
@Override
public void onPanelCollapsingChanged(boolean isCollapsing) {
mNotifPanelCollapsing = isCollapsing;
updateAllowedStates("notifPanelCollapsing", isCollapsing);
}
@Override
public void onLaunchingActivityChanged(boolean isLaunchingActivity) {
mNotifPanelLaunchingActivity = isLaunchingActivity;
updateAllowedStates("notifPanelLaunchingActivity", isLaunchingActivity);
}
}