diff options
4 files changed, 279 insertions, 5 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 63a4fd2189d8..7945470b424a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -85,6 +85,8 @@ import com.android.systemui.statusbar.notification.collection.inflation.Notifica import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper; +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; import com.android.systemui.statusbar.notification.people.PeopleHubModule; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent; @@ -116,16 +118,16 @@ import com.android.systemui.wallet.dagger.WalletModule; import com.android.systemui.wmshell.BubblesManager; import com.android.wm.shell.bubbles.Bubbles; -import java.util.Optional; -import java.util.concurrent.Executor; - -import javax.inject.Named; - import dagger.Binds; import dagger.BindsOptionalOf; import dagger.Module; import dagger.Provides; +import java.util.Optional; +import java.util.concurrent.Executor; + +import javax.inject.Named; + /** * A dagger module for injecting components of System UI that are required by System UI. * @@ -315,4 +317,11 @@ public abstract class SystemUIModule { @Binds abstract LargeScreenShadeInterpolator largeScreensShadeInterpolator( LargeScreenShadeInterpolatorImpl impl); + + @SysUISingleton + @Provides + static VisualInterruptionDecisionProvider provideVisualInterruptionDecisionProvider( + NotificationInterruptStateProvider innerProvider) { + return new NotificationInterruptStateProviderWrapper(innerProvider); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt new file mode 100644 index 000000000000..f2216fce6fef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt @@ -0,0 +1,75 @@ +/* + * 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 com.android.internal.annotations.VisibleForTesting +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.Decision +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision + +/** + * Wraps a [NotificationInterruptStateProvider] to convert it to the new + * [VisualInterruptionDecisionProvider] interface. + */ +@SysUISingleton +class NotificationInterruptStateProviderWrapper( + private val wrapped: NotificationInterruptStateProvider +) : VisualInterruptionDecisionProvider { + + @VisibleForTesting + enum class DecisionImpl(override val shouldInterrupt: Boolean) : Decision { + SHOULD_INTERRUPT(shouldInterrupt = true), + SHOULD_NOT_INTERRUPT(shouldInterrupt = false); + + companion object { + fun of(booleanDecision: Boolean) = + if (booleanDecision) SHOULD_INTERRUPT else SHOULD_NOT_INTERRUPT + } + } + + @VisibleForTesting + class FullScreenIntentDecisionImpl( + val originalEntry: NotificationEntry, + val originalDecision: NotificationInterruptStateProvider.FullScreenIntentDecision + ) : FullScreenIntentDecision { + override val shouldInterrupt = originalDecision.shouldLaunch + override val wouldInterruptWithoutDnd = originalDecision == NO_FSI_SUPPRESSED_ONLY_BY_DND + } + + override fun addSuppressor(suppressor: NotificationInterruptSuppressor) { + wrapped.addSuppressor(suppressor) + } + + override fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision = + wrapped.checkHeadsUp(entry, /* log= */ false).let { DecisionImpl.of(it) } + + override fun makeAndLogHeadsUpDecision(entry: NotificationEntry): Decision = + wrapped.checkHeadsUp(entry, /* log= */ true).let { DecisionImpl.of(it) } + + override fun makeUnloggedFullScreenIntentDecision(entry: NotificationEntry) = + wrapped.getFullScreenIntentDecision(entry).let { FullScreenIntentDecisionImpl(entry, it) } + + override fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) { + (decision as FullScreenIntentDecisionImpl).let { + wrapped.logFullScreenIntentDecision(it.originalEntry, it.originalDecision) + } + } + + override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision = + wrapped.shouldBubbleUp(entry).let { DecisionImpl.of(it) } +} 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 new file mode 100644 index 000000000000..c0f4fcda56bb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt @@ -0,0 +1,112 @@ +/* + * 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 com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** + * Decides whether a notification should visually interrupt the user in various ways. + * + * These include displaying the notification as heads-up (peeking while the device is awake or + * pulsing while the device is dozing), displaying the notification as a bubble, and launching a + * full-screen intent for the notification. + */ +interface VisualInterruptionDecisionProvider { + /** + * Represents the decision to visually interrupt or not. + * + * Used for heads-up and bubble decisions; subclassed by [FullScreenIntentDecision] for + * full-screen intent decisions. + * + * @property[shouldInterrupt] whether a visual interruption should be triggered + */ + interface Decision { + val shouldInterrupt: Boolean + } + + /** + * Represents the decision to launch a full-screen intent for a notification or not. + * + * @property[wouldInterruptWithoutDnd] whether a full-screen intent should not be launched only + * because Do Not Disturb has suppressed it + */ + interface FullScreenIntentDecision : Decision { + val wouldInterruptWithoutDnd: Boolean + } + + /** + * Adds a [component][suppressor] that can suppress visual interruptions. + * + * This class may call suppressors in any order. + * + * @param[suppressor] the suppressor to add + */ + fun addSuppressor(suppressor: NotificationInterruptSuppressor) + + /** + * Decides whether a [notification][entry] should display as heads-up or not, but does not log + * that decision. + * + * @param[entry] the notification that this decision is about + * @return the decision to display that notification as heads-up or not + */ + fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision + + /** + * Decides whether a [notification][entry] should display as heads-up or not, and logs that + * decision. + * + * If the device is awake, the decision will consider whether the notification should "peek" + * (slide in from the top of the screen over the current activity). + * + * If the device is dozing, the decision will consider whether the notification should "pulse" + * (wake the screen up and display the ambient view of the notification). + * + * @see[makeUnloggedHeadsUpDecision] + * + * @param[entry] the notification that this decision is about + * @return the decision to display that notification as heads-up or not + */ + fun makeAndLogHeadsUpDecision(entry: NotificationEntry): Decision + + /** + * Decides whether a [notification][entry] should launch a full-screen intent or not, but does + * not log that decision. + * + * The returned decision can be logged by passing it to [logFullScreenIntentDecision]. + * + * @see[makeAndLogHeadsUpDecision] + * + * @param[entry] the notification that this decision is about + * @return the decision to launch a full-screen intent for that notification or not + */ + fun makeUnloggedFullScreenIntentDecision(entry: NotificationEntry): FullScreenIntentDecision + + /** + * Logs a previous [decision] to launch a full-screen intent or not. + * + * @param[decision] the decision to log + */ + fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) + + /** + * Decides whether a [notification][entry] should display as a bubble or not. + * + * @param[entry] the notification that this decision is about + * @return the decision to display that notification as a bubble or not + */ + fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision +} 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 new file mode 100644 index 000000000000..cbb08946a1b0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt @@ -0,0 +1,78 @@ +package com.android.systemui.statusbar.notification.interruption + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_NOT_IMPORTANT_ENOUGH +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_BY_DND +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationInterruptStateProviderWrapperTest : SysuiTestCase() { + + @Test + fun decisionOfTrue() { + assertTrue(DecisionImpl.of(true).shouldInterrupt) + } + + @Test + fun decisionOfFalse() { + assertFalse(DecisionImpl.of(false).shouldInterrupt) + } + + @Test + fun decisionOfTrueInterned() { + assertEquals(DecisionImpl.of(true), DecisionImpl.of(true)) + } + + @Test + fun decisionOfFalseInterned() { + assertEquals(DecisionImpl.of(false), DecisionImpl.of(false)) + } + + @Test + fun fullScreenIntentDecisionShouldInterrupt() { + makeFsiDecision(FSI_DEVICE_NOT_INTERACTIVE).let { + assertTrue(it.shouldInterrupt) + assertFalse(it.wouldInterruptWithoutDnd) + } + } + + @Test + fun fullScreenIntentDecisionShouldNotInterrupt() { + makeFsiDecision(NO_FSI_NOT_IMPORTANT_ENOUGH).let { + assertFalse(it.shouldInterrupt) + assertFalse(it.wouldInterruptWithoutDnd) + } + } + + @Test + fun fullScreenIntentDecisionWouldInterruptWithoutDnd() { + makeFsiDecision(NO_FSI_SUPPRESSED_ONLY_BY_DND).let { + assertFalse(it.shouldInterrupt) + assertTrue(it.wouldInterruptWithoutDnd) + } + } + + @Test + fun fullScreenIntentDecisionWouldNotInterruptEvenWithoutDnd() { + makeFsiDecision(NO_FSI_SUPPRESSED_BY_DND).let { + assertFalse(it.shouldInterrupt) + assertFalse(it.wouldInterruptWithoutDnd) + } + } + + private fun makeFsiDecision(originalDecision: FullScreenIntentDecision) = + FullScreenIntentDecisionImpl(NotificationEntryBuilder().build(), originalDecision) +} |