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

Commit 03488720 authored by Darrell Shi's avatar Darrell Shi
Browse files

Add scene transitions from Dream to Shade and Bouncer

Test: atest DreamUserActionsViewModelTest
Test: verified both scene transitions work on device
Fix: 365997099
Fix: 365999644
Flag: com.android.systemui.scene_container
Change-Id: I897c63cb9b8b9b7d922d6cf7a0a8418c02b80d98
parent fc463999
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -15,7 +15,9 @@ import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransit
import com.android.systemui.scene.ui.composable.transitions.bouncerToLockscreenPreview
import com.android.systemui.scene.ui.composable.transitions.communalToBouncerTransition
import com.android.systemui.scene.ui.composable.transitions.communalToShadeTransition
import com.android.systemui.scene.ui.composable.transitions.dreamToBouncerTransition
import com.android.systemui.scene.ui.composable.transitions.dreamToGoneTransition
import com.android.systemui.scene.ui.composable.transitions.dreamToShadeTransition
import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition
@@ -55,7 +57,9 @@ val SceneContainerTransitions = transitions {
    // Scene transitions

    from(Scenes.Bouncer, to = Scenes.Gone) { bouncerToGoneTransition() }
    from(Scenes.Dream, to = Scenes.Bouncer) { dreamToBouncerTransition() }
    from(Scenes.Dream, to = Scenes.Gone) { dreamToGoneTransition() }
    from(Scenes.Dream, to = Scenes.Shade) { dreamToShadeTransition() }
    from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
    from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() }
    from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
+23 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.scene.ui.composable.transitions

import com.android.compose.animation.scene.TransitionBuilder

fun TransitionBuilder.dreamToBouncerTransition() {
    toBouncerTransition()
}
+23 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.scene.ui.composable.transitions

import com.android.compose.animation.scene.TransitionBuilder

fun TransitionBuilder.dreamToShadeTransition(durationScale: Double = 1.0) {
    toShadeTransition(durationScale = durationScale)
}
+229 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.dreams.ui.viewmodel

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.Swipe
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
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.TransitionKeys.ToSplitShade
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
class DreamUserActionsViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private lateinit var underTest: DreamUserActionsViewModel

    @Before
    fun setUp() {
        underTest = kosmos.dreamUserActionsViewModel
        underTest.activateIn(testScope)
    }

    @Test
    @DisableFlags(DualShade.FLAG_NAME)
    fun actions_singleShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Single,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))

            setUpState(
                isShadeTouchable = false,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Single,
            )
            assertThat(actions).isEmpty()

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = true,
                shadeMode = ShadeMode.Single,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
        }

    @Test
    @DisableFlags(DualShade.FLAG_NAME)
    fun actions_splitShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Split,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))

            setUpState(
                isShadeTouchable = false,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Split,
            )
            assertThat(actions).isEmpty()

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = true,
                shadeMode = ShadeMode.Split,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
        }

    @Test
    @EnableFlags(DualShade.FLAG_NAME)
    fun actions_dualShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Dual,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(
                    UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
                )

            setUpState(
                isShadeTouchable = false,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Dual,
            )
            assertThat(actions).isEmpty()

            setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual)
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(
                    UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
                )
        }

    private fun TestScope.setUpState(
        isShadeTouchable: Boolean,
        isDeviceUnlocked: Boolean,
        shadeMode: ShadeMode,
    ) {
        if (isShadeTouchable) {
            kosmos.powerInteractor.setAwakeForTest()
        } else {
            kosmos.powerInteractor.setAsleepForTest()
        }

        if (isDeviceUnlocked) {
            unlockDevice()
        } else {
            lockDevice()
        }

        if (shadeMode == ShadeMode.Dual) {
            assertThat(DualShade.isEnabled).isTrue()
        } else {
            assertThat(DualShade.isEnabled).isFalse()
            kosmos.fakeShadeRepository.setShadeLayoutWide(shadeMode == ShadeMode.Split)
        }
        runCurrent()
    }

    private fun TestScope.lockDevice() {
        val deviceUnlockStatus by
            collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)

        kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
        assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
        kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
        runCurrent()
    }

    private fun TestScope.unlockDevice() {
        val deviceUnlockStatus by
            collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)

        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
            SuccessFingerprintAuthenticationStatus(0, true)
        )
        assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
        kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason")
        runCurrent()
    }
}
+49 −2
Original line number Diff line number Diff line
@@ -16,17 +16,64 @@

package com.android.systemui.dreams.ui.viewmodel

import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.shade.ui.viewmodel.dualShadeActions
import com.android.systemui.shade.ui.viewmodel.singleShadeActions
import com.android.systemui.shade.ui.viewmodel.splitShadeActions
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

/** Handles user input for the dream scene. */
class DreamUserActionsViewModel @AssistedInject constructor() : UserActionsViewModel() {
class DreamUserActionsViewModel
@AssistedInject
constructor(
    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
    private val shadeInteractor: ShadeInteractor,
) : UserActionsViewModel() {

    override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
        setActions(emptyMap())
        shadeInteractor.isShadeTouchable
            .flatMapLatestConflated { isShadeTouchable ->
                if (!isShadeTouchable) {
                    flowOf(emptyMap())
                } else {
                    combine(
                        deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked },
                        shadeInteractor.shadeMode,
                    ) { isDeviceUnlocked, shadeMode ->
                        buildList {
                                val bouncerOrGone =
                                    if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
                                add(Swipe.Up to bouncerOrGone)

                                // "Home" is either Dream, Lockscreen, or Gone.
                                add(Swipe.End to SceneFamilies.Home)

                                addAll(
                                    when (shadeMode) {
                                        ShadeMode.Single -> singleShadeActions()
                                        ShadeMode.Split -> splitShadeActions()
                                        ShadeMode.Dual -> dualShadeActions()
                                    }
                                )
                            }
                            .associate { it }
                    }
                }
            }
            .collect { setActions(it) }
    }

    @AssistedFactory
Loading