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

Commit 4219e165 authored by Arpan's avatar Arpan
Browse files

Propagate DevCancellation to BioPrompt

This CL aims to propagate the developer cancellation signal to the
biometric prompt, by creating a 'wait/notify' system based on the
underlying viewModel's mutableState. This specifically modifies
BiometricState to contain a new property that acts as the 'notifier' for
cancellation in the SysService.

Bug: 333445003
Test: Sample app configuration test, build test
Change-Id: I10aedc7bd2adeb571a9ffe7d89c5f201fe02e660
parent e81739a8
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -125,6 +125,7 @@ class CredentialSelectorActivity : ComponentActivity() {
            return Triple(true, false, null)
        }
        val shouldShowCancellationUi = cancelUiRequest.shouldShowCancellationExplanation()
        viewModel?.onDeveloperCancellationReceivedForBiometricPrompt()
        Log.d(
            Constants.LOG_TAG, "Received UI cancellation intent. Should show cancellation" +
            " ui = $shouldShowCancellationUi")
+46 −7
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.credentialmanager
import android.app.Activity
import android.hardware.biometrics.BiometricPrompt
import android.hardware.biometrics.BiometricPrompt.AuthenticationResult
import android.os.CancellationSignal
import android.os.IBinder
import android.text.TextUtils
import android.util.Log
@@ -232,8 +233,13 @@ class CredentialSelectorViewModel(
        authResult: BiometricPrompt.AuthenticationResult? = null,
        authError: BiometricError? = null,
    ) {
        if (authError == null) {
            Log.d(Constants.LOG_TAG, "credential selected: {provider=${entry.providerId}" +
                        ", key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
        } else {
                Log.d(Constants.LOG_TAG, "Biometric flow error: ${authError.errorCode} " +
                        "propagating to provider, message: ${authError.errorMessage}.")
        }
        uiState = if (entry.pendingIntent != null) {
            uiState.copy(
                selectedEntry = entry,
@@ -385,9 +391,15 @@ class CredentialSelectorViewModel(
        val providerId = selectedEntry.providerId
        val entryKey = selectedEntry.entryKey
        val entrySubkey = selectedEntry.entrySubkey
        if (authError == null) {
            Log.d(
                Constants.LOG_TAG, "Option selected for entry: " +
            " {provider=$providerId, key=$entryKey, subkey=$entrySubkey")
                        " {provider=$providerId, key=$entryKey, subkey=$entrySubkey"
            )
        } else {
            Log.d(Constants.LOG_TAG, "Biometric flow error: ${authError.errorCode} " +
                    "propagating to provider, message: ${authError.errorMessage}.")
        }
        if (selectedEntry.pendingIntent != null) {
            uiState = uiState.copy(
                selectedEntry = selectedEntry,
@@ -423,6 +435,33 @@ class CredentialSelectorViewModel(
    /*****                     Biometric Flow Callbacks                   *****/
    /**************************************************************************/

    /**
     * Cancels the biometric prompt's cancellation signal. Should only be called when the credential
     * manager ui receives a developer cancellation signal. If the prompt is already done, we do
     * not allow a cancellation, given the UI cancellation will be caught by the backend. We also
     * set the biometricStatus to CANCELED, so that only in this case, we do *not* propagate the
     * ERROR_CANCELED when a developer cancellation signal is the root cause.
     */
    fun onDeveloperCancellationReceivedForBiometricPrompt() {
        val biometricCancellationSignal = uiState.biometricState.biometricCancellationSignal
        if (!biometricCancellationSignal.isCanceled && uiState.biometricState.biometricStatus
            != BiometricPromptState.COMPLETE) {
            uiState = uiState.copy(
                biometricState = uiState.biometricState.copy(
                    biometricStatus = BiometricPromptState.CANCELED
                )
            )
            biometricCancellationSignal.cancel()
        }
    }

    /**
     * Retrieve the biometric prompt's cancellation signal (e.g. to pass into the 'authenticate'
     * API).
     */
    fun getBiometricCancellationSignal(): CancellationSignal =
        uiState.biometricState.biometricCancellationSignal

    /**
     * This allows falling back from the biometric prompt screen to the normal get flow by applying
     * a reset to all necessary states involved in the fallback.
@@ -450,9 +489,9 @@ class CredentialSelectorViewModel(
    }

    /**
     * This returns the present biometric state.
     * This returns the present biometric prompt state's status.
     */
    fun getBiometricPromptState(): BiometricPromptState =
    fun getBiometricPromptStateStatus(): BiometricPromptState =
        uiState.biometricState.biometricStatus

    /**************************************************************************/
+28 −23
Original line number Diff line number Diff line
@@ -65,7 +65,6 @@ import java.lang.Exception
 * ).
 *
 * The above are examples; the credential type can change depending on scenario.
 * // TODO(b/333445112) : Finalize once all the strings and create flow is iterated to completion
 */
data class BiometricDisplayInfo(
    val providerIcon: Bitmap,
@@ -84,7 +83,8 @@ data class BiometricDisplayInfo(
data class BiometricState(
    val biometricResult: BiometricResult? = null,
    val biometricError: BiometricError? = null,
    val biometricStatus: BiometricPromptState = BiometricPromptState.INACTIVE
    val biometricStatus: BiometricPromptState = BiometricPromptState.INACTIVE,
    val biometricCancellationSignal: CancellationSignal = CancellationSignal(),
)

/**
@@ -118,6 +118,7 @@ fun runBiometricFlowForGet(
    getBiometricPromptState: () -> BiometricPromptState,
    onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
    onBiometricFailureFallback: (BiometricFlowType) -> Unit,
    getBiometricCancellationSignal: () -> CancellationSignal,
    getRequestDisplayInfo: RequestDisplayInfo? = null,
    getProviderInfoList: List<ProviderInfo>? = null,
    getProviderDisplayInfo: ProviderDisplayInfo? = null,
@@ -141,11 +142,13 @@ fun runBiometricFlowForGet(

    val callback: BiometricPrompt.AuthenticationCallback =
        setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry,
            onCancelFlowAndFinish, onIllegalStateAndFinish, onBiometricPromptStateChange)
            onCancelFlowAndFinish, onIllegalStateAndFinish, onBiometricPromptStateChange,
            getBiometricPromptState)

    Log.d(TAG, "The BiometricPrompt API call begins.")
    Log.d(TAG, "The BiometricPrompt API call begins for Get.")
    runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
        onBiometricFailureFallback, BiometricFlowType.GET, onCancelFlowAndFinish)
        onBiometricFailureFallback, BiometricFlowType.GET, onCancelFlowAndFinish,
        getBiometricCancellationSignal)
}

/**
@@ -163,6 +166,7 @@ fun runBiometricFlowForCreate(
    getBiometricPromptState: () -> BiometricPromptState,
    onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
    onBiometricFailureFallback: (BiometricFlowType) -> Unit,
    getBiometricCancellationSignal: () -> CancellationSignal,
    createRequestDisplayInfo: com.android.credentialmanager.createflow
    .RequestDisplayInfo? = null,
    createProviderInfo: EnabledProviderInfo? = null,
@@ -185,11 +189,13 @@ fun runBiometricFlowForCreate(

    val callback: BiometricPrompt.AuthenticationCallback =
        setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry,
            onCancelFlowAndFinish, onIllegalStateAndFinish, onBiometricPromptStateChange)
            onCancelFlowAndFinish, onIllegalStateAndFinish, onBiometricPromptStateChange,
            getBiometricPromptState)

    Log.d(TAG, "The BiometricPrompt API call begins.")
    Log.d(TAG, "The BiometricPrompt API call begins for Create.")
    runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
        onBiometricFailureFallback, BiometricFlowType.CREATE, onCancelFlowAndFinish)
        onBiometricFailureFallback, BiometricFlowType.CREATE, onCancelFlowAndFinish,
        getBiometricCancellationSignal)
}

/**
@@ -206,7 +212,8 @@ private fun runBiometricFlow(
    openMoreOptionsPage: () -> Unit,
    onBiometricFailureFallback: (BiometricFlowType) -> Unit,
    biometricFlowType: BiometricFlowType,
    onCancelFlowAndFinish: () -> Unit
    onCancelFlowAndFinish: () -> Unit,
    getBiometricCancellationSignal: () -> CancellationSignal,
) {
    try {
        if (!canCallBiometricPrompt(biometricDisplayInfo, context)) {
@@ -217,12 +224,7 @@ private fun runBiometricFlow(
        val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo,
            openMoreOptionsPage, biometricDisplayInfo.biometricRequestInfo, onCancelFlowAndFinish)

        val cancellationSignal = CancellationSignal()
        cancellationSignal.setOnCancelListener {
            Log.d(TAG, "Your cancellation signal was called.")
            // TODO(b/333445112) : Migrate towards passing along the developer cancellation signal
            // or validate the necessity for this
        }
        val cancellationSignal = getBiometricCancellationSignal()

        val executor = getMainExecutor(context)

@@ -251,8 +253,6 @@ private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Int? {
 * Note that if device credential is the only available modality but not requested, or if none
 * of the requested modalities are available, we fallback to the normal flow to ensure a selector
 * shows up.
 * // TODO(b/334197980) : While we already fallback in cases the selector doesn't show, confirm
 * // final plan.
 */
private fun canCallBiometricPrompt(
    biometricDisplayInfo: BiometricDisplayInfo,
@@ -270,12 +270,12 @@ private fun canCallBiometricPrompt(
        return false
    }

    if (ifOnlySupportsAtMostDeviceCredentials(biometricManager)) return false
    if (onlySupportsAtMostDeviceCredentials(biometricManager)) return false

    return true
}

private fun ifOnlySupportsAtMostDeviceCredentials(biometricManager: BiometricManager): Boolean {
private fun onlySupportsAtMostDeviceCredentials(biometricManager: BiometricManager): Boolean {
    if (biometricManager.canAuthenticate(Authenticators.BIOMETRIC_WEAK) !=
        BiometricManager.BIOMETRIC_SUCCESS &&
        biometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG) !=
@@ -343,11 +343,11 @@ private fun setupBiometricAuthenticationCallback(
    selectedEntry: EntryInfo,
    onCancelFlowAndFinish: () -> Unit,
    onIllegalStateAndFinish: (String) -> Unit,
    onBiometricPromptStateChange: (BiometricPromptState) -> Unit
    onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
    getBiometricPromptState: () -> BiometricPromptState,
): BiometricPrompt.AuthenticationCallback {
    val callback: BiometricPrompt.AuthenticationCallback =
        object : BiometricPrompt.AuthenticationCallback() {
            // TODO(b/333445772) : Validate remaining callbacks
            override fun onAuthenticationSucceeded(
                authResult: BiometricPrompt.AuthenticationResult?
            ) {
@@ -374,6 +374,12 @@ private fun setupBiometricAuthenticationCallback(
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
                super.onAuthenticationError(errorCode, errString)
                Log.d(TAG, "Authentication error-ed out: $errorCode and $errString")
                if (getBiometricPromptState() == BiometricPromptState.CANCELED && errorCode
                    == BiometricPrompt.BIOMETRIC_ERROR_CANCELED) {
                    Log.d(TAG, "Developer cancellation signal received. Nothing more to do.")
                    // This unique edge case means a developer cancellation signal was sent.
                    return
                }
                onBiometricPromptStateChange(BiometricPromptState.COMPLETE)
                if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED) {
                    // Note that because the biometric prompt is imbued directly
@@ -471,8 +477,7 @@ private fun retrieveBiometricGetDisplayValues(
    val singleEntryType = selectedEntry.credentialType
    val username = selectedEntry.userName

    // TODO(b/330396140) : Finalize localization and parsing for specific sign in option flows
    //  (fingerprint, face, etc...))
    // TODO(b/336362538) : In W, utilize updated localization strings
    displayTitleText = context.getString(
        generateDisplayTitleTextResCode(singleEntryType),
        getRequestDisplayInfo.appName
+6 −1
Original line number Diff line number Diff line
@@ -22,5 +22,10 @@ enum class BiometricPromptState {
    /** The biometric prompt is active but data hasn't been returned yet. */
    PENDING,
    /** The biometric prompt has closed and returned data we then send to the provider activity. */
    COMPLETE
    COMPLETE,
    /**
     * The biometric prompt has been canceled by a developer signal. If this is true, certain
     * conditions can be triggered, such as no longer propagating ERROR_CANCELED.
     */
    CANCELED,
}
 No newline at end of file
+7 −2
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.credentialmanager.createflow

import android.credentials.flags.Flags.selectorUiImprovementsEnabled
import android.hardware.biometrics.BiometricPrompt
import android.os.CancellationSignal
import android.text.TextUtils
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
@@ -118,9 +119,11 @@ fun CreateCredentialScreen(
                                fallbackToOriginalFlow =
                                viewModel::fallbackFromBiometricToNormalFlow,
                                getBiometricPromptState =
                                viewModel::getBiometricPromptState,
                                viewModel::getBiometricPromptStateStatus,
                                onBiometricPromptStateChange =
                                viewModel::onBiometricPromptStateChange
                                viewModel::onBiometricPromptStateChange,
                                getBiometricCancellationSignal =
                                viewModel::getBiometricCancellationSignal
                            )
                        CreateScreenState.MORE_OPTIONS_SELECTION_ONLY -> MoreOptionsSelectionCard(
                                requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
@@ -638,6 +641,7 @@ internal fun BiometricSelectionPage(
    fallbackToOriginalFlow: (BiometricFlowType) -> Unit,
    getBiometricPromptState: () -> BiometricPromptState,
    onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
    getBiometricCancellationSignal: () -> CancellationSignal,
) {
    if (biometricEntry == null) {
        fallbackToOriginalFlow(BiometricFlowType.CREATE)
@@ -655,5 +659,6 @@ internal fun BiometricSelectionPage(
        createProviderInfo = enabledProviderInfo,
        onBiometricFailureFallback = fallbackToOriginalFlow,
        onIllegalStateAndFinish = onIllegalScreenStateAndFinish,
        getBiometricCancellationSignal = getBiometricCancellationSignal,
    )
}
Loading