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
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
index 8b749f0..d467225 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
@@ -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.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 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 @@
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 @@
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(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
index 9e7dec4..b354cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -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 @@
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.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 9ca6dce..e042564 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -418,13 +418,9 @@
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)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
index 80700e5..ee5c1cc3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -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.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.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 @@
underTest =
SystemUIKeyguardFaceAuthInteractor(
+ mContext,
testScope.backgroundScope,
dispatcher,
faceAuthRepository,
@@ -144,6 +148,22 @@
}
@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 {
underTest.start()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index 738f09d..2715aaa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -41,7 +41,9 @@
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 @@
_isAuthRunning.value = true
}
+ fun setLockedOut(value: Boolean) {
+ _isLockedOut.value = value
+ }
+
override fun cancel() {
_isAuthRunning.value = false
_runningAuthRequest.value = null