summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt62
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt371
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt80
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt64
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt33
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt123
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt125
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt94
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt125
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt47
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt8
18 files changed, 560 insertions, 616 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index 64ddbc7828ac..c961be946f79 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -35,8 +35,10 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobile
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.function.Function
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runCurrent
@@ -58,6 +60,7 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
private val testUtils = SceneTestUtils(this)
private val testScope = testUtils.testScope
+ private val clock = FakeSystemClock()
private val userRepository = FakeUserRepository()
private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
@@ -78,8 +81,10 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
underTest =
AuthenticationRepositoryImpl(
applicationScope = testScope.backgroundScope,
- getSecurityMode = getSecurityMode,
backgroundDispatcher = testUtils.testDispatcher,
+ flags = testUtils.sceneContainerFlags,
+ clock = clock,
+ getSecurityMode = getSecurityMode,
userRepository = userRepository,
lockPatternUtils = lockPatternUtils,
broadcastDispatcher = fakeBroadcastDispatcher,
@@ -141,22 +146,6 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
}
@Test
- fun reportAuthenticationAttempt_emitsAuthenticationChallengeResult() =
- testScope.runTest {
- val authenticationChallengeResults by
- collectValues(underTest.authenticationChallengeResult)
-
- runCurrent()
- underTest.reportAuthenticationAttempt(true)
- runCurrent()
- underTest.reportAuthenticationAttempt(false)
- runCurrent()
- underTest.reportAuthenticationAttempt(true)
-
- assertThat(authenticationChallengeResults).isEqualTo(listOf(true, false, true))
- }
-
- @Test
fun isPinEnhancedPrivacyEnabled() =
testScope.runTest {
whenever(lockPatternUtils.isPinEnhancedPrivacyEnabled(USER_INFOS[0].id))
@@ -172,6 +161,45 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
assertThat(values.last()).isTrue()
}
+ @Test
+ fun lockoutEndTimestamp() =
+ testScope.runTest {
+ val lockoutEndMs = clock.elapsedRealtime() + 30.seconds.inWholeMilliseconds
+ whenever(lockPatternUtils.getLockoutAttemptDeadline(USER_INFOS[0].id))
+ .thenReturn(lockoutEndMs)
+ whenever(lockPatternUtils.getLockoutAttemptDeadline(USER_INFOS[1].id)).thenReturn(0)
+
+ // Switch to a user who is not locked-out.
+ userRepository.setSelectedUserInfo(USER_INFOS[1])
+ assertThat(underTest.lockoutEndTimestamp).isNull()
+
+ // Switch back to the locked-out user, verify the timestamp is up-to-date.
+ userRepository.setSelectedUserInfo(USER_INFOS[0])
+ assertThat(underTest.lockoutEndTimestamp).isEqualTo(lockoutEndMs)
+
+ // After the lockout expires, null is returned.
+ clock.setElapsedRealtime(lockoutEndMs)
+ assertThat(underTest.lockoutEndTimestamp).isNull()
+ }
+
+ @Test
+ fun hasLockoutOccurred() =
+ testScope.runTest {
+ val hasLockoutOccurred by collectLastValue(underTest.hasLockoutOccurred)
+ assertThat(hasLockoutOccurred).isFalse()
+
+ underTest.reportLockoutStarted(1000)
+ assertThat(hasLockoutOccurred).isTrue()
+
+ clock.setElapsedRealtime(clock.elapsedRealtime() + 60.seconds.inWholeMilliseconds)
+
+ underTest.reportAuthenticationAttempt(isSuccessful = false)
+ assertThat(hasLockoutOccurred).isTrue()
+
+ underTest.reportAuthenticationAttempt(isSuccessful = true)
+ assertThat(hasLockoutOccurred).isFalse()
+ }
+
private fun setSecurityModeAndDispatchBroadcast(
securityMode: KeyguardSecurityModel.SecurityMode,
) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 08cd7edba6af..c113b3744bc0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -21,14 +21,18 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.None
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -43,21 +47,23 @@ class AuthenticationInteractorTest : SysuiTestCase() {
private val testScope = utils.testScope
private val underTest = utils.authenticationInteractor()
+ private val onAuthenticationResult by
+ testScope.collectLastValue(underTest.onAuthenticationResult)
+ private val failedAuthenticationAttempts by
+ testScope.collectLastValue(underTest.failedAuthenticationAttempts)
+
@Test
fun authenticationMethod() =
testScope.runTest {
val authMethod by collectLastValue(underTest.authenticationMethod)
runCurrent()
- assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pin)
- assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Pin)
+ assertThat(authMethod).isEqualTo(Pin)
+ assertThat(underTest.getAuthenticationMethod()).isEqualTo(Pin)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
+ utils.authenticationRepository.setAuthenticationMethod(Password)
- assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password)
- assertThat(underTest.getAuthenticationMethod())
- .isEqualTo(AuthenticationMethodModel.Password)
+ assertThat(authMethod).isEqualTo(Password)
+ assertThat(underTest.getAuthenticationMethod()).isEqualTo(Password)
}
@Test
@@ -66,51 +72,45 @@ class AuthenticationInteractorTest : SysuiTestCase() {
val authMethod by collectLastValue(underTest.authenticationMethod)
runCurrent()
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
+ utils.authenticationRepository.setAuthenticationMethod(None)
- assertThat(authMethod).isEqualTo(AuthenticationMethodModel.None)
- assertThat(underTest.getAuthenticationMethod())
- .isEqualTo(AuthenticationMethodModel.None)
+ assertThat(authMethod).isEqualTo(None)
+ assertThat(underTest.getAuthenticationMethod()).isEqualTo(None)
}
@Test
fun authenticate_withCorrectPin_succeeds() =
testScope.runTest {
- val lockout by collectLastValue(underTest.lockout)
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ utils.authenticationRepository.setAuthenticationMethod(Pin)
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
- .isEqualTo(AuthenticationResult.SUCCEEDED)
- assertThat(lockout).isNull()
- assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0)
+ assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
}
@Test
fun authenticate_withIncorrectPin_fails() =
testScope.runTest {
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ utils.authenticationRepository.setAuthenticationMethod(Pin)
- assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4)))
- .isEqualTo(AuthenticationResult.FAILED)
+ assertFailed(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4)))
}
@Test(expected = IllegalArgumentException::class)
fun authenticate_withEmptyPin_throwsException() =
testScope.runTest {
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ utils.authenticationRepository.setAuthenticationMethod(Pin)
underTest.authenticate(listOf())
}
@Test
fun authenticate_withCorrectMaxLengthPin_succeeds() =
testScope.runTest {
- val pin = List(16) { 9 }
+ val correctMaxLengthPin = List(16) { 9 }
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
- overrideCredential(pin)
+ setAuthenticationMethod(Pin)
+ overrideCredential(correctMaxLengthPin)
}
- assertThat(underTest.authenticate(pin)).isEqualTo(AuthenticationResult.SUCCEEDED)
+ assertSucceeded(underTest.authenticate(correctMaxLengthPin))
}
@Test
@@ -122,88 +122,64 @@ class AuthenticationInteractorTest : SysuiTestCase() {
// If the policy changes, there is work to do in SysUI.
assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17)
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- assertThat(underTest.authenticate(List(17) { 9 }))
- .isEqualTo(AuthenticationResult.FAILED)
+ utils.authenticationRepository.setAuthenticationMethod(Pin)
+
+ assertFailed(underTest.authenticate(List(17) { 9 }))
}
@Test
fun authenticate_withCorrectPassword_succeeds() =
testScope.runTest {
- val lockout by collectLastValue(underTest.lockout)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
+ utils.authenticationRepository.setAuthenticationMethod(Password)
- assertThat(underTest.authenticate("password".toList()))
- .isEqualTo(AuthenticationResult.SUCCEEDED)
- assertThat(lockout).isNull()
- assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0)
+ assertSucceeded(underTest.authenticate("password".toList()))
}
@Test
fun authenticate_withIncorrectPassword_fails() =
testScope.runTest {
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
+ utils.authenticationRepository.setAuthenticationMethod(Password)
- assertThat(underTest.authenticate("alohomora".toList()))
- .isEqualTo(AuthenticationResult.FAILED)
+ assertFailed(underTest.authenticate("alohomora".toList()))
}
@Test
fun authenticate_withCorrectPattern_succeeds() =
testScope.runTest {
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pattern
- )
+ utils.authenticationRepository.setAuthenticationMethod(Pattern)
- assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
- .isEqualTo(AuthenticationResult.SUCCEEDED)
+ assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
}
@Test
fun authenticate_withIncorrectPattern_fails() =
testScope.runTest {
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pattern
- )
-
- assertThat(
- underTest.authenticate(
- listOf(
- AuthenticationPatternCoordinate(x = 2, y = 0),
- AuthenticationPatternCoordinate(x = 2, y = 1),
- AuthenticationPatternCoordinate(x = 2, y = 2),
- AuthenticationPatternCoordinate(x = 1, y = 2),
- )
- )
+ utils.authenticationRepository.setAuthenticationMethod(Pattern)
+ val wrongPattern =
+ listOf(
+ AuthenticationPatternCoordinate(x = 2, y = 0),
+ AuthenticationPatternCoordinate(x = 2, y = 1),
+ AuthenticationPatternCoordinate(x = 2, y = 2),
+ AuthenticationPatternCoordinate(x = 1, y = 2),
)
- .isEqualTo(AuthenticationResult.FAILED)
+
+ assertFailed(underTest.authenticate(wrongPattern))
}
@Test
fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNull() =
testScope.runTest {
val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
- val lockout by collectLastValue(underTest.lockout)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
setAutoConfirmFeatureEnabled(true)
}
assertThat(isAutoConfirmEnabled).isTrue()
+ val shorterPin =
+ FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply { removeLast() }
- assertThat(
- underTest.authenticate(
- FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply {
- removeLast()
- },
- tryAutoConfirm = true
- )
- )
- .isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(lockout).isNull()
+ assertSkipped(underTest.authenticate(shorterPin, tryAutoConfirm = true))
+ assertThat(underTest.lockoutEndTimestamp).isNull()
assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0)
}
@@ -212,18 +188,17 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
setAutoConfirmFeatureEnabled(true)
}
assertThat(isAutoConfirmEnabled).isTrue()
- assertThat(
- underTest.authenticate(
- FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 },
- tryAutoConfirm = true
- )
- )
- .isEqualTo(AuthenticationResult.FAILED)
+ val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+
+ assertFailed(
+ underTest.authenticate(wrongPin, tryAutoConfirm = true),
+ assertNoResultEvents = true,
+ )
}
@Test
@@ -231,18 +206,17 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
setAutoConfirmFeatureEnabled(true)
}
assertThat(isAutoConfirmEnabled).isTrue()
- assertThat(
- underTest.authenticate(
- FakeAuthenticationRepository.DEFAULT_PIN + listOf(7),
- tryAutoConfirm = true
- )
- )
- .isEqualTo(AuthenticationResult.FAILED)
+ val longerPin = FakeAuthenticationRepository.DEFAULT_PIN + listOf(7)
+
+ assertFailed(
+ underTest.authenticate(longerPin, tryAutoConfirm = true),
+ assertNoResultEvents = true,
+ )
}
@Test
@@ -250,69 +224,54 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
setAutoConfirmFeatureEnabled(true)
}
assertThat(isAutoConfirmEnabled).isTrue()
- assertThat(
- underTest.authenticate(
- FakeAuthenticationRepository.DEFAULT_PIN,
- tryAutoConfirm = true
- )
- )
- .isEqualTo(AuthenticationResult.SUCCEEDED)
+ val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
+
+ assertSucceeded(underTest.authenticate(correctPin, tryAutoConfirm = true))
}
@Test
fun tryAutoConfirm_withAutoConfirmCorrectPinButDuringLockout_returnsNull() =
testScope.runTest {
val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
- val isUnlocked by collectLastValue(utils.deviceEntryRepository.isUnlocked)
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
setAutoConfirmFeatureEnabled(true)
- setLockoutDuration(42)
+ reportLockoutStarted(42)
}
- val authResult =
- underTest.authenticate(
- FakeAuthenticationRepository.DEFAULT_PIN,
- tryAutoConfirm = true
- )
+ val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
- assertThat(authResult).isEqualTo(AuthenticationResult.SKIPPED)
+ assertSkipped(underTest.authenticate(correctPin, tryAutoConfirm = true))
assertThat(isAutoConfirmEnabled).isFalse()
- assertThat(isUnlocked).isFalse()
assertThat(hintedPinLength).isNull()
+ assertThat(underTest.lockoutEndTimestamp).isNotNull()
}
@Test
fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNull() =
testScope.runTest {
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
setAutoConfirmFeatureEnabled(false)
}
- assertThat(
- underTest.authenticate(
- FakeAuthenticationRepository.DEFAULT_PIN,
- tryAutoConfirm = true
- )
- )
- .isEqualTo(AuthenticationResult.SKIPPED)
+
+ val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
+
+ assertSkipped(underTest.authenticate(correctPin, tryAutoConfirm = true))
}
@Test
fun tryAutoConfirm_withoutCorrectPassword_returnsNull() =
testScope.runTest {
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
+ utils.authenticationRepository.setAuthenticationMethod(Password)
- assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true))
- .isEqualTo(AuthenticationResult.SKIPPED)
+ assertSkipped(underTest.authenticate("password".toList(), tryAutoConfirm = true))
}
@Test
@@ -337,7 +296,6 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun isAutoConfirmEnabled_featureEnabledButDisabledByLockout() =
testScope.runTest {
val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
- val lockout by collectLastValue(underTest.lockout)
utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
// The feature is enabled.
@@ -345,92 +303,104 @@ class AuthenticationInteractorTest : SysuiTestCase() {
// Make many wrong attempts to trigger lockout.
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
- underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
+ assertFailed(underTest.authenticate(listOf(5, 6, 7))) // Wrong PIN
}
- assertThat(lockout).isNotNull()
+ assertThat(underTest.lockoutEndTimestamp).isNotNull()
assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1)
// Lockout disabled auto-confirm.
assertThat(isAutoConfirmEnabled).isFalse()
// Move the clock forward one more second, to completely finish the lockout period:
- advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_MS + 1000L)
- assertThat(lockout).isNull()
+ advanceTimeBy(
+ FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds.plus(1.seconds)
+ )
+ assertThat(underTest.lockoutEndTimestamp).isNull()
// Auto-confirm is still disabled, because lockout occurred at least once in this
// session.
assertThat(isAutoConfirmEnabled).isFalse()
// Correct PIN and unlocks successfully, resetting the 'session'.
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
- .isEqualTo(AuthenticationResult.SUCCEEDED)
+ assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
// Auto-confirm is re-enabled.
assertThat(isAutoConfirmEnabled).isTrue()
+ }
- assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1)
+ @Test
+ fun failedAuthenticationAttempts() =
+ testScope.runTest {
+ val failedAuthenticationAttempts by
+ collectLastValue(underTest.failedAuthenticationAttempts)
+
+ utils.authenticationRepository.setAuthenticationMethod(Pin)
+ val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
+
+ assertSucceeded(underTest.authenticate(correctPin))
+ assertThat(failedAuthenticationAttempts).isEqualTo(0)
+
+ // Make many wrong attempts, leading to lockout:
+ repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { index ->
+ underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
+ assertThat(failedAuthenticationAttempts).isEqualTo(index + 1)
+ }
+
+ // Correct PIN, but locked out, so doesn't attempt it:
+ assertSkipped(underTest.authenticate(correctPin), assertNoResultEvents = false)
+ assertThat(failedAuthenticationAttempts)
+ .isEqualTo(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT)
+
+ // Move the clock forward to finish the lockout period:
+ advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds)
+ assertThat(failedAuthenticationAttempts)
+ .isEqualTo(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT)
+
+ // Correct PIN and no longer locked out so unlocks successfully:
+ assertSucceeded(underTest.authenticate(correctPin))
+ assertThat(failedAuthenticationAttempts).isEqualTo(0)
}
@Test
- fun lockout() =
+ fun lockoutEndTimestamp() =
testScope.runTest {
- val lockout by collectLastValue(underTest.lockout)
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
- assertThat(lockout).isNull()
+ utils.authenticationRepository.setAuthenticationMethod(Pin)
+ val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
+
+ underTest.authenticate(correctPin)
+ assertThat(underTest.lockoutEndTimestamp).isNull()
// Make many wrong attempts, but just shy of what's needed to get locked out:
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
- assertThat(lockout).isNull()
+ assertThat(underTest.lockoutEndTimestamp).isNull()
}
// Make one more wrong attempt, leading to lockout:
underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN
- assertThat(lockout)
- .isEqualTo(
- AuthenticationLockoutModel(
- failedAttemptCount =
- FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT,
- remainingSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS,
- )
- )
+
+ val expectedLockoutEndTimestamp =
+ testScope.currentTime + FakeAuthenticationRepository.LOCKOUT_DURATION_MS
+ assertThat(underTest.lockoutEndTimestamp).isEqualTo(expectedLockoutEndTimestamp)
assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1)
// Correct PIN, but locked out, so doesn't attempt it:
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
- .isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(lockout)
- .isEqualTo(
- AuthenticationLockoutModel(
- failedAttemptCount =
- FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT,
- remainingSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS,
- )
- )
+ assertSkipped(underTest.authenticate(correctPin), assertNoResultEvents = false)
+ assertThat(underTest.lockoutEndTimestamp).isEqualTo(expectedLockoutEndTimestamp)
// Move the clock forward to ALMOST skip the lockout, leaving one second to go:
- val lockoutTimeoutSec = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
- repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS - 1) { time ->
- advanceTimeBy(1000)
- assertThat(lockout)
- .isEqualTo(
- AuthenticationLockoutModel(
- failedAttemptCount =
- FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT,
- remainingSeconds = lockoutTimeoutSec - (time + 1),
- )
- )
+ repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS - 1) {
+ advanceTimeBy(1.seconds)
+ assertThat(underTest.lockoutEndTimestamp).isEqualTo(expectedLockoutEndTimestamp)
}
// Move the clock forward one more second, to completely finish the lockout period:
- advanceTimeBy(1000)
- assertThat(lockout).isNull()
+ advanceTimeBy(1.seconds)
+ assertThat(underTest.lockoutEndTimestamp).isNull()
// Correct PIN and no longer locked out so unlocks successfully:
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
- .isEqualTo(AuthenticationResult.SUCCEEDED)
- assertThat(lockout).isNull()
+ assertSucceeded(underTest.authenticate(correctPin))
+ assertThat(underTest.lockoutEndTimestamp).isNull()
}
@Test
@@ -438,7 +408,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
setAutoConfirmFeatureEnabled(false)
}
@@ -450,7 +420,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
overrideCredential(
buildList {
repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) }
@@ -467,7 +437,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
setAutoConfirmFeatureEnabled(true)
overrideCredential(
buildList {
@@ -484,7 +454,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.apply {
- setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ setAuthenticationMethod(Pin)
overrideCredential(
buildList {
repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) }
@@ -499,18 +469,45 @@ class AuthenticationInteractorTest : SysuiTestCase() {
@Test
fun authenticate_withTooShortPassword() =
testScope.runTest {
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
- assertThat(
- underTest.authenticate(
- buildList {
- repeat(utils.authenticationRepository.minPasswordLength - 1) { time ->
- add("$time")
- }
- }
- )
- )
- .isEqualTo(AuthenticationResult.SKIPPED)
+ utils.authenticationRepository.setAuthenticationMethod(Password)
+
+ val tooShortPassword = buildList {
+ repeat(utils.authenticationRepository.minPasswordLength - 1) { time ->
+ add("$time")
+ }
+ }
+ assertSkipped(underTest.authenticate(tooShortPassword))
+ }
+
+ private fun assertSucceeded(authenticationResult: AuthenticationResult) {
+ assertThat(authenticationResult).isEqualTo(AuthenticationResult.SUCCEEDED)
+ assertThat(onAuthenticationResult).isTrue()
+ assertThat(underTest.lockoutEndTimestamp).isNull()
+ assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0)
+ assertThat(failedAuthenticationAttempts).isEqualTo(0)
+ }
+
+ private fun assertFailed(
+ authenticationResult: AuthenticationResult,
+ assertNoResultEvents: Boolean = false,
+ ) {
+ assertThat(authenticationResult).isEqualTo(AuthenticationResult.FAILED)
+ if (assertNoResultEvents) {
+ assertThat(onAuthenticationResult).isNull()
+ } else {
+ assertThat(onAuthenticationResult).isFalse()
+ }
+ }
+
+ private fun assertSkipped(
+ authenticationResult: AuthenticationResult,
+ assertNoResultEvents: Boolean = true,
+ ) {
+ assertThat(authenticationResult).isEqualTo(AuthenticationResult.SKIPPED)
+ if (assertNoResultEvents) {
+ assertThat(onAuthenticationResult).isNull()
+ } else {
+ assertThat(onAuthenticationResult).isNotNull()
}
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 9b1df7c0ffc0..99c18744f9b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -21,15 +21,15 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
import com.android.systemui.res.R
import com.android.systemui.scene.SceneTestUtils
import com.google.common.truth.Truth.assertThat
-import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
@@ -79,7 +79,7 @@ class BouncerInteractorTest : SysuiTestCase() {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
runCurrent()
underTest.clearMessage()
- assertThat(message).isEmpty()
+ assertThat(message).isNull()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
@@ -149,7 +149,7 @@ class BouncerInteractorTest : SysuiTestCase() {
// Incomplete input.
assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isEmpty()
+ assertThat(message).isNull()
// Correct input.
assertThat(
@@ -159,7 +159,7 @@ class BouncerInteractorTest : SysuiTestCase() {
)
)
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isEmpty()
+ assertThat(message).isNull()
}
@Test
@@ -246,57 +246,40 @@ class BouncerInteractorTest : SysuiTestCase() {
}
@Test
- fun lockout() =
+ fun lockoutStarted() =
testScope.runTest {
- val lockout by collectLastValue(underTest.lockout)
+ val lockoutStartedEvents by collectValues(underTest.onLockoutStarted)
val message by collectLastValue(underTest.message)
+
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- assertThat(lockout).isNull()
+ assertThat(lockoutStartedEvents).isEmpty()
+
+ // Try the wrong PIN repeatedly, until lockout is triggered:
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
// Wrong PIN.
assertThat(underTest.authenticate(listOf(6, 7, 8, 9)))
.isEqualTo(AuthenticationResult.FAILED)
if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
- assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
+ assertThat(lockoutStartedEvents).isEmpty()
+ assertThat(message).isNotEmpty()
}
}
- assertThat(lockout)
- .isEqualTo(
- AuthenticationLockoutModel(
- failedAttemptCount =
- FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT,
- remainingSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS,
- )
- )
- assertTryAgainMessage(
- message,
- FakeAuthenticationRepository.LOCKOUT_DURATION_MS.milliseconds.inWholeSeconds.toInt()
- )
-
- // Correct PIN, but locked out, so doesn't change away from the bouncer scene:
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
- .isEqualTo(AuthenticationResult.SKIPPED)
- assertTryAgainMessage(
- message,
- FakeAuthenticationRepository.LOCKOUT_DURATION_MS.milliseconds.inWholeSeconds.toInt()
- )
-
- lockout?.remainingSeconds?.let { seconds ->
- repeat(seconds) { time ->
- advanceTimeBy(1000)
- val remainingTimeSec = seconds - time - 1
- if (remainingTimeSec > 0) {
- assertTryAgainMessage(message, remainingTimeSec)
- }
- }
+ assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull()
+ assertThat(lockoutStartedEvents.size).isEqualTo(1)
+ assertThat(message).isNull()
+
+ // Advance the time to finish the lockout:
+ advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds)
+ assertThat(authenticationInteractor.lockoutEndTimestamp).isNull()
+ assertThat(message).isNull()
+ assertThat(lockoutStartedEvents.size).isEqualTo(1)
+
+ // Trigger lockout again:
+ repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
+ // Wrong PIN.
+ underTest.authenticate(listOf(6, 7, 8, 9))
}
- assertThat(message).isEqualTo("")
- assertThat(lockout).isNull()
-
- // Correct PIN and no longer locked out so changes to the Gone scene:
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
- .isEqualTo(AuthenticationResult.SUCCEEDED)
- assertThat(lockout).isNull()
+ assertThat(lockoutStartedEvents.size).isEqualTo(2)
}
@Test
@@ -326,13 +309,6 @@ class BouncerInteractorTest : SysuiTestCase() {
verify(keyguardFaceAuthInteractor).onPrimaryBouncerUserInput()
}
- private fun assertTryAgainMessage(
- message: String?,
- time: Int,
- ) {
- assertThat(message).isEqualTo("Try again in $time seconds.")
- }
-
companion object {
private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 16a935943dbf..4be9b0a49a69 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -135,19 +136,47 @@ class BouncerViewModelTest : SysuiTestCase() {
fun message() =
testScope.runTest {
val message by collectLastValue(underTest.message)
- val lockout by collectLastValue(bouncerInteractor.lockout)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
assertThat(message?.isUpdateAnimated).isTrue()
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
- // Wrong PIN.
- bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ bouncerInteractor.authenticate(WRONG_PIN)
}
assertThat(message?.isUpdateAnimated).isFalse()
- lockout?.remainingSeconds?.let { remainingSeconds ->
- advanceTimeBy(remainingSeconds.seconds.inWholeMilliseconds)
+ val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+ advanceTimeBy(lockoutEndMs - testScope.currentTime)
+ assertThat(message?.isUpdateAnimated).isTrue()
+ }
+
+ @Test
+ fun lockoutMessage() =
+ testScope.runTest {
+ val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
+ val message by collectLastValue(underTest.message)
+ utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ assertThat(utils.authenticationRepository.lockoutEndTimestamp).isNull()
+ assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
+
+ repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
+ bouncerInteractor.authenticate(WRONG_PIN)
+ if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
+ assertThat(message?.text).isEqualTo(bouncerInteractor.message.value)
+ assertThat(message?.isUpdateAnimated).isTrue()
+ }
}
+ val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
+ assertTryAgainMessage(message?.text, lockoutSeconds)
+ assertThat(message?.isUpdateAnimated).isFalse()
+
+ repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
+ advanceTimeBy(1.seconds)
+ val remainingSeconds = lockoutSeconds - time - 1
+ if (remainingSeconds > 0) {
+ assertTryAgainMessage(message?.text, remainingSeconds)
+ }
+ }
+ assertThat(message?.text).isEmpty()
assertThat(message?.isUpdateAnimated).isTrue()
}
@@ -160,32 +189,30 @@ class BouncerViewModelTest : SysuiTestCase() {
authViewModel?.isInputEnabled ?: emptyFlow()
}
)
- val lockout by collectLastValue(bouncerInteractor.lockout)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
assertThat(isInputEnabled).isTrue()
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
- // Wrong PIN.
- bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ bouncerInteractor.authenticate(WRONG_PIN)
}
assertThat(isInputEnabled).isFalse()
- lockout?.remainingSeconds?.let { remainingSeconds ->
- advanceTimeBy(remainingSeconds.seconds.inWholeMilliseconds)
- }
+ val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+ advanceTimeBy(lockoutEndMs - testScope.currentTime)
assertThat(isInputEnabled).isTrue()
}
@Test
fun dialogMessage() =
testScope.runTest {
+ val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
val dialogMessage by collectLastValue(underTest.dialogMessage)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
- // Wrong PIN.
assertThat(dialogMessage).isNull()
- bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ bouncerInteractor.authenticate(WRONG_PIN)
}
assertThat(dialogMessage).isNotEmpty()
@@ -241,4 +268,15 @@ class BouncerViewModelTest : SysuiTestCase() {
AuthenticationMethodModel.Sim,
)
}
+
+ private fun assertTryAgainMessage(
+ message: String?,
+ time: Int,
+ ) {
+ assertThat(message).isEqualTo("Try again in $time seconds.")
+ }
+
+ companion object {
+ private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 6d6baa57bb9d..64e6e5707d75 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -19,7 +19,6 @@ package com.android.systemui.bouncer.ui.viewmodel
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
@@ -28,6 +27,7 @@ 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.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -45,11 +45,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
private val utils = SceneTestUtils(this)
private val testScope = utils.testScope
- private val authenticationRepository = utils.authenticationRepository
- private val authenticationInteractor =
- utils.authenticationInteractor(
- repository = authenticationRepository,
- )
+ private val authenticationInteractor = utils.authenticationInteractor()
private val sceneInteractor = utils.sceneInteractor()
private val bouncerInteractor =
utils.bouncerInteractor(
@@ -61,12 +57,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
authenticationInteractor = authenticationInteractor,
actionButtonInteractor = utils.bouncerActionButtonInteractor(),
)
+ private val isInputEnabled = MutableStateFlow(true)
private val underTest =
PasswordBouncerViewModel(
viewModelScope = testScope.backgroundScope,
interactor = bouncerInteractor,
- isInputEnabled = MutableStateFlow(true).asStateFlow(),
+ isInputEnabled.asStateFlow(),
)
@Before
@@ -123,8 +120,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
@Test
fun onAuthenticateKeyPressed_whenCorrect() =
testScope.runTest {
- val authResult by
- collectLastValue(authenticationInteractor.authenticationChallengeResult)
+ val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
lockDeviceAndOpenPasswordBouncer()
underTest.onPasswordInputChanged("password")
@@ -169,8 +165,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
@Test
fun onAuthenticateKeyPressed_correctAfterWrong() =
testScope.runTest {
- val authResult by
- collectLastValue(authenticationInteractor.authenticationChallengeResult)
+ val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
@@ -333,19 +328,15 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
) {
if (isLockedOut) {
repeat(failedAttemptCount) {
- authenticationRepository.reportAuthenticationAttempt(false)
+ utils.authenticationRepository.reportAuthenticationAttempt(false)
}
- val remainingTimeSeconds = 30
- authenticationRepository.setLockoutDuration(remainingTimeSeconds * 1000)
- authenticationRepository.lockout.value =
- AuthenticationLockoutModel(
- failedAttemptCount = failedAttemptCount,
- remainingSeconds = remainingTimeSeconds,
- )
+ utils.authenticationRepository.reportLockoutStarted(
+ 30.seconds.inWholeMilliseconds.toInt()
+ )
} else {
- authenticationRepository.reportAuthenticationAttempt(true)
- authenticationRepository.lockout.value = null
+ utils.authenticationRepository.reportAuthenticationAttempt(true)
}
+ isInputEnabled.value = !isLockedOut
runCurrent()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 8971423edd52..ed7609999804 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -111,8 +111,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
@Test
fun onDragEnd_whenCorrect() =
testScope.runTest {
- val authResult by
- collectLastValue(authenticationInteractor.authenticationChallengeResult)
+ val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
@@ -334,8 +333,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
@Test
fun onDragEnd_correctAfterWrong() =
testScope.runTest {
- val authResult by
- collectLastValue(authenticationInteractor.authenticationChallengeResult)
+ val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index c30e405ab911..db98d7632910 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -201,8 +201,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
@Test
fun onAuthenticateButtonClicked_whenCorrect() =
testScope.runTest {
- val authResult by
- collectLastValue(authenticationInteractor.authenticationChallengeResult)
+ val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
lockDeviceAndOpenPinBouncer()
FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
@@ -236,8 +235,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
@Test
fun onAuthenticateButtonClicked_correctAfterWrong() =
testScope.runTest {
- val authResult by
- collectLastValue(authenticationInteractor.authenticationChallengeResult)
+ val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -265,8 +263,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
fun onAutoConfirm_whenCorrect() =
testScope.runTest {
utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
- val authResult by
- collectLastValue(authenticationInteractor.authenticationChallengeResult)
+ val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
lockDeviceAndOpenPinBouncer()
FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
index d3049d9080f3..565049baf6f4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
@@ -99,7 +99,7 @@ class DeviceEntryRepositoryTest : SysuiTestCase() {
}
@Test
- fun reportSuccessfulAuthentication_shouldUpdateIsUnlocked() =
+ fun reportSuccessfulAuthentication_updatesIsUnlocked() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
assertThat(isUnlocked).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index 910097eece52..ea19cb799b10 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
@@ -346,12 +347,14 @@ class DeviceEntryInteractorTest : SysuiTestCase() {
}
@Test
- fun successfulAuthenticationChallengeAttempt_updatedIsUnlockedState() =
+ fun successfulAuthenticationChallengeAttempt_updatesIsUnlockedState() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
+ utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ utils.deviceEntryRepository.setLockscreenEnabled(true)
assertThat(isUnlocked).isFalse()
- utils.authenticationRepository.reportAuthenticationAttempt(true)
+ authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
assertThat(isUnlocked).isTrue()
}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index fda23b7f2a9c..f2b55f456f00 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -24,13 +24,13 @@ import android.os.UserHandle
import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockscreenCredential
import com.android.keyguard.KeyguardSecurityModel
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationResultModel
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.pairwise
@@ -43,9 +43,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
@@ -60,12 +58,6 @@ import kotlinx.coroutines.withContext
/** Defines interface for classes that can access authentication-related application state. */
interface AuthenticationRepository {
/**
- * Emits the result whenever a PIN/Pattern/Password security challenge is attempted by the user
- * in order to unlock the device.
- */
- val authenticationChallengeResult: SharedFlow<Boolean>
-
- /**
* The exact length a PIN should be for us to enable PIN length hinting.
*
* A PIN that's shorter or longer than this is not eligible for the UI to render hints showing
@@ -80,16 +72,6 @@ interface AuthenticationRepository {
val isPatternVisible: StateFlow<Boolean>
/**
- * The current authentication lockout (aka "throttling") state, set when the user has to wait
- * before being able to try another authentication attempt. `null` indicates throttling isn't
- * active.
- */
- val lockout: MutableStateFlow<AuthenticationLockoutModel?>
-
- /** Whether throttling has occurred at least once since the last successful authentication. */
- val hasLockoutOccurred: MutableStateFlow<Boolean>
-
- /**
* Whether the auto confirm feature is enabled for the currently-selected user.
*
* Note that the length of the PIN is also important to take into consideration, please see
@@ -98,6 +80,28 @@ interface AuthenticationRepository {
val isAutoConfirmFeatureEnabled: StateFlow<Boolean>
/**
+ * The number of failed authentication attempts for the selected user since their last
+ * successful authentication.
+ */
+ val failedAuthenticationAttempts: StateFlow<Int>
+
+ /**
+ * Timestamp for when the current lockout (aka "throttling") will end, allowing the user to
+ * attempt authentication again. Returns `null` if no lockout is active.
+ *
+ * Note that the value is in milliseconds and matches [SystemClock.elapsedRealtime].
+ *
+ * Also note that the value may change when the selected user is changed.
+ */
+ val lockoutEndTimestamp: Long?
+
+ /**
+ * Whether lockout has occurred at least once since the last successful authentication of any
+ * user.
+ */
+ val hasLockoutOccurred: StateFlow<Boolean>
+
+ /**
* The currently-configured authentication method. This determines how the authentication
* challenge needs to be completed in order to unlock an otherwise locked device.
*
@@ -142,23 +146,6 @@ interface AuthenticationRepository {
/** Reports that the user has entered a temporary device lockout (throttling). */
suspend fun reportLockoutStarted(durationMs: Int)
- /** Returns the current number of failed authentication attempts. */
- suspend fun getFailedAuthenticationAttemptCount(): Int
-
- /**
- * Returns the timestamp for when the current lockout will end, allowing the user to attempt
- * authentication again.
- *
- * Note that this is in milliseconds and it matches [SystemClock.elapsedRealtime].
- */
- suspend fun getLockoutEndTimestamp(): Long
-
- /**
- * Sets the lockout timeout duration (time during which the user should not be allowed to
- * attempt authentication).
- */
- suspend fun setLockoutDuration(durationMs: Int)
-
/**
* Checks the given [LockscreenCredential] to see if it's correct, returning an
* [AuthenticationResultModel] representing what happened.
@@ -172,6 +159,8 @@ class AuthenticationRepositoryImpl
constructor(
@Application private val applicationScope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher,
+ flags: SceneContainerFlags,
+ private val clock: SystemClock,
private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>,
private val userRepository: UserRepository,
private val lockPatternUtils: LockPatternUtils,
@@ -179,8 +168,6 @@ constructor(
mobileConnectionsRepository: MobileConnectionsRepository,
) : AuthenticationRepository {
- override val authenticationChallengeResult = MutableSharedFlow<Boolean>()
-
override val hintedPinLength: Int = 6
override val isPatternVisible: StateFlow<Boolean> =
@@ -189,10 +176,6 @@ constructor(
getFreshValue = lockPatternUtils::isVisiblePatternEnabled,
)
- override val lockout: MutableStateFlow<AuthenticationLockoutModel?> = MutableStateFlow(null)
-
- override val hasLockoutOccurred: MutableStateFlow<Boolean> = MutableStateFlow(false)
-
override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> =
refreshingFlow(
initialValue = false,
@@ -234,6 +217,31 @@ constructor(
getFreshValue = { userId -> lockPatternUtils.isPinEnhancedPrivacyEnabled(userId) },
)
+ private val _failedAuthenticationAttempts = MutableStateFlow(0)
+ override val failedAuthenticationAttempts: StateFlow<Int> =
+ _failedAuthenticationAttempts.asStateFlow()
+
+ override val lockoutEndTimestamp: Long?
+ get() =
+ lockPatternUtils.getLockoutAttemptDeadline(selectedUserId).takeIf {
+ clock.elapsedRealtime() < it
+ }
+
+ private val _hasLockoutOccurred = MutableStateFlow(false)
+ override val hasLockoutOccurred: StateFlow<Boolean> = _hasLockoutOccurred.asStateFlow()
+
+ init {
+ if (flags.isEnabled()) {
+ // Hydrate failedAuthenticationAttempts initially and whenever the selected user
+ // changes.
+ applicationScope.launch {
+ userRepository.selectedUserInfo.collect {
+ _failedAuthenticationAttempts.value = getFailedAuthenticationAttemptCount()
+ }
+ }
+ }
+ }
+
override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
return withContext(backgroundDispatcher) {
blockingAuthenticationMethodInternal(selectedUserId)
@@ -248,35 +256,20 @@ constructor(
withContext(backgroundDispatcher) {
if (isSuccessful) {
lockPatternUtils.reportSuccessfulPasswordAttempt(selectedUserId)
+ _hasLockoutOccurred.value = false
} else {
lockPatternUtils.reportFailedPasswordAttempt(selectedUserId)
}
- authenticationChallengeResult.emit(isSuccessful)
+ _failedAuthenticationAttempts.value = getFailedAuthenticationAttemptCount()
}
}
override suspend fun reportLockoutStarted(durationMs: Int) {
- return withContext(backgroundDispatcher) {
- lockPatternUtils.reportPasswordLockout(durationMs, selectedUserId)
- }
- }
-
- override suspend fun getFailedAuthenticationAttemptCount(): Int {
- return withContext(backgroundDispatcher) {
- lockPatternUtils.getCurrentFailedPasswordAttempts(selectedUserId)
- }
- }
-
- override suspend fun getLockoutEndTimestamp(): Long {
- return withContext(backgroundDispatcher) {
- lockPatternUtils.getLockoutAttemptDeadline(selectedUserId)
- }
- }
-
- override suspend fun setLockoutDuration(durationMs: Int) {
+ lockPatternUtils.setLockoutAttemptDeadline(selectedUserId, durationMs)
withContext(backgroundDispatcher) {
- lockPatternUtils.setLockoutAttemptDeadline(selectedUserId, durationMs)
+ lockPatternUtils.reportPasswordLockout(durationMs, selectedUserId)
}
+ _hasLockoutOccurred.value = true
}
override suspend fun checkCredential(
@@ -292,6 +285,12 @@ constructor(
}
}
+ private suspend fun getFailedAuthenticationAttemptCount(): Int {
+ return withContext(backgroundDispatcher) {
+ lockPatternUtils.getCurrentFailedPasswordAttempts(selectedUserId)
+ }
+ }
+
private val selectedUserId: Int
get() = userRepository.getSelectedUserInfo().id
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 797154e85082..c85ffe6ca56f 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -16,36 +16,25 @@
package com.android.systemui.authentication.domain.interactor
-import com.android.app.tracing.TraceUtils.Companion.withContext
import com.android.internal.widget.LockPatternView
import com.android.internal.widget.LockscreenCredential
import com.android.systemui.authentication.data.repository.AuthenticationRepository
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
-import kotlin.math.ceil
-import kotlin.math.max
-import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.async
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
/**
* Hosts application business logic related to user authentication.
@@ -59,10 +48,7 @@ class AuthenticationInteractor
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
private val repository: AuthenticationRepository,
- private val userRepository: UserRepository,
- private val clock: SystemClock,
) {
/**
* The currently-configured authentication method. This determines how the authentication
@@ -85,13 +71,6 @@ constructor(
val authenticationMethod: Flow<AuthenticationMethodModel> = repository.authenticationMethod
/**
- * The current authentication lockout (aka "throttling") state, set when the user has to wait
- * before being able to try another authentication attempt. `null` indicates lockout isn't
- * active.
- */
- val lockout: StateFlow<AuthenticationLockoutModel?> = repository.lockout
-
- /**
* Whether the auto confirm feature is enabled for the currently-selected user.
*
* Note that the length of the PIN is also important to take into consideration, please see
@@ -130,26 +109,35 @@ constructor(
/** Whether the pattern should be visible for the currently-selected user. */
val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible
+ private val _onAuthenticationResult = MutableSharedFlow<Boolean>()
/**
* Emits the outcome (successful or unsuccessful) whenever a PIN/Pattern/Password security
* challenge is attempted by the user in order to unlock the device.
*/
- val authenticationChallengeResult: SharedFlow<Boolean> =
- repository.authenticationChallengeResult
+ val onAuthenticationResult: SharedFlow<Boolean> = _onAuthenticationResult.asSharedFlow()
/** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> = repository.isPinEnhancedPrivacyEnabled
- private var lockoutCountdownJob: Job? = null
+ /**
+ * The number of failed authentication attempts for the selected user since the last successful
+ * authentication.
+ */
+ val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts
- init {
- applicationScope.launch {
- userRepository.selectedUserInfo
- .map { it.id }
- .distinctUntilChanged()
- .collect { onSelectedUserChanged() }
- }
- }
+ /**
+ * Timestamp for when the current lockout (aka "throttling") will end, allowing the user to
+ * attempt authentication again. Returns `null` if no lockout is active.
+ *
+ * To be notified whenever a lockout is started, the caller should subscribe to
+ * [onAuthenticationResult].
+ *
+ * Note that the value is in milliseconds and matches [SystemClock.elapsedRealtime].
+ *
+ * Also note that the value may change when the selected user is changed.
+ */
+ val lockoutEndTimestamp: Long?
+ get() = repository.lockoutEndTimestamp
/**
* Returns the currently-configured authentication method. This determines how the
@@ -190,7 +178,7 @@ constructor(
val skipCheck =
when {
// Lockout is active, the UI layer should not have called this; skip the attempt.
- lockout.value != null -> true
+ repository.lockoutEndTimestamp != null -> true
// The input is too short; skip the attempt.
input.isTooShort(authMethod) -> true
// Auto-confirm attempt when the feature is not enabled; skip the attempt.
@@ -211,27 +199,16 @@ constructor(
credential.zeroize()
if (authenticationResult.isSuccessful || !tryAutoConfirm) {
- repository.reportAuthenticationAttempt(
- isSuccessful = authenticationResult.isSuccessful,
- )
+ repository.reportAuthenticationAttempt(authenticationResult.isSuccessful)
}
// Check if lockout should start and, if so, kick off the countdown:
if (!authenticationResult.isSuccessful && authenticationResult.lockoutDurationMs > 0) {
- repository.apply {
- setLockoutDuration(durationMs = authenticationResult.lockoutDurationMs)
- reportLockoutStarted(durationMs = authenticationResult.lockoutDurationMs)
- hasLockoutOccurred.value = true
- }
- startLockoutCountdown()
+ repository.reportLockoutStarted(authenticationResult.lockoutDurationMs)
}
- if (authenticationResult.isSuccessful) {
- // Since authentication succeeded, refresh lockout to make sure the state is completely
- // reflecting the upstream source of truth.
- refreshLockout()
-
- repository.hasLockoutOccurred.value = false
+ if (authenticationResult.isSuccessful || !tryAutoConfirm) {
+ _onAuthenticationResult.emit(authenticationResult.isSuccessful)
}
return if (authenticationResult.isSuccessful) {
@@ -249,54 +226,6 @@ constructor(
}
}
- /** Starts refreshing the lockout state every second. */
- private suspend fun startLockoutCountdown() {
- cancelLockoutCountdown()
- lockoutCountdownJob =
- applicationScope.launch {
- while (refreshLockout()) {
- delay(1.seconds.inWholeMilliseconds)
- }
- }
- }
-
- /** Cancels any lockout state countdown started in [startLockoutCountdown]. */
- private fun cancelLockoutCountdown() {
- lockoutCountdownJob?.cancel()
- lockoutCountdownJob = null
- }
-
- /** Notifies that the currently-selected user has changed. */
- private suspend fun onSelectedUserChanged() {
- cancelLockoutCountdown()
- if (refreshLockout()) {
- startLockoutCountdown()
- }
- }
-
- /**
- * Refreshes the lockout state, hydrating the repository with the latest state.
- *
- * @return Whether lockout is active or not.
- */
- private suspend fun refreshLockout(): Boolean {
- withContext("$TAG#refreshLockout", backgroundDispatcher) {
- val failedAttemptCount = async { repository.getFailedAuthenticationAttemptCount() }
- val deadline = async { repository.getLockoutEndTimestamp() }
- val remainingMs = max(0, deadline.await() - clock.elapsedRealtime())
- repository.lockout.value =
- if (remainingMs > 0) {
- AuthenticationLockoutModel(
- failedAttemptCount = failedAttemptCount.await(),
- remainingSeconds = ceil(remainingMs / 1000f).toInt(),
- )
- } else {
- null // Lockout ended.
- }
- }
- return repository.lockout.value != null
- }
-
private fun AuthenticationMethodModel.createCredential(
input: List<Any>
): LockscreenCredential? {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 724c0fe1e4e4..1095abe24b47 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -19,7 +19,6 @@ package com.android.systemui.bouncer.domain.interactor
import android.content.Context
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repository.BouncerRepository
import com.android.systemui.classifier.FalsingClassifier
@@ -29,17 +28,15 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
/** Encapsulates business logic and application state accessing use-cases. */
@@ -52,33 +49,12 @@ constructor(
private val repository: BouncerRepository,
private val authenticationInteractor: AuthenticationInteractor,
private val keyguardFaceAuthInteractor: KeyguardFaceAuthInteractor,
- flags: SceneContainerFlags,
private val falsingInteractor: FalsingInteractor,
private val powerInteractor: PowerInteractor,
private val simBouncerInteractor: SimBouncerInteractor,
) {
-
- /** The user-facing message to show in the bouncer. */
- val message: StateFlow<String?> =
- combine(repository.message, authenticationInteractor.lockout) { message, lockout ->
- messageOrLockoutMessage(message, lockout)
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue =
- messageOrLockoutMessage(
- repository.message.value,
- authenticationInteractor.lockout.value,
- )
- )
-
- /**
- * The current authentication lockout (aka "throttling") state, set when the user has to wait
- * before being able to try another authentication attempt. `null` indicates lockout isn't
- * active.
- */
- val lockout: StateFlow<AuthenticationLockoutModel?> = authenticationInteractor.lockout
+ /** The user-facing message to show in the bouncer when lockout is not active. */
+ val message: StateFlow<String?> = repository.message
/** Whether the auto confirm feature is enabled for the currently-selected user. */
val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
@@ -101,18 +77,13 @@ constructor(
/** Emits a [Unit] each time the IME (keyboard) is hidden by the user. */
val onImeHiddenByUser: SharedFlow<Unit> = _onImeHiddenByUser
- init {
- if (flags.isEnabled()) {
- // Clear the message if moved from locked-out to no-longer locked-out.
- applicationScope.launch {
- lockout.pairwise().collect { (previous, current) ->
- if (previous != null && current == null) {
- clearMessage()
- }
- }
+ /** Emits a [Unit] each time a lockout is started for the selected user. */
+ val onLockoutStarted: Flow<Unit> =
+ authenticationInteractor.onAuthenticationResult
+ .filter { successfullyAuthenticated ->
+ !successfullyAuthenticated && authenticationInteractor.lockoutEndTimestamp != null
}
- }
- }
+ .map {}
/** Notifies that the user has places down a pointer, not necessarily dragging just yet. */
fun onDown() {
@@ -188,7 +159,7 @@ constructor(
}
if (authenticationInteractor.getAuthenticationMethod() == AuthenticationMethodModel.Sim) {
- // We authenticate sim in SimInteractor
+ // SIM is authenticated in SimBouncerInteractor.
return AuthenticationResult.SKIPPED
}
@@ -196,18 +167,20 @@ constructor(
// view-models, whose lifecycle (and thus scope) is shorter than this interactor.
// This allows the task to continue running properly even when the calling scope has been
// cancelled.
- return applicationScope
- .async {
- val authResult = authenticationInteractor.authenticate(input, tryAutoConfirm)
- if (
- authResult == AuthenticationResult.FAILED ||
- (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
- ) {
- showErrorMessage()
- }
- authResult
- }
- .await()
+ val authResult =
+ applicationScope
+ .async { authenticationInteractor.authenticate(input, tryAutoConfirm) }
+ .await()
+
+ if (authenticationInteractor.lockoutEndTimestamp != null) {
+ clearMessage()
+ } else if (
+ authResult == AuthenticationResult.FAILED ||
+ (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
+ ) {
+ showErrorMessage()
+ }
+ return authResult
}
/**
@@ -250,19 +223,4 @@ constructor(
else -> ""
}
}
-
- private fun messageOrLockoutMessage(
- message: String?,
- lockoutModel: AuthenticationLockoutModel?,
- ): String {
- return when {
- lockoutModel != null ->
- applicationContext.getString(
- com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown,
- lockoutModel.remainingSeconds,
- )
- message != null -> message
- else -> ""
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 4b1434323886..be6cf85a5a0e 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -19,6 +19,7 @@ package com.android.systemui.bouncer.ui.viewmodel
import android.content.Context
import android.graphics.Bitmap
import androidx.core.graphics.drawable.toBitmap
+import com.android.internal.R
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
@@ -34,19 +35,24 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.user.ui.viewmodel.UserActionViewModel
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import com.android.systemui.user.ui.viewmodel.UserViewModel
+import com.android.systemui.util.time.SystemClock
import dagger.Module
import dagger.Provides
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.job
@@ -58,13 +64,14 @@ class BouncerViewModel(
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
private val bouncerInteractor: BouncerInteractor,
- authenticationInteractor: AuthenticationInteractor,
+ private val authenticationInteractor: AuthenticationInteractor,
flags: SceneContainerFlags,
selectedUser: Flow<UserViewModel>,
users: Flow<List<UserViewModel>>,
userSwitcherMenu: Flow<List<UserActionViewModel>>,
actionButtonInteractor: BouncerActionButtonInteractor,
private val simBouncerInteractor: SimBouncerInteractor,
+ private val clock: SystemClock,
) {
val selectedUserImage: StateFlow<Bitmap?> =
selectedUser
@@ -104,15 +111,6 @@ class BouncerViewModel(
val isUserSwitcherVisible: Boolean
get() = bouncerInteractor.isUserSwitcherVisible
- private val isInputEnabled: StateFlow<Boolean> =
- bouncerInteractor.lockout
- .map { it == null }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = bouncerInteractor.lockout.value == null,
- )
-
// Handle to the scope of the child ViewModel (stored in [authMethod]).
private var childViewModelScope: CoroutineScope? = null
private val _dialogMessage = MutableStateFlow<String?>(null)
@@ -138,19 +136,22 @@ class BouncerViewModel(
*/
val dialogMessage: StateFlow<String?> = _dialogMessage.asStateFlow()
+ /**
+ * A message shown when the user has attempted the wrong credential too many times and now must
+ * wait a while before attempting to authenticate again.
+ *
+ * This is updated every second (countdown) during the lockout duration. When lockout is not
+ * active, this is `null` and no lockout message should be shown.
+ */
+ private val lockoutMessage = MutableStateFlow<String?>(null)
+
/** The user-facing message to show in the bouncer. */
val message: StateFlow<MessageViewModel> =
- combine(bouncerInteractor.message, bouncerInteractor.lockout) { message, lockout ->
- toMessageViewModel(message, isLockedOut = lockout != null)
- }
+ combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
- initialValue =
- toMessageViewModel(
- message = bouncerInteractor.message.value,
- isLockedOut = bouncerInteractor.lockout.value != null,
- ),
+ initialValue = createMessageViewModel(),
)
/**
@@ -194,31 +195,78 @@ class BouncerViewModel(
initialValue = isFoldSplitRequired(authMethodViewModel.value),
)
+ private val isInputEnabled: StateFlow<Boolean> =
+ lockoutMessage
+ .map { it == null }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = authenticationInteractor.lockoutEndTimestamp == null,
+ )
+
+ private var lockoutCountdownJob: Job? = null
+
init {
if (flags.isEnabled()) {
applicationScope.launch {
- combine(bouncerInteractor.lockout, authMethodViewModel) {
- lockout,
- authMethodViewModel ->
- if (lockout != null && authMethodViewModel != null) {
+ bouncerInteractor.onLockoutStarted.collect {
+ showLockoutDialog()
+ startLockoutCountdown()
+ }
+ }
+
+ applicationScope.launch {
+ // Update the lockout countdown whenever the selected user is switched.
+ selectedUser.collect { startLockoutCountdown() }
+ }
+ }
+ }
+
+ /** Notifies that the dialog has been dismissed by the user. */
+ fun onDialogDismissed() {
+ _dialogMessage.value = null
+ }
+
+ private fun showLockoutDialog() {
+ applicationScope.launch {
+ val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
+ _dialogMessage.value =
+ authMethodViewModel.value?.lockoutMessageId?.let { messageId ->
+ applicationContext.getString(
+ messageId,
+ failedAttempts,
+ remainingLockoutSeconds()
+ )
+ }
+ }
+ }
+
+ /** Shows the countdown message and refreshes it every second. */
+ private fun startLockoutCountdown() {
+ lockoutCountdownJob?.cancel()
+ lockoutCountdownJob =
+ applicationScope.launch {
+ do {
+ val remainingSeconds = remainingLockoutSeconds()
+ lockoutMessage.value =
+ if (remainingSeconds > 0) {
applicationContext.getString(
- authMethodViewModel.lockoutMessageId,
- lockout.failedAttemptCount,
- lockout.remainingSeconds,
+ R.string.lockscreen_too_many_failed_attempts_countdown,
+ remainingSeconds,
)
} else {
null
}
- }
- .distinctUntilChanged()
- .collect { dialogMessage -> _dialogMessage.value = dialogMessage }
+ delay(1.seconds)
+ } while (remainingSeconds > 0)
+ lockoutCountdownJob = null
}
- }
}
- /** Notifies that the dialog has been dismissed by the user. */
- fun onDialogDismissed() {
- _dialogMessage.value = null
+ private fun remainingLockoutSeconds(): Int {
+ val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+ val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
+ return ceil(remainingMs / 1000f).toInt()
}
private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
@@ -229,12 +277,11 @@ class BouncerViewModel(
return authMethod !is PasswordBouncerViewModel
}
- private fun toMessageViewModel(
- message: String?,
- isLockedOut: Boolean,
- ): MessageViewModel {
+ private fun createMessageViewModel(): MessageViewModel {
+ val isLockedOut = lockoutMessage.value != null
return MessageViewModel(
- text = message ?: "",
+ // A lockout message takes precedence over the non-lockout message.
+ text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "",
isUpdateAnimated = !isLockedOut,
)
}
@@ -328,6 +375,7 @@ object BouncerViewModelModule {
userSwitcherViewModel: UserSwitcherViewModel,
actionButtonInteractor: BouncerActionButtonInteractor,
simBouncerInteractor: SimBouncerInteractor,
+ clock: SystemClock,
): BouncerViewModel {
return BouncerViewModel(
applicationContext = applicationContext,
@@ -341,6 +389,7 @@ object BouncerViewModelModule {
userSwitcherMenu = userSwitcherViewModel.menu,
actionButtonInteractor = actionButtonInteractor,
simBouncerInteractor = simBouncerInteractor,
+ clock = clock,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index b68271767fc2..8e14778f1aa7 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -56,13 +56,11 @@ class PasswordBouncerViewModel(
/** Whether the UI should request focus on the text field element. */
val isTextFieldFocusRequested =
- combine(interactor.lockout, isTextFieldFocused) { throttling, hasFocus ->
- throttling == null && !hasFocus
- }
+ combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> hasInput && !hasFocus }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
- initialValue = interactor.lockout.value == null && !isTextFieldFocused.value,
+ initialValue = isInputEnabled.value && !isTextFieldFocused.value,
)
override fun onHidden() {
@@ -104,7 +102,7 @@ class PasswordBouncerViewModel(
* hidden.
*/
suspend fun onImeVisibilityChanged(isVisible: Boolean) {
- if (isImeVisible && !isVisible && interactor.lockout.value == null) {
+ if (isImeVisible && !isVisible && isInputEnabled.value) {
interactor.onImeHiddenByUser()
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 47be8ab0c0a2..f6a9570fc94c 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -159,7 +159,7 @@ constructor(
fun attemptDeviceEntry() {
// TODO (b/307768356),
// 1. Check if the device is already authenticated by trust agent/passive biometrics
- // 2. show SPFS/UDFPS bouncer if it is available AlternateBouncerInteractor.show
+ // 2. Show SPFS/UDFPS bouncer if it is available AlternateBouncerInteractor.show
// 3. For face auth only setups trigger face auth, delay transitioning to bouncer for
// a small amount of time.
// 4. Transition to bouncer scene
@@ -197,8 +197,8 @@ constructor(
init {
if (flags.isEnabled()) {
applicationScope.launch {
- authenticationInteractor.authenticationChallengeResult.collectLatest { successful ->
- if (successful) {
+ authenticationInteractor.onAuthenticationResult.collectLatest { isSuccessful ->
+ if (isSuccessful) {
repository.reportSuccessfulAuthentication()
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index 7c5696c716d8..848650810582 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -20,7 +20,6 @@ import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockPatternView
import com.android.internal.widget.LockscreenCredential
import com.android.keyguard.KeyguardSecurityModel.SecurityMode
-import com.android.systemui.authentication.shared.model.AuthenticationLockoutModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.AuthenticationResultModel
@@ -29,7 +28,6 @@ import dagger.Binds
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -40,15 +38,11 @@ class FakeAuthenticationRepository(
private val currentTime: () -> Long,
) : AuthenticationRepository {
- override val authenticationChallengeResult = MutableSharedFlow<Boolean>()
-
override val hintedPinLength: Int = HINTING_PIN_LENGTH
private val _isPatternVisible = MutableStateFlow(true)
override val isPatternVisible: StateFlow<Boolean> = _isPatternVisible.asStateFlow()
- override val lockout: MutableStateFlow<AuthenticationLockoutModel?> = MutableStateFlow(null)
-
override val hasLockoutOccurred = MutableStateFlow(false)
private val _isAutoConfirmFeatureEnabled = MutableStateFlow(false)
@@ -68,8 +62,6 @@ class FakeAuthenticationRepository(
override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
_isPinEnhancedPrivacyEnabled.asStateFlow()
- private var failedAttemptCount = 0
- private var lockoutEndTimestamp = 0L
private var credentialOverride: List<Any>? = null
private var securityMode: SecurityMode = DEFAULT_AUTHENTICATION_METHOD.toSecurityMode()
@@ -89,11 +81,27 @@ class FakeAuthenticationRepository(
}
override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) {
- failedAttemptCount = if (isSuccessful) 0 else failedAttemptCount + 1
- authenticationChallengeResult.emit(isSuccessful)
+ if (isSuccessful) {
+ _failedAuthenticationAttempts.value = 0
+ _lockoutEndTimestamp = null
+ hasLockoutOccurred.value = false
+ lockoutStartedReportCount = 0
+ } else {
+ _failedAuthenticationAttempts.value++
+ }
}
+ private var _failedAuthenticationAttempts = MutableStateFlow(0)
+ override val failedAuthenticationAttempts: StateFlow<Int> =
+ _failedAuthenticationAttempts.asStateFlow()
+
+ private var _lockoutEndTimestamp: Long? = null
+ override val lockoutEndTimestamp: Long?
+ get() = if (currentTime() < (_lockoutEndTimestamp ?: 0)) _lockoutEndTimestamp else null
+
override suspend fun reportLockoutStarted(durationMs: Int) {
+ _lockoutEndTimestamp = (currentTime() + durationMs).takeIf { durationMs > 0 }
+ hasLockoutOccurred.value = true
lockoutStartedReportCount++
}
@@ -101,25 +109,10 @@ class FakeAuthenticationRepository(
return (credentialOverride ?: DEFAULT_PIN).size
}
- override suspend fun getFailedAuthenticationAttemptCount(): Int {
- return failedAttemptCount
- }
-
- override suspend fun getLockoutEndTimestamp(): Long {
- return lockoutEndTimestamp
- }
-
fun setAutoConfirmFeatureEnabled(isEnabled: Boolean) {
_isAutoConfirmFeatureEnabled.value = isEnabled
}
- override suspend fun setLockoutDuration(durationMs: Int) {
- lockoutEndTimestamp = if (durationMs > 0) currentTime() + durationMs else 0
- if (durationMs > 0) {
- hasLockoutOccurred.value = true
- }
- }
-
override suspend fun checkCredential(
credential: LockscreenCredential
): AuthenticationResultModel {
@@ -136,8 +129,8 @@ class FakeAuthenticationRepository(
else -> error("Unexpected credential type ${credential.type}!")
}
- return if (isSuccessful || failedAttemptCount < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
- hasLockoutOccurred.value = false
+ val failedAttempts = _failedAuthenticationAttempts.value
+ return if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
AuthenticationResultModel(
isSuccessful = isSuccessful,
lockoutDurationMs = 0,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt
index 060ca4c7e912..05cb059a00cd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorKosmos.kt
@@ -19,17 +19,11 @@ package com.android.systemui.authentication.domain.interactor
import com.android.systemui.authentication.data.repository.authenticationRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.user.data.repository.userRepository
-import com.android.systemui.util.time.fakeSystemClock
val Kosmos.authenticationInteractor by
Kosmos.Fixture {
AuthenticationInteractor(
applicationScope = applicationCoroutineScope,
repository = authenticationRepository,
- backgroundDispatcher = testDispatcher,
- userRepository = userRepository,
- clock = fakeSystemClock,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 0b41926ed13e..25b97b3dab5d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -91,7 +91,6 @@ import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.data.repository.TelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.user.ui.viewmodel.UserActionViewModel
import com.android.systemui.user.ui.viewmodel.UserViewModel
@@ -174,7 +173,7 @@ class SceneTestUtils(
mobileConnectionsRepository = mobileConnectionsRepository,
)
- val userRepository: UserRepository by lazy {
+ val userRepository: FakeUserRepository by lazy {
FakeUserRepository().apply {
val users = listOf(UserInfo(/* id= */ 0, "name", /* flags= */ 0))
setUserInfos(users)
@@ -236,9 +235,6 @@ class SceneTestUtils(
return AuthenticationInteractor(
applicationScope = applicationScope(),
repository = repository,
- backgroundDispatcher = testDispatcher,
- userRepository = userRepository,
- clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } }
)
}
@@ -274,7 +270,6 @@ class SceneTestUtils(
repository = bouncerRepository,
authenticationInteractor = authenticationInteractor,
keyguardFaceAuthInteractor = keyguardFaceAuthInteractor,
- flags = sceneContainerFlags,
falsingInteractor = falsingInteractor(),
powerInteractor = powerInteractor(),
simBouncerInteractor = simBouncerInteractor,
@@ -312,6 +307,7 @@ class SceneTestUtils(
userSwitcherMenu = flowOf(createMenuActions()),
actionButtonInteractor = actionButtonInteractor,
simBouncerInteractor = simBouncerInteractor,
+ clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } },
)
}