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

Commit 7a859403 authored by burakov's avatar burakov
Browse files

[bc25] Handle transitions involving overlays.

This also removes most of the remaining references to
SceneFamilies.NotifShade and SceneFamilies.QuickSettings, which will be
deleted soon.

Bug: 359173565
Bug: 356596436
Flag: com.android.systemui.scene_container
Flag: com.android.systemui.dual_shade
Test: Manually. On a locked device, I double pressed the power button to
 show the camera app on top of the lockscreen. Pulled down one shade and
 then the other, verified that the scene container becomes visible and
 the shade overlays are shown on top of the camera app. Collapsed both,
 verified that the scene container became invisible again.
Test: Added new unit tests.
Test: Existing unit tests still pass.
Change-Id: I5e00be81c7ab29aab0e4a7fd2ddf7438c1178278
parent 48cdc816
Loading
Loading
Loading
Loading
+6 −10
Original line number Diff line number Diff line
@@ -48,17 +48,13 @@ import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS
import com.android.systemui.scene.shared.model.Scenes

object QuickSettings {
    private val SCENES =
        setOf(
            Scenes.QuickSettings,
            Scenes.Shade,
        )
    private val SCENES = setOf(Scenes.QuickSettings, Scenes.Shade)

    object Elements {
        val Content =
            MovableElementKey(
                "QuickSettingsContent",
                contentPicker = MovableElementContentPicker(SCENES)
                contentPicker = MovableElementContentPicker(SCENES),
            )
        val QuickQuickSettings = ElementKey("QuickQuickSettings")
        val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
@@ -87,7 +83,7 @@ object QuickSettings {

private fun SceneScope.stateForQuickSettingsContent(
    isSplitShade: Boolean,
    squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default }
    squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default },
): QSSceneAdapter.State {
    return when (val transitionState = layoutState.transitionState) {
        is TransitionState.Idle -> {
@@ -122,7 +118,7 @@ private fun SceneScope.stateForQuickSettingsContent(
                }
            }
        is TransitionState.Transition.OverlayTransition ->
            TODO("b/359173565: Handle overlay transitions")
            error("Bad transition for QuickSettings scene: overlays not supported")
    }
}

@@ -172,7 +168,7 @@ fun SceneScope.QuickSettings(
                val height = heightProvider().coerceAtLeast(0)

                layout(placeable.width, height) { placeable.placeRelative(0, 0) }
            }
            },
    ) {
        content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
    }
@@ -225,7 +221,7 @@ private fun QuickSettingsContent(
                            it.addView(view)
                        }
                    },
                    onRelease = { it.removeAllViews() }
                    onRelease = { it.removeAllViews() },
                )
            }
        }
+137 −15
Original line number Diff line number Diff line
@@ -18,9 +18,12 @@

