diff options
6 files changed, 169 insertions, 19 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt new file mode 100644 index 000000000000..75e04841d4b0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 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 android.database.ContentObserver +import android.hardware.display.AmbientDisplayConfiguration +import android.os.Handler +import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED +import android.provider.Settings.Global.HEADS_UP_OFF +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.util.settings.GlobalSettings + +class PeekDisabledSuppressor( + private val globalSettings: GlobalSettings, + private val headsUpManager: HeadsUpManager, + private val logger: NotificationInterruptLogger, + @Main private val mainHandler: Handler, +) : VisualInterruptionCondition(types = setOf(PEEK), reason = "peek setting disabled") { + private var isEnabled = false + + override fun shouldSuppress(): Boolean = !isEnabled + + override fun start() { + val observer = + object : ContentObserver(mainHandler) { + override fun onChange(selfChange: Boolean) { + val wasEnabled = isEnabled + + isEnabled = + globalSettings.getInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_OFF) != + HEADS_UP_OFF + + // QQQ: Do we want to log this even if it hasn't changed? + logger.logHeadsUpFeatureChanged(isEnabled) + + // QQQ: Is there a better place for this side effect? What if HeadsUpManager + // registered for it directly? + if (wasEnabled && !isEnabled) { + logger.logWillDismissAll() + headsUpManager.releaseAllImmediately() + } + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor(HEADS_UP_NOTIFICATIONS_ENABLED), + /* notifyForDescendants = */ true, + observer + ) + + // QQQ: Do we need to register for SETTING_HEADS_UP_TICKER? It seems unused. + + observer.onChange(/* selfChange = */ true) + } +} + +class PulseDisabledSuppressor( + private val ambientDisplayConfiguration: AmbientDisplayConfiguration, + private val userTracker: UserTracker, +) : VisualInterruptionCondition(types = setOf(PULSE), reason = "pulse setting disabled") { + override fun shouldSuppress(): Boolean = + !ambientDisplayConfiguration.pulseOnNotificationEnabled(userTracker.userId) +} + +class PulseBatterySaverSuppressor(private val batteryController: BatteryController) : + VisualInterruptionCondition( + types = setOf(PULSE), + reason = "pulsing disabled by battery saver" + ) { + override fun shouldSuppress() = batteryController.isAodPowerSave() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt index 7ead4bf3ed66..da8474e92629 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt @@ -52,6 +52,13 @@ interface VisualInterruptionDecisionProvider { } /** + * Initializes the provider. + * + * Must be called before any method except [addLegacySuppressor]. + */ + fun start() {} + + /** * Adds a [component][suppressor] that can suppress visual interruptions. * * This class may call suppressors in any order. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index ba0604410f12..bae713486be5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -48,6 +48,18 @@ constructor( private val systemClock: SystemClock, private val userTracker: UserTracker, ) : VisualInterruptionDecisionProvider { + private var started = false + + override fun start() { + check(!started) + + addCondition(PeekDisabledSuppressor(globalSettings, headsUpManager, logger, mainHandler)) + addCondition(PulseDisabledSuppressor(ambientDisplayConfiguration, userTracker)) + addCondition(PulseBatterySaverSuppressor(batteryController)) + + started = true + } + private class DecisionImpl( override val shouldInterrupt: Boolean, override val logReason: String @@ -76,27 +88,33 @@ constructor( fun addCondition(condition: VisualInterruptionCondition) { conditions.add(condition) + condition.start() } fun addFilter(filter: VisualInterruptionFilter) { filters.add(filter) + filter.start() } override fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision { + check(started) return makeHeadsUpDecision(entry) } override fun makeAndLogHeadsUpDecision(entry: NotificationEntry): Decision { + check(started) return makeHeadsUpDecision(entry).also { logHeadsUpDecision(entry, it) } } override fun makeUnloggedFullScreenIntentDecision( entry: NotificationEntry ): FullScreenIntentDecision { + check(started) return makeFullScreenDecision(entry) } override fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) { + check(started) val decisionImpl = decision as? FullScreenIntentDecisionImpl ?: run { @@ -112,6 +130,7 @@ constructor( } override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision { + check(started) return makeBubbleDecision(entry).also { logBubbleDecision(entry, it) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt index d524637443cb..39199df37bd4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt @@ -51,6 +51,12 @@ sealed interface VisualInterruptionSuppressor { /** An optional UiEvent ID to be recorded when this suppresses an interruption. */ val uiEventId: UiEventEnum? + + /** + * Called after the suppressor is added to the [VisualInterruptionDecisionProvider] but before + * any other methods are called on the suppressor. + */ + fun start() {} } /** A reason why visual interruptions might be suppressed regardless of the notification. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt index 21de73ac4c7b..1d2055ec2e30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt @@ -38,23 +38,22 @@ class NotificationInterruptStateProviderWrapperTest : VisualInterruptionDecision override val provider by lazy { NotificationInterruptStateProviderWrapper( NotificationInterruptStateProviderImpl( - powerManager, - ambientDisplayConfiguration, - batteryController, - statusBarStateController, - keyguardStateController, - headsUpManager, - logger, - mainHandler, - flags, - keyguardNotificationVisibilityProvider, - uiEventLogger, - userTracker, - deviceProvisionedController, - systemClock, - globalSettings, - ) - .also { it.mUseHeadsUp = true } + powerManager, + ambientDisplayConfiguration, + batteryController, + statusBarStateController, + keyguardStateController, + headsUpManager, + logger, + mainHandler, + flags, + keyguardNotificationVisibilityProvider, + uiEventLogger, + userTracker, + deviceProvisionedController, + systemClock, + globalSettings, + ) ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt index c0aaa3670ace..551119436c7f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt @@ -30,9 +30,10 @@ import android.content.Intent import android.content.pm.UserInfo import android.graphics.drawable.Icon import android.hardware.display.FakeAmbientDisplayConfiguration -import android.os.Handler +import android.os.Looper import android.os.PowerManager import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED +import android.provider.Settings.Global.HEADS_UP_OFF import android.provider.Settings.Global.HEADS_UP_ON import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase @@ -54,6 +55,7 @@ import com.android.systemui.util.settings.FakeGlobalSettings import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.utils.leaks.FakeBatteryController import com.android.systemui.utils.leaks.LeakCheckedTest +import com.android.systemui.utils.os.FakeHandler import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import org.junit.Before @@ -73,7 +75,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { mock() protected val keyguardStateController: KeyguardStateController = mock() protected val logger: NotificationInterruptLogger = mock() - protected val mainHandler: Handler = mock() + protected val mainHandler = FakeHandler(Looper.getMainLooper()) protected val powerManager: PowerManager = mock() protected val statusBarStateController = FakeStatusBarStateController() protected val systemClock = FakeSystemClock() @@ -108,6 +110,8 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { whenever(keyguardNotificationVisibilityProvider.shouldHideNotification(any())) .thenReturn(false) + + provider.start() } @Test @@ -117,6 +121,12 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } @Test + fun testShouldNotPeek_settingDisabled() { + ensurePeekState { hunSettingEnabled = false } + assertShouldNotHeadsUp(buildPeekEntry()) + } + + @Test fun testShouldPeek_defaultLegacySuppressor() { ensurePeekState() provider.addLegacySuppressor(neverSuppresses) @@ -179,6 +189,18 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } @Test + fun testShouldNotPulse_disabled() { + ensurePulseState { pulseOnNotificationsEnabled = false } + assertShouldNotHeadsUp(buildPulseEntry()) + } + + @Test + fun testShouldNotPulse_batterySaver() { + ensurePulseState { isAodPowerSave = true } + assertShouldNotHeadsUp(buildPulseEntry()) + } + + @Test fun testShouldBubble() { ensureBubbleState() assertShouldBubble(buildBubbleEntry()) @@ -231,6 +253,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } private data class State( + var hunSettingEnabled: Boolean? = null, var hunSnoozed: Boolean? = null, var isAodPowerSave: Boolean? = null, var isDozing: Boolean? = null, @@ -244,6 +267,11 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { private fun setState(state: State): Unit = state.run { + hunSettingEnabled?.let { + val newSetting = if (it) HEADS_UP_ON else HEADS_UP_OFF + globalSettings.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, newSetting) + } + hunSnoozed?.let { whenever(headsUpManager.isSnoozed(TEST_PACKAGE)).thenReturn(it) } isAodPowerSave?.let { batteryController.setIsAodPowerSave(it) } @@ -277,6 +305,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { .run(this::setState) private fun ensurePeekState(block: State.() -> Unit = {}) = ensureState { + hunSettingEnabled = true hunSnoozed = false isDozing = false isDreaming = false |