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

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

Merge "[bc25] Handle transitions involving overlays." into main

parents 4fefeb0c 7a859403
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