Merge "Add face auth enrollment state to BiometricSettingsRepository" into tm-qpr-dev
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index c492f2e..baadc66 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -20,6 +20,8 @@
import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
import android.content.Context
import android.content.IntentFilter
+import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback
import android.os.Looper
import android.os.UserHandle
import com.android.internal.widget.LockPatternUtils
@@ -42,10 +44,12 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
@@ -60,6 +64,15 @@
/** Whether any fingerprints are enrolled for the current user. */
val isFingerprintEnrolled: StateFlow<Boolean>
+ /** Whether face authentication is enrolled for the current user. */
+ val isFaceEnrolled: Flow<Boolean>
+
+ /**
+ * Whether face authentication is enabled/disabled based on system settings like device policy,
+ * biometrics setting.
+ */
+ val isFaceAuthenticationEnabled: Flow<Boolean>
+
/**
* Whether the current user is allowed to use a strong biometric for device entry based on
* Android Security policies. If false, the user may be able to use primary authentication for
@@ -83,6 +96,7 @@
devicePolicyManager: DevicePolicyManager,
@Application scope: CoroutineScope,
@Background backgroundDispatcher: CoroutineDispatcher,
+ biometricManager: BiometricManager?,
@Main looper: Looper,
dumpManager: DumpManager,
) : BiometricSettingsRepository, Dumpable {
@@ -101,9 +115,15 @@
private val selectedUserId: Flow<Int> =
userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged()
+ private val devicePolicyChangedForAllUsers =
+ broadcastDispatcher.broadcastFlow(
+ filter = IntentFilter(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+ user = UserHandle.ALL
+ )
+
override val isFingerprintEnrolled: StateFlow<Boolean> =
selectedUserId
- .flatMapLatest {
+ .flatMapLatest { currentUserId ->
conflatedCallbackFlow {
val callback =
object : AuthController.Callback {
@@ -112,7 +132,7 @@
userId: Int,
hasEnrollments: Boolean
) {
- if (sensorBiometricType.isFingerprint) {
+ if (sensorBiometricType.isFingerprint && userId == currentUserId) {
trySendWithFailureLogging(
hasEnrollments,
TAG,
@@ -132,6 +152,77 @@
authController.isFingerprintEnrolled(userRepository.getSelectedUserInfo().id)
)
+ override val isFaceEnrolled: Flow<Boolean> =
+ selectedUserId.flatMapLatest { selectedUserId: Int ->
+ conflatedCallbackFlow {
+ val callback =
+ object : AuthController.Callback {
+ override fun onEnrollmentsChanged(
+ sensorBiometricType: BiometricType,
+ userId: Int,
+ hasEnrollments: Boolean
+ ) {
+ // TODO(b/242022358), use authController.isFaceAuthEnrolled after
+ // ag/20176811 is available.
+ if (
+ sensorBiometricType == BiometricType.FACE &&
+ userId == selectedUserId
+ ) {
+ trySendWithFailureLogging(
+ hasEnrollments,
+ TAG,
+ "Face enrollment changed"
+ )
+ }
+ }
+ }
+ authController.addCallback(callback)
+ trySendWithFailureLogging(
+ authController.isFaceAuthEnrolled(selectedUserId),
+ TAG,
+ "Initial value of face auth enrollment"
+ )
+ awaitClose { authController.removeCallback(callback) }
+ }
+ }
+
+ override val isFaceAuthenticationEnabled: Flow<Boolean>
+ get() =
+ combine(isFaceEnabledByBiometricsManager, isFaceEnabledByDevicePolicy) {
+ biometricsManagerSetting,
+ devicePolicySetting ->
+ biometricsManagerSetting && devicePolicySetting
+ }
+
+ private val isFaceEnabledByDevicePolicy: Flow<Boolean> =
+ combine(selectedUserId, devicePolicyChangedForAllUsers) { userId, _ ->
+ devicePolicyManager.isFaceDisabled(userId)
+ }
+ .onStart {
+ emit(devicePolicyManager.isFaceDisabled(userRepository.getSelectedUserInfo().id))
+ }
+ .flowOn(backgroundDispatcher)
+ .distinctUntilChanged()
+
+ private val isFaceEnabledByBiometricsManager =
+ conflatedCallbackFlow {
+ val callback =
+ object : IBiometricEnabledOnKeyguardCallback.Stub() {
+ override fun onChanged(enabled: Boolean, userId: Int) {
+ trySendWithFailureLogging(
+ enabled,
+ TAG,
+ "biometricsEnabled state changed"
+ )
+ }
+ }
+ biometricManager?.registerEnabledOnKeyguardCallback(callback)
+ awaitClose {}
+ }
+ // This is because the callback is binder-based and we want to avoid multiple callbacks
+ // being registered.
+ .stateIn(scope, SharingStarted.Eagerly, false)
+
override val isStrongBiometricAllowed: StateFlow<Boolean> =
selectedUserId
.flatMapLatest { currUserId ->
@@ -169,17 +260,8 @@
override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> =
selectedUserId
.flatMapLatest { userId ->
- broadcastDispatcher
- .broadcastFlow(
- filter = IntentFilter(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
- user = UserHandle.ALL
- )
- .transformLatest {
- emit(
- (devicePolicyManager.getKeyguardDisabledFeatures(null, userId) and
- DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) == 0
- )
- }
+ devicePolicyChangedForAllUsers
+ .transformLatest { emit(devicePolicyManager.isFingerprintDisabled(userId)) }
.flowOn(backgroundDispatcher)
.distinctUntilChanged()
}
@@ -187,13 +269,21 @@
scope,
started = SharingStarted.Eagerly,
initialValue =
- devicePolicyManager.getKeyguardDisabledFeatures(
- null,
+ devicePolicyManager.isFingerprintDisabled(
userRepository.getSelectedUserInfo().id
- ) and DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT == 0
+ )
)
companion object {
private const val TAG = "BiometricsRepositoryImpl"
}
}
+
+private fun DevicePolicyManager.isFaceDisabled(userId: Int): Boolean =
+ isNotActive(userId, DevicePolicyManager.KEYGUARD_DISABLE_FACE)
+
+private fun DevicePolicyManager.isFingerprintDisabled(userId: Int): Boolean =
+ isNotActive(userId, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT)
+
+private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean =
+ (getKeyguardDisabledFeatures(null, userId) and policy) == 0
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index 9d79976..21ad5e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -18,8 +18,12 @@
package com.android.systemui.keyguard.data.repository
import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FACE
+import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT
import android.content.Intent
import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
@@ -30,8 +34,13 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.data.repository.BiometricType.FACE
+import com.android.systemui.keyguard.data.repository.BiometricType.REAR_FINGERPRINT
+import com.android.systemui.keyguard.data.repository.BiometricType.SIDE_FINGERPRINT
+import com.android.systemui.keyguard.data.repository.BiometricType.UNDER_DISPLAY_FINGERPRINT
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -42,9 +51,14 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -58,6 +72,11 @@
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Mock private lateinit var dumpManager: DumpManager
+ @Mock private lateinit var biometricManager: BiometricManager
+ @Captor private lateinit var authControllerCallback: ArgumentCaptor<AuthController.Callback>
+ @Captor
+ private lateinit var biometricManagerCallback:
+ ArgumentCaptor<IBiometricEnabledOnKeyguardCallback.Stub>
private lateinit var userRepository: FakeUserRepository
private lateinit var testDispatcher: TestDispatcher
@@ -74,7 +93,7 @@
}
private suspend fun createBiometricSettingsRepository() {
- userRepository.setUserInfos(listOf(PRIMARY_USER))
+ userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
userRepository.setSelectedUserInfo(PRIMARY_USER)
underTest =
BiometricSettingsRepositoryImpl(
@@ -88,33 +107,29 @@
backgroundDispatcher = testDispatcher,
looper = testableLooper!!.looper,
dumpManager = dumpManager,
+ biometricManager = biometricManager,
)
+ testScope.runCurrent()
}
@Test
fun fingerprintEnrollmentChange() =
testScope.runTest {
createBiometricSettingsRepository()
- val fingerprintEnabledByDevicePolicy = collectLastValue(underTest.isFingerprintEnrolled)
+ val fingerprintEnrolled = collectLastValue(underTest.isFingerprintEnrolled)
runCurrent()
- val captor = argumentCaptor<AuthController.Callback>()
- verify(authController).addCallback(captor.capture())
+ verify(authController).addCallback(authControllerCallback.capture())
whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(true)
- captor.value.onEnrollmentsChanged(
- BiometricType.UNDER_DISPLAY_FINGERPRINT,
- PRIMARY_USER_ID,
- true
- )
- assertThat(fingerprintEnabledByDevicePolicy()).isTrue()
+ enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true)
+ assertThat(fingerprintEnrolled()).isTrue()
whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(false)
- captor.value.onEnrollmentsChanged(
- BiometricType.UNDER_DISPLAY_FINGERPRINT,
- PRIMARY_USER_ID,
- false
- )
- assertThat(fingerprintEnabledByDevicePolicy()).isFalse()
+ enrollmentChange(UNDER_DISPLAY_FINGERPRINT, ANOTHER_USER_ID, false)
+ assertThat(fingerprintEnrolled()).isTrue()
+
+ enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, false)
+ assertThat(fingerprintEnrolled()).isFalse()
}
@Test
@@ -127,15 +142,14 @@
val captor = argumentCaptor<LockPatternUtils.StrongAuthTracker>()
verify(lockPatternUtils).registerStrongAuthTracker(captor.capture())
- captor.value
- .getStub()
- .onStrongAuthRequiredChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID)
+ captor.value.stub.onStrongAuthRequiredChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID)
testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper
assertThat(strongBiometricAllowed()).isTrue()
- captor.value
- .getStub()
- .onStrongAuthRequiredChanged(STRONG_AUTH_REQUIRED_AFTER_BOOT, PRIMARY_USER_ID)
+ captor.value.stub.onStrongAuthRequiredChanged(
+ STRONG_AUTH_REQUIRED_AFTER_BOOT,
+ PRIMARY_USER_ID
+ )
testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper
assertThat(strongBiometricAllowed()).isFalse()
}
@@ -149,7 +163,7 @@
runCurrent()
whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt()))
- .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT)
+ .thenReturn(KEYGUARD_DISABLE_FINGERPRINT)
broadcastDPMStateChange()
assertThat(fingerprintEnabledByDevicePolicy()).isFalse()
@@ -158,6 +172,137 @@
assertThat(fingerprintEnabledByDevicePolicy()).isTrue()
}
+ @Test
+ fun faceEnrollmentChangeIsPropagatedForTheCurrentUser() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+ runCurrent()
+ clearInvocations(authController)
+
+ whenever(authController.isFaceAuthEnrolled(PRIMARY_USER_ID)).thenReturn(false)
+ val faceEnrolled = collectLastValue(underTest.isFaceEnrolled)
+
+ assertThat(faceEnrolled()).isFalse()
+ verify(authController).addCallback(authControllerCallback.capture())
+ enrollmentChange(REAR_FINGERPRINT, PRIMARY_USER_ID, true)
+
+ assertThat(faceEnrolled()).isFalse()
+
+ enrollmentChange(SIDE_FINGERPRINT, PRIMARY_USER_ID, true)
+
+ assertThat(faceEnrolled()).isFalse()
+
+ enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true)
+
+ assertThat(faceEnrolled()).isFalse()
+
+ enrollmentChange(FACE, ANOTHER_USER_ID, true)
+
+ assertThat(faceEnrolled()).isFalse()
+
+ enrollmentChange(FACE, PRIMARY_USER_ID, true)
+
+ assertThat(faceEnrolled()).isTrue()
+ }
+
+ @Test
+ fun faceEnrollmentStatusOfNewUserUponUserSwitch() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+ runCurrent()
+ clearInvocations(authController)
+
+ whenever(authController.isFaceAuthEnrolled(PRIMARY_USER_ID)).thenReturn(false)
+ whenever(authController.isFaceAuthEnrolled(ANOTHER_USER_ID)).thenReturn(true)
+ val faceEnrolled = collectLastValue(underTest.isFaceEnrolled)
+
+ assertThat(faceEnrolled()).isFalse()
+ }
+
+ @Test
+ fun faceEnrollmentChangesArePropagatedAfterUserSwitch() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+
+ userRepository.setSelectedUserInfo(ANOTHER_USER)
+ runCurrent()
+ clearInvocations(authController)
+
+ val faceEnrolled = collectLastValue(underTest.isFaceEnrolled)
+ runCurrent()
+
+ verify(authController).addCallback(authControllerCallback.capture())
+
+ enrollmentChange(FACE, ANOTHER_USER_ID, true)
+
+ assertThat(faceEnrolled()).isTrue()
+ }
+
+ @Test
+ fun devicePolicyControlsFaceAuthenticationEnabledState() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+ verify(biometricManager)
+ .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture())
+
+ whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID)))
+ .thenReturn(KEYGUARD_DISABLE_FINGERPRINT or KEYGUARD_DISABLE_FACE)
+
+ val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled)
+ runCurrent()
+
+ broadcastDPMStateChange()
+
+ assertThat(isFaceAuthEnabled()).isFalse()
+
+ biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID)
+ runCurrent()
+ assertThat(isFaceAuthEnabled()).isFalse()
+
+ whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID)))
+ .thenReturn(KEYGUARD_DISABLE_FINGERPRINT)
+ broadcastDPMStateChange()
+
+ assertThat(isFaceAuthEnabled()).isTrue()
+ }
+
+ @Test
+ fun biometricManagerControlsFaceAuthenticationEnabledStatus() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+ verify(biometricManager)
+ .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture())
+
+ whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID)))
+ .thenReturn(0)
+ broadcastDPMStateChange()
+
+ biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID)
+ val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled)
+
+ assertThat(isFaceAuthEnabled()).isTrue()
+
+ biometricManagerCallback.value.onChanged(false, PRIMARY_USER_ID)
+
+ assertThat(isFaceAuthEnabled()).isFalse()
+ }
+
+ @Test
+ fun biometricManagerCallbackIsRegisteredOnlyOnce() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+
+ collectLastValue(underTest.isFaceAuthenticationEnabled)()
+ collectLastValue(underTest.isFaceAuthenticationEnabled)()
+ collectLastValue(underTest.isFaceAuthenticationEnabled)()
+
+ verify(biometricManager, times(1)).registerEnabledOnKeyguardCallback(any())
+ }
+
+ private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) {
+ authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled)
+ }
+
private fun broadcastDPMStateChange() {
fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
receiver.onReceive(
@@ -175,5 +320,13 @@
/* name= */ "primary user",
/* flags= */ UserInfo.FLAG_PRIMARY
)
+
+ private const val ANOTHER_USER_ID = 1
+ private val ANOTHER_USER =
+ UserInfo(
+ /* id= */ ANOTHER_USER_ID,
+ /* name= */ "another user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
index 044679d..01dac36 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.repository
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -26,6 +27,14 @@
private val _isFingerprintEnrolled = MutableStateFlow<Boolean>(false)
override val isFingerprintEnrolled: StateFlow<Boolean> = _isFingerprintEnrolled.asStateFlow()
+ private val _isFaceEnrolled = MutableStateFlow(false)
+ override val isFaceEnrolled: Flow<Boolean>
+ get() = _isFaceEnrolled
+
+ private val _isFaceAuthEnabled = MutableStateFlow(false)
+ override val isFaceAuthenticationEnabled: Flow<Boolean>
+ get() = _isFaceAuthEnabled
+
private val _isStrongBiometricAllowed = MutableStateFlow(false)
override val isStrongBiometricAllowed = _isStrongBiometricAllowed.asStateFlow()
@@ -44,4 +53,12 @@
fun setFingerprintEnabledByDevicePolicy(isFingerprintEnabledByDevicePolicy: Boolean) {
_isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy
}
+
+ fun setFaceEnrolled(isFaceEnrolled: Boolean) {
+ _isFaceEnrolled.value = isFaceEnrolled
+ }
+
+ fun setIsFaceAuthEnabled(enabled: Boolean) {
+ _isFaceAuthEnabled.value = enabled
+ }
}