Loading packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +106 −16 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ import android.app.admin.DevicePolicyManager 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 Loading @@ -42,10 +44,12 @@ import kotlinx.coroutines.channels.awaitClose 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 Loading @@ -60,6 +64,15 @@ interface BiometricSettingsRepository { /** 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 Loading @@ -83,6 +96,7 @@ constructor( devicePolicyManager: DevicePolicyManager, @Application scope: CoroutineScope, @Background backgroundDispatcher: CoroutineDispatcher, biometricManager: BiometricManager?, @Main looper: Looper, dumpManager: DumpManager, ) : BiometricSettingsRepository, Dumpable { Loading @@ -101,9 +115,15 @@ constructor( 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 { Loading @@ -112,7 +132,7 @@ constructor( userId: Int, hasEnrollments: Boolean ) { if (sensorBiometricType.isFingerprint) { if (sensorBiometricType.isFingerprint && userId == currentUserId) { trySendWithFailureLogging( hasEnrollments, TAG, Loading @@ -132,6 +152,77 @@ constructor( 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 -> Loading Loading @@ -169,17 +260,8 @@ constructor( 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() } Loading @@ -187,13 +269,21 @@ constructor( 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 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +176 −23 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -30,8 +34,13 @@ import com.android.systemui.SysuiTestCase 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 Loading @@ -42,9 +51,14 @@ import kotlinx.coroutines.test.runTest 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 Loading @@ -58,6 +72,11 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { @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 Loading @@ -74,7 +93,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { } private suspend fun createBiometricSettingsRepository() { userRepository.setUserInfos(listOf(PRIMARY_USER)) userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) userRepository.setSelectedUserInfo(PRIMARY_USER) underTest = BiometricSettingsRepositoryImpl( Loading @@ -88,33 +107,29 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 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 Loading @@ -127,15 +142,14 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 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() } Loading @@ -149,7 +163,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { runCurrent() whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) .thenReturn(KEYGUARD_DISABLE_FINGERPRINT) broadcastDPMStateChange() assertThat(fingerprintEnabledByDevicePolicy()).isFalse() Loading @@ -158,6 +172,137 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 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( Loading @@ -175,5 +320,13 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { /* 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 ) } } packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt +17 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -26,6 +27,14 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { 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() Loading @@ -44,4 +53,12 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { fun setFingerprintEnabledByDevicePolicy(isFingerprintEnabledByDevicePolicy: Boolean) { _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy } fun setFaceEnrolled(isFaceEnrolled: Boolean) { _isFaceEnrolled.value = isFaceEnrolled } fun setIsFaceAuthEnabled(enabled: Boolean) { _isFaceAuthEnabled.value = enabled } } Loading
packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +106 −16 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ import android.app.admin.DevicePolicyManager 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 Loading @@ -42,10 +44,12 @@ import kotlinx.coroutines.channels.awaitClose 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 Loading @@ -60,6 +64,15 @@ interface BiometricSettingsRepository { /** 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 Loading @@ -83,6 +96,7 @@ constructor( devicePolicyManager: DevicePolicyManager, @Application scope: CoroutineScope, @Background backgroundDispatcher: CoroutineDispatcher, biometricManager: BiometricManager?, @Main looper: Looper, dumpManager: DumpManager, ) : BiometricSettingsRepository, Dumpable { Loading @@ -101,9 +115,15 @@ constructor( 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 { Loading @@ -112,7 +132,7 @@ constructor( userId: Int, hasEnrollments: Boolean ) { if (sensorBiometricType.isFingerprint) { if (sensorBiometricType.isFingerprint && userId == currentUserId) { trySendWithFailureLogging( hasEnrollments, TAG, Loading @@ -132,6 +152,77 @@ constructor( 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 -> Loading Loading @@ -169,17 +260,8 @@ constructor( 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() } Loading @@ -187,13 +269,21 @@ constructor( 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
packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +176 −23 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -30,8 +34,13 @@ import com.android.systemui.SysuiTestCase 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 Loading @@ -42,9 +51,14 @@ import kotlinx.coroutines.test.runTest 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 Loading @@ -58,6 +72,11 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { @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 Loading @@ -74,7 +93,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { } private suspend fun createBiometricSettingsRepository() { userRepository.setUserInfos(listOf(PRIMARY_USER)) userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) userRepository.setSelectedUserInfo(PRIMARY_USER) underTest = BiometricSettingsRepositoryImpl( Loading @@ -88,33 +107,29 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 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 Loading @@ -127,15 +142,14 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 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() } Loading @@ -149,7 +163,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { runCurrent() whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) .thenReturn(KEYGUARD_DISABLE_FINGERPRINT) broadcastDPMStateChange() assertThat(fingerprintEnabledByDevicePolicy()).isFalse() Loading @@ -158,6 +172,137 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 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( Loading @@ -175,5 +320,13 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { /* 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 ) } }
packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt +17 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -26,6 +27,14 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { 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() Loading @@ -44,4 +53,12 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { fun setFingerprintEnabledByDevicePolicy(isFingerprintEnabledByDevicePolicy: Boolean) { _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy } fun setFaceEnrolled(isFaceEnrolled: Boolean) { _isFaceEnrolled.value = isFaceEnrolled } fun setIsFaceAuthEnabled(enabled: Boolean) { _isFaceAuthEnabled.value = enabled } }