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

Commit 06b953fa authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add face auth enrollment state to BiometricSettingsRepository" into tm-qpr-dev

parents 0aafe55e 63b229c0
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
    }
}