blob: 778a0a90cd85dd7efecb1f86dbd7d9c973a651f2 [file] [log] [blame]
/*
* 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.notification.interruption;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD;
import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA;
import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR;
import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.Handler;
import android.os.PowerManager;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
/**
* Provides heads-up and pulsing state for notification entries.
*/
@SysUISingleton
public class NotificationInterruptStateProviderImpl implements NotificationInterruptStateProvider {
private static final String TAG = "InterruptionStateProvider";
private static final boolean ENABLE_HEADS_UP = true;
private static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
private final List<NotificationInterruptSuppressor> mSuppressors = new ArrayList<>();
private final StatusBarStateController mStatusBarStateController;
private final KeyguardStateController mKeyguardStateController;
private final ContentResolver mContentResolver;
private final PowerManager mPowerManager;
private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
private final BatteryController mBatteryController;
private final HeadsUpManager mHeadsUpManager;
private final NotificationInterruptLogger mLogger;
private final NotifPipelineFlags mFlags;
private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
private final UiEventLogger mUiEventLogger;
private final UserTracker mUserTracker;
private final DeviceProvisionedController mDeviceProvisionedController;
@VisibleForTesting
protected boolean mUseHeadsUp = false;
public enum NotificationInterruptEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "FSI suppressed for suppressive GroupAlertBehavior")
FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235),
@UiEvent(doc = "FSI suppressed for suppressive BubbleMetadata")
FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA(1353),
@UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard")
FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236),
@UiEvent(doc = "HUN suppressed for old when")
HUN_SUPPRESSED_OLD_WHEN(1237),
@UiEvent(doc = "HUN snooze bypassed for potentially suppressed FSI")
HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI(1269);
private final int mId;
NotificationInterruptEvent(int id) {
mId = id;
}
@Override
public int getId() {
return mId;
}
}
@Inject
public NotificationInterruptStateProviderImpl(
ContentResolver contentResolver,
PowerManager powerManager,
AmbientDisplayConfiguration ambientDisplayConfiguration,
BatteryController batteryController,
StatusBarStateController statusBarStateController,
KeyguardStateController keyguardStateController,
HeadsUpManager headsUpManager,
NotificationInterruptLogger logger,
@Main Handler mainHandler,
NotifPipelineFlags flags,
KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
UiEventLogger uiEventLogger,
UserTracker userTracker,
DeviceProvisionedController deviceProvisionedController) {
mContentResolver = contentResolver;
mPowerManager = powerManager;
mBatteryController = batteryController;
mAmbientDisplayConfiguration = ambientDisplayConfiguration;
mStatusBarStateController = statusBarStateController;
mKeyguardStateController = keyguardStateController;
mHeadsUpManager = headsUpManager;
mLogger = logger;
mFlags = flags;
mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
mUiEventLogger = uiEventLogger;
mUserTracker = userTracker;
ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
@Override
public void onChange(boolean selfChange) {
boolean wasUsing = mUseHeadsUp;
mUseHeadsUp = ENABLE_HEADS_UP
&& Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
mContentResolver,
Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
Settings.Global.HEADS_UP_OFF);
mLogger.logHeadsUpFeatureChanged(mUseHeadsUp);
if (wasUsing != mUseHeadsUp) {
if (!mUseHeadsUp) {
mLogger.logWillDismissAll();
mHeadsUpManager.releaseAllImmediately();
}
}
}
};
if (ENABLE_HEADS_UP) {
mContentResolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED),
true,
headsUpObserver);
mContentResolver.registerContentObserver(
Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
headsUpObserver);
}
headsUpObserver.onChange(true); // set up
mDeviceProvisionedController = deviceProvisionedController;
}
@Override
public void addSuppressor(NotificationInterruptSuppressor suppressor) {
mSuppressors.add(suppressor);
}
@Override
public void removeSuppressor(NotificationInterruptSuppressor suppressor) {
mSuppressors.remove(suppressor);
}
@Override
public boolean shouldBubbleUp(NotificationEntry entry) {
final StatusBarNotification sbn = entry.getSbn();
if (!canAlertCommon(entry, true)) {
return false;
}
if (!canAlertAwakeCommon(entry, true)) {
return false;
}
if (!entry.canBubble()) {
mLogger.logNoBubbleNotAllowed(entry);
return false;
}
if (entry.getBubbleMetadata() == null
|| (entry.getBubbleMetadata().getShortcutId() == null
&& entry.getBubbleMetadata().getIntent() == null)) {
mLogger.logNoBubbleNoMetadata(entry);
return false;
}
return true;
}
@Override
public boolean shouldHeadsUp(NotificationEntry entry) {
return checkHeadsUp(entry, true);
}
@Override
public boolean checkHeadsUp(NotificationEntry entry, boolean log) {
if (mStatusBarStateController.isDozing()) {
return shouldHeadsUpWhenDozing(entry, log);
} else {
return shouldHeadsUpWhenAwake(entry, log);
}
}
/**
* When an entry was added, should we launch its fullscreen intent? Examples are Alarms or
* incoming calls.
*/
@Override
public boolean shouldLaunchFullScreenIntentWhenAdded(NotificationEntry entry) {
FullScreenIntentDecision decision = getFullScreenIntentDecision(entry);
logFullScreenIntentDecision(entry, decision);
return decision.shouldLaunch;
}
// Given whether the relevant entry was suppressed by DND, and the full screen intent launch
// decision independent of the DND decision, returns the combined FullScreenIntentDecision that
// results. If the entry was suppressed by DND but the decision otherwise would launch the
// FSI, then it is suppressed *only* by DND, whereas (because the DND decision happens before
// all others) if the entry would not otherwise have launched the FSI, DND is the effective
// suppressor.
//
// If the entry was not suppressed by DND, just returns the given decision.
@NonNull
private FullScreenIntentDecision getDecisionGivenSuppression(FullScreenIntentDecision decision,
boolean suppressedByDND) {
if (suppressedByDND) {
return decision.shouldLaunch
? FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND
: FullScreenIntentDecision.NO_FSI_SUPPRESSED_BY_DND;
}
return decision;
}
@Override
public FullScreenIntentDecision getFullScreenIntentDecision(@NonNull NotificationEntry entry) {
if (entry.getSbn().getNotification().fullScreenIntent == null) {
if (entry.isStickyAndNotDemoted()) {
return FullScreenIntentDecision.NO_FSI_SHOW_STICKY_HUN;
}
return FullScreenIntentDecision.NO_FULL_SCREEN_INTENT;
}
// Boolean indicating whether this FSI would have been suppressed by DND. Because we
// want to be able to identify when something would have shown an FSI if not for being
// suppressed, we need to keep track of this value for future decisions.
boolean suppressedByDND = false;
// Never show FSI when suppressed by DND
if (entry.shouldSuppressFullScreenIntent()) {
suppressedByDND = true;
}
// Never show FSI if importance is not HIGH
if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
return getDecisionGivenSuppression(FullScreenIntentDecision.NO_FSI_NOT_IMPORTANT_ENOUGH,
suppressedByDND);
}
// If the notification has suppressive GroupAlertBehavior, block FSI and warn.
StatusBarNotification sbn = entry.getSbn();
if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
// b/231322873: Detect and report an event when a notification has both an FSI and a
// suppressive groupAlertBehavior, and now correctly block the FSI from firing.
return getDecisionGivenSuppression(
FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR,
suppressedByDND);
}
// If the notification has suppressive BubbleMetadata, block FSI and warn.
Notification.BubbleMetadata bubbleMetadata = sbn.getNotification().getBubbleMetadata();
if (bubbleMetadata != null && bubbleMetadata.isNotificationSuppressed()) {
// b/274759612: Detect and report an event when a notification has both an FSI and a
// suppressive BubbleMetadata, and now correctly block the FSI from firing.
return getDecisionGivenSuppression(
FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA,
suppressedByDND);
}
// Notification is coming from a suspended package, block FSI
if (entry.getRanking().isSuspended()) {
return getDecisionGivenSuppression(FullScreenIntentDecision.NO_FSI_SUSPENDED,
suppressedByDND);
}
// If the screen is off, then launch the FullScreenIntent
if (!mPowerManager.isInteractive()) {
return getDecisionGivenSuppression(FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE,
suppressedByDND);
}
// If the device is currently dreaming, then launch the FullScreenIntent
// We avoid using IDreamManager#isDreaming here as that method will return false during
// the dream's wake-up phase.
if (mStatusBarStateController.isDreaming()) {
return getDecisionGivenSuppression(FullScreenIntentDecision.FSI_DEVICE_IS_DREAMING,
suppressedByDND);
}
// If the keyguard is showing, then launch the FullScreenIntent
if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
return getDecisionGivenSuppression(FullScreenIntentDecision.FSI_KEYGUARD_SHOWING,
suppressedByDND);
}
// If the notification should HUN, then we don't need FSI
// Because this is not the heads-up decision-making point, and checking whether it would
// HUN, don't log this specific check.
if (checkHeadsUp(entry, /* log= */ false)) {
return getDecisionGivenSuppression(FullScreenIntentDecision.NO_FSI_EXPECTED_TO_HUN,
suppressedByDND);
}
// If notification won't HUN and keyguard is showing, launch the FSI.
if (mKeyguardStateController.isShowing()) {
if (mKeyguardStateController.isOccluded()) {
return getDecisionGivenSuppression(
FullScreenIntentDecision.FSI_KEYGUARD_OCCLUDED,
suppressedByDND);
} else {
// Likely LOCKED_SHADE, but launch FSI anyway
return getDecisionGivenSuppression(FullScreenIntentDecision.FSI_LOCKED_SHADE,
suppressedByDND);
}
}
// The device is not provisioned, launch FSI.
if (!mDeviceProvisionedController.isDeviceProvisioned()) {
return getDecisionGivenSuppression(FullScreenIntentDecision.FSI_NOT_PROVISIONED,
suppressedByDND);
}
// Detect the case determined by b/231322873 to launch FSI while device is in use,
// as blocked by the correct implementation, and report the event.
return getDecisionGivenSuppression(FullScreenIntentDecision.NO_FSI_NO_HUN_OR_KEYGUARD,
suppressedByDND);
}
@Override
public void logFullScreenIntentDecision(NotificationEntry entry,
FullScreenIntentDecision decision) {
final int uid = entry.getSbn().getUid();
final String packageName = entry.getSbn().getPackageName();
switch (decision) {
case NO_FULL_SCREEN_INTENT:
// explicitly prevent logging for this (frequent) case
return;
case NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR:
android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
"groupAlertBehavior");
mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid,
packageName);
mLogger.logNoFullscreenWarning(entry,
decision + ": GroupAlertBehavior will prevent HUN");
return;
case NO_FSI_SUPPRESSIVE_BUBBLE_METADATA:
android.util.EventLog.writeEvent(0x534e4554, "274759612", uid,
"bubbleMetadata");
mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA, uid,
packageName);
mLogger.logNoFullscreenWarning(entry,
decision + ": BubbleMetadata may prevent HUN");
return;
case NO_FSI_NO_HUN_OR_KEYGUARD:
android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
"no hun or keyguard");
mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName);
mLogger.logNoFullscreenWarning(entry,
decision + ": Expected not to HUN while not on keyguard");
return;
default:
if (decision.shouldLaunch) {
mLogger.logFullscreen(entry, decision.name());
} else {
mLogger.logNoFullscreen(entry, decision.name());
}
}
}
private boolean shouldHeadsUpWhenAwake(NotificationEntry entry, boolean log) {
StatusBarNotification sbn = entry.getSbn();
if (!mUseHeadsUp) {
if (log) mLogger.logNoHeadsUpFeatureDisabled();
return false;
}
if (!canAlertCommon(entry, log)) {
return false;
}
if (!canAlertHeadsUpCommon(entry, log)) {
return false;
}
if (!canAlertAwakeCommon(entry, log)) {
return false;
}
final boolean isSnoozedPackage = isSnoozedPackage(sbn);
final boolean hasFsi = sbn.getNotification().fullScreenIntent != null;
// Assume any notification with an FSI is time-sensitive (like an alarm or incoming call)
// and ignore whether HUNs have been snoozed for the package.
if (isSnoozedPackage && !hasFsi) {
if (log) mLogger.logNoHeadsUpPackageSnoozed(entry);
return false;
}
boolean inShade = mStatusBarStateController.getState() == SHADE;
if (entry.isBubble() && inShade) {
if (log) mLogger.logNoHeadsUpAlreadyBubbled(entry);
return false;
}
if (entry.shouldSuppressPeek()) {
if (log) mLogger.logNoHeadsUpSuppressedByDnd(entry);
return false;
}
if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
if (log) mLogger.logNoHeadsUpNotImportant(entry);
return false;
}
boolean inUse = mPowerManager.isScreenOn() && !mStatusBarStateController.isDreaming();
if (!inUse) {
if (log) mLogger.logNoHeadsUpNotInUse(entry);
return false;
}
if (shouldSuppressHeadsUpWhenAwakeForOldWhen(entry, log)) {
return false;
}
for (int i = 0; i < mSuppressors.size(); i++) {
if (mSuppressors.get(i).suppressAwakeHeadsUp(entry)) {
if (log) mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i));
return false;
}
}
if (isSnoozedPackage) {
if (log) {
mLogger.logHeadsUpPackageSnoozeBypassedHasFsi(entry);
final int uid = entry.getSbn().getUid();
final String packageName = entry.getSbn().getPackageName();
mUiEventLogger.log(HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI, uid,
packageName);
}
return true;
}
if (log) mLogger.logHeadsUp(entry);
return true;
}
/**
* Whether or not the notification should "pulse" on the user's display when the phone is
* dozing. This displays the ambient view of the notification.
*
* @param entry the entry to check
* @return true if the entry should ambient pulse, false otherwise
*/
private boolean shouldHeadsUpWhenDozing(NotificationEntry entry, boolean log) {
if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(mUserTracker.getUserId())) {
if (log) mLogger.logNoPulsingSettingDisabled(entry);
return false;
}
if (mBatteryController.isAodPowerSave()) {
if (log) mLogger.logNoPulsingBatteryDisabled(entry);
return false;
}
if (!canAlertCommon(entry, log)) {
if (log) mLogger.logNoPulsingNoAlert(entry);
return false;
}
if (!canAlertHeadsUpCommon(entry, log)) {
if (log) mLogger.logNoPulsingNoAlert(entry);
return false;
}
if (entry.shouldSuppressAmbient()) {
if (log) mLogger.logNoPulsingNoAmbientEffect(entry);
return false;
}
if (entry.getRanking().getLockscreenVisibilityOverride()
== Notification.VISIBILITY_PRIVATE) {
if (log) mLogger.logNoPulsingNotificationHidden(entry);
return false;
}
if (entry.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) {
if (log) mLogger.logNoPulsingNotImportant(entry);
return false;
}
if (log) mLogger.logPulsing(entry);
return true;
}
/**
* Common checks between regular & AOD heads up and bubbles.
*
* @param entry the entry to check
* @param log whether or not to log the results of these checks
* @return true if these checks pass, false if the notification should not alert
*/
private boolean canAlertCommon(NotificationEntry entry, boolean log) {
for (int i = 0; i < mSuppressors.size(); i++) {
if (mSuppressors.get(i).suppressInterruptions(entry)) {
if (log) {
mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i),
/* awake */ false);
}
return false;
}
}
if (mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry)) {
if (log) mLogger.keyguardHideNotification(entry);
return false;
}
return true;
}
/**
* Common checks for heads up notifications on regular and AOD displays.
*
* @param entry the entry to check
* @param log whether or not to log the results of these checks
* @return true if these checks pass, false if the notification should not alert
*/
private boolean canAlertHeadsUpCommon(NotificationEntry entry, boolean log) {
StatusBarNotification sbn = entry.getSbn();
// Don't alert notifications that are suppressed due to group alert behavior
if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
if (log) mLogger.logNoAlertingGroupAlertBehavior(entry);
return false;
}
if (entry.hasJustLaunchedFullScreenIntent()) {
if (log) mLogger.logNoAlertingRecentFullscreen(entry);
return false;
}
return true;
}
/**
* Common checks between alerts that occur while the device is awake (heads up & bubbles).
*
* @param entry the entry to check
* @return true if these checks pass, false if the notification should not alert
*/
private boolean canAlertAwakeCommon(NotificationEntry entry, boolean log) {
StatusBarNotification sbn = entry.getSbn();
for (int i = 0; i < mSuppressors.size(); i++) {
if (mSuppressors.get(i).suppressAwakeInterruptions(entry)) {
if (log) {
mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ true);
}
return false;
}
}
return true;
}
private boolean isSnoozedPackage(StatusBarNotification sbn) {
return mHeadsUpManager.isSnoozed(sbn.getPackageName());
}
private boolean shouldSuppressHeadsUpWhenAwakeForOldWhen(NotificationEntry entry, boolean log) {
final Notification notification = entry.getSbn().getNotification();
if (notification == null) {
return false;
}
final long when = notification.when;
final long now = System.currentTimeMillis();
final long age = now - when;
if (age < MAX_HUN_WHEN_AGE_MS) {
return false;
}
if (when <= 0) {
// Some notifications (including many system notifications) are posted with the "when"
// field set to 0. Nothing in the Javadocs for Notification mentions a special meaning
// for a "when" of 0, but Android didn't even exist at the dawn of the Unix epoch.
// Therefore, assume that these notifications effectively don't have a "when" value,
// and don't suppress HUNs.
if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "when <= 0");
return false;
}
if (notification.fullScreenIntent != null) {
if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "full-screen intent");
return false;
}
if (notification.isForegroundService()) {
if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "foreground service");
return false;
}
if (notification.isUserInitiatedJob()) {
if (log) mLogger.logMaybeHeadsUpDespiteOldWhen(entry, when, age, "user initiated job");
return false;
}
if (log) mLogger.logNoHeadsUpOldWhen(entry, when, age);
final int uid = entry.getSbn().getUid();
final String packageName = entry.getSbn().getPackageName();
mUiEventLogger.log(NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN, uid, packageName);
return true;
}
public static final long MAX_HUN_WHEN_AGE_MS = 24 * 60 * 60 * 1000;
}