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

Commit 63b229c0 authored by Chandru S's avatar Chandru S
Browse files

Add face auth enrollment state to BiometricSettingsRepository

Bug: 262838215
Test: atest BiometricSettingsRepository
Change-Id: I23a5a97796049c087ef8aefeaf0a12e44c096df0
parent c93bbad0
Loading
Loading
Loading
Loading
+106 −16
Original line number Diff line number Diff line
@@ -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
@@ -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

@@ -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
@@ -83,6 +96,7 @@ constructor(
    devicePolicyManager: DevicePolicyManager,
    @Application scope: CoroutineScope,
    @Background backgroundDispatcher: CoroutineDispatcher,
    biometricManager: BiometricManager?,
    @Main looper: Looper,
    dumpManager: DumpManager,
) : BiometricSettingsRepository, Dumpable {
@@ -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 {
@@ -112,7 +132,7 @@ constructor(
                                userId: Int,
                                hasEnrollments: Boolean
                            ) {
                                if (sensorBiometricType.isFingerprint) {
                                if (sensorBiometricType.isFingerprint && userId == currentUserId) {
                                    trySendWithFailureLogging(
                                        hasEnrollments,
                                        TAG,
@@ -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 ->
@@ -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()
            }
@@ -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
+176 −23
Original line number Diff line number Diff line
@@ -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.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
@@ -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

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

@@ -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(
@@ -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
            )
    }
}
+17 −0
Original line number Diff line number Diff line
@@ -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 @@ 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()

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