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

Commit d4cd244e authored by Grace Cheng's avatar Grace Cheng Committed by Android (Google) Code Review
Browse files

Merge "Migrate shared biometric icon logic, setup Secure Lock Device ViewModel" into main

parents dbae455e 14fee555
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@ import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.wakefulnessLifecycle
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.testKosmos
@@ -674,7 +675,9 @@ open class AuthContainerViewTest : SysuiTestCase() {
            kosmos.lockPatternUtils,
            kosmos.interactionJankMonitor,
            { kosmos.promptSelectorInteractor },
            kosmos.promptViewModel,
            kosmos.promptViewModel.apply {
                this.iconViewModel.internal.activateIn(kosmos.testScope)
            },
            { kosmos.credentialViewModel },
            kosmos.fakeExecutor,
            kosmos.vibratorHelper,
+288 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.biometrics.ui.viewmodel

import android.hardware.biometrics.PromptInfo
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.facePropertyRepository
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
import com.android.systemui.biometrics.domain.interactor.promptSelectorInteractor
import com.android.systemui.biometrics.extractAuthenticatorTypes
import com.android.systemui.biometrics.faceSensorPropertiesInternal
import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.FaceSensorInfo
import com.android.systemui.biometrics.shared.model.FingerprintSensorInfo
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.biometrics.shared.model.toSensorType
import com.android.systemui.biometrics.ui.viewmodel.BiometricAuthIconViewModel.BiometricAuthModalities
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.securelockdevice.ui.viewmodel.SecureLockDeviceBiometricAuthContentViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class BiometricAuthIconViewModelTest() : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val defaultHelpMsg = "default help msg"

    private var promptViewModel: PromptViewModel? = null
    private var secureLockDeviceViewModel: SecureLockDeviceBiometricAuthContentViewModel? = null
    private var faceSensorInfo: FaceSensorInfo? = null
    private var fingerprintSensorInfo: FingerprintSensorInfo? = null
    private lateinit var underTest: BiometricAuthIconViewModel

    private fun enrollFingerprint(
        sensorStrength: SensorStrength = SensorStrength.STRONG,
        @FingerprintSensorProperties.SensorType sensorType: Int,
    ) {
        fingerprintSensorInfo =
            FingerprintSensorInfo(type = sensorType.toSensorType(), strength = sensorStrength)
        if (sensorType == TYPE_POWER_BUTTON) {
            kosmos.fingerprintPropertyRepository.supportsSideFps(sensorStrength)
        } else if (sensorType == TYPE_UDFPS_OPTICAL || sensorType == TYPE_UDFPS_ULTRASONIC) {
            kosmos.fingerprintPropertyRepository.supportsUdfps(sensorStrength)
        } else if (sensorType == TYPE_REAR) {
            kosmos.fingerprintPropertyRepository.supportsRearFps(sensorStrength)
        }
    }

    private fun enrollFace(isStrongBiometric: Boolean) {
        faceSensorInfo =
            FaceSensorInfo(
                id = 0,
                strength = if (isStrongBiometric) SensorStrength.STRONG else SensorStrength.WEAK,
            )
        kosmos.facePropertyRepository.setSensorInfo(faceSensorInfo)
    }

    private fun startBiometricPrompt(hasFpAuth: Boolean, isImplicitFlow: Boolean = false) {
        if (isImplicitFlow) {
            promptViewModel!!.showAuthenticating()
        } else {
            promptViewModel!!.ensureFingerprintHasStarted(isDelayed = false)
            val helpMsg =
                if (hasFpAuth) {
                    defaultHelpMsg
                } else {
                    ""
                }
            promptViewModel!!.showAuthenticating(helpMsg)
        }
    }

    private fun initPromptViewModel() {
        promptViewModel = kosmos.promptViewModel
        underTest = promptViewModel!!.iconViewModel.internal
        underTest.activateIn(testScope)
    }

    @Test
    fun activeBiometricAuthType_basedOnModalitiesAndFaceMode_forBiometricPrompt_face() {
        testScope.runTest {
            initPromptViewModel()
            val activeBiometricAuthType by collectLastValue(underTest.activeBiometricAuthType)

            enrollFace(isStrongBiometric = false)
            runCurrent()

            val faceProps = faceSensorPropertiesInternal().first()
            kosmos.promptSelectorInteractor.initializePrompt(null, faceProps)
            runCurrent()
            startBiometricPrompt(hasFpAuth = false)

            assertThat(activeBiometricAuthType).isEqualTo(BiometricAuthModalities.Face)
        }
    }

    @Test
    fun activeBiometricAuthType_basedOnModalitiesAndFaceMode_forBiometricPrompt_sfps() {
        testScope.runTest {
            initPromptViewModel()
            val activeBiometricAuthType by collectLastValue(underTest.activeBiometricAuthType)

            enrollFingerprint(sensorType = TYPE_POWER_BUTTON)
            runCurrent()

            val fpProps =
                fingerprintSensorPropertiesInternal(sensorType = TYPE_POWER_BUTTON).first()
            val faceProps = faceSensorPropertiesInternal().first()
            kosmos.promptSelectorInteractor.initializePrompt(fpProps, faceProps)
            runCurrent()
            startBiometricPrompt(hasFpAuth = true)

            assertThat(activeBiometricAuthType).isEqualTo(BiometricAuthModalities.Sfps)
        }
    }

    @Test
    fun activeBiometricAuthType_basedOnModalitiesAndFaceMode_forBiometricPrompt_nonSfps() {
        testScope.runTest {
            initPromptViewModel()
            val activeBiometricAuthType by collectLastValue(underTest.activeBiometricAuthType)

            enrollFingerprint(sensorType = TYPE_UDFPS_OPTICAL)
            runCurrent()

            val fpProps =
                fingerprintSensorPropertiesInternal(strong = false, sensorType = TYPE_UDFPS_OPTICAL)
                    .first()
            val faceProps = faceSensorPropertiesInternal().first()
            kosmos.promptSelectorInteractor.initializePrompt(fpProps, faceProps)
            runCurrent()
            startBiometricPrompt(hasFpAuth = true)

            assertThat(activeBiometricAuthType).isEqualTo(BiometricAuthModalities.NonSfps)
        }
    }

    @Test
    fun activeBiometricAuthType_basedOnModalitiesAndFaceMode_forBiometricPrompt_sfpsCoexImplicit() {
        testScope.runTest {
            initPromptViewModel()
            val activeBiometricAuthType by collectLastValue(underTest.activeBiometricAuthType)

            enrollFingerprint(sensorType = TYPE_POWER_BUTTON)
            enrollFace(isStrongBiometric = false)
            runCurrent()

            val fpProps =
                fingerprintSensorPropertiesInternal(strong = false, sensorType = TYPE_POWER_BUTTON)
                    .first()
            val faceProps = faceSensorPropertiesInternal(strong = false).first()
            kosmos.promptSelectorInteractor.initializePrompt(fpProps, faceProps)
            runCurrent()
            startBiometricPrompt(hasFpAuth = true, isImplicitFlow = true)

            assertThat(activeBiometricAuthType).isEqualTo(BiometricAuthModalities.Face)
        }
    }

    @Test
    fun activeBiometricAuthType_basedOnModalitiesAndFaceMode_forBiometricPrompt_coexSfpsExplicit() {
        testScope.runTest {
            initPromptViewModel()
            val activeBiometricAuthType by collectLastValue(underTest.activeBiometricAuthType)

            enrollFingerprint(sensorType = TYPE_POWER_BUTTON)
            enrollFace(isStrongBiometric = false)
            runCurrent()

            val fpProps =
                fingerprintSensorPropertiesInternal(strong = false, sensorType = TYPE_POWER_BUTTON)
                    .first()
            val faceProps = faceSensorPropertiesInternal(strong = false).first()
            kosmos.promptSelectorInteractor.initializePrompt(fpProps, faceProps)
            runCurrent()
            startBiometricPrompt(hasFpAuth = true)

            assertThat(activeBiometricAuthType).isEqualTo(BiometricAuthModalities.SfpsCoex)
        }
    }

    @Test
    fun activeBiometricAuthType_basedOnModalitiesAndFaceMode_forBiometricPrompt_coexNonSfpsImplicit() {
        testScope.runTest {
            initPromptViewModel()
            val activeBiometricAuthType by collectLastValue(underTest.activeBiometricAuthType)

            enrollFingerprint(sensorType = TYPE_UDFPS_OPTICAL)
            enrollFace(isStrongBiometric = false)
            runCurrent()

            val fpProps =
                fingerprintSensorPropertiesInternal(strong = false, sensorType = TYPE_POWER_BUTTON)
                    .first()
            val faceProps = faceSensorPropertiesInternal(strong = false).first()
            kosmos.promptSelectorInteractor.initializePrompt(fpProps, faceProps)
            runCurrent()
            startBiometricPrompt(hasFpAuth = true, isImplicitFlow = true)

            assertThat(activeBiometricAuthType).isEqualTo(BiometricAuthModalities.Face)
        }
    }

    @Test
    fun activeBiometricAuthType_basedOnModalitiesAndFaceMode_forBiometricPrompt_coexNonSfpsExplicit() {
        testScope.runTest {
            initPromptViewModel()
            val activeBiometricAuthType by collectLastValue(underTest.activeBiometricAuthType)

            enrollFingerprint(sensorType = TYPE_UDFPS_OPTICAL)
            enrollFace(isStrongBiometric = false)
            runCurrent()

            val fpProps =
                fingerprintSensorPropertiesInternal(strong = false, sensorType = TYPE_POWER_BUTTON)
                    .first()
            val faceProps = faceSensorPropertiesInternal(strong = false).first()
            kosmos.promptSelectorInteractor.initializePrompt(fpProps, faceProps)
            runCurrent()
            startBiometricPrompt(hasFpAuth = true)

            assertThat(activeBiometricAuthType).isEqualTo(BiometricAuthModalities.NonSfpsCoex)
        }
    }

    /** Initialize the prompt according to the test configuration. */
    private fun PromptSelectorInteractor.initializePrompt(
        fingerprint: FingerprintSensorPropertiesInternal? = null,
        face: FaceSensorPropertiesInternal? = null,
        requireConfirmation: Boolean = false,
    ) {
        val info =
            PromptInfo().apply {
                logoDescription = "logo"
                title = "title"
                subtitle = "subtitle"
                description = "description"
                contentView = null
                authenticators = listOf(face, fingerprint).extractAuthenticatorTypes()
                isDeviceCredentialAllowed = false
                isConfirmationRequested = requireConfirmation
            }

        setPrompt(
            info,
            0,
            0,
            BiometricModalities(fingerprintSensorInfo, faceSensorInfo),
            0L,
            "packageName",
            onSwitchToCredential = false,
            isLandscape = false,
        )
    }
}
+72 −1
Original line number Diff line number Diff line
@@ -16,18 +16,28 @@

