Loading packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +1 −47 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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 Loading packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +97 −51 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +14 −15 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading @@ -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, Loading @@ -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, Loading @@ -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, Loading Loading @@ -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) } Loading @@ -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, Loading @@ -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) Loading @@ -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) Loading packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +66 −27 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. */ Loading @@ -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, ) } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +1 −47 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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 Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +97 −51 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +14 −15 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading @@ -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, Loading @@ -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, Loading @@ -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, Loading Loading @@ -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) } Loading @@ -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, Loading @@ -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) Loading @@ -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) Loading
packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +66 −27 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. */ Loading @@ -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, ) } }