diff options
4 files changed, 178 insertions, 140 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 96520b21cc72..7acb4d5498db 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -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 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 19950a5fb89d..2fd2ef1f3240 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -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( - AuthenticationMethodModel.Pin + if (canSwipeToEnter) { + AuthenticationMethodModel.None + } else { + AuthenticationMethodModel.Pin + } ) - kosmos.fakeDeviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(canSwipeToEnter) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + kosmos.shadeRepository.setShadeMode( + if (isSingleShade) { + ShadeMode.Single + } else { + ShadeMode.Split + } + ) + kosmos.setCommunalAvailable(isCommunalAvailable) - assertThat(upTransitionSceneKey).isEqualTo(Scenes.Bouncer) - } - - @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 { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 42c33544416d..af9abcda73fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -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) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt index 288ef3c52e21..993e81bfbf69 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -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, - /** 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, + // 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) } + } + + 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, + ) + } } |