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

Commit f318409c authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[Flexiglass] Fixes down swipe destinations to match old impl." into main

parents b38b9f70 afaecdb3
Loading
Loading
Loading
Loading
+1 −47
Original line number Diff line number Diff line
@@ -19,61 +19,31 @@ package com.android.systemui.keyguard.ui.composable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
import com.android.systemui.qs.ui.composable.QuickSettings
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.ComposableScene
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn

/** The lock screen scene shows when the device is locked. */
@SysUISingleton
class LockscreenScene
@Inject
constructor(
    @Application private val applicationScope: CoroutineScope,
    viewModel: LockscreenSceneViewModel,
    private val lockscreenContent: Lazy<LockscreenContent>,
) : ComposableScene {
    override val key = Scenes.Lockscreen

    override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
        combine(
                viewModel.upDestinationSceneKey,
                viewModel.leftDestinationSceneKey,
                viewModel.downFromTopEdgeDestinationSceneKey,
            ) { upKey, leftKey, downFromTopEdgeKey ->
                destinationScenes(
                    up = upKey,
                    left = leftKey,
                    downFromTopEdge = downFromTopEdgeKey,
                )
            }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.Eagerly,
                initialValue =
                    destinationScenes(
                        up = viewModel.upDestinationSceneKey.value,
                        left = viewModel.leftDestinationSceneKey.value,
                        downFromTopEdge = viewModel.downFromTopEdgeDestinationSceneKey.value,
                    )
            )
        viewModel.destinationScenes

    @Composable
    override fun SceneScope.Content(
@@ -84,22 +54,6 @@ constructor(
            modifier = modifier,
        )
    }

    private fun destinationScenes(
        up: SceneKey?,
        left: SceneKey?,
        downFromTopEdge: SceneKey?,
    ): Map<UserAction, UserActionResult> {
        return buildMap {
            up?.let { this[Swipe(SwipeDirection.Up)] = UserActionResult(up) }
            left?.let { this[Swipe(SwipeDirection.Left)] = UserActionResult(left) }
            downFromTopEdge?.let {
                this[Swipe(fromSource = Edge.Top, direction = SwipeDirection.Down)] =
                    UserActionResult(downFromTopEdge)
            }
            this[Swipe(direction = SwipeDirection.Down)] = UserActionResult(Scenes.Shade)
        }
    }
}

@Composable
+97 −51
Original line number Diff line number Diff line
@@ -19,9 +19,12 @@
package com.android.systemui.keyguard.ui.viewmodel

import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -31,86 +34,129 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.startable.shadeStartable
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import platform.test.runner.parameterized.Parameter
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWith(ParameterizedAndroidJunit4::class)
class LockscreenSceneViewModelTest : SysuiTestCase() {

    companion object {
        @Parameters(
            name =
                "canSwipeToEnter={0}, downWithTwoPointers={1}, downFromEdge={2}," +
                    " isSingleShade={3}, isCommunalAvailable={4}"
        )
        @JvmStatic
        fun combinations() = buildList {
            repeat(32) { combination ->
                add(
                    arrayOf(
                        /* canSwipeToEnter= */ combination and 1 != 0,
                        /* downWithTwoPointers= */ combination and 2 != 0,
                        /* downFromEdge= */ combination and 4 != 0,
                        /* isSingleShade= */ combination and 8 != 0,
                        /* isCommunalAvailable= */ combination and 16 != 0,
                    )
                )
            }
        }

        @JvmStatic
        @BeforeClass
        fun setUp() {
            val combinationStrings =
                combinations().map { array ->
                    check(array.size == 5)
                    "${array[4]},${array[3]},${array[2]},${array[1]},${array[0]}"
                }
            val uniqueCombinations = combinationStrings.toSet()
            assertThat(combinationStrings).hasSize(uniqueCombinations.size)
        }

        private fun expectedDownDestination(
            downFromEdge: Boolean,
            isSingleShade: Boolean,
        ): SceneKey {
            return if (downFromEdge && isSingleShade) Scenes.QuickSettings else Scenes.Shade
        }
    }

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val sceneInteractor by lazy { kosmos.sceneInteractor }

    @JvmField @Parameter(0) var canSwipeToEnter: Boolean = false
    @JvmField @Parameter(1) var downWithTwoPointers: Boolean = false
    @JvmField @Parameter(2) var downFromEdge: Boolean = false
    @JvmField @Parameter(3) var isSingleShade: Boolean = true
    @JvmField @Parameter(4) var isCommunalAvailable: Boolean = false

