summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/CredentialManager/res/values/strings.xml7
-rw-r--r--packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt30
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt21
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt35
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt102
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt22
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt67
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt100
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt2
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt1
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,