Add BiometricMessageInteractor for fingerprint msgs

BiometricMessageInteractor filters fingerprint
error, help and failure messages.

Test: atest BiometricMessageInteractorTest
Bug: 288308594
Change-Id: Ic18303ab489672796de35feedae2f3f24bd8551c
This commit is contained in:
Beverly
2023-07-14 17:55:58 +00:00
parent 2427c53a28
commit ea2ffe4a7b
5 changed files with 403 additions and 5 deletions

View File

@@ -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,
)
}

View File

@@ -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,
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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()
}
}