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

Commit a638f918 authored by Arpan's avatar Arpan
Browse files

Create Flow States, UI, BiometricFlow, and Parsing

Similar to the Get flow, this combines 4 previously scoped out bugs.

This adds on all the state management for the single tap create flow,
and sets up detailed conditionals.

This flow can *already* be triggered (as can the get flow) in the present setup,
and further chains will complete the feature. Thus, we introduce an E2E
with which we can continue extending. For example, there have been
recent alignments in UX with the BiometricTeam, and those iterations can
follow once the E2E is embedded, especially since we will continue to
utilize parameters that are in the process of being released.

Bug: 327619148
Bug: 327620245
Bug: 327620327
Bug: 327621520
Test: Junit tests, UX tests, and build tests (flow not triggerable in
normal use cases yet)

Change-Id: I5532662315e9a06b6a74f4454418ee1ff3cd243d
parent 3242d4ec
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -68,6 +68,13 @@
  <string name="choose_create_option_password_title">Save password to sign in to <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string>
  <!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is others. [CHAR LIMIT=200] -->
  <string name="choose_create_option_sign_in_title">Save sign-in info for <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string>
  <!-- This appears as a description of the modal bottom sheet when the single tap sign in flow is used for the create passkey flow. [CHAR LIMIT=200] -->
  <string name="choose_create_single_tap_passkey_title">Use your screen lock to create a passkey for <xliff:g id="app_name" example="Shrine">%1$s</xliff:g>?</string>
  <!-- This appears as a description of the modal bottom sheet when the single tap sign in flow is used for the create password flow. [CHAR LIMIT=200] -->
  <string name="choose_create_single_tap_password_title">Use your screen lock to create a password for <xliff:g id="app_name" example="Shrine">%1$s</xliff:g>?</string>
  <!-- This appears as a description of the modal bottom sheet when the single tap sign in flow is used for the create flow when the credential type is others. [CHAR LIMIT=200] -->
  <!-- TODO(b/326243891) : Confirm with team on dynamically setting this based on recent product and ux discussions (does not disrupt e2e) -->
  <string name="choose_create_single_tap_sign_in_title">Use your screen lock to save sign in info for <xliff:g id="app_name" example="Shrine">%1$s</xliff:g>?</string>
  <!-- Types which are inserted as a placeholder as credentialTypes for other strings. [CHAR LIMIT=200] -->
  <string name="passkey">passkey</string>
  <string name="password">password</string>
+20 −10
Original line number Diff line number Diff line
@@ -51,6 +51,8 @@ import com.android.credentialmanager.TAG
import com.android.credentialmanager.model.BiometricRequestInfo
import com.android.credentialmanager.model.EntryInfo

const val CREDENTIAL_ENTRY_PREFIX = "androidx.credentials.provider.credentialEntry."

fun EntryInfo.getIntentSenderRequest(
    isAutoSelected: Boolean = false
): IntentSenderRequest? {
@@ -140,7 +142,8 @@ private fun getCredentialOptionInfoList(
                    isDefaultIconPreferredAsSingleProvider =
                            credentialEntry.isDefaultIconPreferredAsSingleProvider,
                    affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
                    biometricRequest = predetermineAndValidateBiometricFlow(it),
                    biometricRequest = predetermineAndValidateBiometricFlow(it,
                        CREDENTIAL_ENTRY_PREFIX),
                )
                )
            }
@@ -169,7 +172,8 @@ private fun getCredentialOptionInfoList(
                    isDefaultIconPreferredAsSingleProvider =
                            credentialEntry.isDefaultIconPreferredAsSingleProvider,
                    affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
                    biometricRequest = predetermineAndValidateBiometricFlow(it),
                    biometricRequest = predetermineAndValidateBiometricFlow(it,
                        CREDENTIAL_ENTRY_PREFIX),
                )
                )
            }
@@ -197,7 +201,8 @@ private fun getCredentialOptionInfoList(
                    isDefaultIconPreferredAsSingleProvider =
                            credentialEntry.isDefaultIconPreferredAsSingleProvider,
                    affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
                    biometricRequest = predetermineAndValidateBiometricFlow(it),
                    biometricRequest = predetermineAndValidateBiometricFlow(it,
                        CREDENTIAL_ENTRY_PREFIX),
                )
                )
            }
