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

Commit 7ac60153 authored by Austin Delgado's avatar Austin Delgado
Browse files

Improve biometric prompt view transitions

Test: atest com.android.systemui.biometrics
Test: BiometricPromptScreenshotTest
Flag: android.hardware.biometrics.bp_fallback_options
Bug: 391644182

Change-Id: I248b674e6dd9b06a0f250530299a7fad5b7affa8
parent 3b18f643
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import android.widget.ScrollView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN
import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN
import com.android.internal.widget.lockPatternUtils
@@ -113,6 +114,8 @@ open class AuthContainerViewTest : SysuiTestCase() {
        whenever(packageManager.getPackageInfo(any(String::class.java), anyInt()))
            .thenReturn(PackageInfo())
        context.setMockPackageManager(packageManager)
        whenever(lockPatternUtils.getCredentialTypeForUser(anyInt()))
            .thenReturn(CREDENTIAL_TYPE_PASSWORD)
    }

    @After
+34 −7
Original line number Diff line number Diff line
@@ -16,16 +16,19 @@

package com.android.systemui.biometrics.domain.interactor

import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.BiometricManager.Authenticators
import android.hardware.biometrics.Flags
import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.PromptVerticalListContentView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN
import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
@@ -172,6 +175,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
        val isCredentialAllowed by collectLastValue(interactor.isCredentialAllowed)
        val credentialKind by collectLastValue(interactor.credentialKind)
        val isConfirmationRequired by collectLastValue(interactor.isConfirmationRequired)
        val currentView by collectLastValue(interactor.currentView)

        assertThat(currentPrompt).isNull()

@@ -192,6 +196,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
        assertThat(currentPrompt?.subtitle).isEqualTo(SUBTITLE)
        assertThat(currentPrompt?.negativeButtonText).isEqualTo(NEGATIVE_TEXT)
        assertThat(currentPrompt?.opPackageName).isEqualTo(OP_PACKAGE_NAME)
        assertThat(currentView).isEqualTo(BiometricPromptView.BIOMETRIC)
        assertThat(promptKind!!.isBiometric()).isTrue()
        assertThat(currentPrompt?.componentNameForConfirmDeviceCredentialActivity)
            .isEqualTo(
@@ -203,8 +208,12 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
        if (allowCredentialFallback) {
            assertThat(credentialKind).isSameInstanceAs(PromptKind.Password)
            assertThat(isCredentialAllowed).isTrue()
        } else {
            if (Flags.bpFallbackOptions()) {
                assertThat(credentialKind).isEqualTo(PromptKind.Password)
            } else {
                assertThat(credentialKind).isEqualTo(PromptKind.None)
            }
            assertThat(isCredentialAllowed).isFalse()
        }
        assertThat(isConfirmationRequired).isEqualTo(confirmationRequired)
@@ -282,11 +291,13 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
            setUserCredentialType(isPassword = true)

            val promptKind by collectLastValue(interactor.promptKind)
            val currentView by collectLastValue(interactor.currentView)
            assertThat(promptKind).isEqualTo(PromptKind.None)

            setPrompt(onSwitchToCredential = true)

            assertThat(promptKind).isEqualTo(PromptKind.Password)
            assertThat(currentView).isEqualTo(BiometricPromptView.CREDENTIAL)

            interactor.resetPrompt(REQUEST_ID)
            verifyUnset()
@@ -303,11 +314,13 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
                }

            val promptKind by collectLastValue(interactor.promptKind)
            val currentView by collectLastValue(interactor.currentView)
            assertThat(promptKind).isEqualTo(PromptKind.None)

            setPrompt(info)

            assertThat(promptKind).isEqualTo(PromptKind.Password)
            assertThat(currentView).isEqualTo(BiometricPromptView.CREDENTIAL)

            interactor.resetPrompt(REQUEST_ID)
            verifyUnset()
@@ -328,11 +341,13 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
                }

            val promptKind by collectLastValue(interactor.promptKind)
            val currentView by collectLastValue(interactor.currentView)
            assertThat(promptKind).isEqualTo(PromptKind.None)

            setPrompt(info)

            assertThat(promptKind).isEqualTo(PromptKind.Password)
            assertThat(currentView).isEqualTo(BiometricPromptView.CREDENTIAL)

            interactor.resetPrompt(REQUEST_ID)
            verifyUnset()
