Loading packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +43 −1 Original line number Diff line number Diff line Loading @@ -27,6 +27,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.time.SystemClock import javax.inject.Inject Loading @@ -42,6 +44,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch Loading @@ -57,6 +60,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val userRepository: UserRepository, private val keyguardRepository: KeyguardRepository, sceneInteractor: SceneInteractor, private val clock: SystemClock, ) { /** Loading Loading @@ -93,7 +97,7 @@ constructor( repository.isUnlocked, authenticationMethod, ) { isUnlocked, authenticationMethod -> authenticationMethod is DomainLayerAuthenticationMethodModel.None || isUnlocked !authenticationMethod.isSecure || isUnlocked } .stateIn( scope = applicationScope, Loading @@ -101,6 +105,44 @@ constructor( initialValue = true, ) /** * Whether the lockscreen has been dismissed (by any method). This can be false even when the * device is unlocked, e.g. when swipe to unlock is enabled. * * Note: * - `false` doesn't mean the lockscreen is visible (it may be occluded or covered by other UI). * - `true` doesn't mean the lockscreen is invisible (since this state changes before the * transition occurs). */ private val isLockscreenDismissed = sceneInteractor.desiredScene .map { it.key } .filter { currentScene -> currentScene == SceneKey.Gone || currentScene == SceneKey.Lockscreen } .map { it == SceneKey.Gone } .distinctUntilChanged() /** * Whether it's currently possible to swipe up to dismiss the lockscreen without requiring * authentication. This returns false whenever the lockscreen has been dismissed. * * Note: `true` doesn't mean the lockscreen is visible. It may be occluded or covered by other * UI. */ val canSwipeToDismiss = combine(authenticationMethod, isLockscreenDismissed) { authenticationMethod, isLockscreenDismissed -> authenticationMethod is DomainLayerAuthenticationMethodModel.Swipe && !isLockscreenDismissed } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = false, ) /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */ val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling Loading packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +22 −29 Original line number Diff line number Diff line Loading @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.R import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.domain.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon Loading @@ -27,7 +26,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneKey import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map Loading @@ -53,14 +51,15 @@ constructor( ) /** The key of the scene we should switch to when swiping up. */ val upDestinationSceneKey: Flow<SceneKey> = authenticationInteractor.authenticationMethod.map { authenticationMethod -> if (authenticationMethod is AuthenticationMethodModel.Swipe) { SceneKey.Gone } else { SceneKey.Bouncer } } val upDestinationSceneKey = authenticationInteractor.canSwipeToDismiss .map { canSwipeToDismiss -> upDestinationSceneKey(canSwipeToDismiss) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = upDestinationSceneKey(authenticationInteractor.canSwipeToDismiss.value), ) /** Notifies that the lock button on the lock screen was clicked. */ fun onLockButtonClicked() { Loading @@ -73,30 +72,24 @@ constructor( } private fun upDestinationSceneKey( isSwipeToUnlockEnabled: Boolean, canSwipeToDismiss: Boolean, ): SceneKey { return if (isSwipeToUnlockEnabled) SceneKey.Gone else SceneKey.Bouncer return if (canSwipeToDismiss) SceneKey.Gone else SceneKey.Bouncer } private fun lockIcon( isUnlocked: Boolean, ): Icon { return Icon.Resource( res = if (isUnlocked) { R.drawable.ic_device_lock_off } else { R.drawable.ic_device_lock_on }, contentDescription = ContentDescription.Resource( res = if (isUnlocked) { R.string.accessibility_unlock_button } else { R.string.accessibility_lock_icon } return if (isUnlocked) { Icon.Resource( R.drawable.ic_device_lock_off, ContentDescription.Resource(R.string.accessibility_unlock_button) ) } else { Icon.Resource( R.drawable.ic_device_lock_on, ContentDescription.Resource(R.string.accessibility_lock_icon) ) } } } packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +155 −108 Original line number Diff line number Diff line Loading @@ -27,6 +27,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationPatternCoo import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds Loading @@ -46,9 +48,11 @@ class AuthenticationInteractorTest : SysuiTestCase() { private val utils = SceneTestUtils(this) private val testScope = utils.testScope private val repository: AuthenticationRepository = utils.authenticationRepository() private val sceneInteractor = utils.sceneInteractor() private val underTest = utils.authenticationInteractor( repository = repository, sceneInteractor = sceneInteractor, ) @Test Loading @@ -75,10 +79,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { val authMethod by collectLastValue(underTest.authenticationMethod) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) utils.authenticationRepository.setLockscreenEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(true) } assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.Swipe) assertThat(underTest.getAuthenticationMethod()) Loading @@ -91,10 +95,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { val authMethod by collectLastValue(underTest.authenticationMethod) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) utils.authenticationRepository.setLockscreenEnabled(false) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(false) } assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.None) assertThat(underTest.getAuthenticationMethod()) Loading @@ -104,51 +108,87 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun isUnlocked_whenAuthMethodIsNoneAndLockscreenDisabled_isTrue() = testScope.runTest { utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) utils.authenticationRepository.setLockscreenEnabled(false) val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(false) // Toggle isUnlocked, twice. // // This is done because the underTest.isUnlocked flow doesn't receive values from // just changing the state above; the actual isUnlocked state needs to change to // cause the logic under test to "pick up" the current state again. // // It is done twice to make sure that we don't actually change the isUnlocked // state from what it originally was. utils.authenticationRepository.setUnlocked( !utils.authenticationRepository.isUnlocked.value ) // It is done twice to make sure that we don't actually change the isUnlocked state // from what it originally was. setUnlocked(!utils.authenticationRepository.isUnlocked.value) runCurrent() utils.authenticationRepository.setUnlocked( !utils.authenticationRepository.isUnlocked.value ) setUnlocked(!utils.authenticationRepository.isUnlocked.value) runCurrent() } assertThat(isUnlocked).isTrue() } @Test fun isUnlocked_whenAuthMethodIsNoneAndLockscreenEnabled_isFalse() = fun isUnlocked_whenAuthMethodIsNoneAndLockscreenEnabled_isTrue() = testScope.runTest { utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) utils.authenticationRepository.setLockscreenEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(true) } val isUnlocked by collectLastValue(underTest.isUnlocked) assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isTrue() } @Test fun canSwipeToDismiss_onLockscreenWithSwipe_isTrue() = testScope.runTest { utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(true) } switchToScene(SceneKey.Lockscreen) val canSwipeToDismiss by collectLastValue(underTest.canSwipeToDismiss) assertThat(canSwipeToDismiss).isTrue() } @Test fun canSwipeToDismiss_onLockscreenWithPin_isFalse() = testScope.runTest { utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setLockscreenEnabled(true) } switchToScene(SceneKey.Lockscreen) val canSwipeToDismiss by collectLastValue(underTest.canSwipeToDismiss) assertThat(canSwipeToDismiss).isFalse() } @Test fun canSwipeToDismiss_afterLockscreenDismissedInSwipeMode_isFalse() = testScope.runTest { utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(true) } switchToScene(SceneKey.Lockscreen) switchToScene(SceneKey.Gone) val canSwipeToDismiss by collectLastValue(underTest.canSwipeToDismiss) assertThat(canSwipeToDismiss).isFalse() } @Test fun isAuthenticationRequired_lockedAndSecured_true() = testScope.runTest { utils.authenticationRepository.setUnlocked(false) utils.authenticationRepository.apply { setUnlocked(false) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Password ) setAuthenticationMethod(DataLayerAuthenticationMethodModel.Password) } assertThat(underTest.isAuthenticationRequired()).isTrue() } Loading @@ -156,11 +196,11 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_lockedAndNotSecured_false() = testScope.runTest { utils.authenticationRepository.setUnlocked(false) utils.authenticationRepository.apply { setUnlocked(false) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) } assertThat(underTest.isAuthenticationRequired()).isFalse() } Loading @@ -168,11 +208,11 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_unlockedAndSecured_false() = testScope.runTest { utils.authenticationRepository.setUnlocked(true) utils.authenticationRepository.apply { setUnlocked(true) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Password ) setAuthenticationMethod(DataLayerAuthenticationMethodModel.Password) } assertThat(underTest.isAuthenticationRequired()).isFalse() } Loading @@ -180,11 +220,11 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_unlockedAndNotSecured_false() = testScope.runTest { utils.authenticationRepository.setUnlocked(true) utils.authenticationRepository.apply { setUnlocked(true) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) } assertThat(underTest.isAuthenticationRequired()).isFalse() } Loading Loading @@ -221,11 +261,12 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectMaxLengthPin_returnsTrue() = testScope.runTest { utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) val pin = List(16) { 9 } utils.authenticationRepository.overrideCredential(pin) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) overrideCredential(pin) } assertThat(underTest.authenticate(pin)).isTrue() } Loading Loading @@ -308,10 +349,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNullAndHasNoEffect() = testScope.runTest { val isThrottled by collectLastValue(underTest.isThrottled) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply { Loading @@ -328,10 +369,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalseAndDoesNotUnlockDevice() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }, Loading @@ -346,10 +387,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalseAndDoesNotUnlockDevice() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN + listOf(7), Loading @@ -364,10 +405,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrueAndUnlocksDevice() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN, Loading @@ -382,10 +423,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNullAndHasNoEffects() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(false) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(false) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN, Loading Loading @@ -505,10 +546,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withoutAutoConfirm_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(false) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(false) } assertThat(hintedPinLength).isNull() } Loading @@ -517,15 +558,15 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinTooShort_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.overrideCredential( utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) overrideCredential( buildList { repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) } } ) utils.authenticationRepository.setAutoConfirmEnabled(true) setAutoConfirmEnabled(true) } assertThat(hintedPinLength).isNull() } Loading @@ -534,13 +575,15 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinAtRightLength_isSameLength() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.overrideCredential( buildList { repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) } } utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) overrideCredential( buildList { repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) } } ) } assertThat(hintedPinLength).isEqualTo(utils.authenticationRepository.hintedPinLength) } Loading @@ -549,16 +592,20 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinTooLong_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.overrideCredential( utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) overrideCredential( buildList { repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) } } ) utils.authenticationRepository.setAutoConfirmEnabled(true) setAutoConfirmEnabled(true) } assertThat(hintedPinLength).isNull() } private fun switchToScene(sceneKey: SceneKey) { sceneInteractor.changeScene(SceneModel(sceneKey), "reason") } } packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +5 −3 Original line number Diff line number Diff line Loading @@ -84,22 +84,24 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { } @Test fun upTransitionSceneKey_swipeToUnlockEnabled_gone() = fun upTransitionSceneKey_canSwipeToUnlock_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) utils.authenticationRepository.setLockscreenEnabled(true) utils.authenticationRepository.setUnlocked(false) utils.authenticationRepository.setUnlocked(true) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) } @Test fun upTransitionSceneKey_swipeToUnlockNotEnabled_bouncer() = fun upTransitionSceneKey_cannotSwipeToUnlock_bouncer() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) } Loading packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +2 −0 Original line number Diff line number Diff line Loading @@ -134,6 +134,7 @@ class SceneTestUtils( fun authenticationInteractor( repository: AuthenticationRepository, sceneInteractor: SceneInteractor = sceneInteractor(), ): AuthenticationInteractor { return AuthenticationInteractor( applicationScope = applicationScope(), Loading @@ -141,6 +142,7 @@ class SceneTestUtils( backgroundDispatcher = testDispatcher, userRepository = userRepository, keyguardRepository = keyguardRepository, sceneInteractor = sceneInteractor, clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } } ) } Loading Loading
packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +43 −1 Original line number Diff line number Diff line Loading @@ -27,6 +27,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.time.SystemClock import javax.inject.Inject Loading @@ -42,6 +44,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch Loading @@ -57,6 +60,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val userRepository: UserRepository, private val keyguardRepository: KeyguardRepository, sceneInteractor: SceneInteractor, private val clock: SystemClock, ) { /** Loading Loading @@ -93,7 +97,7 @@ constructor( repository.isUnlocked, authenticationMethod, ) { isUnlocked, authenticationMethod -> authenticationMethod is DomainLayerAuthenticationMethodModel.None || isUnlocked !authenticationMethod.isSecure || isUnlocked } .stateIn( scope = applicationScope, Loading @@ -101,6 +105,44 @@ constructor( initialValue = true, ) /** * Whether the lockscreen has been dismissed (by any method). This can be false even when the * device is unlocked, e.g. when swipe to unlock is enabled. * * Note: * - `false` doesn't mean the lockscreen is visible (it may be occluded or covered by other UI). * - `true` doesn't mean the lockscreen is invisible (since this state changes before the * transition occurs). */ private val isLockscreenDismissed = sceneInteractor.desiredScene .map { it.key } .filter { currentScene -> currentScene == SceneKey.Gone || currentScene == SceneKey.Lockscreen } .map { it == SceneKey.Gone } .distinctUntilChanged() /** * Whether it's currently possible to swipe up to dismiss the lockscreen without requiring * authentication. This returns false whenever the lockscreen has been dismissed. * * Note: `true` doesn't mean the lockscreen is visible. It may be occluded or covered by other * UI. */ val canSwipeToDismiss = combine(authenticationMethod, isLockscreenDismissed) { authenticationMethod, isLockscreenDismissed -> authenticationMethod is DomainLayerAuthenticationMethodModel.Swipe && !isLockscreenDismissed } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = false, ) /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */ val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling Loading
packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +22 −29 Original line number Diff line number Diff line Loading @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.R import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.domain.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon Loading @@ -27,7 +26,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneKey import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map Loading @@ -53,14 +51,15 @@ constructor( ) /** The key of the scene we should switch to when swiping up. */ val upDestinationSceneKey: Flow<SceneKey> = authenticationInteractor.authenticationMethod.map { authenticationMethod -> if (authenticationMethod is AuthenticationMethodModel.Swipe) { SceneKey.Gone } else { SceneKey.Bouncer } } val upDestinationSceneKey = authenticationInteractor.canSwipeToDismiss .map { canSwipeToDismiss -> upDestinationSceneKey(canSwipeToDismiss) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = upDestinationSceneKey(authenticationInteractor.canSwipeToDismiss.value), ) /** Notifies that the lock button on the lock screen was clicked. */ fun onLockButtonClicked() { Loading @@ -73,30 +72,24 @@ constructor( } private fun upDestinationSceneKey( isSwipeToUnlockEnabled: Boolean, canSwipeToDismiss: Boolean, ): SceneKey { return if (isSwipeToUnlockEnabled) SceneKey.Gone else SceneKey.Bouncer return if (canSwipeToDismiss) SceneKey.Gone else SceneKey.Bouncer } private fun lockIcon( isUnlocked: Boolean, ): Icon { return Icon.Resource( res = if (isUnlocked) { R.drawable.ic_device_lock_off } else { R.drawable.ic_device_lock_on }, contentDescription = ContentDescription.Resource( res = if (isUnlocked) { R.string.accessibility_unlock_button } else { R.string.accessibility_lock_icon } return if (isUnlocked) { Icon.Resource( R.drawable.ic_device_lock_off, ContentDescription.Resource(R.string.accessibility_unlock_button) ) } else { Icon.Resource( R.drawable.ic_device_lock_on, ContentDescription.Resource(R.string.accessibility_lock_icon) ) } } }
packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +155 −108 Original line number Diff line number Diff line Loading @@ -27,6 +27,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationPatternCoo import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds Loading @@ -46,9 +48,11 @@ class AuthenticationInteractorTest : SysuiTestCase() { private val utils = SceneTestUtils(this) private val testScope = utils.testScope private val repository: AuthenticationRepository = utils.authenticationRepository() private val sceneInteractor = utils.sceneInteractor() private val underTest = utils.authenticationInteractor( repository = repository, sceneInteractor = sceneInteractor, ) @Test Loading @@ -75,10 +79,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { val authMethod by collectLastValue(underTest.authenticationMethod) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) utils.authenticationRepository.setLockscreenEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(true) } assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.Swipe) assertThat(underTest.getAuthenticationMethod()) Loading @@ -91,10 +95,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { val authMethod by collectLastValue(underTest.authenticationMethod) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) utils.authenticationRepository.setLockscreenEnabled(false) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(false) } assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.None) assertThat(underTest.getAuthenticationMethod()) Loading @@ -104,51 +108,87 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun isUnlocked_whenAuthMethodIsNoneAndLockscreenDisabled_isTrue() = testScope.runTest { utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) utils.authenticationRepository.setLockscreenEnabled(false) val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(false) // Toggle isUnlocked, twice. // // This is done because the underTest.isUnlocked flow doesn't receive values from // just changing the state above; the actual isUnlocked state needs to change to // cause the logic under test to "pick up" the current state again. // // It is done twice to make sure that we don't actually change the isUnlocked // state from what it originally was. utils.authenticationRepository.setUnlocked( !utils.authenticationRepository.isUnlocked.value ) // It is done twice to make sure that we don't actually change the isUnlocked state // from what it originally was. setUnlocked(!utils.authenticationRepository.isUnlocked.value) runCurrent() utils.authenticationRepository.setUnlocked( !utils.authenticationRepository.isUnlocked.value ) setUnlocked(!utils.authenticationRepository.isUnlocked.value) runCurrent() } assertThat(isUnlocked).isTrue() } @Test fun isUnlocked_whenAuthMethodIsNoneAndLockscreenEnabled_isFalse() = fun isUnlocked_whenAuthMethodIsNoneAndLockscreenEnabled_isTrue() = testScope.runTest { utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) utils.authenticationRepository.setLockscreenEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(true) } val isUnlocked by collectLastValue(underTest.isUnlocked) assertThat(isUnlocked).isFalse() assertThat(isUnlocked).isTrue() } @Test fun canSwipeToDismiss_onLockscreenWithSwipe_isTrue() = testScope.runTest { utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(true) } switchToScene(SceneKey.Lockscreen) val canSwipeToDismiss by collectLastValue(underTest.canSwipeToDismiss) assertThat(canSwipeToDismiss).isTrue() } @Test fun canSwipeToDismiss_onLockscreenWithPin_isFalse() = testScope.runTest { utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setLockscreenEnabled(true) } switchToScene(SceneKey.Lockscreen) val canSwipeToDismiss by collectLastValue(underTest.canSwipeToDismiss) assertThat(canSwipeToDismiss).isFalse() } @Test fun canSwipeToDismiss_afterLockscreenDismissedInSwipeMode_isFalse() = testScope.runTest { utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) setLockscreenEnabled(true) } switchToScene(SceneKey.Lockscreen) switchToScene(SceneKey.Gone) val canSwipeToDismiss by collectLastValue(underTest.canSwipeToDismiss) assertThat(canSwipeToDismiss).isFalse() } @Test fun isAuthenticationRequired_lockedAndSecured_true() = testScope.runTest { utils.authenticationRepository.setUnlocked(false) utils.authenticationRepository.apply { setUnlocked(false) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Password ) setAuthenticationMethod(DataLayerAuthenticationMethodModel.Password) } assertThat(underTest.isAuthenticationRequired()).isTrue() } Loading @@ -156,11 +196,11 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_lockedAndNotSecured_false() = testScope.runTest { utils.authenticationRepository.setUnlocked(false) utils.authenticationRepository.apply { setUnlocked(false) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) } assertThat(underTest.isAuthenticationRequired()).isFalse() } Loading @@ -168,11 +208,11 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_unlockedAndSecured_false() = testScope.runTest { utils.authenticationRepository.setUnlocked(true) utils.authenticationRepository.apply { setUnlocked(true) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Password ) setAuthenticationMethod(DataLayerAuthenticationMethodModel.Password) } assertThat(underTest.isAuthenticationRequired()).isFalse() } Loading @@ -180,11 +220,11 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_unlockedAndNotSecured_false() = testScope.runTest { utils.authenticationRepository.setUnlocked(true) utils.authenticationRepository.apply { setUnlocked(true) runCurrent() utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.None ) setAuthenticationMethod(DataLayerAuthenticationMethodModel.None) } assertThat(underTest.isAuthenticationRequired()).isFalse() } Loading Loading @@ -221,11 +261,12 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectMaxLengthPin_returnsTrue() = testScope.runTest { utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) val pin = List(16) { 9 } utils.authenticationRepository.overrideCredential(pin) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) overrideCredential(pin) } assertThat(underTest.authenticate(pin)).isTrue() } Loading Loading @@ -308,10 +349,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNullAndHasNoEffect() = testScope.runTest { val isThrottled by collectLastValue(underTest.isThrottled) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply { Loading @@ -328,10 +369,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalseAndDoesNotUnlockDevice() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }, Loading @@ -346,10 +387,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalseAndDoesNotUnlockDevice() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN + listOf(7), Loading @@ -364,10 +405,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrueAndUnlocksDevice() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN, Loading @@ -382,10 +423,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNullAndHasNoEffects() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(false) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(false) } assertThat( underTest.authenticate( FakeAuthenticationRepository.DEFAULT_PIN, Loading Loading @@ -505,10 +546,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withoutAutoConfirm_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(false) utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(false) } assertThat(hintedPinLength).isNull() } Loading @@ -517,15 +558,15 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinTooShort_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.overrideCredential( utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) overrideCredential( buildList { repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) } } ) utils.authenticationRepository.setAutoConfirmEnabled(true) setAutoConfirmEnabled(true) } assertThat(hintedPinLength).isNull() } Loading @@ -534,13 +575,15 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinAtRightLength_isSameLength() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.overrideCredential( buildList { repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) } } utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) setAutoConfirmEnabled(true) overrideCredential( buildList { repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) } } ) } assertThat(hintedPinLength).isEqualTo(utils.authenticationRepository.hintedPinLength) } Loading @@ -549,16 +592,20 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinTooLong_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) utils.authenticationRepository.setAuthenticationMethod( DataLayerAuthenticationMethodModel.Pin ) utils.authenticationRepository.overrideCredential( utils.authenticationRepository.apply { setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin) overrideCredential( buildList { repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) } } ) utils.authenticationRepository.setAutoConfirmEnabled(true) setAutoConfirmEnabled(true) } assertThat(hintedPinLength).isNull() } private fun switchToScene(sceneKey: SceneKey) { sceneInteractor.changeScene(SceneModel(sceneKey), "reason") } }
packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +5 −3 Original line number Diff line number Diff line Loading @@ -84,22 +84,24 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { } @Test fun upTransitionSceneKey_swipeToUnlockEnabled_gone() = fun upTransitionSceneKey_canSwipeToUnlock_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) utils.authenticationRepository.setLockscreenEnabled(true) utils.authenticationRepository.setUnlocked(false) utils.authenticationRepository.setUnlocked(true) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) } @Test fun upTransitionSceneKey_swipeToUnlockNotEnabled_bouncer() = fun upTransitionSceneKey_cannotSwipeToUnlock_bouncer() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) } Loading
packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +2 −0 Original line number Diff line number Diff line Loading @@ -134,6 +134,7 @@ class SceneTestUtils( fun authenticationInteractor( repository: AuthenticationRepository, sceneInteractor: SceneInteractor = sceneInteractor(), ): AuthenticationInteractor { return AuthenticationInteractor( applicationScope = applicationScope(), Loading @@ -141,6 +142,7 @@ class SceneTestUtils( backgroundDispatcher = testDispatcher, userRepository = userRepository, keyguardRepository = keyguardRepository, sceneInteractor = sceneInteractor, clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } } ) } Loading