Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 11c49171 authored by Danny Burakov's avatar Danny Burakov Committed by Android (Google) Code Review
Browse files

Merge "[Dual Shade] Blur the notifications shade when the bouncer is open." into main

parents c92559ee 71f503bf
Loading
Loading
Loading
Loading
+14 −3
Original line number Diff line number Diff line
@@ -16,11 +16,17 @@

package com.android.systemui.notifications.ui.composable

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
@@ -29,7 +35,6 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.compose.lifecycle.DisposableEffectWithLifecycle
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.media.remedia.ui.compose.Media
import com.android.systemui.media.remedia.ui.compose.MediaPresentationStyle
@@ -56,7 +61,6 @@ constructor(
    private val contentViewModelFactory: NotificationsShadeOverlayContentViewModel.Factory,
    private val shadeSession: SaveableSession,
    private val stackScrollView: Lazy<NotificationScrollView>,
    private val keyguardClockViewModel: KeyguardClockViewModel,
    private val jankMonitor: InteractionJankMonitor,
) : Overlay {
    override val key = Overlays.NotificationsShade
@@ -92,11 +96,18 @@ constructor(

        val isFullWidth = isFullWidthShade()

        val targetBlurRadiusPx: Float by
            remember(layoutState) {
                derivedStateOf { viewModel.calculateTargetBlurRadius(layoutState.transitionState) }
            }
        val animatedBlurRadiusPx: Float by
            animateFloatAsState(targetValue = targetBlurRadiusPx, label = "NSOverlay-blurRadius")

        OverlayShade(
            panelElement = NotificationsShade.Elements.Panel,
            alignmentOnWideScreens = viewModel.alignmentOnWideScreens,
            enableTransparency = viewModel.isTransparencyEnabled,
            modifier = modifier,
            modifier = modifier.blur(with(LocalDensity.current) { animatedBlurRadiusPx.toDp() }),
            onScrimClicked = viewModel::onScrimClicked,
            onBackgroundPlaced = { bounds, _, _ -> viewModel.onShadeOverlayBoundsChanged(bounds) },
            header = {
+115 −77
Original line number Diff line number Diff line
@@ -27,17 +27,20 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.geometry.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.systemui.Flags.FLAG_NOTIFICATION_SHADE_BLUR
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.domain.interactor.AuthenticationResult
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.desktop.domain.interactor.DesktopInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.ui.transitions.blurConfig
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
@@ -70,16 +73,11 @@ import com.android.systemui.testKosmos
import com.android.systemui.window.data.repository.fakeWindowRootViewBlurRepository
import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@@ -87,21 +85,19 @@ import org.junit.runner.RunWith
class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope
    private val sceneInteractor by lazy { kosmos.sceneInteractor }
    private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel }
    private val Kosmos.underTest by Fixture { notificationsShadeOverlayContentViewModel }

    @Before
    fun setUp() {
        kosmos.sceneContainerStartable.start()
        kosmos.enableDualShade()
        kosmos.runCurrent()
    fun setUp() =
        with(kosmos) {
            sceneContainerStartable.start()
            enableDualShade()
            underTest.activateIn(testScope)
        }

    @Test
    fun showHeader_desktopStatusBarDisabled_true() =
        testScope.runTest {
        kosmos.runTest {
            setUseDesktopStatusBar(false)
            assertThat(underTest.showHeader).isTrue()
        }
@@ -109,7 +105,7 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
    @Test
    @EnableFlags(StatusBarForDesktop.FLAG_NAME)
    fun showHeader_desktopStatusBarEnabled_statusBarForDesktopEnabled_false() =
        testScope.runTest {
        kosmos.runTest {
            setUseDesktopStatusBar(true)
            assertThat(underTest.showHeader).isFalse()
        }
@@ -117,7 +113,7 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
    @Test
    @DisableFlags(StatusBarForDesktop.FLAG_NAME)
    fun showHeader_desktopStatusBarEnabled_statusBarForDesktopDisabled_true() =
        testScope.runTest {
        kosmos.runTest {
            setUseDesktopStatusBar(true)
            assertThat(underTest.showHeader).isTrue()
        }
@@ -125,32 +121,29 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
    @Test
    @DisableFlags(StatusBarForDesktop.FLAG_NAME)
    fun alignmentOnWideScreens_statusBarForDesktopDisabled_topStart() =
        testScope.runTest {
            assertThat(kosmos.createTestInstance().alignmentOnWideScreens)
                .isEqualTo(Alignment.TopStart)
        kosmos.runTest {
            assertThat(createTestInstance().alignmentOnWideScreens).isEqualTo(Alignment.TopStart)
        }

    @Test
    @EnableFlags(StatusBarForDesktop.FLAG_NAME)
    fun alignmentOnWideScreens_configDisabled_statusBarForDesktopEnabled_topStart() =
        testScope.runTest {
        kosmos.runTest {
            overrideConfig(R.bool.config_notificationShadeOnTopEnd, false)
            assertThat(kosmos.createTestInstance().alignmentOnWideScreens)
                .isEqualTo(Alignment.TopStart)
            assertThat(createTestInstance().alignmentOnWideScreens).isEqualTo(Alignment.TopStart)
        }

    @Test
    @EnableFlags(StatusBarForDesktop.FLAG_NAME)
    fun alignmentOnWideScreens_configEnabled_statusBarForDesktopEnabled_topEnd() =
        testScope.runTest {
        kosmos.runTest {
            overrideConfig(R.bool.config_notificationShadeOnTopEnd, true)
            assertThat(kosmos.createTestInstance().alignmentOnWideScreens)
                .isEqualTo(Alignment.TopEnd)
            assertThat(createTestInstance().alignmentOnWideScreens).isEqualTo(Alignment.TopEnd)
        }

    @Test
    fun onScrimClicked_hidesShade() =
        testScope.runTest {
        kosmos.runTest {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
@@ -162,7 +155,7 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {

    @Test
    fun deviceLocked_hidesShade() =
        testScope.runTest {
        kosmos.runTest {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            unlockDevice()
            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
@@ -175,9 +168,9 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {

    @Test
    fun shadeNotTouchable_hidesShade() =
        testScope.runTest {
        kosmos.runTest {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            val isShadeTouchable by collectLastValue(kosmos.shadeInteractor.isShadeTouchable)
            val isShadeTouchable by collectLastValue(shadeInteractor.isShadeTouchable)
            assertThat(isShadeTouchable).isTrue()
            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
@@ -189,40 +182,36 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {

    @Test
    fun showMedia_activeMedia_true() =
        testScope.runTest {
            kosmos.mediaPipelineRepository.addCurrentUserMediaEntry(MediaData(active = true))
            runCurrent()
        kosmos.runTest {
            mediaPipelineRepository.addCurrentUserMediaEntry(MediaData(active = true))

            assertThat(underTest.showMedia).isTrue()
        }

    @Test
    fun showMedia_InactiveMedia_false() =
        testScope.runTest {
            kosmos.mediaPipelineRepository.addCurrentUserMediaEntry(MediaData(active = false))
            runCurrent()
        kosmos.runTest {
            mediaPipelineRepository.addCurrentUserMediaEntry(MediaData(active = false))

            assertThat(underTest.showMedia).isFalse()
        }

    @Test
    fun showMedia_noMedia_false() =
        testScope.runTest {
            kosmos.mediaPipelineRepository.addCurrentUserMediaEntry(MediaData(active = true))
            kosmos.mediaPipelineRepository.clearCurrentUserMedia()
            runCurrent()
        kosmos.runTest {
            mediaPipelineRepository.addCurrentUserMediaEntry(MediaData(active = true))
            mediaPipelineRepository.clearCurrentUserMedia()

            assertThat(underTest.showMedia).isFalse()
        }

    @Test
    fun showMedia_qsDisabled_false() =
        testScope.runTest {
            kosmos.mediaPipelineRepository.addCurrentUserMediaEntry(MediaData(active = true))
            kosmos.fakeDisableFlagsRepository.disableFlags.update {
        kosmos.runTest {
            mediaPipelineRepository.addCurrentUserMediaEntry(MediaData(active = true))
            fakeDisableFlagsRepository.disableFlags.update {
                it.copy(disable2 = DISABLE2_QUICK_SETTINGS)
            }
            runCurrent()

            assertThat(underTest.showMedia).isFalse()
        }
@@ -230,9 +219,8 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
    @Test
    @DisableFlags(FLAG_NOTIFICATION_SHADE_BLUR)
    fun transparencyEnabled_shadeBlurFlagOff_isDisabled() =
        testScope.runTest {
            kosmos.fakeWindowRootViewBlurRepository.isBlurSupported.value = true
            runCurrent()
        kosmos.runTest {
            fakeWindowRootViewBlurRepository.isBlurSupported.value = true

            assertThat(underTest.isTransparencyEnabled).isFalse()
        }
@@ -240,9 +228,8 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
    @Test
    @EnableFlags(FLAG_NOTIFICATION_SHADE_BLUR)
    fun transparencyEnabled_shadeBlurFlagOn_blurSupported_isEnabled() =
        testScope.runTest {
            kosmos.fakeWindowRootViewBlurRepository.isBlurSupported.value = true
            runCurrent()
        kosmos.runTest {
            fakeWindowRootViewBlurRepository.isBlurSupported.value = true

            assertThat(underTest.isTransparencyEnabled).isTrue()
        }
@@ -250,16 +237,72 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
    @Test
    @EnableFlags(FLAG_NOTIFICATION_SHADE_BLUR)
    fun transparencyEnabled_shadeBlurFlagOn_blurUnsupported_isDisabled() =
        testScope.runTest {
            kosmos.fakeWindowRootViewBlurRepository.isBlurSupported.value = false
            runCurrent()
        kosmos.runTest {
            fakeWindowRootViewBlurRepository.isBlurSupported.value = false

            assertThat(underTest.isTransparencyEnabled).isFalse()
        }

    @Test
    @EnableFlags(FLAG_NOTIFICATION_SHADE_BLUR)
    fun calculateTargetBlurRadius() =
        kosmos.runTest {
            // Only bouncer shown: no blur.
            fakeWindowRootViewBlurRepository.isBlurSupported.value = true
            assertThat(
                    underTest.calculateTargetBlurRadius(
                        transitionState =
                            TransitionState.Idle(
                                currentScene = Scenes.Lockscreen,
                                currentOverlays = setOf(Overlays.Bouncer),
                            )
                    )
                )
                .isEqualTo(0f)

            // Notifications shade and bouncer shown: apply blur.
            assertThat(
                    underTest.calculateTargetBlurRadius(
                        transitionState =
                            TransitionState.Idle(
                                currentScene = Scenes.Lockscreen,
                                currentOverlays =
                                    setOf(Overlays.Bouncer, Overlays.NotificationsShade),
                            )
                    )
                )
                .isEqualTo(blurConfig.maxBlurRadiusPx)

            // No bouncer shown: no blur.
            assertThat(
                    underTest.calculateTargetBlurRadius(
                        transitionState =
                            TransitionState.Idle(
                                currentScene = Scenes.Lockscreen,
                                currentOverlays = setOf(Overlays.NotificationsShade),
                            )
                    )
                )
                .isEqualTo(0)

            // Blur not supported: no blur.
            fakeWindowRootViewBlurRepository.isBlurSupported.value = false
            assertThat(
                    underTest.calculateTargetBlurRadius(
                        transitionState =
                            TransitionState.Idle(
                                currentScene = Scenes.Lockscreen,
                                currentOverlays =
                                    setOf(Overlays.Bouncer, Overlays.NotificationsShade),
                            )
                    )
                )
                .isEqualTo(0f)
        }

    @Test
    fun shadeModeChanged_single_switchesToShadeScene() =
        testScope.runTest {
        kosmos.runTest {
            val currentScene by collectLastValue(sceneInteractor.currentScene)
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

@@ -275,35 +318,34 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {

    @Test
    fun shadeModeChanged_split_switchesToShadeScene() =
        testScope.runTest {
        kosmos.runTest {
            val currentScene by collectLastValue(sceneInteractor.currentScene)
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            kosmos.enableDualShade()
            kosmos.shadeInteractor.expandNotificationsShade("test")
            enableDualShade()
            shadeInteractor.expandNotificationsShade("test")
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays).contains(Overlays.NotificationsShade)

            kosmos.enableSplitShade()
            enableSplitShade()
            assertThat(currentScene).isEqualTo(Scenes.Shade)
            assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
        }

    private fun TestScope.lockDevice() {
    private fun Kosmos.lockDevice() {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        kosmos.powerInteractor.setAsleepForTest()
        runCurrent()
        powerInteractor.setAsleepForTest()

        assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
    }

    private fun setUseDesktopStatusBar(enable: Boolean) {
    private fun Kosmos.setUseDesktopStatusBar(enable: Boolean) {
        overrideConfig(R.bool.config_useDesktopStatusBar, enable)
        kosmos.configurationController.onConfigurationChanged(Configuration())
        configurationController.onConfigurationChanged(Configuration())
    }

    private fun overrideConfig(configId: Int, value: Boolean) {
        kosmos.testableContext.orCreateTestableResources.addOverride(configId, value)
    private fun Kosmos.overrideConfig(configId: Int, value: Boolean) {
        testableContext.orCreateTestableResources.addOverride(configId, value)
    }

    private fun Kosmos.createTestInstance(): NotificationsShadeOverlayContentViewModel {
@@ -324,19 +366,15 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
            mediaCarouselInteractor = mediaCarouselInteractor,
            windowRootViewBlurInteractor = windowRootViewBlurInteractor,
            desktopInteractor = desktopInteractor,
            blurConfig = blurConfig,
            mediaViewModelFactory = mediaViewModelFactory,
        )
    }

    private suspend fun TestScope.unlockDevice() {
    private suspend fun Kosmos.unlockDevice() {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        kosmos.powerInteractor.setAwakeForTest()
        runCurrent()
        assertThat(
                kosmos.authenticationInteractor.authenticate(
                    FakeAuthenticationRepository.DEFAULT_PIN
                )
            )
        powerInteractor.setAwakeForTest()
        assertThat(authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
            .isEqualTo(AuthenticationResult.SUCCEEDED)

        assertThat(currentScene).isEqualTo(Scenes.Gone)
@@ -344,9 +382,9 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {

    @Test
    fun onShadeBoundsChanged_forwardsToShadeOverlayInteractor() =
        testScope.runTest {
        kosmos.runTest {
            var shadeBounds: android.graphics.Rect? = null
            kosmos.shadeInteractor.addShadeOverlayBoundsListener { shadeBounds = it }
            shadeInteractor.addShadeOverlayBoundsListener { shadeBounds = it }
            assertThat(shadeBounds).isNull()

            val bounds = Rect(0f, 0f, 100f, 100f)
+20 −1
Original line number Diff line number Diff line
@@ -21,9 +21,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.geometry.Rect
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.systemui.Flags
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.desktop.domain.interactor.DesktopInteractor
import com.android.systemui.keyguard.ui.transitions.BlurConfig
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
@@ -31,6 +33,7 @@ import com.android.systemui.media.remedia.ui.compose.MediaUiBehavior
import com.android.systemui.media.remedia.ui.viewmodel.MediaCarouselVisibility
import com.android.systemui.media.remedia.ui.viewmodel.MediaViewModel
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
@@ -65,13 +68,14 @@ constructor(
    @Main private val mainDispatcher: CoroutineDispatcher,
    val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
    val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
    private val desktopInteractor: DesktopInteractor,
    desktopInteractor: DesktopInteractor,
    val sceneInteractor: SceneInteractor,
    private val shadeInteractor: ShadeInteractor,
    private val shadeModeInteractor: ShadeModeInteractor,
    disableFlagsInteractor: DisableFlagsInteractor,
    private val mediaCarouselInteractor: MediaCarouselInteractor,
    val mediaViewModelFactory: MediaViewModel.Factory,
    private val blurConfig: BlurConfig,
    windowRootViewBlurInteractor: WindowRootViewBlurInteractor,
) : ExclusiveActivatable() {

@@ -126,6 +130,21 @@ constructor(
                },
        )

    /**
     * Calculates the blur radius to apply to the overlay.
     *
     * @param transitionState The current transition state of the scene (from its `ContentScope`)
     * @return The blur radius to apply to the scene UI, in pixels.
     */
    fun calculateTargetBlurRadius(transitionState: TransitionState): Float {
        return when {
            !isTransparencyEnabled -> 0f
            Overlays.NotificationsShade !in transitionState.currentOverlays -> 0f
            Overlays.Bouncer in transitionState.currentOverlays -> blurConfig.maxBlurRadiusPx
            else -> 0f
        }
    }

    val alignmentOnWideScreens =
        if (desktopInteractor.isNotificationShadeOnTopEnd) Alignment.TopEnd else Alignment.TopStart

+2 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.shade.ui.viewmodel

import com.android.systemui.desktop.domain.interactor.desktopInteractor
import com.android.systemui.keyguard.ui.transitions.blurConfig
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
@@ -41,6 +42,7 @@ val Kosmos.notificationsShadeOverlayContentViewModel:
        shadeModeInteractor = shadeModeInteractor,
        disableFlagsInteractor = disableFlagsInteractor,
        mediaCarouselInteractor = mediaCarouselInteractor,
        blurConfig = blurConfig,
        windowRootViewBlurInteractor = windowRootViewBlurInteractor,
        desktopInteractor = desktopInteractor,
        mediaViewModelFactory = mediaViewModelFactory,