@@ -351,11 +366,13 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
                }

            val promptKind by collectLastValue(interactor.promptKind)
            val currentView by collectLastValue(interactor.currentView)
            assertThat(promptKind).isEqualTo(PromptKind.None)

            setPrompt(info)

            assertThat(promptKind?.isOnePaneNoSensorLandscapeBiometric()).isTrue()
            assertThat(currentView).isEqualTo(BiometricPromptView.BIOMETRIC)

            interactor.resetPrompt(REQUEST_ID)
            verifyUnset()
@@ -413,7 +430,17 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {

        // not using biometrics, should be null with no fallback option
        assertThat(currentPrompt).isNull()
        if (Flags.bpFallbackOptions()) {
            if (kind == PromptKind.Password) {
                assertThat(credentialKind).isEqualTo(PromptKind.Password)
            } else if (kind == PromptKind.Pin) {
                assertThat(credentialKind).isEqualTo(PromptKind.Pin)
            } else {
                assertThat(credentialKind).isEqualTo(PromptKind.Pattern)
            }
        } else {
            assertThat(credentialKind).isEqualTo(PromptKind.None)
        }

        interactor.resetPrompt(REQUEST_ID)
        verifyUnset()
@@ -434,12 +461,12 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {
    }

    private fun setUserCredentialType(isPin: Boolean = false, isPassword: Boolean = false) {
        whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(any()))
        whenever(lockPatternUtils.getCredentialTypeForUser(any()))
            .thenReturn(
                when {
                    isPin -> DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
                    isPassword -> DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
                    else -> DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
                    isPin -> CREDENTIAL_TYPE_PIN
                    isPassword -> CREDENTIAL_TYPE_PASSWORD
                    else -> CREDENTIAL_TYPE_PATTERN
                }
            )
    }
+2 −2
Original line number Diff line number Diff line
@@ -35,7 +35,7 @@ android:layout_height="match_parent">
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/rightGuideline"
        app:layout_constraintLeft_toLeftOf="@+id/leftGuideline"
        app:layout_constraintTop_toTopOf="@+id/topGuideline" />
        app:layout_constraintTop_toTopOf="@+id/topBarrier" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/fallback_view"
@@ -45,7 +45,7 @@ android:layout_height="match_parent">
        android:fillViewport="true"
        app:layout_constrainedHeight="true"
        app:layout_constrainedWidth="true"
        app:layout_constraintBottom_toTopOf="@id/button_bar"
        app:layout_constraintBottom_toTopOf="@id/bottomGuideline"
        app:layout_constraintEnd_toEndOf="@id/panel"
        app:layout_constraintStart_toStartOf="@id/panel"
        app:layout_constraintTop_toTopOf="@+id/topGuideline"
+8 −1
Original line number Diff line number Diff line
@@ -350,9 +350,10 @@ public class AuthContainerView extends LinearLayout
        final boolean isLandscape = mContext.getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE;
        mPromptSelectorInteractorProvider = promptSelectorInteractorProvider;
        // If the intro (animation) is being skipped, don't reset the prompt
        mPromptSelectorInteractorProvider.get().setPrompt(mConfig.mPromptInfo, mConfig.mUserId,
                getRequestId(), biometricModalities, mConfig.mOperationId, mConfig.mOpPackageName,
                false /*onSwitchToCredential*/, isLandscape);
                false /*onSwitchToCredential*/, isLandscape, !mConfig.mSkipIntro);

        final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        final PromptKind kind = mPromptViewModel.getPromptKind().getValue();
@@ -364,10 +365,16 @@ public class AuthContainerView extends LinearLayout
                mLayout = (ConstraintLayout) layoutInflater.inflate(
                        R.layout.biometric_prompt_one_pane_layout, this, false /* attachToRoot */);
            }

            // Setting visibility here to avoid unflagged layout changes
            if (Flags.bpFallbackOptions()) {
                mLayout.findViewById(R.id.auth_screen).setVisibility(View.GONE);
            }
        } else {
            mLayout = (FrameLayout) layoutInflater.inflate(
                    R.layout.auth_container_view, this, false /* attachToRoot */);
        }

        addView(mLayout);
        mBackgroundView = mLayout.findViewById(R.id.background);