@@ -217,21 +222,26 @@ private fun getCredentialOptionInfoList(
 * Note that the required values, such as the provider info's icon or display name, or the entries
 * credential type or userName, and finally the display info's app name, are non-null and must
 * exist to run through the flow.
 *
 * @param hintPrefix a string prefix indicating the type of entry being utilized, since both create
 * and get flows utilize slice params; includes the final '.' before the name of the type (e.g.
 * androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS must have
 * 'hintPrefix' up to "androidx.credentials.provider.credentialEntry.")
 * // TODO(b/326243754) : Presently, due to dependencies, the opId bit is parsed but is never
 * // expected to be used. When it is added, it should be lightly validated.
 */
private fun predetermineAndValidateBiometricFlow(
    it: Entry
fun predetermineAndValidateBiometricFlow(
    entry: Entry,
    hintPrefix: String,
): BiometricRequestInfo? {
    // TODO(b/326243754) : When available, use the official jetpack structured type
    val allowedAuthenticators: Int? = it.slice.items.firstOrNull {
        it.hasHint("androidx.credentials." +
                "provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS")
    val allowedAuthenticators: Int? = entry.slice.items.firstOrNull {
        it.hasHint(hintPrefix + "SLICE_HINT_ALLOWED_AUTHENTICATORS")
    }?.int

    // This is optional and does not affect validating the biometric flow in any case
    val opId: Int? = it.slice.items.firstOrNull {
        it.hasHint("androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID")
    val opId: Int? = entry.slice.items.firstOrNull {
        it.hasHint(hintPrefix + "SLICE_HINT_CRYPTO_OP_ID")
    }?.int
    if (allowedAuthenticators != null) {
        return BiometricRequestInfo(opId = opId, allowedAuthenticators = allowedAuthenticators)
+14 −7
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.credentialmanager

import android.app.Activity
import android.hardware.biometrics.BiometricPrompt
import android.hardware.biometrics.BiometricPrompt.AuthenticationResult
import android.os.IBinder
import android.text.TextUtils
import android.util.Log
@@ -39,6 +40,7 @@ import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.createflow.ActiveEntry
import com.android.credentialmanager.createflow.CreateCredentialUiState
import com.android.credentialmanager.createflow.CreateScreenState
import com.android.credentialmanager.createflow.findBiometricFlowEntry
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.GetScreenState
import com.android.credentialmanager.logging.LifecycleEvent
@@ -304,7 +306,11 @@ class CredentialSelectorViewModel(
        uiState = uiState.copy(
            createCredentialUiState = uiState.createCredentialUiState?.copy(
                currentScreenState =
                if (uiState.createCredentialUiState?.requestDisplayInfo?.userSetDefaultProviderIds
                // An autoselect flow never makes it to the more options screen
                if (findBiometricFlowEntry(activeEntry = activeEntry,
                        isAutoSelectFlow = false) != null) CreateScreenState.BIOMETRIC_SELECTION
                else if (
                    uiState.createCredentialUiState?.requestDisplayInfo?.userSetDefaultProviderIds
                        ?.contains(activeEntry.activeProvider.id) ?: true ||
                    !(uiState.createCredentialUiState?.foundCandidateFromUserDefaultProvider
                    ?: false) ||
@@ -330,7 +336,10 @@ class CredentialSelectorViewModel(
        )
    }

    fun createFlowOnEntrySelected(selectedEntry: EntryInfo) {
    fun createFlowOnEntrySelected(
        selectedEntry: EntryInfo,
        authResult: AuthenticationResult? = null
    ) {
        val providerId = selectedEntry.providerId
        val entryKey = selectedEntry.entryKey
        val entrySubkey = selectedEntry.entrySubkey
@@ -341,6 +350,9 @@ class CredentialSelectorViewModel(
            uiState = uiState.copy(
                selectedEntry = selectedEntry,
                providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
                biometricState = if (authResult == null) uiState.biometricState else uiState
                    .biometricState.copy(biometricResult = BiometricResult(
                        biometricAuthenticationResult = authResult))
            )
        } else {
            credManRepo.onOptionSelected(
@@ -367,9 +379,4 @@ class CredentialSelectorViewModel(
    fun logUiEvent(uiEventEnum: UiEventEnum) {
        this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.packageName)
    }

    companion object {
        // TODO(b/326243754) : Replace/remove once all failure flows added in
        const val TEMPORARY_FAILURE_CODE = Integer.MIN_VALUE
    }
}
 No newline at end of file
+23 −12
Original line number Diff line number Diff line
@@ -52,10 +52,11 @@ import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.RemoteEntry
import org.json.JSONObject
import android.credentials.flags.Flags
import com.android.credentialmanager.createflow.isBiometricFlow
import com.android.credentialmanager.getflow.TopBrandingContent
import com.android.credentialmanager.ktx.predetermineAndValidateBiometricFlow
import java.time.Instant


fun getAppLabel(
    pm: PackageManager,
    appPackageName: String
@@ -237,6 +238,9 @@ class GetFlowUtils {

class CreateFlowUtils {
    companion object {

        private const val CREATE_ENTRY_PREFIX = "androidx.credentials.provider.createEntry."

        /**
         * Note: caller required handle empty list due to parsing error.
         */
@@ -417,12 +421,21 @@ class CreateFlowUtils {
                }
            }
            val defaultProvider = defaultProviderPreferredByApp ?: defaultProviderSetByUser
            val sortedCreateOptionsPairs = createOptionsPairs.sortedWith(
                compareByDescending { it.first.lastUsedTime }
            )
            val activeEntry = toActiveEntry(
                defaultProvider = defaultProvider,
                sortedCreateOptionsPairs = sortedCreateOptionsPairs,
                remoteEntry = remoteEntry,
                remoteEntryProvider = remoteEntryProvider,
            )
            val isBiometricFlow = if (activeEntry == null) false else isBiometricFlow(activeEntry,
                sortedCreateOptionsPairs, requestDisplayInfo)
            val initialScreenState = toCreateScreenState(
                createOptionSize = createOptionsPairs.size,
                remoteEntry = remoteEntry,
            )
            val sortedCreateOptionsPairs = createOptionsPairs.sortedWith(
                compareByDescending { it.first.lastUsedTime }
                isBiometricFlow = isBiometricFlow
            )
            return CreateCredentialUiState(
                enabledProviders = enabledProviders,
@@ -430,12 +443,7 @@ class CreateFlowUtils {
                currentScreenState = initialScreenState,
                requestDisplayInfo = requestDisplayInfo,
                sortedCreateOptionsPairs = sortedCreateOptionsPairs,
                activeEntry = toActiveEntry(
                    defaultProvider = defaultProvider,
                    sortedCreateOptionsPairs = sortedCreateOptionsPairs,
                    remoteEntry = remoteEntry,
                    remoteEntryProvider = remoteEntryProvider,
                ),
                activeEntry = activeEntry,
                remoteEntry = remoteEntry,
                foundCandidateFromUserDefaultProvider = defaultProviderSetByUser != null,
            )
@@ -444,9 +452,12 @@ class CreateFlowUtils {
        fun toCreateScreenState(
            createOptionSize: Int,
            remoteEntry: RemoteInfo?,
            isBiometricFlow: Boolean,
        ): CreateScreenState {
            return if (createOptionSize == 0 && remoteEntry != null) {
                CreateScreenState.EXTERNAL_ONLY_SELECTION
            } else if (isBiometricFlow) {
                CreateScreenState.BIOMETRIC_SELECTION
            } else {
                CreateScreenState.CREATION_OPTION_SELECTION
            }
@@ -503,8 +514,8 @@ class CreateFlowUtils {
                        it.hasHint("androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_" +
                            "SELECT_ALLOWED")
                    }?.text == "true",
                    // TODO(b/326243754) : Handle this when the create flow is added; for now the
                    // create flow does not support biometric values
                    biometricRequest = predetermineAndValidateBiometricFlow(it,
                        CREATE_ENTRY_PREFIX),
                )
                )
            }
+75 −27
Original line number Diff line number Diff line
@@ -26,11 +26,14 @@ import androidx.core.content.ContextCompat.getMainExecutor
import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.R
import com.android.credentialmanager.createflow.EnabledProviderInfo
import com.android.credentialmanager.createflow.getCreateTitleResCode
import com.android.credentialmanager.getflow.ProviderDisplayInfo
import com.android.credentialmanager.getflow.RequestDisplayInfo
import com.android.credentialmanager.getflow.generateDisplayTitleTextResCode
import com.android.credentialmanager.model.BiometricRequestInfo
import com.android.credentialmanager.model.CredentialType
import com.android.credentialmanager.model.EntryInfo
import com.android.credentialmanager.model.creation.CreateOptionInfo
import com.android.credentialmanager.model.get.CredentialEntryInfo
import com.android.credentialmanager.model.get.ProviderInfo
import java.lang.Exception
@@ -39,14 +42,30 @@ import java.lang.Exception
 * Aggregates common display information used for the Biometric Flow.
 * Namely, this adds the ability to encapsulate the [providerIcon], the providers icon, the
 * [providerName], which represents the name of the provider, the [displayTitleText] which is
 * the large text displaying the flow in progress, and the [descriptionAboveBiometricButton], which
 * the large text displaying the flow in progress, and the [descriptionForCredential], which
 * describes details of where the credential is being saved, and how.
 * (E.g. assume a hypothetical provider 'Any Provider' for *passkey* flows with Your@Email.com:
 *
 * 'get' flow:
 *     - [providerIcon] and [providerName] = 'Any Provider' (and it's icon)
 *     - [displayTitleText] = "Use your saved passkey for Any Provider?"
 *     - [descriptionForCredential] = "Use your screen lock to sign in to Any Provider with
 *     Your@Email.com"
 *
 * 'create' flow:
 *     - [providerIcon] and [providerName] = 'Any Provider' (and it's icon)
 *     - [displayTitleText] = "Create passkey to sign in to Any Provider?"
 *     - [descriptionForCredential] = "Use your screen lock to create a passkey for Any Provider?"
 * ).
 *
 * The above are examples; the credential type can change depending on scenario.
 * // TODO(b/326243891) : Finalize once all the strings and create flow is iterated to completion
 */
data class BiometricDisplayInfo(
    val providerIcon: Bitmap,
    val providerName: String,
    val displayTitleText: String,
    val descriptionAboveBiometricButton: String,
    val descriptionForCredential: String,
    val biometricRequestInfo: BiometricRequestInfo,
)

@@ -56,10 +75,7 @@ data class BiometricDisplayInfo(
 * additional states that may improve the flow.
 */
data class BiometricState(
    val biometricResult: BiometricResult? = null,
    val biometricError: BiometricError? = null,
    val biometricHelp: BiometricHelp? = null,
    val biometricAcquireInfo: Int? = null,
    val biometricResult: BiometricResult? = null
)

/**
@@ -108,18 +124,20 @@ fun runBiometricFlow(
    .RequestDisplayInfo? = null,
    createProviderInfo: EnabledProviderInfo? = null,
) {
    // TODO(b/330396089) : Add rotation configuration fix with state machine
    var biometricDisplayInfo: BiometricDisplayInfo? = null
    var flowType = FlowType.GET
    if (getRequestDisplayInfo != null) {
        biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(getRequestDisplayInfo,
            getProviderInfoList,
            getProviderDisplayInfo,
            context, biometricEntry)
    } else if (createRequestDisplayInfo != null) {
        // TODO(b/326243754) : Create Flow to be implemented in follow up
        biometricDisplayInfo = validateBiometricCreateFlow(
        flowType = FlowType.CREATE
        biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo(
            createRequestDisplayInfo,
            createProviderInfo
        )
            createProviderInfo,
            context, biometricEntry)
    }

    if (biometricDisplayInfo == null) {
@@ -128,7 +146,7 @@ fun runBiometricFlow(
    }

    val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage,
        biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators)
        biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators, flowType)

    val callback: BiometricPrompt.AuthenticationCallback =
        setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry,
@@ -154,23 +172,21 @@ fun runBiometricFlow(
/**
 * Sets up the biometric prompt with the UI specific bits.
 * // TODO(b/326243754) : Pass in opId once dependency is confirmed via CryptoObject
 * // TODO(b/326243754) : Given fallbacks aren't allowed, for now we validate that device creds
 * // are NOT allowed to be passed in to avoid throwing an error. Later, however, once target
 * // alignments occur, we should add the bit back properly.
 */
private fun setupBiometricPrompt(
    context: Context,
    biometricDisplayInfo: BiometricDisplayInfo,
    openMoreOptionsPage: () -> Unit,
    requestAllowedAuthenticators: Int,
    flowType: FlowType,
): BiometricPrompt {
    val finalAuthenticators = removeDeviceCredential(requestAllowedAuthenticators)

    val biometricPrompt = BiometricPrompt.Builder(context)
        .setTitle(biometricDisplayInfo.displayTitleText)
        // TODO(b/326243754) : Migrate to using new methods recently aligned upon
        .setNegativeButton(context.getString(R.string
                .dropdown_presentation_more_sign_in_options_text),
        .setNegativeButton(context.getString(if (flowType == FlowType.GET) R.string
                .dropdown_presentation_more_sign_in_options_text else R.string.string_more_options),
            getMainExecutor(context)) { _, _ ->
            openMoreOptionsPage()
        }
@@ -178,7 +194,7 @@ private fun setupBiometricPrompt(
        .setConfirmationRequired(true)
        .setLogoBitmap(biometricDisplayInfo.providerIcon)
        .setLogoDescription(biometricDisplayInfo.providerName)
        .setDescription(biometricDisplayInfo.descriptionAboveBiometricButton)
        .setDescription(biometricDisplayInfo.descriptionForCredential)
        .build()

    return biometricPrompt
@@ -294,14 +310,16 @@ private fun validateAndRetrieveBiometricGetDisplayInfo(
 * checking between the two. The reason for this method matches the logic for the
 * [validateBiometricGetFlow] with the only difference being that this is for the create flow.
 */
private fun validateBiometricCreateFlow(
private fun validateAndRetrieveBiometricCreateDisplayInfo(
    createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo?,
    createProviderInfo: EnabledProviderInfo?,
    context: Context,
    selectedEntry: EntryInfo,
): BiometricDisplayInfo? {
    if (createRequestDisplayInfo != null && createProviderInfo != null) {
    } else if (createRequestDisplayInfo != null && createProviderInfo != null) {
        // TODO(b/326243754) : Create Flow to be implemented in follow up
        return createFlowDisplayValues()
        if (selectedEntry !is CreateOptionInfo) { return null }
        return createBiometricDisplayValues(createRequestDisplayInfo, createProviderInfo, context,
            selectedEntry)
    }
    return null
}
@@ -346,17 +364,47 @@ private fun getBiometricDisplayValues(
        username
    )
    return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
        displayTitleText = displayTitleText, descriptionAboveBiometricButton = descriptionText,
        displayTitleText = displayTitleText, descriptionForCredential = descriptionText,
        biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo)
}

/**
 * Handles the biometric sign in via the 'create credentials' flow, or early validates this flow
 * needs to fallback.
 * Handles the biometric sign in via the create credentials flow. Stricter in the get flow in that
 * if this is called, a result is guaranteed. Specifically, this is guaranteed to return a non-null
 * value unlike the get counterpart.
 */
private fun createFlowDisplayValues(): BiometricDisplayInfo? {
    // TODO(b/326243754) : Create Flow to be implemented in follow up
    return null
private fun createBiometricDisplayValues(
    createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo,
    createProviderInfo: EnabledProviderInfo,
    context: Context,
    selectedEntry: CreateOptionInfo,
): BiometricDisplayInfo {
    val icon: Bitmap?
    val providerName: String?
    val displayTitleText: String?
    icon = createProviderInfo.icon.toBitmap()
    providerName = createProviderInfo.displayName
    displayTitleText = context.getString(
        getCreateTitleResCode(createRequestDisplayInfo),
        createRequestDisplayInfo.appName
    )
    val descriptionText: String = context.getString(
        when (createRequestDisplayInfo.type) {
            CredentialType.PASSKEY ->
                R.string.choose_create_single_tap_passkey_title

            CredentialType.PASSWORD ->
                R.string.choose_create_single_tap_password_title

            CredentialType.UNKNOWN ->
                R.string.choose_create_single_tap_sign_in_title
        },
        createRequestDisplayInfo.appName,
    )
    // TODO(b/327620327) : Add a subtitle and any other recently aligned ideas
    return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
        displayTitleText = displayTitleText, descriptionForCredential = descriptionText,
        biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo)
}

/**
Loading