Merge changes I1a0a4767,I261a3f3e into udc-qpr-dev

* changes:
  Support class 3 face auth
  Add FaceSensorRepository to provide the face sensor state when it becomes available.
This commit is contained in:
Chandru S
2023-07-25 21:22:57 +00:00
committed by Android (Google) Code Review
10 changed files with 303 additions and 41 deletions

View File

@@ -17,6 +17,8 @@
package com.android.systemui.biometrics.dagger
import com.android.settingslib.udfps.UdfpsUtils
import com.android.systemui.biometrics.data.repository.FacePropertyRepository
import com.android.systemui.biometrics.data.repository.FacePropertyRepositoryImpl
import com.android.systemui.biometrics.data.repository.FaceSettingsRepository
import com.android.systemui.biometrics.data.repository.FaceSettingsRepositoryImpl
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
@@ -51,6 +53,10 @@ interface BiometricsModule {
@SysUISingleton
fun faceSettings(impl: FaceSettingsRepositoryImpl): FaceSettingsRepository
@Binds
@SysUISingleton
fun faceSensors(impl: FacePropertyRepositoryImpl): FacePropertyRepository
@Binds
@SysUISingleton
fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository

View File

@@ -0,0 +1,81 @@
/*
* 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.biometrics.data.repository
import android.hardware.face.FaceManager
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.biometrics.shared.model.toSensorStrength
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
/** A repository for the global state of Face sensor. */
interface FacePropertyRepository {
/** Face sensor information, null if it is not available. */
val sensorInfo: Flow<FaceSensorInfo?>
}
/** Describes a biometric sensor */
data class FaceSensorInfo(val id: Int, val strength: SensorStrength)
private const val TAG = "FaceSensorPropertyRepositoryImpl"
@SysUISingleton
class FacePropertyRepositoryImpl
@Inject
constructor(@Application private val applicationScope: CoroutineScope, faceManager: FaceManager?) :
FacePropertyRepository {
private val sensorProps: Flow<List<FaceSensorPropertiesInternal>> =
faceManager?.let {
ConflatedCallbackFlow.conflatedCallbackFlow {
val callback =
object : IFaceAuthenticatorsRegisteredCallback.Stub() {
override fun onAllAuthenticatorsRegistered(
sensors: List<FaceSensorPropertiesInternal>
) {
trySendWithFailureLogging(
sensors,
TAG,
"onAllAuthenticatorsRegistered"
)
}
}
it.addAuthenticatorsRegisteredCallback(callback)
awaitClose {}
}
.shareIn(applicationScope, SharingStarted.Eagerly)
}
?: flowOf(emptyList())
override val sensorInfo: Flow<FaceSensorInfo?> =
sensorProps
.map { it.firstOrNull() }
.map { it?.let { FaceSensorInfo(it.sensorId, it.sensorStrength.toSensorStrength()) } }
}

View File

@@ -22,6 +22,7 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.biometrics.shared.model.toSensorStrength
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -106,7 +107,7 @@ constructor(
private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
_sensorId.value = prop.sensorId
_strength.value = sensorStrengthIntToObject(prop.sensorStrength)
_strength.value = prop.sensorStrength.toSensorStrength()
_sensorType.value = sensorTypeIntToObject(prop.sensorType)
_sensorLocations.value =
prop.allLocations.associateBy { sensorLocationInternal ->
@@ -119,15 +120,6 @@ constructor(
}
}
private fun sensorStrengthIntToObject(value: Int): SensorStrength {
return when (value) {
0 -> SensorStrength.CONVENIENCE
1 -> SensorStrength.WEAK
2 -> SensorStrength.STRONG
else -> throw IllegalArgumentException("Invalid SensorStrength value: $value")
}
}
private fun sensorTypeIntToObject(value: Int): FingerprintSensorType {
return when (value) {
0 -> FingerprintSensorType.UNKNOWN

View File

@@ -18,9 +18,18 @@ package com.android.systemui.biometrics.shared.model
import android.hardware.biometrics.SensorProperties
/** Fingerprint sensor security strength. Represents [SensorProperties.Strength]. */
/** Sensor security strength. Represents [SensorProperties.Strength]. */
enum class SensorStrength {
CONVENIENCE,
WEAK,
STRONG,
}
/** Convert [this] to corresponding [SensorStrength] */
fun Int.toSensorStrength(): SensorStrength =
when (this) {
0 -> SensorStrength.CONVENIENCE
1 -> SensorStrength.WEAK
2 -> SensorStrength.STRONG
else -> throw IllegalArgumentException("Invalid SensorStrength value: $this")
}

View File

@@ -26,6 +26,8 @@ import com.android.internal.logging.UiEventLogger
import com.android.keyguard.FaceAuthUiEvent
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.biometrics.data.repository.FacePropertyRepository
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -58,6 +60,7 @@ import java.util.stream.Collectors
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
@@ -68,6 +71,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -120,6 +124,7 @@ interface DeviceEntryFaceAuthRepository {
fun cancel()
}
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class DeviceEntryFaceAuthRepositoryImpl
@Inject
@@ -143,7 +148,8 @@ constructor(
@FaceDetectTableLog private val faceDetectLog: TableLogBuffer,
@FaceAuthTableLog private val faceAuthLog: TableLogBuffer,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val featureFlags: FeatureFlags,
featureFlags: FeatureFlags,
facePropertyRepository: FacePropertyRepository,
dumpManager: DumpManager,
) : DeviceEntryFaceAuthRepository, Dumpable {
private var authCancellationSignal: CancellationSignal? = null
@@ -163,6 +169,13 @@ constructor(
override val detectionStatus: Flow<FaceDetectionStatus>
get() = _detectionStatus.filterNotNull()
private val isFaceBiometricsAllowed: Flow<Boolean> =
facePropertyRepository.sensorInfo.flatMapLatest {
if (it?.strength == SensorStrength.STRONG)
biometricSettingsRepository.isStrongBiometricAllowed
else biometricSettingsRepository.isNonStrongBiometricAllowed
}
private val _isLockedOut = MutableStateFlow(false)
override val isLockedOut: StateFlow<Boolean> = _isLockedOut
@@ -274,10 +287,8 @@ constructor(
canFaceAuthOrDetectRun(faceDetectLog),
logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog),
logAndObserve(
biometricSettingsRepository.isNonStrongBiometricAllowed
.isFalse()
.or(trustRepository.isCurrentUserTrusted),
"nonStrongBiometricIsNotAllowedOrCurrentUserIsTrusted",
isFaceBiometricsAllowed.isFalse().or(trustRepository.isCurrentUserTrusted),
"biometricIsNotAllowedOrCurrentUserIsTrusted",
faceDetectLog
),
// We don't want to run face detect if fingerprint can be used to unlock the device
@@ -368,21 +379,12 @@ constructor(
listOf(
canFaceAuthOrDetectRun(faceAuthLog),
logAndObserve(isLockedOut.isFalse(), "isNotInLockOutState", faceAuthLog),
logAndObserve(
deviceEntryFingerprintAuthRepository.isLockedOut.isFalse(),
"fpIsNotLockedOut",
faceAuthLog
),
logAndObserve(
trustRepository.isCurrentUserTrusted.isFalse(),
"currentUserIsNotTrusted",
faceAuthLog
),
logAndObserve(
biometricSettingsRepository.isNonStrongBiometricAllowed,
"nonStrongBiometricIsAllowed",
faceAuthLog
),
logAndObserve(isFaceBiometricsAllowed, "isFaceBiometricsAllowed", faceAuthLog),
logAndObserve(isAuthenticated.isFalse(), "faceNotAuthenticated", faceAuthLog),
)
.reduce(::and)

View File

@@ -0,0 +1,91 @@
/*
* 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.biometrics.data.repository
import android.hardware.biometrics.SensorProperties
import android.hardware.face.FaceManager
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class FacePropertyRepositoryImplTest : SysuiTestCase() {
@JvmField @Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private lateinit var underTest: FacePropertyRepository
private lateinit var testScope: TestScope
@Captor private lateinit var callback: ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback>
@Mock private lateinit var faceManager: FaceManager
@Before
fun setup() {
testScope = TestScope()
underTest = createRepository(faceManager)
}
private fun createRepository(manager: FaceManager? = faceManager) =
FacePropertyRepositoryImpl(testScope.backgroundScope, manager)
@Test
fun whenFaceManagerIsNotPresentIsNull() =
testScope.runTest {
underTest = createRepository(null)
val sensor = collectLastValue(underTest.sensorInfo)
assertThat(sensor()).isNull()
}
@Test
fun providesTheValuePassedToTheAuthenticatorsRegisteredCallback() {
testScope.runTest {
val sensor by collectLastValue(underTest.sensorInfo)
runCurrent()
verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture())
callback.value.onAllAuthenticatorsRegistered(
listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG))
)
assertThat(sensor).isEqualTo(FaceSensorInfo(1, SensorStrength.STRONG))
}
}
private fun createSensorProperties(id: Int, strength: Int) =
FaceSensorPropertiesInternal(id, strength, 0, emptyList(), 1, false, false, false)
}

View File

@@ -40,6 +40,9 @@ import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FaceSensorInfo
import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.coroutines.FlowValue
@@ -151,6 +154,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
private lateinit var fakeCommandQueue: FakeCommandQueue
private lateinit var featureFlags: FakeFeatureFlags
private lateinit var fakeFacePropertyRepository: FakeFacePropertyRepository
private var wasAuthCancelled = false
private var wasDetectCancelled = false
@@ -224,6 +228,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
repository = keyguardTransitionRepository,
)
.keyguardTransitionInteractor
fakeFacePropertyRepository = FakeFacePropertyRepository()
return DeviceEntryFaceAuthRepositoryImpl(
mContext,
fmOverride,
@@ -245,6 +250,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthBuffer,
keyguardTransitionInteractor,
featureFlags,
fakeFacePropertyRepository,
dumpManager,
)
}
@@ -590,6 +596,17 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
}
@Test
fun authenticateDoesNotRunWhenStrongBiometricIsNotAllowedAndFaceSensorIsStrong() =
testScope.runTest {
fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
runCurrent()
testGatingCheckForFaceAuth(isFaceStrong = true) {
biometricSettingsRepository.setIsStrongBiometricAllowed(false)
}
}
@Test
fun authenticateDoesNotRunWhenSecureCameraIsActive() =
testScope.runTest {
@@ -922,6 +939,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
}
@Test
fun detectDoesNotRunWhenStrongBiometricIsAllowedAndFaceAuthSensorStrengthIsStrong() =
testScope.runTest {
fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
runCurrent()
testGatingCheckForDetect(isFaceStrong = true) {
biometricSettingsRepository.setIsStrongBiometricAllowed(true)
// this shouldn't matter as face is set as a strong sensor
biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
}
}
@Test
fun detectDoesNotRunIfUdfpsIsRunning() =
testScope.runTest {
@@ -1013,9 +1043,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthenticateIsCalled()
}
private suspend fun TestScope.testGatingCheckForFaceAuth(gatingCheckModifier: () -> Unit) {
private suspend fun TestScope.testGatingCheckForFaceAuth(
isFaceStrong: Boolean = false,
gatingCheckModifier: () -> Unit
) {
initCollectors()
allPreconditionsToRunFaceAuthAreTrue()
allPreconditionsToRunFaceAuthAreTrue(isFaceStrong)
gatingCheckModifier()
runCurrent()
@@ -1024,7 +1057,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
assertThat(underTest.canRunFaceAuth.value).isFalse()
// flip the gating check back on.
allPreconditionsToRunFaceAuthAreTrue()
allPreconditionsToRunFaceAuthAreTrue(isFaceStrong)
triggerFaceAuth(false)
@@ -1043,12 +1076,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthenticateIsNotCalled()
}
private suspend fun TestScope.testGatingCheckForDetect(gatingCheckModifier: () -> Unit) {
private suspend fun TestScope.testGatingCheckForDetect(
isFaceStrong: Boolean = false,
gatingCheckModifier: () -> Unit
) {
initCollectors()
allPreconditionsToRunFaceAuthAreTrue()
// This will stop face auth from running but is required to be false for detect.
biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
if (isFaceStrong) {
biometricSettingsRepository.setStrongBiometricAllowed(false)
} else {
// This will stop face auth from running but is required to be false for detect.
biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
}
runCurrent()
assertThat(canFaceAuthRun()).isFalse()
@@ -1083,7 +1123,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true }
}
private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() {
private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue(
isFaceStrong: Boolean = false
) {
verify(faceManager, atLeastOnce())
.addLockoutResetCallback(faceLockoutResetCallback.capture())
biometricSettingsRepository.setFaceEnrolled(true)
@@ -1098,7 +1140,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
WakeSleepReason.OTHER
)
)
biometricSettingsRepository.setIsNonStrongBiometricAllowed(true)
if (isFaceStrong) {
biometricSettingsRepository.setStrongBiometricAllowed(true)
} else {
biometricSettingsRepository.setIsNonStrongBiometricAllowed(true)
}
biometricSettingsRepository.setIsUserInLockdown(false)
fakeUserRepository.setSelectedUserInfo(primaryUser)
biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true)

View File

@@ -342,12 +342,13 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
}
@Test
fun faceUnlockIsDisabledWhenFpIsLockedOut() = testScope.runTest {
underTest.start()
fun faceUnlockIsDisabledWhenFpIsLockedOut() =
testScope.runTest {
underTest.start()
fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
runCurrent()
fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
runCurrent()
assertThat(faceAuthRepository.wasDisabled).isTrue()
}
assertThat(faceAuthRepository.wasDisabled).isTrue()
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.biometrics.data.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeFacePropertyRepository : FacePropertyRepository {
private val faceSensorInfo = MutableStateFlow<FaceSensorInfo?>(null)
override val sensorInfo: Flow<FaceSensorInfo?>
get() = faceSensorInfo
fun setSensorInfo(value: FaceSensorInfo?) {
faceSensorInfo.value = value
}
}

View File

@@ -59,7 +59,6 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
private val _authFlags = MutableStateFlow(AuthenticationFlags(0, 0))
override val authenticationFlags: Flow<AuthenticationFlags>
get() = _authFlags
fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) {
_isFingerprintEnrolled.value = isFingerprintEnrolled
}
@@ -110,4 +109,8 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
fun setIsNonStrongBiometricAllowed(value: Boolean) {
_isNonStrongBiometricAllowed.value = value
}
fun setIsStrongBiometricAllowed(value: Boolean) {
_isStrongBiometricAllowed.value = value
}
}