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

Commit afaecdb3 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[Flexiglass] Fixes down swipe destinations to match old impl.

When locked, regardless of how many fingers are used, when swiping down
from the top edge of the display, navigates to the quick settings scene,
if not in split shade mode (in which case it goes to the shade scene).

Test: updated unit tests
Test: manually verified by swiping down from the top-edge with one and
with two fingers, when locked and unlocked. Also verified that dragging
down with one finger, not from the top edge, when locked - still goes to
the shade when split shade and when single shade.
Fix: 328812365
Bug: 328473018
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT

Change-Id: I5fd81f7aae3f6f15e97ebfdf2728e119544b9549
parent ed1a8280
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,
        )
    }
}