From ea2ffe4a7b43d7c7def899422d5e15fc5b553ff2 Mon Sep 17 00:00:00 2001 From: Beverly Date: Fri, 14 Jul 2023 17:55:58 +0000 Subject: [PATCH] Add BiometricMessageInteractor for fingerprint msgs BiometricMessageInteractor filters fingerprint error, help and failure messages. Test: atest BiometricMessageInteractorTest Bug: 288308594 Change-Id: Ic18303ab489672796de35feedae2f3f24bd8551c --- .../DeviceEntryFingerprintAuthRepository.kt | 4 +- .../interactor/BiometricMessageInteractor.kt | 138 ++++++++++ .../model/FingerprintAuthenticationModels.kt | 2 +- ...eviceEntryFingerprintAuthRepositoryTest.kt | 4 +- .../BiometricMessageInteractorTest.kt | 260 ++++++++++++++++++ 5 files changed, 403 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt index 616ce39d3feea..9bec30052476b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt @@ -28,7 +28,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FailedFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus @@ -214,7 +214,7 @@ constructor( ) { sendUpdateIfFingerprint( biometricSourceType, - FailedFingerprintAuthenticationStatus, + FailFingerprintAuthenticationStatus, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt new file mode 100644 index 0000000000000..c849b8495a26b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt @@ -0,0 +1,138 @@ +/* + * 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.keyguard.domain.interactor + +import android.content.res.Resources +import android.hardware.biometrics.BiometricSourceType +import android.hardware.biometrics.BiometricSourceType.FINGERPRINT +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED +import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus +import com.android.systemui.keyguard.util.IndicationHelper +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map + +/** + * BiometricMessage business logic. Filters biometric error/acquired/fail/success events for + * authentication events that should never surface a message to the user at the current device + * state. + */ +@ExperimentalCoroutinesApi +@SysUISingleton +class BiometricMessageInteractor +@Inject +constructor( + @Main private val resources: Resources, + private val fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, + private val fingerprintPropertyRepository: FingerprintPropertyRepository, + private val indicationHelper: IndicationHelper, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, +) { + val fingerprintErrorMessage: Flow = + fingerprintAuthRepository.authenticationStatus + .filter { + it is ErrorFingerprintAuthenticationStatus && + !indicationHelper.shouldSuppressErrorMsg(FINGERPRINT, it.msgId) + } + .map { + val errorStatus = it as ErrorFingerprintAuthenticationStatus + BiometricMessage( + FINGERPRINT, + BiometricMessageType.ERROR, + errorStatus.msgId, + errorStatus.msg, + ) + } + + val fingerprintHelpMessage: Flow = + fingerprintAuthRepository.authenticationStatus + .filter { it is HelpFingerprintAuthenticationStatus } + .filterNot { isPrimaryAuthRequired() } + .map { + val helpStatus = it as HelpFingerprintAuthenticationStatus + BiometricMessage( + FINGERPRINT, + BiometricMessageType.HELP, + helpStatus.msgId, + helpStatus.msg, + ) + } + + val fingerprintFailMessage: Flow = + isUdfps().flatMapLatest { isUdfps -> + fingerprintAuthRepository.authenticationStatus + .filter { it is FailFingerprintAuthenticationStatus } + .filterNot { isPrimaryAuthRequired() } + .map { + BiometricMessage( + FINGERPRINT, + BiometricMessageType.FAIL, + BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED, + if (isUdfps) { + resources.getString( + com.android.internal.R.string.fingerprint_udfps_error_not_match + ) + } else { + resources.getString( + com.android.internal.R.string.fingerprint_error_not_match + ) + }, + ) + } + } + + private fun isUdfps() = + fingerprintPropertyRepository.sensorType.map { + it == FingerprintSensorType.UDFPS_OPTICAL || + it == FingerprintSensorType.UDFPS_ULTRASONIC + } + + private fun isPrimaryAuthRequired(): Boolean { + // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong + // as long as primary auth, i.e. PIN/pattern/password, is required), so it's ok to + // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the + // check of whether non-strong biometric is allowed since strong biometrics can still be + // used. + return !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) + } +} + +data class BiometricMessage( + val source: BiometricSourceType, + val type: BiometricMessageType, + val id: Int, + val message: String?, +) + +enum class BiometricMessageType { + HELP, + ERROR, + FAIL, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt index 5fb2cbf16a516..7fc6016bf087b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt @@ -41,7 +41,7 @@ data class AcquiredFingerprintAuthenticationStatus(val acquiredInfo: Int) : FingerprintAuthenticationStatus() /** Fingerprint authentication failed message. */ -object FailedFingerprintAuthenticationStatus : FingerprintAuthenticationStatus() +object FailFingerprintAuthenticationStatus : FingerprintAuthenticationStatus() /** Fingerprint authentication error message */ data class ErrorFingerprintAuthenticationStatus( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt index a73b57c3bba2f..def016ad83810 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt @@ -28,7 +28,7 @@ import com.android.systemui.biometrics.AuthController import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus -import com.android.systemui.keyguard.shared.model.FailedFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.util.mockito.whenever @@ -210,7 +210,7 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { ) assertThat(authenticationStatus) - .isInstanceOf(FailedFingerprintAuthenticationStatus::class.java) + .isInstanceOf(FailFingerprintAuthenticationStatus::class.java) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt new file mode 100644 index 0000000000000..3389fa9a48afa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt @@ -0,0 +1,260 @@ +/* + * 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.keyguard.domain.interactor + +import android.hardware.biometrics.BiometricSourceType.FINGERPRINT +import android.hardware.fingerprint.FingerprintManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus +import com.android.systemui.keyguard.util.IndicationHelper +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class BiometricMessageInteractorTest : SysuiTestCase() { + + private lateinit var underTest: BiometricMessageInteractor + private lateinit var testScope: TestScope + private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository + private lateinit var fingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository + + @Mock private lateinit var indicationHelper: IndicationHelper + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + testScope = TestScope() + fingerprintPropertyRepository = FakeFingerprintPropertyRepository() + fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() + underTest = + BiometricMessageInteractor( + mContext.resources, + fingerprintAuthRepository, + fingerprintPropertyRepository, + indicationHelper, + keyguardUpdateMonitor, + ) + } + + @Test + fun fingerprintErrorMessage() = + testScope.runTest { + val fingerprintErrorMessage by collectLastValue(underTest.fingerprintErrorMessage) + + // GIVEN FINGERPRINT_ERROR_HW_UNAVAILABLE should NOT be suppressed + whenever( + indicationHelper.shouldSuppressErrorMsg( + FINGERPRINT, + FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE + ) + ) + .thenReturn(false) + + // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, + msg = "test" + ) + ) + + // THEN fingerprintErrorMessage is updated + assertThat(fingerprintErrorMessage?.source).isEqualTo(FINGERPRINT) + assertThat(fingerprintErrorMessage?.type).isEqualTo(BiometricMessageType.ERROR) + assertThat(fingerprintErrorMessage?.id) + .isEqualTo(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE) + assertThat(fingerprintErrorMessage?.message).isEqualTo("test") + } + + @Test + fun fingerprintErrorMessage_suppressedError() = + testScope.runTest { + val fingerprintErrorMessage by collectLastValue(underTest.fingerprintErrorMessage) + + // GIVEN FINGERPRINT_ERROR_HW_UNAVAILABLE should be suppressed + whenever( + indicationHelper.shouldSuppressErrorMsg( + FINGERPRINT, + FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE + ) + ) + .thenReturn(true) + + // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, + msg = "test" + ) + ) + + // THEN fingerprintErrorMessage isn't update - it's still null + assertThat(fingerprintErrorMessage).isNull() + } + + @Test + fun fingerprintHelpMessage() = + testScope.runTest { + val fingerprintHelpMessage by collectLastValue(underTest.fingerprintHelpMessage) + + // GIVEN primary auth is NOT required + whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(true) + + // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY + fingerprintAuthRepository.setAuthenticationStatus( + HelpFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, + msg = "test" + ) + ) + + // THEN fingerprintHelpMessage is updated + assertThat(fingerprintHelpMessage?.source).isEqualTo(FINGERPRINT) + assertThat(fingerprintHelpMessage?.type).isEqualTo(BiometricMessageType.HELP) + assertThat(fingerprintHelpMessage?.id) + .isEqualTo(FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY) + assertThat(fingerprintHelpMessage?.message).isEqualTo("test") + } + + @Test + fun fingerprintHelpMessage_primaryAuthRequired() = + testScope.runTest { + val fingerprintHelpMessage by collectLastValue(underTest.fingerprintHelpMessage) + + // GIVEN primary auth is required + whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(false) + + // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY + fingerprintAuthRepository.setAuthenticationStatus( + HelpFingerprintAuthenticationStatus( + msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, + msg = "test" + ) + ) + + // THEN fingerprintHelpMessage isn't update - it's still null + assertThat(fingerprintHelpMessage).isNull() + } + + @Test + fun fingerprintFailMessage_nonUdfps() = + testScope.runTest { + val fingerprintFailMessage by collectLastValue(underTest.fingerprintFailMessage) + + // GIVEN primary auth is NOT required + whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(true) + + // GIVEN rear fingerprint (not UDFPS) + fingerprintPropertyRepository.setProperties( + 0, + SensorStrength.STRONG, + FingerprintSensorType.REAR, + mapOf() + ) + + // WHEN authentication status fail + fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) + + // THEN fingerprintFailMessage is updated + assertThat(fingerprintFailMessage?.source).isEqualTo(FINGERPRINT) + assertThat(fingerprintFailMessage?.type).isEqualTo(BiometricMessageType.FAIL) + assertThat(fingerprintFailMessage?.id) + .isEqualTo(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED) + assertThat(fingerprintFailMessage?.message) + .isEqualTo( + mContext.resources.getString( + com.android.internal.R.string.fingerprint_error_not_match + ) + ) + } + + @Test + fun fingerprintFailMessage_udfps() = + testScope.runTest { + val fingerprintFailMessage by collectLastValue(underTest.fingerprintFailMessage) + + // GIVEN primary auth is NOT required + whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(true) + + // GIVEN UDFPS + fingerprintPropertyRepository.setProperties( + 0, + SensorStrength.STRONG, + FingerprintSensorType.UDFPS_OPTICAL, + mapOf() + ) + + // WHEN authentication status fail + fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) + + // THEN fingerprintFailMessage is updated to udfps message + assertThat(fingerprintFailMessage?.source).isEqualTo(FINGERPRINT) + assertThat(fingerprintFailMessage?.type).isEqualTo(BiometricMessageType.FAIL) + assertThat(fingerprintFailMessage?.id) + .isEqualTo(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED) + assertThat(fingerprintFailMessage?.message) + .isEqualTo( + mContext.resources.getString( + com.android.internal.R.string.fingerprint_udfps_error_not_match + ) + ) + } + + @Test + fun fingerprintFailedMessage_primaryAuthRequired() = + testScope.runTest { + val fingerprintFailedMessage by collectLastValue(underTest.fingerprintFailMessage) + + // GIVEN primary auth is required + whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(false) + + // WHEN authentication status fail + fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus) + + // THEN fingerprintFailedMessage isn't update - it's still null + assertThat(fingerprintFailedMessage).isNull() + } +}