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

Commit 14fee555 authored by Grace Cheng's avatar Grace Cheng
Browse files

Migrate shared biometric icon logic, setup Secure Lock Device ViewModel

Migrate all BiometricPrompt icon-related logic into shared
BiometricAuthIconViewModel class and shared BiometricAuthIconUtils. This
includes the biometric iconAsset and content description, as well as
properties of the asset like shouldAnimateIconView and
shouldLoopIconView. This will allow this logic to be reused for the
biometric auth step of Secure Lock Device.

Also sets up the ViewModel for the eventual Secure Lock Device MVVM
structure, which will use an ExclusiveActivatable ViewModel and have the
Compose view observe its state and update the UI directly

Flag: android.security.secure_lock_device
Fixes: 398990889
Bug: 401645997
Test: atest AuthContainerViewTest
Test: atest BiometricAuthIconViewModelTest
Test: atest SecureLockDeviceInteractorTest
Test: atest PromptViewModelTest
Change-Id: I2d68c604e4a73e1815bd3b13fce440693bb16376
parent 6a1789fb
Loading
Loading
Loading
Loading
+4 −1
Original line number Original line 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.jank.interactionJankMonitor
import com.android.systemui.keyguard.wakefulnessLifecycle
import com.android.systemui.keyguard.wakefulnessLifecycle
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.testKosmos
import com.android.systemui.testKosmos
@@ -674,7 +675,9 @@ open class AuthContainerViewTest : SysuiTestCase() {
            kosmos.lockPatternUtils,
            kosmos.lockPatternUtils,
            kosmos.interactionJankMonitor,
            kosmos.interactionJankMonitor,
            { kosmos.promptSelectorInteractor },
            { kosmos.promptSelectorInteractor },
            kosmos.promptViewModel,
            kosmos.promptViewModel.apply {
                this.iconViewModel.internal.activateIn(kosmos.testScope)
            },
            { kosmos.credentialViewModel },
            { kosmos.credentialViewModel },
            kosmos.fakeExecutor,
            kosmos.fakeExecutor,
            kosmos.vibratorHelper,
            kosmos.vibratorHelper,
+288 −0
Original line number Original line 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 Original line Diff line number Diff line
@@ -16,18 +16,28 @@


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


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


    private val kosmos = testKosmos()
    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
    @Test
    fun secureLockDeviceStateUpdates_acrossAuthenticationProgress() =
    fun secureLockDeviceStateUpdates_acrossAuthenticationProgress() =
@@ -74,4 +91,58 @@ class SecureLockDeviceInteractorTest : SysuiTestCase() {
            assertThat(requiresPrimaryAuthForSecureLockDevice).isEqualTo(false)
            assertThat(requiresPrimaryAuthForSecureLockDevice).isEqualTo(false)
            assertThat(requiresStrongBiometricAuthForSecureLockDevice).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))
        }
    }
}
}
Loading