Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 13feec74 authored by Beverly Tai's avatar Beverly Tai Committed by Android (Google) Code Review
Browse files

Merge "Add BiometricMessageInteractor for fingerprint msgs" into udc-qpr-dev

parents 8b3d1a8f ea2ffe4a
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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,
                        )
                    }

+138 −0
Original line number Diff line number Diff line
/*
 *  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,
}
+1 −1
Original line number Diff line number Diff line
@@ -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(
+2 −2
Original line number Diff line number Diff line
@@ -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
+260 −0
Original line number Diff line number Diff line
/*
 *  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()
        }
}