package com.android.systemui.scene.domain.interactor

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -29,8 +32,10 @@ import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.sceneDataSource
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
@@ -63,10 +68,11 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() {
    private val sceneDataSource =
        kosmos.sceneDataSource.apply { changeScene(toScene = Scenes.Lockscreen) }

    private val underTest = kosmos.sceneContainerOcclusionInteractor
    private val underTest by lazy { kosmos.sceneContainerOcclusionInteractor }

    @Test
    fun invisibleDueToOcclusion() =
    @DisableFlags(DualShade.FLAG_NAME)
    fun invisibleDueToOcclusion_dualShadeDisabled() =
        testScope.runTest {
            val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion)
            val keyguardState by collectLastValue(keyguardTransitionInteractor.currentKeyguardState)
@@ -126,6 +132,68 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() {
                .isFalse()
        }

    @Test
    @EnableFlags(DualShade.FLAG_NAME)
    fun invisibleDueToOcclusion_dualShadeEnabled() =
        testScope.runTest {
            val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion)
            val keyguardState by collectLastValue(keyguardTransitionInteractor.currentKeyguardState)

            // Assert that we have the desired preconditions:
            assertThat(keyguardState).isEqualTo(KeyguardState.LOCKSCREEN)
            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
            assertThat(sceneInteractor.transitionState.value)
                .isEqualTo(ObservableTransitionState.Idle(Scenes.Lockscreen))
            assertWithMessage("Should start unoccluded").that(invisibleDueToOcclusion).isFalse()

            // Actual testing starts here:
            showOccludingActivity()
            assertWithMessage("Should become occluded when occluding activity is shown")
                .that(invisibleDueToOcclusion)
                .isTrue()

            transitionIntoAod {
                assertWithMessage("Should become unoccluded when transitioning into AOD")
                    .that(invisibleDueToOcclusion)
                    .isFalse()
            }
            assertWithMessage("Should stay unoccluded when in AOD")
                .that(invisibleDueToOcclusion)
                .isFalse()

            transitionOutOfAod {
                assertWithMessage("Should remain unoccluded while transitioning away from AOD")
                    .that(invisibleDueToOcclusion)
                    .isFalse()
            }
            assertWithMessage("Should become occluded now that no longer in AOD")
                .that(invisibleDueToOcclusion)
                .isTrue()

            expandDualShade {
                assertWithMessage("Should become unoccluded once shade begins to expand")
                    .that(invisibleDueToOcclusion)
                    .isFalse()
            }
            assertWithMessage("Should be unoccluded when shade is fully expanded")
                .that(invisibleDueToOcclusion)
                .isFalse()

            collapseDualShade {
                assertWithMessage("Should remain unoccluded while shade is collapsing")
                    .that(invisibleDueToOcclusion)
                    .isFalse()
            }
            assertWithMessage("Should become occluded now that shade is fully collapsed")
                .that(invisibleDueToOcclusion)
                .isTrue()

            hideOccludingActivity()
            assertWithMessage("Should become unoccluded once the occluding activity is hidden")
                .that(invisibleDueToOcclusion)
                .isFalse()
        }

    /** Simulates the appearance of a show-when-locked `Activity` in the foreground. */
    private fun TestScope.showOccludingActivity() {
        keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
@@ -138,15 +206,13 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() {
    /** Simulates the disappearance of a show-when-locked `Activity` from the foreground. */
    private fun TestScope.hideOccludingActivity() {
        keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
            showWhenLockedActivityOnTop = false,
            showWhenLockedActivityOnTop = false
        )
        runCurrent()
    }

    /** Simulates a user-driven gradual expansion of the shade. */
    private fun TestScope.expandShade(
        assertMidTransition: () -> Unit = {},
    ) {
    private fun TestScope.expandShade(assertMidTransition: () -> Unit = {}) {
        val progress = MutableStateFlow(0f)
        mutableTransitionState.value =
            ObservableTransitionState.Transition(
@@ -170,10 +236,41 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() {
        runCurrent()
    }

    /** Simulates a user-driven gradual expansion of the dual shade (notifications). */
    private fun TestScope.expandDualShade(assertMidTransition: () -> Unit = {}) {
        val progress = MutableStateFlow(0f)
        mutableTransitionState.value =
            ShowOrHideOverlay(
                overlay = Overlays.NotificationsShade,
                fromContent = sceneDataSource.currentScene.value,
                toContent = Overlays.NotificationsShade,
                currentScene = sceneDataSource.currentScene.value,
                currentOverlays = sceneDataSource.currentOverlays,
                progress = progress,
                isInitiatedByUserInput = true,
                isUserInputOngoing = flowOf(true),
                previewProgress = flowOf(0f),
                isInPreviewStage = flowOf(false),
            )
        runCurrent()

        progress.value = 0.5f
        runCurrent()
        assertMidTransition()

        progress.value = 1f
        runCurrent()

        mutableTransitionState.value =
            ObservableTransitionState.Idle(
                sceneDataSource.currentScene.value,
                setOf(Overlays.NotificationsShade),
            )
        runCurrent()
    }

    /** Simulates a user-driven gradual collapse of the shade. */
    private fun TestScope.collapseShade(
        assertMidTransition: () -> Unit = {},
    ) {
    private fun TestScope.collapseShade(assertMidTransition: () -> Unit = {}) {
        val progress = MutableStateFlow(0f)
        mutableTransitionState.value =
            ObservableTransitionState.Transition(
@@ -197,10 +294,37 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() {
        runCurrent()
    }

    /** Simulates a user-driven gradual collapse of the dual shade (notifications). */
    private fun TestScope.collapseDualShade(assertMidTransition: () -> Unit = {}) {
        val progress = MutableStateFlow(0f)
        mutableTransitionState.value =
            ShowOrHideOverlay(
                overlay = Overlays.NotificationsShade,
                fromContent = Overlays.NotificationsShade,
                toContent = Scenes.Lockscreen,
                currentScene = Scenes.Lockscreen,
                currentOverlays = flowOf(setOf(Overlays.NotificationsShade)),
                progress = progress,
                isInitiatedByUserInput = true,
                isUserInputOngoing = flowOf(true),
                previewProgress = flowOf(0f),
                isInPreviewStage = flowOf(false),
            )
        runCurrent()

        progress.value = 0.5f
        runCurrent()
        assertMidTransition()

        progress.value = 1f
        runCurrent()

        mutableTransitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
        runCurrent()
    }

    /** Simulates a transition into AOD. */
    private suspend fun TestScope.transitionIntoAod(
        assertMidTransition: () -> Unit = {},
    ) {
    private suspend fun TestScope.transitionIntoAod(assertMidTransition: () -> Unit = {}) {
        val currentKeyguardState = keyguardTransitionInteractor.getCurrentState()
        keyguardTransitionRepository.sendTransitionStep(
            TransitionStep(
@@ -235,9 +359,7 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() {
    }

    /** Simulates a transition away from AOD. */
    private suspend fun TestScope.transitionOutOfAod(
        assertMidTransition: () -> Unit = {},
    ) {
    private suspend fun TestScope.transitionOutOfAod(assertMidTransition: () -> Unit = {}) {
        keyguardTransitionRepository.sendTransitionStep(
            TransitionStep(
                from = KeyguardState.AOD,
+48 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ package com.android.systemui.scene.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -38,6 +39,7 @@ import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
import com.android.systemui.scene.overlayKeys
import com.android.systemui.scene.sceneContainerConfig
import com.android.systemui.scene.sceneKeys
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
@@ -256,7 +258,7 @@ class SceneInteractorTest : SysuiTestCase() {
        }

    @Test
    fun transitioningTo() =
    fun transitioningTo_sceneChange() =
        testScope.runTest {
            val transitionState =
                MutableStateFlow<ObservableTransitionState>(
@@ -292,6 +294,51 @@ class SceneInteractorTest : SysuiTestCase() {
            assertThat(transitionTo).isNull()
        }

    @Test
    fun transitioningTo_overlayChange() =
        testScope.runTest {
            val transitionState =
                MutableStateFlow<ObservableTransitionState>(
                    ObservableTransitionState.Idle(underTest.currentScene.value)
                )
            underTest.setTransitionState(transitionState)

            val transitionTo by collectLastValue(underTest.transitioningTo)
            assertThat(transitionTo).isNull()

            underTest.showOverlay(Overlays.NotificationsShade, "reason")
            assertThat(transitionTo).isNull()

            val progress = MutableStateFlow(0f)
            transitionState.value =
                ShowOrHideOverlay(
                    overlay = Overlays.NotificationsShade,
                    fromContent = underTest.currentScene.value,
                    toContent = Overlays.NotificationsShade,
                    currentScene = underTest.currentScene.value,
                    currentOverlays = underTest.currentOverlays,
                    progress = progress,
                    isInitiatedByUserInput = true,
                    isUserInputOngoing = flowOf(true),
                    previewProgress = flowOf(0f),
                    isInPreviewStage = flowOf(false),
                )
            assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)

            progress.value = 0.5f
            assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)

            progress.value = 1f
            assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)

            transitionState.value =
                ObservableTransitionState.Idle(
                    currentScene = underTest.currentScene.value,
                    currentOverlays = setOf(Overlays.NotificationsShade),
                )
            assertThat(transitionTo).isNull()
        }

    @Test
    fun isTransitionUserInputOngoing_idle_false() =
        testScope.runTest {
+128 −0
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ package com.android.systemui.shade.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -32,6 +34,7 @@ import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintA
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
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.scene.shared.model.fakeSceneDataSource
import com.android.systemui.testKosmos
@@ -123,6 +126,63 @@ class PanelExpansionInteractorImplTest : SysuiTestCase() {
            assertThat(panelExpansion).isEqualTo(1f)
        }

    @Test
    @EnableSceneContainer
    fun legacyPanelExpansion_dualShade_whenIdle_whenLocked() =
        testScope.runTest {
            underTest = kosmos.panelExpansionInteractorImpl
            val panelExpansion by collectLastValue(underTest.legacyPanelExpansion)

            changeScene(Scenes.Lockscreen) { assertThat(panelExpansion).isEqualTo(1f) }
            assertThat(panelExpansion).isEqualTo(1f)

            changeScene(Scenes.Bouncer) { assertThat(panelExpansion).isEqualTo(1f) }
            assertThat(panelExpansion).isEqualTo(1f)

            showOverlay(Overlays.NotificationsShade) { assertThat(panelExpansion).isEqualTo(1f) }
            assertThat(panelExpansion).isEqualTo(1f)

            showOverlay(Overlays.QuickSettingsShade) { assertThat(panelExpansion).isEqualTo(1f) }
            assertThat(panelExpansion).isEqualTo(1f)

            changeScene(Scenes.Communal) { assertThat(panelExpansion).isEqualTo(1f) }
            assertThat(panelExpansion).isEqualTo(1f)
        }

    @Test
    @EnableSceneContainer
    fun legacyPanelExpansion_dualShade_whenIdle_whenUnlocked() =
        testScope.runTest {
            underTest = kosmos.panelExpansionInteractorImpl
            val unlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
                SuccessFingerprintAuthenticationStatus(0, true)
            )
            runCurrent()

            assertThat(unlockStatus)
                .isEqualTo(DeviceUnlockStatus(true, DeviceUnlockSource.Fingerprint))

            val panelExpansion by collectLastValue(underTest.legacyPanelExpansion)

            changeScene(Scenes.Gone) { assertThat(panelExpansion).isEqualTo(0f) }
            assertThat(panelExpansion).isEqualTo(0f)

            showOverlay(Overlays.NotificationsShade) { progress ->
                assertThat(panelExpansion).isEqualTo(progress)
            }
            assertThat(panelExpansion).isEqualTo(1f)

            showOverlay(Overlays.QuickSettingsShade) {
                // Notification shade is already expanded, so moving to QS shade should also be 1f.
                assertThat(panelExpansion).isEqualTo(1f)
            }
            assertThat(panelExpansion).isEqualTo(1f)

            changeScene(Scenes.Communal) { assertThat(panelExpansion).isEqualTo(1f) }
            assertThat(panelExpansion).isEqualTo(1f)
        }

    @Test
    @EnableSceneContainer
    fun shouldHideStatusBarIconsWhenExpanded_goneScene() =
@@ -193,4 +253,72 @@ class PanelExpansionInteractorImplTest : SysuiTestCase() {

        assertThat(currentScene).isEqualTo(toScene)
    }

    private fun TestScope.showOverlay(
        toOverlay: OverlayKey,
        assertDuringProgress: ((progress: Float) -> Unit) = {},
    ) {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
        val progressFlow = MutableStateFlow(0f)
        transitionState.value =
            if (checkNotNull(currentOverlays).isEmpty()) {
                ShowOrHideOverlay(
                    overlay = toOverlay,
                    fromContent = checkNotNull(currentScene),
                    toContent = toOverlay,
                    currentScene = checkNotNull(currentScene),
                    currentOverlays = flowOf(emptySet()),
                    progress = progressFlow,
                    isInitiatedByUserInput = true,
                    isUserInputOngoing = flowOf(true),
                    previewProgress = flowOf(0f),
                    isInPreviewStage = flowOf(false),
                )
            } else {
                ObservableTransitionState.Transition.ReplaceOverlay(
                    fromOverlay = checkNotNull(currentOverlays).first(),
                    toOverlay = toOverlay,
                    currentScene = checkNotNull(currentScene),
                    currentOverlays = flowOf(emptySet()),
                    progress = progressFlow,
                    isInitiatedByUserInput = true,
                    isUserInputOngoing = flowOf(true),
                    previewProgress = flowOf(0f),
                    isInPreviewStage = flowOf(false),
                )
            }
        runCurrent()
        assertDuringProgress(progressFlow.value)

        progressFlow.value = 0.2f
        runCurrent()
        assertDuringProgress(progressFlow.value)

        progressFlow.value = 0.6f
        runCurrent()
        assertDuringProgress(progressFlow.value)

        progressFlow.value = 1f
        runCurrent()
        assertDuringProgress(progressFlow.value)

        transitionState.value =
            ObservableTransitionState.Idle(
                currentScene = checkNotNull(currentScene),
                currentOverlays = setOf(toOverlay),
            )
        if (checkNotNull(currentOverlays).isEmpty()) {
            fakeSceneDataSource.showOverlay(toOverlay)
        } else {
            fakeSceneDataSource.replaceOverlay(
                from = checkNotNull(currentOverlays).first(),
                to = toOverlay,
            )
        }
        runCurrent()
        assertDuringProgress(progressFlow.value)

        assertThat(currentOverlays).containsExactly(toOverlay)
    }
}
+13 −10
Original line number Diff line number Diff line
@@ -16,13 +16,14 @@

package com.android.systemui.scene.domain.interactor

import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -117,28 +118,30 @@ constructor(
    private val ObservableTransitionState.canBeOccluded: Boolean
        get() =
            when (this) {
                is ObservableTransitionState.Idle -> currentScene.canBeOccluded
                is ObservableTransitionState.Transition.ChangeScene ->
                    fromScene.canBeOccluded && toScene.canBeOccluded
                is ObservableTransitionState.Transition.ReplaceOverlay,
                is ObservableTransitionState.Transition.ShowOrHideOverlay ->
                    TODO("b/359173565: Handle overlay transitions")
                is ObservableTransitionState.Idle ->
                    currentOverlays.all { it.canBeOccluded } && currentScene.canBeOccluded
                is ObservableTransitionState.Transition ->
                    // TODO(b/356596436): Should also verify currentOverlays.isEmpty(), but
                    //  currentOverlays is a Flow and we need a state.
                    fromContent.canBeOccluded && toContent.canBeOccluded
            }

    /**
     * Whether the scene can be occluded by a "show when locked" activity. Some scenes should, on
     * Whether the content can be occluded by a "show when locked" activity. Some content should, on
     * principle not be occlude-able because they render as if they are expanding on top of the
     * occluding activity.
     */
    private val SceneKey.canBeOccluded: Boolean
    private val ContentKey.canBeOccluded: Boolean
        get() =
            when (this) {
                Overlays.NotificationsShade -> false
                Overlays.QuickSettingsShade -> false
                Scenes.Bouncer -> false
                Scenes.Communal -> true
                Scenes.Gone -> true
                Scenes.Lockscreen -> true
                Scenes.QuickSettings -> false
                Scenes.Shade -> false
                else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!")
                else -> error("ContentKey \"$this\" doesn't have a mapping for canBeOccluded!")
            }
}
Loading