+37 −11
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.domain.interactor.DisplayStateInteractor
import com.android.systemui.display.shared.model.isDefaultOrientation
import com.android.systemui.kairos.awaitClose
import com.android.systemui.util.kotlin.combine
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -72,6 +73,9 @@ interface PromptSelectorInteractor {
    /** The kind of prompt to use (biometric, pin, pattern, etc.). */
    val promptKind: StateFlow<PromptKind>

    /** The modalities available in the prompt */
    val modalities: Flow<BiometricModalities>

    /** If using a credential is allowed. */
    val isCredentialAllowed: Flow<Boolean>

@@ -125,6 +129,7 @@ interface PromptSelectorInteractor {
        opPackageName: String,
        onSwitchToCredential: Boolean,
        isLandscape: Boolean,
        updateView: Boolean = true,
    )

    /** Unset the current authentication request. */
@@ -151,7 +156,8 @@ constructor(
            promptRepository.userId,
            promptRepository.promptKind,
            promptRepository.opPackageName,
        ) { promptInfo, challenge, userId, kind, opPackageName ->
            promptRepository.modalities,
        ) { promptInfo, challenge, userId, kind, opPackageName, modalities ->
            if (
                promptInfo == null || userId == null || challenge == null || opPackageName == null
            ) {
@@ -171,7 +177,7 @@ constructor(
                        operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge),
                        modalities =
                            if (Flags.bpFallbackOptions()) {
                                promptRepository.modalities.value
                                modalities
                            } else {
                                kind.activeModalities
                            },
@@ -183,6 +189,8 @@ constructor(

    override val promptKind: StateFlow<PromptKind> = promptRepository.promptKind

    override val modalities: StateFlow<BiometricModalities> = promptRepository.modalities

    override val isConfirmationRequired: Flow<Boolean> =
        promptRepository.isConfirmationRequired.distinctUntilChanged()

@@ -236,6 +244,15 @@ constructor(
    override val fallbackOptions: Flow<List<FallbackOptionModel>> = promptRepository.fallbackOptions

    override val credentialKind: Flow<PromptKind> =
        if (Flags.bpFallbackOptions()) {
            promptRepository.userId.map { userId ->
                if (userId != null) {
                    getCredentialType(lockPatternUtils, userId)
                } else {
                    PromptKind.None
                }
            }
        } else {
            combine(prompt, isCredentialAllowed) { prompt, isAllowed ->
                if (prompt != null && isAllowed) {
                    getCredentialType(lockPatternUtils, prompt.userInfo.deviceCredentialOwnerId)
@@ -243,6 +260,7 @@ constructor(
                    PromptKind.None
                }
            }
        }

    override val fingerprintSensorType: Flow<FingerprintSensorType> =
        fingerprintPropertyRepository.sensorType
@@ -298,6 +316,7 @@ constructor(
        opPackageName: String,
        onSwitchToCredential: Boolean,
        isLandscape: Boolean,
        updateView: Boolean,
    ) {
        val effectiveUserId = credentialInteractor.getCredentialOwnerOrSelfId(userId)
        val hasCredentialViewShown = promptKind.value.isCredential()
@@ -309,11 +328,15 @@ constructor(
        val showBpWithoutIconForCredential = showBpForCredential && !hasCredentialViewShown
        var kind: PromptKind = PromptKind.None

        if (onSwitchToCredential) {
        if (onSwitchToCredential || _currentView.value == BiometricPromptView.CREDENTIAL) {
            kind = getCredentialType(lockPatternUtils, effectiveUserId)
            if (updateView) {
                _currentView.value = BiometricPromptView.CREDENTIAL
            }
        } else if (Utils.isBiometricAllowed(promptInfo) || showBpWithoutIconForCredential) {
            if (updateView) {
                _currentView.value = BiometricPromptView.BIOMETRIC
            }
            // TODO(b/330908557): Subscribe to
            // displayStateInteractor.currentRotation.value.isDefaultOrientation() for checking
            // `isLandscape` after removing AuthContainerView.
@@ -332,7 +355,9 @@ constructor(
                    PromptKind.Biometric(modalities)
                }
        } else if (isDeviceCredentialAllowed(promptInfo)) {
            if (updateView) {
                _currentView.value = BiometricPromptView.CREDENTIAL
            }
            kind = getCredentialType(lockPatternUtils, effectiveUserId)
        }

@@ -348,6 +373,7 @@ constructor(
    }

    override fun resetPrompt(requestId: Long) {
        _currentView.value = BiometricPromptView.BIOMETRIC
        promptRepository.unsetPrompt(requestId)
    }

Loading