    private val underTest by lazy { createLockscreenSceneViewModel() }

    @Test
    fun upTransitionSceneKey_canSwipeToUnlock_gone() =
    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
    fun destinationScenes() =
        testScope.runTest {
            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.None
            )
            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
            kosmos.fakeDeviceEntryRepository.setUnlocked(true)
            sceneInteractor.changeScene(Scenes.Lockscreen, "reason")

            assertThat(upTransitionSceneKey).isEqualTo(Scenes.Gone)
        }

    @Test
    fun upTransitionSceneKey_cannotSwipeToUnlock_bouncer() =
        testScope.runTest {
            val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                if (canSwipeToEnter) {
                    AuthenticationMethodModel.None
                } else {
                    AuthenticationMethodModel.Pin
                }
            )
            kosmos.fakeDeviceEntryRepository.setUnlocked(false)
            kosmos.fakeDeviceEntryRepository.setUnlocked(canSwipeToEnter)
            sceneInteractor.changeScene(Scenes.Lockscreen, "reason")

            assertThat(upTransitionSceneKey).isEqualTo(Scenes.Bouncer)
            kosmos.shadeRepository.setShadeMode(
                if (isSingleShade) {
                    ShadeMode.Single
                } else {
                    ShadeMode.Split
                }
            )
            kosmos.setCommunalAvailable(isCommunalAvailable)

    @EnableFlags(FLAG_COMMUNAL_HUB)
    @Test
    fun leftTransitionSceneKey_communalIsAvailable_communal() =
        testScope.runTest {
            val leftDestinationSceneKey by collectLastValue(underTest.leftDestinationSceneKey)
            assertThat(leftDestinationSceneKey).isNull()
            val destinationScenes by collectLastValue(underTest.destinationScenes)

            kosmos.setCommunalAvailable(true)
            runCurrent()
            assertThat(leftDestinationSceneKey).isEqualTo(Scenes.Communal)
        }
            assertThat(
                    destinationScenes
                        ?.get(
                            Swipe(
                                SwipeDirection.Down,
                                fromSource = Edge.Top.takeIf { downFromEdge },
                                pointerCount = if (downWithTwoPointers) 2 else 1,
                            )
                        )
                        ?.toScene
                )
                .isEqualTo(
                    expectedDownDestination(
                        downFromEdge = downFromEdge,
                        isSingleShade = isSingleShade,
                    )
                )

    @Test
    fun downFromTopEdgeDestinationSceneKey_whenNotSplitShade_quickSettings() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, false)
            kosmos.shadeStartable.start()
            val sceneKey by collectLastValue(underTest.downFromTopEdgeDestinationSceneKey)
            assertThat(sceneKey).isEqualTo(Scenes.QuickSettings)
        }
            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
                .isEqualTo(if (canSwipeToEnter) Scenes.Gone else Scenes.Bouncer)

    @Test
    fun downFromTopEdgeDestinationSceneKey_whenSplitShade_null() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, true)
            kosmos.shadeStartable.start()
            val sceneKey by collectLastValue(underTest.downFromTopEdgeDestinationSceneKey)
            assertThat(sceneKey).isNull()
            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Left))?.toScene)
                .isEqualTo(Scenes.Communal.takeIf { isCommunalAvailable })
        }

    private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel {
+14 −15
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.internal.R
import com.android.internal.util.EmergencyAffordanceManager
import com.android.internal.util.emergencyAffordanceManager
@@ -317,8 +316,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
    @Test
    fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() =
        testScope.runTest {
            val upDestinationSceneKey by
                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
            emulateUserDrivenTransition(
                to = upDestinationSceneKey,
@@ -337,8 +336,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
        testScope.runTest {
            setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)

            val upDestinationSceneKey by
                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
            emulateUserDrivenTransition(
                to = upDestinationSceneKey,
@@ -356,7 +355,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
            emulateUserDrivenTransition(to = Scenes.Shade)
            assertCurrentScene(Scenes.Shade)

            val upDestinationSceneKey = destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene
            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Lockscreen)
            emulateUserDrivenTransition(
                to = upDestinationSceneKey,
@@ -379,7 +378,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
            emulateUserDrivenTransition(to = Scenes.Shade)
            assertCurrentScene(Scenes.Shade)

            val upDestinationSceneKey = destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene
            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
            emulateUserDrivenTransition(
                to = upDestinationSceneKey,
@@ -447,8 +446,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
    fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() =
        testScope.runTest {
            unlockDevice()
            val upDestinationSceneKey by
                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
        }

@@ -469,8 +468,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
    fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() =
        testScope.runTest {
            setAuthMethod(AuthenticationMethodModel.Password)
            val upDestinationSceneKey by
                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
            emulateUserDrivenTransition(
                to = upDestinationSceneKey,
@@ -487,8 +486,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
    fun bouncerActionButtonClick_opensEmergencyServicesDialer() =
        testScope.runTest {
            setAuthMethod(AuthenticationMethodModel.Password)
            val upDestinationSceneKey by
                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
            emulateUserDrivenTransition(to = upDestinationSceneKey)

@@ -507,8 +506,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
        testScope.runTest {
            setAuthMethod(AuthenticationMethodModel.Password)
            startPhoneCall()
            val upDestinationSceneKey by
                collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey)
            val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes)
            val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
            assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
            emulateUserDrivenTransition(to = upDestinationSceneKey)

+66 −27
Original line number Diff line number Diff line
@@ -14,9 +14,15 @@
 * limitations under the License.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.keyguard.ui.viewmodel

import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -27,9 +33,10 @@ import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn

/** Models UI state and handles user input for the lockscreen scene. */
@@ -44,37 +51,69 @@ constructor(
    val longPress: KeyguardLongPressViewModel,
    val notifications: NotificationsPlaceholderViewModel,
) {
    /** The key of the scene we should switch to when swiping up. */
    val upDestinationSceneKey: StateFlow<SceneKey> =
        deviceEntryInteractor.isUnlocked
            .map { isUnlocked -> upDestinationSceneKey(isUnlocked) }
    val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
        combine(
                deviceEntryInteractor.isUnlocked,
                communalInteractor.isCommunalAvailable,
                shadeInteractor.shadeMode,
            ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
                destinationScenes(
                    isDeviceUnlocked = isDeviceUnlocked,
                    isCommunalAvailable = isCommunalAvailable,
                    shadeMode = shadeMode,
                )
            }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = upDestinationSceneKey(deviceEntryInteractor.isUnlocked.value),
                initialValue =
                    destinationScenes(
                        isDeviceUnlocked = deviceEntryInteractor.isUnlocked.value,
                        isCommunalAvailable = false,
                        shadeMode = shadeInteractor.shadeMode.value,
                    ),
            )

    private fun upDestinationSceneKey(isUnlocked: Boolean): SceneKey {
        return if (isUnlocked) Scenes.Gone else Scenes.Bouncer
    private fun destinationScenes(
        isDeviceUnlocked: Boolean,
        isCommunalAvailable: Boolean,
        shadeMode: ShadeMode,
    ): Map<UserAction, UserActionResult> {
        val quickSettingsIfSingleShade =
            if (shadeMode is ShadeMode.Single) {
                Scenes.QuickSettings
            } else {
                Scenes.Shade
            }

    /** The key of the scene we should switch to when swiping left. */
    val leftDestinationSceneKey: StateFlow<SceneKey?> =
        communalInteractor.isCommunalAvailable
            .map { available -> if (available) Scenes.Communal else null }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = null,
        return mapOf(
                Swipe.Left to UserActionResult(Scenes.Communal).takeIf { isCommunalAvailable },
                Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer,

                // Swiping down from the top edge goes to QS (or shade if in split shade mode).
                swipeDownFromTop(pointerCount = 1) to quickSettingsIfSingleShade,
                swipeDownFromTop(pointerCount = 2) to quickSettingsIfSingleShade,

                // Swiping down, not from the edge, always navigates to the shade scene.
                swipeDown(pointerCount = 1) to Scenes.Shade,
                swipeDown(pointerCount = 2) to Scenes.Shade,
            )
            .filterValues { it != null }
            .mapValues { checkNotNull(it.value) }
    }

    /** The key of the scene we should switch to when swiping down from the top edge. */
    val downFromTopEdgeDestinationSceneKey: StateFlow<SceneKey?> =
        shadeInteractor.shadeMode
            .map { shadeMode -> Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single } }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = null,
    private fun swipeDownFromTop(pointerCount: Int): Swipe {
        return Swipe(
            SwipeDirection.Down,
            fromSource = Edge.Top,
            pointerCount = pointerCount,
        )
    }

    private fun swipeDown(pointerCount: Int): Swipe {
        return Swipe(
            SwipeDirection.Down,
            pointerCount = pointerCount,
        )
    }
}