Whenever face auth is locked out, provide the locked out error message in response to face auth triggers

Fixes: 282093034
Test: atest KeyguardFaceAuthInteractorTest
Change-Id: Ibffbeadceb1716a7090c00ef007b961802db2024
This commit is contained in:
Chandru S
2023-06-25 02:02:53 +08:00
parent 12d35805d3
commit f2c66dee85
5 changed files with 60 additions and 14 deletions

View File

@@ -16,9 +16,12 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.Context
import android.hardware.biometrics.BiometricFaceConstants
import com.android.keyguard.FaceAuthUiEvent
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.CoreStartable
import com.android.systemui.R
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.dagger.SysUISingleton
@@ -27,6 +30,8 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.shared.model.AuthenticationStatus
import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.log.FaceAuthenticationLogger
import com.android.systemui.util.kotlin.pairwise
@@ -34,7 +39,9 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -50,6 +57,7 @@ import kotlinx.coroutines.launch
class SystemUIKeyguardFaceAuthInteractor
@Inject
constructor(
private val context: Context,
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
private val repository: DeviceEntryFaceAuthRepository,
@@ -157,17 +165,28 @@ constructor(
repository.cancel()
}
private val _authenticationStatusOverride = MutableStateFlow<AuthenticationStatus?>(null)
/** Provide the status of face authentication */
override val authenticationStatus = repository.authenticationStatus
override val authenticationStatus =
merge(_authenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
/** Provide the status of face detection */
override val detectionStatus = repository.detectionStatus
private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) {
if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) {
applicationScope.launch {
faceAuthenticationLogger.authRequested(uiEvent)
repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect)
if (repository.isLockedOut.value) {
_authenticationStatusOverride.value =
ErrorAuthenticationStatus(
BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT,
context.resources.getString(R.string.keyguard_face_unlock_unavailable)
)
} else {
_authenticationStatusOverride.value = null
applicationScope.launch {
faceAuthenticationLogger.authRequested(uiEvent)
repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect)
}
}
} else {
faceAuthenticationLogger.ignoredFaceAuthTrigger(

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.shared.model
import android.hardware.face.FaceManager
import android.os.SystemClock.elapsedRealtime
/**
* Authentication status provided by
@@ -38,8 +39,12 @@ data class AcquiredAuthenticationStatus(val acquiredInfo: Int) : AuthenticationS
object FailedAuthenticationStatus : AuthenticationStatus()
/** Face authentication error message */
data class ErrorAuthenticationStatus(val msgId: Int, val msg: String? = null) :
AuthenticationStatus() {
data class ErrorAuthenticationStatus(
val msgId: Int,
val msg: String? = null,
// present to break equality check if the same error occurs repeatedly.
val createdAt: Long = elapsedRealtime()
) : AuthenticationStatus() {
/**
* Method that checks if [msgId] is a lockout error. A lockout error means that face
* authentication is locked out.

View File

@@ -418,13 +418,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
FACE_ERROR_CANCELED,
"First auth attempt cancellation completed"
)
assertThat(authStatus())
.isEqualTo(
ErrorAuthenticationStatus(
FACE_ERROR_CANCELED,
"First auth attempt cancellation completed"
)
)
val value = authStatus() as ErrorAuthenticationStatus
assertThat(value.msgId).isEqualTo(FACE_ERROR_CANCELED)
assertThat(value.msg).isEqualTo("First auth attempt cancellation completed")
faceAuthenticateIsCalled()
uiEventIsLogged(FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import android.hardware.biometrics.BiometricFaceConstants
import android.os.Handler
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -30,6 +31,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInte
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.bouncer.ui.BouncerView
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
@@ -39,6 +41,7 @@ import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepo
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -90,6 +93,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
underTest =
SystemUIKeyguardFaceAuthInteractor(
mContext,
testScope.backgroundScope,
dispatcher,
faceAuthRepository,
@@ -143,6 +147,22 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
)
}
@Test
fun whenFaceIsLockedOutAnyAttemptsToTriggerFaceAuthMustProvideLockoutError() =
testScope.runTest {
underTest.start()
val authenticationStatus = collectLastValue(underTest.authenticationStatus)
faceAuthRepository.setLockedOut(true)
underTest.onDeviceLifted()
val outputValue = authenticationStatus()!! as ErrorAuthenticationStatus
assertThat(outputValue.msgId)
.isEqualTo(BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT)
assertThat(outputValue.msg).isEqualTo("Face Unlock unavailable")
assertThat(faceAuthRepository.runningAuthRequest.value).isNull()
}
@Test
fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromAodState() =
testScope.runTest {

View File

@@ -41,7 +41,9 @@ class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
fun setDetectionStatus(status: DetectionStatus) {
_detectionStatus.value = status
}
override val isLockedOut = MutableStateFlow(false)
private val _isLockedOut = MutableStateFlow(false)
override val isLockedOut = _isLockedOut
private val _runningAuthRequest = MutableStateFlow<Pair<FaceAuthUiEvent, Boolean>?>(null)
val runningAuthRequest: StateFlow<Pair<FaceAuthUiEvent, Boolean>?> =
_runningAuthRequest.asStateFlow()
@@ -56,6 +58,10 @@ class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
_isAuthRunning.value = true
}
fun setLockedOut(value: Boolean) {
_isLockedOut.value = value
}
override fun cancel() {
_isAuthRunning.value = false
_runningAuthRequest.value = null