| /* |
| * 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_GROUP_ALERT_BEHAVIOR; |
| |
| 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.os.RemoteException; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.service.dreams.IDreamManager; |
| import android.service.notification.StatusBarNotification; |
| import android.util.Log; |
| |
| 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.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.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 IDreamManager mDreamManager; |
| 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; |
| |
| @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 requiring neither HUN nor keyguard") |
| FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236); |
| |
| private final int mId; |
| |
| NotificationInterruptEvent(int id) { |
| mId = id; |
| } |
| |
| @Override |
| public int getId() { |
| return mId; |
| } |
| } |
| |
| @Inject |
| public NotificationInterruptStateProviderImpl( |
| ContentResolver contentResolver, |
| PowerManager powerManager, |
| IDreamManager dreamManager, |
| AmbientDisplayConfiguration ambientDisplayConfiguration, |
| BatteryController batteryController, |
| StatusBarStateController statusBarStateController, |
| KeyguardStateController keyguardStateController, |
| HeadsUpManager headsUpManager, |
| NotificationInterruptLogger logger, |
| @Main Handler mainHandler, |
| NotifPipelineFlags flags, |
| KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, |
| UiEventLogger uiEventLogger) { |
| mContentResolver = contentResolver; |
| mPowerManager = powerManager; |
| mDreamManager = dreamManager; |
| mBatteryController = batteryController; |
| mAmbientDisplayConfiguration = ambientDisplayConfiguration; |
| mStatusBarStateController = statusBarStateController; |
| mKeyguardStateController = keyguardStateController; |
| mHeadsUpManager = headsUpManager; |
| mLogger = logger; |
| mFlags = flags; |
| mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider; |
| mUiEventLogger = uiEventLogger; |
| 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 |
| } |
| |
| @Override |
| public void addSuppressor(NotificationInterruptSuppressor suppressor) { |
| mSuppressors.add(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. |
| 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(NotificationEntry entry) { |
| if (entry.getSbn().getNotification().fullScreenIntent == null) { |
| 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 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 |
| if (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); |
| } |
| |
| // Check whether FSI requires the keyguard to be showing. |
| if (mFlags.fullScreenIntentRequiresKeyguard()) { |
| |
| // 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); |
| } |
| } |
| |
| // 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); |
| } |
| |
| // If the notification won't HUN for some other reason (DND/snooze/etc), launch FSI. |
| return getDecisionGivenSuppression(FullScreenIntentDecision.FSI_EXPECTED_NOT_TO_HUN, |
| 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: |
| return; |
| case NO_FSI_SUPPRESSED_BY_DND: |
| case NO_FSI_SUPPRESSED_ONLY_BY_DND: |
| mLogger.logNoFullscreen(entry, "Suppressed by DND"); |
| return; |
| case NO_FSI_NOT_IMPORTANT_ENOUGH: |
| mLogger.logNoFullscreen(entry, "Not important enough"); |
| 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, "GroupAlertBehavior will prevent HUN"); |
| return; |
| case FSI_DEVICE_NOT_INTERACTIVE: |
| mLogger.logFullscreen(entry, "Device is not interactive"); |
| return; |
| case FSI_DEVICE_IS_DREAMING: |
| mLogger.logFullscreen(entry, "Device is dreaming"); |
| return; |
| case FSI_KEYGUARD_SHOWING: |
| mLogger.logFullscreen(entry, "Keyguard is showing"); |
| return; |
| case NO_FSI_EXPECTED_TO_HUN: |
| mLogger.logNoFullscreen(entry, "Expected to HUN"); |
| return; |
| case FSI_KEYGUARD_OCCLUDED: |
| mLogger.logFullscreen(entry, |
| "Expected not to HUN while keyguard occluded"); |
| return; |
| case FSI_LOCKED_SHADE: |
| mLogger.logFullscreen(entry, "Keyguard is showing and not occluded"); |
| 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, "Expected not to HUN while not on keyguard"); |
| return; |
| case FSI_EXPECTED_NOT_TO_HUN: |
| mLogger.logFullscreen(entry, "Expected not to HUN"); |
| } |
| } |
| |
| private boolean isDreaming() { |
| try { |
| return mDreamManager.isDreaming(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to query dream manager.", e); |
| return false; |
| } |
| } |
| |
| 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; |
| } |
| |
| if (isSnoozedPackage(sbn)) { |
| 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() && !isDreaming(); |
| |
| if (!inUse) { |
| if (log) mLogger.logNoHeadsUpNotInUse(entry); |
| 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 (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(UserHandle.USER_CURRENT)) { |
| 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.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()); |
| } |
| } |