From f2c66dee859cf50eed77d1f2c6b4ade9ca10f719 Mon Sep 17 00:00:00 2001 From: Chandru S Date: Sun, 25 Jun 2023 02:02:53 +0800 Subject: [PATCH] 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 --- .../SystemUIKeyguardFaceAuthInteractor.kt | 27 ++++++++++++++++--- .../shared/model/FaceAuthenticationModels.kt | 9 +++++-- .../DeviceEntryFaceAuthRepositoryTest.kt | 10 +++---- .../KeyguardFaceAuthInteractorTest.kt | 20 ++++++++++++++ .../FakeDeviceEntryFaceAuthRepository.kt | 8 +++++- 5 files changed, 60 insertions(+), 14 deletions(-) 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 8b749f0f4bc48..d467225a9d639 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.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(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 9e7dec4dc1d09..b354cfd276870 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 @@ 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. 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 9ca6dce165e03..e042564646e74 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 @@ 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) 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 80700e59a2d4f..ee5c1cc31b0b3 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.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 { 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 738f09ddce3d9..2715aaa82253c 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 @@ 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?>(null) val runningAuthRequest: StateFlow?> = _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