[flexiglass] Add a "canSwipeToDismissState" state.

This tracks whether it's currently possible to swipe up to dismiss the
lockscreen without requiring authentication.

Bonus: Fixes a bug in the `isUnlocked` state definition.

Fix: 294982161
Fix: 294984006
Test: new unit tests added, and existing ones modified.

Change-Id: I1fa9d4ca97da737a1bcf386bd27ec3340aed1871
This commit is contained in:
burakov
2023-08-07 22:32:09 +00:00
parent f26dc0ca0a
commit ecc77c557b
5 changed files with 227 additions and 141 deletions

View File

@@ -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
@@ -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
@@ -57,6 +60,7 @@ constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val userRepository: UserRepository,
private val keyguardRepository: KeyguardRepository,
sceneInteractor: SceneInteractor,
private val clock: SystemClock,
) {
/**
@@ -93,7 +97,7 @@ constructor(
repository.isUnlocked,
authenticationMethod,
) { isUnlocked, authenticationMethod ->
authenticationMethod is DomainLayerAuthenticationMethodModel.None || isUnlocked
!authenticationMethod.isSecure || isUnlocked
}
.stateIn(
scope = applicationScope,
@@ -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

View File

@@ -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
@@ -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
@@ -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() {
@@ -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)
)
}
}
}

View File

@@ -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
@@ -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
@@ -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())
@@ -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())
@@ -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)
// 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
)
runCurrent()
utils.authenticationRepository.setUnlocked(
!utils.authenticationRepository.isUnlocked.value
)
runCurrent()
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.
setUnlocked(!utils.authenticationRepository.isUnlocked.value)
runCurrent()
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)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Password
)
utils.authenticationRepository.apply {
setUnlocked(false)
runCurrent()
setAuthenticationMethod(DataLayerAuthenticationMethodModel.Password)
}
assertThat(underTest.isAuthenticationRequired()).isTrue()
}
@@ -156,11 +196,11 @@ class AuthenticationInteractorTest : SysuiTestCase() {
@Test
fun isAuthenticationRequired_lockedAndNotSecured_false() =
testScope.runTest {
utils.authenticationRepository.setUnlocked(false)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.None
)
utils.authenticationRepository.apply {
setUnlocked(false)
runCurrent()
setAuthenticationMethod(DataLayerAuthenticationMethodModel.None)
}
assertThat(underTest.isAuthenticationRequired()).isFalse()
}
@@ -168,11 +208,11 @@ class AuthenticationInteractorTest : SysuiTestCase() {
@Test
fun isAuthenticationRequired_unlockedAndSecured_false() =
testScope.runTest {
utils.authenticationRepository.setUnlocked(true)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Password
)
utils.authenticationRepository.apply {
setUnlocked(true)
runCurrent()
setAuthenticationMethod(DataLayerAuthenticationMethodModel.Password)
}
assertThat(underTest.isAuthenticationRequired()).isFalse()
}
@@ -180,11 +220,11 @@ class AuthenticationInteractorTest : SysuiTestCase() {
@Test
fun isAuthenticationRequired_unlockedAndNotSecured_false() =
testScope.runTest {
utils.authenticationRepository.setUnlocked(true)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.None
)
utils.authenticationRepository.apply {
setUnlocked(true)
runCurrent()
setAuthenticationMethod(DataLayerAuthenticationMethodModel.None)
}
assertThat(underTest.isAuthenticationRequired()).isFalse()
}
@@ -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()
}
@@ -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 {
@@ -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 },
@@ -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),
@@ -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,
@@ -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,
@@ -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()
}
@@ -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(
buildList {
repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) }
}
)
utils.authenticationRepository.setAutoConfirmEnabled(true)
utils.authenticationRepository.apply {
setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
overrideCredential(
buildList {
repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) }
}
)
setAutoConfirmEnabled(true)
}
assertThat(hintedPinLength).isNull()
}
@@ -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)
}
@@ -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(
buildList {
repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) }
}
)
utils.authenticationRepository.setAutoConfirmEnabled(true)
utils.authenticationRepository.apply {
setAuthenticationMethod(DataLayerAuthenticationMethodModel.Pin)
overrideCredential(
buildList {
repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) }
}
)
setAutoConfirmEnabled(true)
}
assertThat(hintedPinLength).isNull()
}
private fun switchToScene(sceneKey: SceneKey) {
sceneInteractor.changeScene(SceneModel(sceneKey), "reason")
}
}

View File

@@ -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)
}

View File

@@ -134,6 +134,7 @@ class SceneTestUtils(
fun authenticationInteractor(
repository: AuthenticationRepository,
sceneInteractor: SceneInteractor = sceneInteractor(),
): AuthenticationInteractor {
return AuthenticationInteractor(
applicationScope = applicationScope(),
@@ -141,6 +142,7 @@ class SceneTestUtils(
backgroundDispatcher = testDispatcher,
userRepository = userRepository,
keyguardRepository = keyguardRepository,
sceneInteractor = sceneInteractor,
clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } }
)
}