Merge "[flexiglass] Bouncer throttling - data and domain layers." into udc-dev
This commit is contained in:
@@ -5129,4 +5129,5 @@
|
|||||||
<java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" />
|
<java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" />
|
||||||
|
|
||||||
<java-symbol type="drawable" name="focus_event_pressed_key_background" />
|
<java-symbol type="drawable" name="focus_event_pressed_key_background" />
|
||||||
|
<java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" />
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ interface AuthenticationRepository {
|
|||||||
*/
|
*/
|
||||||
val isBypassEnabled: StateFlow<Boolean>
|
val isBypassEnabled: StateFlow<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of consecutively failed authentication attempts. This resets to `0` when
|
||||||
|
* authentication succeeds.
|
||||||
|
*/
|
||||||
|
val failedAuthenticationAttempts: StateFlow<Int>
|
||||||
|
|
||||||
/** See [isUnlocked]. */
|
/** See [isUnlocked]. */
|
||||||
fun setUnlocked(isUnlocked: Boolean)
|
fun setUnlocked(isUnlocked: Boolean)
|
||||||
|
|
||||||
@@ -59,6 +65,9 @@ interface AuthenticationRepository {
|
|||||||
|
|
||||||
/** See [isBypassEnabled]. */
|
/** See [isBypassEnabled]. */
|
||||||
fun setBypassEnabled(isBypassEnabled: Boolean)
|
fun setBypassEnabled(isBypassEnabled: Boolean)
|
||||||
|
|
||||||
|
/** See [failedAuthenticationAttempts]. */
|
||||||
|
fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
|
class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
|
||||||
@@ -75,6 +84,10 @@ class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationReposit
|
|||||||
private val _isBypassEnabled = MutableStateFlow(false)
|
private val _isBypassEnabled = MutableStateFlow(false)
|
||||||
override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
|
override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _failedAuthenticationAttempts = MutableStateFlow(0)
|
||||||
|
override val failedAuthenticationAttempts: StateFlow<Int> =
|
||||||
|
_failedAuthenticationAttempts.asStateFlow()
|
||||||
|
|
||||||
override fun setUnlocked(isUnlocked: Boolean) {
|
override fun setUnlocked(isUnlocked: Boolean) {
|
||||||
_isUnlocked.value = isUnlocked
|
_isUnlocked.value = isUnlocked
|
||||||
}
|
}
|
||||||
@@ -86,6 +99,10 @@ class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationReposit
|
|||||||
override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
|
override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
|
||||||
_authenticationMethod.value = authenticationMethod
|
_authenticationMethod.value = authenticationMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int) {
|
||||||
|
_failedAuthenticationAttempts.value = failedAuthenticationAttempts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ constructor(
|
|||||||
*/
|
*/
|
||||||
val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
|
val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of consecutively failed authentication attempts. This resets to `0` when
|
||||||
|
* authentication succeeds.
|
||||||
|
*/
|
||||||
|
val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// UNLOCKS WHEN AUTH METHOD REMOVED.
|
// UNLOCKS WHEN AUTH METHOD REMOVED.
|
||||||
//
|
//
|
||||||
@@ -130,7 +136,12 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSuccessful) {
|
if (isSuccessful) {
|
||||||
|
repository.setFailedAuthenticationAttempts(0)
|
||||||
repository.setUnlocked(true)
|
repository.setUnlocked(true)
|
||||||
|
} else {
|
||||||
|
repository.setFailedAuthenticationAttempts(
|
||||||
|
repository.failedAuthenticationAttempts.value + 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return isSuccessful
|
return isSuccessful
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ sealed class AuthenticationMethodModel(
|
|||||||
|
|
||||||
data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
|
data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
|
||||||
|
|
||||||
data class Pattern(val coordinates: List<PatternCoordinate>) :
|
data class Pattern(
|
||||||
AuthenticationMethodModel(isSecure = true) {
|
val coordinates: List<PatternCoordinate>,
|
||||||
|
val isPatternVisible: Boolean = true,
|
||||||
|
) : AuthenticationMethodModel(isSecure = true) {
|
||||||
|
|
||||||
data class PatternCoordinate(
|
data class PatternCoordinate(
|
||||||
val x: Int,
|
val x: Int,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package com.android.systemui.bouncer.data.repo
|
package com.android.systemui.bouncer.data.repo
|
||||||
|
|
||||||
|
import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -29,7 +30,15 @@ class BouncerRepository @Inject constructor() {
|
|||||||
/** The user-facing message to show in the bouncer. */
|
/** The user-facing message to show in the bouncer. */
|
||||||
val message: StateFlow<String?> = _message.asStateFlow()
|
val message: StateFlow<String?> = _message.asStateFlow()
|
||||||
|
|
||||||
|
private val _throttling = MutableStateFlow<AuthenticationThrottledModel?>(null)
|
||||||
|
/** The current authentication throttling state. If `null`, there's no throttling. */
|
||||||
|
val throttling: StateFlow<AuthenticationThrottledModel?> = _throttling.asStateFlow()
|
||||||
|
|
||||||
fun setMessage(message: String?) {
|
fun setMessage(message: String?) {
|
||||||
_message.value = message
|
_message.value = message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setThrottling(throttling: AuthenticationThrottledModel?) {
|
||||||
|
_throttling.value = throttling
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,12 @@
|
|||||||
package com.android.systemui.bouncer.domain.interactor
|
package com.android.systemui.bouncer.domain.interactor
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
|
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
|
||||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||||
import com.android.systemui.bouncer.data.repo.BouncerRepository
|
import com.android.systemui.bouncer.data.repo.BouncerRepository
|
||||||
|
import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
|
||||||
import com.android.systemui.dagger.qualifiers.Application
|
import com.android.systemui.dagger.qualifiers.Application
|
||||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||||
import com.android.systemui.scene.shared.model.SceneKey
|
import com.android.systemui.scene.shared.model.SceneKey
|
||||||
@@ -29,8 +31,11 @@ import dagger.assisted.Assisted
|
|||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/** Encapsulates business logic and application state accessing use-cases. */
|
/** Encapsulates business logic and application state accessing use-cases. */
|
||||||
@@ -46,7 +51,22 @@ constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
/** The user-facing message to show in the bouncer. */
|
/** The user-facing message to show in the bouncer. */
|
||||||
val message: StateFlow<String?> = repository.message
|
val message: StateFlow<String?> =
|
||||||
|
combine(
|
||||||
|
repository.message,
|
||||||
|
repository.throttling,
|
||||||
|
) { message, throttling ->
|
||||||
|
messageOrThrottlingMessage(message, throttling)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = applicationScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(),
|
||||||
|
initialValue =
|
||||||
|
messageOrThrottlingMessage(
|
||||||
|
repository.message.value,
|
||||||
|
repository.throttling.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The currently-configured authentication method. This determines how the authentication
|
* The currently-configured authentication method. This determines how the authentication
|
||||||
@@ -55,6 +75,9 @@ constructor(
|
|||||||
val authenticationMethod: StateFlow<AuthenticationMethodModel> =
|
val authenticationMethod: StateFlow<AuthenticationMethodModel> =
|
||||||
authenticationInteractor.authenticationMethod
|
authenticationInteractor.authenticationMethod
|
||||||
|
|
||||||
|
/** The current authentication throttling state. If `null`, there's no throttling. */
|
||||||
|
val throttling: StateFlow<AuthenticationThrottledModel?> = repository.throttling
|
||||||
|
|
||||||
init {
|
init {
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
combine(
|
combine(
|
||||||
@@ -129,14 +152,39 @@ constructor(
|
|||||||
fun authenticate(
|
fun authenticate(
|
||||||
input: List<Any>,
|
input: List<Any>,
|
||||||
) {
|
) {
|
||||||
|
if (repository.throttling.value != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val isAuthenticated = authenticationInteractor.authenticate(input)
|
val isAuthenticated = authenticationInteractor.authenticate(input)
|
||||||
if (isAuthenticated) {
|
val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
|
||||||
sceneInteractor.setCurrentScene(
|
when {
|
||||||
containerName = containerName,
|
isAuthenticated -> {
|
||||||
scene = SceneModel(SceneKey.Gone),
|
repository.setThrottling(null)
|
||||||
)
|
sceneInteractor.setCurrentScene(
|
||||||
} else {
|
containerName = containerName,
|
||||||
repository.setMessage(errorMessage(authenticationMethod.value))
|
scene = SceneModel(SceneKey.Gone),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
failedAttempts >= THROTTLE_AGGRESSIVELY_AFTER || failedAttempts % THROTTLE_EVERY == 0 ->
|
||||||
|
applicationScope.launch {
|
||||||
|
var remainingDurationSec = THROTTLE_DURATION_SEC
|
||||||
|
while (remainingDurationSec > 0) {
|
||||||
|
repository.setThrottling(
|
||||||
|
AuthenticationThrottledModel(
|
||||||
|
failedAttemptCount = failedAttempts,
|
||||||
|
totalDurationSec = THROTTLE_DURATION_SEC,
|
||||||
|
remainingDurationSec = remainingDurationSec,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
remainingDurationSec--
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.setThrottling(null)
|
||||||
|
clearMessage()
|
||||||
|
}
|
||||||
|
else -> repository.setMessage(errorMessage(authenticationMethod.value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,10 +211,31 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun messageOrThrottlingMessage(
|
||||||
|
message: String?,
|
||||||
|
throttling: AuthenticationThrottledModel?,
|
||||||
|
): String {
|
||||||
|
return when {
|
||||||
|
throttling != null ->
|
||||||
|
applicationContext.getString(
|
||||||
|
com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown,
|
||||||
|
throttling.remainingDurationSec,
|
||||||
|
)
|
||||||
|
message != null -> message
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
containerName: String,
|
containerName: String,
|
||||||
): BouncerInteractor
|
): BouncerInteractor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@VisibleForTesting const val THROTTLE_DURATION_SEC = 30
|
||||||
|
@VisibleForTesting const val THROTTLE_AGGRESSIVELY_AFTER = 15
|
||||||
|
@VisibleForTesting const val THROTTLE_EVERY = 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.bouncer.shared.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models application state for when further authentication attempts are being throttled due to too
|
||||||
|
* many consecutive failed authentication attempts.
|
||||||
|
*/
|
||||||
|
data class AuthenticationThrottledModel(
|
||||||
|
/** Total number of failed attempts so far. */
|
||||||
|
val failedAttemptCount: Int,
|
||||||
|
/** Total amount of time the user has to wait before attempting again. */
|
||||||
|
val totalDurationSec: Int,
|
||||||
|
/** Remaining amount of time the user has to wait before attempting again. */
|
||||||
|
val remainingDurationSec: Int,
|
||||||
|
)
|
||||||
@@ -145,50 +145,59 @@ class AuthenticationInteractorTest : SysuiTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
|
fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
|
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
|
||||||
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
||||||
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
|
|
||||||
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
|
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
|
||||||
assertThat(isUnlocked).isTrue()
|
assertThat(isUnlocked).isTrue()
|
||||||
|
assertThat(failedAttemptCount).isEqualTo(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
|
fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
|
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
|
||||||
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
||||||
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
|
|
||||||
assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
|
assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
|
assertThat(failedAttemptCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
|
fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
|
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
|
||||||
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
||||||
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
|
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
|
|
||||||
assertThat(underTest.authenticate("password".toList())).isTrue()
|
assertThat(underTest.authenticate("password".toList())).isTrue()
|
||||||
assertThat(isUnlocked).isTrue()
|
assertThat(isUnlocked).isTrue()
|
||||||
|
assertThat(failedAttemptCount).isEqualTo(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
|
fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
|
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
|
||||||
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
||||||
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
|
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
|
|
||||||
assertThat(underTest.authenticate("alohomora".toList())).isFalse()
|
assertThat(underTest.authenticate("alohomora".toList())).isFalse()
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
|
assertThat(failedAttemptCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
|
fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
|
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
|
||||||
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
||||||
underTest.setAuthenticationMethod(
|
underTest.setAuthenticationMethod(
|
||||||
AuthenticationMethodModel.Pattern(
|
AuthenticationMethodModel.Pattern(
|
||||||
@@ -230,11 +239,13 @@ class AuthenticationInteractorTest : SysuiTestCase() {
|
|||||||
)
|
)
|
||||||
.isTrue()
|
.isTrue()
|
||||||
assertThat(isUnlocked).isTrue()
|
assertThat(isUnlocked).isTrue()
|
||||||
|
assertThat(failedAttemptCount).isEqualTo(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
|
fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
|
val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
|
||||||
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
val isUnlocked by collectLastValue(underTest.isUnlocked)
|
||||||
underTest.setAuthenticationMethod(
|
underTest.setAuthenticationMethod(
|
||||||
AuthenticationMethodModel.Pattern(
|
AuthenticationMethodModel.Pattern(
|
||||||
@@ -276,6 +287,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
|
|||||||
)
|
)
|
||||||
.isFalse()
|
.isFalse()
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
|
assertThat(failedAttemptCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.android.systemui.scene.shared.model.SceneModel
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -75,7 +76,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
|
||||||
|
|
||||||
underTest.clearMessage()
|
underTest.clearMessage()
|
||||||
assertThat(message).isNull()
|
assertThat(message).isEmpty()
|
||||||
|
|
||||||
underTest.resetMessage()
|
underTest.resetMessage()
|
||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
|
||||||
@@ -107,7 +108,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
|
||||||
|
|
||||||
underTest.clearMessage()
|
underTest.clearMessage()
|
||||||
assertThat(message).isNull()
|
assertThat(message).isEmpty()
|
||||||
|
|
||||||
underTest.resetMessage()
|
underTest.resetMessage()
|
||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
|
||||||
@@ -139,7 +140,7 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
|
||||||
|
|
||||||
underTest.clearMessage()
|
underTest.clearMessage()
|
||||||
assertThat(message).isNull()
|
assertThat(message).isEmpty()
|
||||||
|
|
||||||
underTest.resetMessage()
|
underTest.resetMessage()
|
||||||
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
|
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
|
||||||
@@ -201,6 +202,56 @@ class BouncerInteractorTest : SysuiTestCase() {
|
|||||||
assertThat(message).isEqualTo(customMessage)
|
assertThat(message).isEqualTo(customMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun throttling() =
|
||||||
|
testScope.runTest {
|
||||||
|
val throttling by collectLastValue(underTest.throttling)
|
||||||
|
val message by collectLastValue(underTest.message)
|
||||||
|
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||||
|
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||||
|
assertThat(throttling).isNull()
|
||||||
|
assertThat(message).isEqualTo("")
|
||||||
|
assertThat(isUnlocked).isFalse()
|
||||||
|
repeat(BouncerInteractor.THROTTLE_EVERY) { times ->
|
||||||
|
// Wrong PIN.
|
||||||
|
underTest.authenticate(listOf(6, 7, 8, 9))
|
||||||
|
if (times < BouncerInteractor.THROTTLE_EVERY - 1) {
|
||||||
|
assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(throttling).isNotNull()
|
||||||
|
assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
|
||||||
|
|
||||||
|
// Correct PIN, but throttled, so doesn't unlock:
|
||||||
|
underTest.authenticate(listOf(1, 2, 3, 4))
|
||||||
|
assertThat(isUnlocked).isFalse()
|
||||||
|
assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
|
||||||
|
|
||||||
|
throttling?.totalDurationSec?.let { seconds ->
|
||||||
|
repeat(seconds) { time ->
|
||||||
|
advanceTimeBy(1000)
|
||||||
|
val remainingTime = seconds - time - 1
|
||||||
|
if (remainingTime > 0) {
|
||||||
|
assertTryAgainMessage(message, remainingTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(message).isEqualTo("")
|
||||||
|
assertThat(throttling).isNull()
|
||||||
|
assertThat(isUnlocked).isFalse()
|
||||||
|
|
||||||
|
// Correct PIN and no longer throttled so unlocks:
|
||||||
|
underTest.authenticate(listOf(1, 2, 3, 4))
|
||||||
|
assertThat(isUnlocked).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertTryAgainMessage(
|
||||||
|
message: String?,
|
||||||
|
time: Int,
|
||||||
|
) {
|
||||||
|
assertThat(message).isEqualTo("Try again in $time seconds.")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
|
private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
|
||||||
private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
|
private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
|
||||||
|
|||||||
Reference in New Issue
Block a user