diff options
10 files changed, 307 insertions, 80 deletions
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index bc35a85e48f8..9fd386f38684 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -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> diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt index f2c252ec6422..b408c1553d94 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt @@ -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) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 28c40479962e..a03975375e8a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -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 diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index fd6fc6a44c7c..358ebfa1ec90 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -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), ) ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt index a30956ecf5a5..fa177351be30 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt @@ -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) } /** diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt new file mode 100644 index 000000000000..f6140f51b7b5 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 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.credentialmanager.common + +enum class FlowType { + GET, + CREATE +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index af78573ee9e9..25fb477cbf38 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager.createflow import android.credentials.flags.Flags.selectorUiImprovementsEnabled +import android.hardware.biometrics.BiometricPrompt import android.text.TextUtils import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.result.ActivityResult @@ -26,7 +27,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Divider import androidx.compose.material.icons.Icons @@ -38,6 +38,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -49,6 +50,7 @@ import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.common.material.ModalBottomSheetDefaults +import com.android.credentialmanager.common.runBiometricFlow import com.android.credentialmanager.common.ui.ActionButton import com.android.credentialmanager.common.ui.BodyMediumText import com.android.credentialmanager.common.ui.BodySmallText @@ -95,6 +97,22 @@ fun CreateCredentialScreen( viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection, onLog = { viewModel.logUiEvent(it) }, ) + CreateScreenState.BIOMETRIC_SELECTION -> + BiometricSelectionPage( + biometricEntry = createCredentialUiState + .activeEntry?.activeEntryInfo, + onCancelFlowAndFinish = viewModel::onUserCancel, + onIllegalScreenStateAndFinish = viewModel::onIllegalUiState, + onMoreOptionSelected = + viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + enabledProviderInfo = createCredentialUiState + .activeEntry?.activeProvider!!, + onBiometricEntrySelected = + viewModel::createFlowOnEntrySelected, + fallbackToOriginalFlow = + viewModel::getFlowOnBackToPrimarySelectionScreen, + ) CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( requestDisplayInfo = createCredentialUiState.requestDisplayInfo, enabledProviderList = createCredentialUiState.enabledProviders, @@ -313,20 +331,9 @@ fun CreationSelectionCard( item { Divider(thickness = 16.dp, color = Color.Transparent) } item { HeadlineText( - text = when (requestDisplayInfo.type) { - CredentialType.PASSKEY -> stringResource( - R.string.choose_create_option_passkey_title, - requestDisplayInfo.appName - ) - CredentialType.PASSWORD -> stringResource( - R.string.choose_create_option_password_title, - requestDisplayInfo.appName - ) - CredentialType.UNKNOWN -> stringResource( - R.string.choose_create_option_sign_in_title, - requestDisplayInfo.appName - ) - } + text = stringResource( + getCreateTitleResCode(requestDisplayInfo), + requestDisplayInfo.appName) ) } item { Divider(thickness = 24.dp, color = Color.Transparent) } @@ -560,4 +567,32 @@ fun RemoteEntryRow( iconImageVector = Icons.Outlined.QrCodeScanner, entryHeadlineText = stringResource(R.string.another_device), ) -}
\ No newline at end of file +} + +@Composable +internal fun BiometricSelectionPage( + biometricEntry: EntryInfo?, + onMoreOptionSelected: () -> Unit, + requestDisplayInfo: RequestDisplayInfo, + enabledProviderInfo: EnabledProviderInfo, + onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, + onCancelFlowAndFinish: () -> Unit, + onIllegalScreenStateAndFinish: (String) -> Unit, + fallbackToOriginalFlow: () -> Unit, +) { + if (biometricEntry == null) { + fallbackToOriginalFlow() + return + } + runBiometricFlow( + biometricEntry = biometricEntry, + context = LocalContext.current, + openMoreOptionsPage = onMoreOptionSelected, + sendDataToProvider = onBiometricEntrySelected, + onCancelFlowAndFinish = onCancelFlowAndFinish, + createRequestDisplayInfo = requestDisplayInfo, + createProviderInfo = enabledProviderInfo, + onBiometricFailureFallback = fallbackToOriginalFlow, + onIllegalStateAndFinish = onIllegalScreenStateAndFinish, + ) +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 617a981fc4ba..1d262ba5261a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -16,9 +16,11 @@ package com.android.credentialmanager.createflow +import android.credentials.flags.Flags.credmanBiometricApiEnabled import android.graphics.drawable.Drawable -import com.android.credentialmanager.model.EntryInfo +import com.android.credentialmanager.R import com.android.credentialmanager.model.CredentialType +import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.creation.CreateOptionInfo import com.android.credentialmanager.model.creation.RemoteInfo @@ -33,14 +35,99 @@ data class CreateCredentialUiState( val foundCandidateFromUserDefaultProvider: Boolean, ) +/** + * Checks if this create flow is a biometric flow. Note that this flow differs slightly from the + * autoselect 'get' flow. Namely, given there can be multiple providers, rather than multiple + * accounts, the idea is that autoselect is ever only enabled for a single provider (or even, in + * that case, a single 'type' (family only, or work only) for a provider). However, for all other + * cases, the biometric screen should always show up if that entry contains the biometric bit. + */ +internal fun findBiometricFlowEntry( + activeEntry: ActiveEntry, + isAutoSelectFlow: Boolean, +): CreateOptionInfo? { + if (!credmanBiometricApiEnabled()) { + return null + } + if (isAutoSelectFlow) { + // Since this is the create flow, auto select will only ever be true for a single provider. + // However, for all other cases, biometric should be used if that bit is opted into. If + // they clash, autoselect is always preferred, but that's only if there's a single provider. + return null + } + val biometricEntry = getCreateEntry(activeEntry) + return if (biometricEntry?.biometricRequest != null) biometricEntry else null +} + +/** + * Retrieves the activeEntry by validating it is a [CreateOptionInfo]. This is done by ensuring + * that the [activeEntry] exists as a [CreateOptionInfo] to retrieve its [EntryInfo]. + */ +internal fun getCreateEntry( + activeEntry: ActiveEntry?, +): CreateOptionInfo? { + val entry = activeEntry?.activeEntryInfo + if (entry !is CreateOptionInfo) { + return null + } + return entry +} + +/** +* Determines if the flow is a biometric flow by taking into account autoselect criteria. +*/ +internal fun isBiometricFlow( + activeEntry: ActiveEntry, + sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, + requestDisplayInfo: RequestDisplayInfo, +) = findBiometricFlowEntry(activeEntry, isFlowAutoSelectable( + requestDisplayInfo = requestDisplayInfo, + activeEntry = activeEntry, + sortedCreateOptionsPairs = sortedCreateOptionsPairs +)) != null + +/** + * This utility presents the correct resource string for the create flows title conditionally. + * Similar to generateDisplayTitleTextResCode in the 'get' flow, but for the create flow instead. + * This is for the title, and is a shared resource, unlike the specific unlock request text. + * E.g. this will look something like: "Create passkey to sign in to Tribank." + * // TODO(b/330396140) : Validate approach and add dynamic auth strings + */ +internal fun getCreateTitleResCode(createRequestDisplayInfo: RequestDisplayInfo): Int = + when (createRequestDisplayInfo.type) { + CredentialType.PASSKEY -> + R.string.choose_create_option_passkey_title + + CredentialType.PASSWORD -> + R.string.choose_create_option_password_title + + CredentialType.UNKNOWN -> + R.string.choose_create_option_sign_in_title + } + internal fun isFlowAutoSelectable( uiState: CreateCredentialUiState ): Boolean { - return uiState.requestDisplayInfo.isAutoSelectRequest && - uiState.sortedCreateOptionsPairs.size == 1 && - uiState.activeEntry?.activeEntryInfo?.let { - it is CreateOptionInfo && it.allowAutoSelect - } ?: false + return isFlowAutoSelectable(uiState.requestDisplayInfo, uiState.activeEntry, + uiState.sortedCreateOptionsPairs) +} + +/** + * When initializing, the [CreateCredentialUiState] is generated after the initial screen is set. + * This overloaded method allows identifying if the flow is auto selectable prior to the creation + * of the [CreateCredentialUiState]. + */ +internal fun isFlowAutoSelectable( + requestDisplayInfo: RequestDisplayInfo, + activeEntry: ActiveEntry?, + sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>> +): Boolean { + val isAutoSelectRequest = requestDisplayInfo.isAutoSelectRequest + if (sortedCreateOptionsPairs.size != 1) { + return false + } + val singleEntry = getCreateEntry(activeEntry) + return isAutoSelectRequest && singleEntry?.allowAutoSelect == true } internal fun hasContentToDisplay(state: CreateCredentialUiState): Boolean { @@ -95,6 +182,7 @@ data class ActiveEntry ( /** The name of the current screen. */ enum class CreateScreenState { CREATION_OPTION_SELECTION, + BIOMETRIC_SELECTION, MORE_OPTIONS_SELECTION, DEFAULT_PROVIDER_CONFIRMATION, EXTERNAL_ONLY_SELECTION, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 4d7272c7716e..6d1a3dd98210 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -144,8 +144,6 @@ fun GetCredentialScreen( } else if (credmanBiometricApiEnabled() && getCredentialUiState .currentScreenState == GetScreenState.BIOMETRIC_SELECTION) { BiometricSelectionPage( - // TODO(b/326243754) : Utilize expected entry for this flow, confirm - // activeEntry will always be what represents the single tap flow biometricEntry = getCredentialUiState.activeEntry, onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected, onCancelFlowAndFinish = viewModel::onUserCancel, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index 6d5b52a7a5f9..ac776af4f627 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -238,6 +238,7 @@ fun toProviderDisplayInfo( /** * This generates the res code for the large display title text for the selector. For example, it * retrieves the resource for strings like: "Use your saved passkey for *rpName*". + * TODO(b/330396140) : Validate approach and add dynamic auth strings */ internal fun generateDisplayTitleTextResCode( singleEntryType: CredentialType, |