Add BiometricMessageInteractor for fingerprint msgs

BiometricMessageInteractor filters fingerprint
error, help and failure messages.

Test: atest BiometricMessageInteractorTest
Bug: 288308594
Change-Id: Ic18303ab489672796de35feedae2f3f24bd8551c
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 616ce39..9bec300 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.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 @@
                     ) {
                         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 0000000..c849b84
--- /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<BiometricMessage> =
+        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<BiometricMessage> =
+        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<BiometricMessage> =
+        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 5fb2cbf..7fc6016 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 @@
     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 a73b57c..def016a 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.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 @@
             )
 
             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 0000000..3389fa9
--- /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()
+        }
+}