package com.android.systemui.securelockdevice.domain.interactor

import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON
import android.platform.test.annotations.EnableFlags
import android.security.Flags.FLAG_SECURE_LOCK_DEVICE
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
import com.android.systemui.biometrics.faceSensorPropertiesInternal
import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.toFaceSensorInfo
import com.android.systemui.biometrics.shared.model.toFingerprintSensorInfo
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.securelockdevice.data.repository.fakeSecureLockDeviceRepository
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,7 +51,14 @@ class SecureLockDeviceInteractorTest : SysuiTestCase() {
    @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()

    private val kosmos = testKosmos()
    private val underTest: SecureLockDeviceInteractor = kosmos.secureLockDeviceInteractor
    private val testScope = kosmos.testScope
    private val underTest = kosmos.secureLockDeviceInteractor

    @Before
    fun setup() {
        kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
        kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
    }

    @Test
    fun secureLockDeviceStateUpdates_acrossAuthenticationProgress() =
@@ -74,4 +91,58 @@ class SecureLockDeviceInteractorTest : SysuiTestCase() {
            assertThat(requiresPrimaryAuthForSecureLockDevice).isEqualTo(false)
            assertThat(requiresStrongBiometricAuthForSecureLockDevice).isEqualTo(false)
        }

    @Test
    fun updatesModalitiesFromInteractor_strongFp() {
        testScope.runTest {
            val modalities by collectLastValue(underTest.enrolledStrongBiometricModalities)
            val fpSensorInfo =
                fingerprintSensorPropertiesInternal(sensorType = TYPE_POWER_BUTTON)
                    .first()
                    .toFingerprintSensorInfo()
            assertThat(modalities).isEqualTo(BiometricModalities())

            kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
            runCurrent()

            assertThat(modalities).isEqualTo(BiometricModalities(fpSensorInfo, null))
        }
    }

    @Test
    fun updatesModalitiesFromInteractor_strongFace() {
        testScope.runTest {
            val modalities by collectLastValue(underTest.enrolledStrongBiometricModalities)
            val faceSensorInfo = faceSensorPropertiesInternal().first().toFaceSensorInfo()
            assertThat(modalities).isEqualTo(BiometricModalities())

            kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
            kosmos.fakeFacePropertyRepository.setSensorInfo(faceSensorInfo)
            runCurrent()

            assertThat(modalities).isEqualTo(BiometricModalities(null, faceSensorInfo))
        }
    }

    @Test
    fun updatesModalitiesFromInteractor_strongCoex() {
        testScope.runTest {
            val modalities by collectLastValue(underTest.enrolledStrongBiometricModalities)
            val fpSensorInfo =
                fingerprintSensorPropertiesInternal(sensorType = TYPE_POWER_BUTTON)
                    .first()
                    .toFingerprintSensorInfo()
            val faceSensorInfo = faceSensorPropertiesInternal().first().toFaceSensorInfo()
            assertThat(modalities).isEqualTo(BiometricModalities())

            kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
            kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
            kosmos.fakeFacePropertyRepository.setSensorInfo(faceSensorInfo)
            runCurrent()

            assertThat(modalities).isEqualTo(BiometricModalities(fpSensorInfo, faceSensorInfo))
        }
    }
}
+168 −0

File added.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@
package com.android.systemui.biometrics.ui

import android.annotation.RawRes
import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel
import com.android.systemui.biometrics.ui.viewmodel.BiometricAuthIconViewModel

/* Class for handling the state of the icon in Biometric prompt */
data class PromptIconState(
@@ -26,6 +26,6 @@ data class PromptIconState(
    val shouldLoop: Boolean,
    val contentDescriptionId: Int,
    val rotation: Float,
    val authType: PromptIconViewModel.AuthType,
    val activeBiometricAuthType: BiometricAuthIconViewModel.BiometricAuthModalities,
    val showingError: Boolean,
)
Loading