summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Arpan Kaphle <akaphle@google.com> 2024-03-06 05:56:08 +0000
committer Arpan <akaphle@google.com> 2024-03-20 23:41:06 +0000
commit58e33aa480bbcb3d753f9ef820f14552de0901ed (patch)
treed89c120a1cf51751cea6cc9dc7c82850f51e1466
parentca9930025adc972ae40edaaa8b76f3f66e53eea1 (diff)
Get Flow States, Conversion, and Biometric
Given the tight deadline, I've merged 4 previously already scoped out bugs into one CL. For the Get flow, here's what we have in this CL: 1. We handle the state management for the Get Flow. That involves editing the [GetModel] and using those values within the [GetCredentialComponents] as well as within the [ViewModel] and the [Repo]. The conditionals for when the flows happen were clarified. 2. We handle the UI for the Get Flow - this involves setting up a composable within the [GetCredentialComponents] that calls into the [BiometricHandler] to launch the Biometric API with default values. 3. We then specify the default values to be properly parsed within the [BiometricHandler]. This involves ensuring conditionals are correct, and pulling the right dynamic objects from the flow that contain the necessary information. 4. Finally, while this is still NOT callable from any flows in practice, the conversion from the input was clarified (but always set to 'false') within the [CredentialKtx]. This will be further iterated with slice based flows as we continue checking in code (or, the team may decide to suggest addition in this immediate bug). Bug: 327621520 Bug: 327620300 Bug: 327619284 Bug: 327620327 Test: Unit Tests will follow, and b/326243754 will contain manual tests Change-Id: Ib42c1f7fb95c7d8ef776416a1e9131b6e9d73d29
-rw-r--r--packages/CredentialManager/AndroidManifest.xml2
-rw-r--r--packages/CredentialManager/res/values/strings.xml8
-rw-r--r--packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt5
-rw-r--r--packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt1
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt11
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt322
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt72
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt92
-rw-r--r--packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt1
9 files changed, 485 insertions, 29 deletions
diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml
index a5ccdb6575bb..ab7f419216c0 100644
--- a/packages/CredentialManager/AndroidManifest.xml
+++ b/packages/CredentialManager/AndroidManifest.xml
@@ -22,7 +22,9 @@
<uses-permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/>
+ <uses-permission android:name="android.permission.SET_BIOMETRIC_DIALOG_ADVANCED"/>
<uses-permission android:name="android.permission.ACCESS_INSTANT_APPS" />
+ <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<application
android:allowBackup="true"
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 527701c06bc6..bc35a85e48f8 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -63,11 +63,11 @@
<!-- This appears as the description body of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] -->
<string name="choose_provider_body">Select a password manager to save your info and sign in faster next time</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 passkey. [CHAR LIMIT=200] -->
- <string name="choose_create_option_passkey_title">Create passkey to sign in to <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string>
+ <string name="choose_create_option_passkey_title">Create passkey 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 password. [CHAR LIMIT=200] -->
- <string name="choose_create_option_password_title">Save password to sign in to <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string>
+ <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="appName" example="Tribank">%1$s</xliff:g>?</string>
+ <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>
<!-- 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>
@@ -122,6 +122,8 @@
<string name="get_dialog_title_use_passkey_for">Use your saved passkey for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
<!-- This appears as the title of the modal bottom sheet asking for user confirmation to use the single previously saved password to sign in to the app. [CHAR LIMIT=200] -->
<string name="get_dialog_title_use_password_for">Use your saved password for <xliff:g id="app_name" example="YouTube">%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 get flow. [CHAR LIMIT=200] -->
+ <string name="get_dialog_title_single_tap_for">Use your screen lock to sign in to <xliff:g id="app_name" example="Shrine">%1$s</xliff:g> with <xliff:g id="username" example="beckett-bakery@gmail.com">%2$s</xliff:g></string>
<!-- This appears as the title of the dialog asking for user confirmation to use the single user credential (previously saved or to be created) to sign in to the app. [CHAR LIMIT=200] -->
<string name="get_dialog_title_use_sign_in_for">Use your sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
<!-- This appears as the title of the dialog asking for user confirmation to unlock / authenticate (e.g. via fingerprint, faceId, passcode etc.) so that we can retrieve their sign-in options. [CHAR LIMIT=200] -->
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 892eabf14191..e0ddb125bef7 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
@@ -139,6 +139,7 @@ private fun getCredentialOptionInfoList(
isDefaultIconPreferredAsSingleProvider =
credentialEntry.isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
+ isSupportingSingleTap = false, // TODO(b/326243754) : Fill in as product built
)
)
}
@@ -167,6 +168,7 @@ private fun getCredentialOptionInfoList(
isDefaultIconPreferredAsSingleProvider =
credentialEntry.isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
+ isSupportingSingleTap = false, // TODO(b/326243754) : Fill in as product built
)
)
}
@@ -194,6 +196,9 @@ private fun getCredentialOptionInfoList(
isDefaultIconPreferredAsSingleProvider =
credentialEntry.isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
+ isSupportingSingleTap = false, // TODO(b/326243754) : Fill in as product built
+ // TODO(b/326243754) : If required info above is missing, force condition to
+ // false.
)
)
}
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt
index a657e97de3cc..49c71f17839f 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt
@@ -49,6 +49,7 @@ class CredentialEntryInfo(
// "For <value-of-entryGroupId>" on the more-option screen.
val isDefaultIconPreferredAsSingleProvider: Boolean,
val affiliatedDomain: String?,
+ val isSupportingSingleTap: Boolean,
) : EntryInfo(
providerId,
entryKey,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 1f2fa200e43d..c05544e537d8 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -17,6 +17,7 @@
package com.android.credentialmanager
import android.app.Activity
+import android.hardware.biometrics.BiometricPrompt
import android.os.IBinder
import android.text.TextUtils
import android.util.Log
@@ -28,6 +29,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.common.BiometricState
import com.android.credentialmanager.model.EntryInfo
import com.android.credentialmanager.common.Constants
import com.android.credentialmanager.common.DialogState
@@ -54,6 +56,7 @@ data class UiState(
val isAutoSelectFlow: Boolean = false,
val cancelRequestState: CancelUiRequestState?,
val isInitialRender: Boolean,
+ val biometricState: BiometricState? = null
)
data class CancelUiRequestState(
@@ -200,13 +203,19 @@ class CredentialSelectorViewModel(
/**************************************************************************/
/***** Get Flow Callbacks *****/
/**************************************************************************/
- fun getFlowOnEntrySelected(entry: EntryInfo) {
+ fun getFlowOnEntrySelected(
+ entry: EntryInfo,
+ authResult: BiometricPrompt.AuthenticationResult? = null
+ ) {
Log.d(Constants.LOG_TAG, "credential selected: {provider=${entry.providerId}" +
", key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
uiState = if (entry.pendingIntent != null) {
uiState.copy(
selectedEntry = entry,
providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
+ biometricState = uiState.biometricState?.copy(
+ biometricAuthenticationResult = authResult
+ )
)
} else {
credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
new file mode 100644
index 000000000000..5bba688e7b8c
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
@@ -0,0 +1,322 @@
+/*
+ * 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
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.hardware.biometrics.BiometricPrompt
+import android.os.CancellationSignal
+import android.util.Log
+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.getflow.ProviderDisplayInfo
+import com.android.credentialmanager.getflow.RequestDisplayInfo
+import com.android.credentialmanager.getflow.generateDisplayTitleTextResCode
+import com.android.credentialmanager.model.EntryInfo
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.model.get.ProviderInfo
+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
+ * describes details of where the credential is being saved, and how.
+ */
+data class BiometricDisplayInfo(
+ var providerIcon: Bitmap,
+ var providerName: String,
+ var displayTitleText: String,
+ var descriptionAboveBiometricButton: String
+)
+
+/**
+ * Sets up generic state used by the create and get flows to hold the holistic states for the flow.
+ * These match all the present callback values from [BiometricPrompt], and may be extended to hold
+ * additional states that may improve the flow.
+ */
+data class BiometricState(
+ val biometricAuthenticationResult: BiometricPrompt.AuthenticationResult? = null,
+ val biometricError: BiometricError? = null,
+ val biometricHelp: BiometricHelp? = null,
+ val biometricAcquireInfo: Int? = null
+)
+
+/**
+ * Encapsulates the error callback results to easily manage biometric error states in the flow.
+ */
+data class BiometricError(
+ val errorCode: Int,
+ val errString: CharSequence? = null
+)
+
+/**
+ * Encapsulates the help callback results to easily manage biometric help states in the flow.
+ * To specify, this allows us to parse the onAuthenticationHelp method in the [BiometricPrompt].
+ */
+data class BiometricHelp(
+ val helpCode: Int,
+ var helpString: CharSequence? = null
+)
+
+/**
+ * This will handle the logic for integrating credential manager with the biometric prompt for the
+ * single account biometric experience. This simultaneously handles both the get and create flows,
+ * by retrieving all the data from credential manager, and properly parsing that data into the
+ * biometric prompt.
+ */
+fun runBiometricFlow(
+ selectedEntry: EntryInfo?,
+ context: Context,
+ openMoreOptionsPage: () -> Unit,
+ sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit,
+ onCancelFlowAndFinish: (String) -> Unit,
+ getRequestDisplayInfo: RequestDisplayInfo? = null,
+ getProviderInfoList: List<ProviderInfo>? = null,
+ getProviderDisplayInfo: ProviderDisplayInfo? = null,
+ onBiometricFailureFallback: () -> Unit,
+ createRequestDisplayInfo: com.android.credentialmanager.createflow
+ .RequestDisplayInfo? = null,
+ createProviderInfo: EnabledProviderInfo? = null,
+) {
+ if (selectedEntry == null) {
+ onBiometricFailureFallback()
+ return
+ }
+ var biometricDisplayInfo: BiometricDisplayInfo? = null
+ if (getRequestDisplayInfo != null) {
+ biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(getRequestDisplayInfo,
+ getProviderInfoList,
+ getProviderDisplayInfo,
+ context, selectedEntry)
+ } else if (createRequestDisplayInfo != null) {
+ // TODO(b/326243754) : Create Flow to be implemented in follow up
+ biometricDisplayInfo = validateBiometricCreateFlow(
+ createRequestDisplayInfo,
+ createProviderInfo
+ )
+ }
+
+ if (biometricDisplayInfo == null) {
+ onBiometricFailureFallback()
+ return
+ }
+
+ val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage)
+
+ val callback: BiometricPrompt.AuthenticationCallback =
+ setupBiometricAuthenticationCallback(sendDataToProvider, selectedEntry,
+ onCancelFlowAndFinish)
+
+ val cancellationSignal = CancellationSignal()
+ cancellationSignal.setOnCancelListener {
+ Log.d(TAG, "Your cancellation signal was called.")
+ // TODO(b/326243754) : Migrate towards passing along the developer cancellation signal
+ // or validate the necessity for this
+ }
+
+ val executor = getMainExecutor(context)
+
+ try {
+ biometricPrompt.authenticate(cancellationSignal, executor, callback)
+ } catch (e: IllegalArgumentException) {
+ Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n")
+ onBiometricFailureFallback()
+ }
+}
+
+/**
+ * Sets up the biometric prompt with the UI specific bits.
+ */
+private fun setupBiometricPrompt(
+ context: Context,
+ biometricDisplayInfo: BiometricDisplayInfo,
+ openMoreOptionsPage: () -> Unit
+): 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),
+ getMainExecutor(context)) { _, _ ->
+ openMoreOptionsPage()
+ }
+ .setConfirmationRequired(true)
+ .setLogoBitmap(biometricDisplayInfo.providerIcon)
+ .setLogoDescription(biometricDisplayInfo
+ .providerName)
+ .setDescription(biometricDisplayInfo.descriptionAboveBiometricButton)
+ .build()
+
+/**
+ * Sets up the biometric authentication callback.
+ */
+private fun setupBiometricAuthenticationCallback(
+ sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit,
+ selectedEntry: EntryInfo,
+ onCancelFlowAndFinish: (String) -> Unit
+): BiometricPrompt.AuthenticationCallback {
+ val callback: BiometricPrompt.AuthenticationCallback =
+ object : BiometricPrompt.AuthenticationCallback() {
+ // TODO(b/326243754) : Validate remaining callbacks
+ override fun onAuthenticationSucceeded(
+ authResult: BiometricPrompt.AuthenticationResult?
+ ) {
+ super.onAuthenticationSucceeded(authResult)
+ try {
+ if (authResult != null) {
+ sendDataToProvider(selectedEntry, authResult)
+ } else {
+ onCancelFlowAndFinish("The biometric flow succeeded but unexpectedly " +
+ "returned a null value.")
+ // TODO(b/326243754) : Propagate to provider
+ }
+ } catch (e: Exception) {
+ onCancelFlowAndFinish("The biometric flow succeeded but failed on handling " +
+ "the result. See: \n$e\n")
+ // TODO(b/326243754) : Propagate to provider
+ }
+ }
+
+ override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) {
+ super.onAuthenticationHelp(helpCode, helpString)
+ Log.d(TAG, "Authentication help discovered: $helpCode and $helpString")
+ // TODO(b/326243754) : Decide on strategy with provider (a simple log probably
+ // suffices here)
+ }
+
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
+ super.onAuthenticationError(errorCode, errString)
+ Log.d(TAG, "Authentication error-ed out: $errorCode and $errString")
+ // TODO(b/326243754) : Propagate to provider
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ Log.d(TAG, "Authentication failed.")
+ // TODO(b/326243754) : Propagate to provider
+ }
+ }
+ return callback
+}
+
+/**
+ * Creates the [BiometricDisplayInfo] for get flows, and early handles conditional
+ * checking between the two.
+ */
+private fun validateAndRetrieveBiometricGetDisplayInfo(
+ getRequestDisplayInfo: RequestDisplayInfo?,
+ getProviderInfoList: List<ProviderInfo>?,
+ getProviderDisplayInfo: ProviderDisplayInfo?,
+ context: Context,
+ selectedEntry: EntryInfo
+): BiometricDisplayInfo? {
+ if (getRequestDisplayInfo != null && getProviderInfoList != null &&
+ getProviderDisplayInfo != null) {
+ if (selectedEntry !is CredentialEntryInfo) { return null }
+ return getBiometricDisplayValues(getProviderInfoList,
+ context,
+ getRequestDisplayInfo, selectedEntry, getProviderDisplayInfo)
+ }
+ return null
+}
+
+/**
+ * Creates the [BiometricDisplayInfo] for create flows, and early handles conditional
+ * checking between the two.
+ */
+private fun validateBiometricCreateFlow(
+ createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo?,
+ createProviderInfo: EnabledProviderInfo?,
+): BiometricDisplayInfo? {
+ if (createRequestDisplayInfo != null && createProviderInfo != null) {
+ // TODO(b/326243754) : Create Flow to be implemented in follow up
+ return createFlowDisplayValues()
+ }
+ return null
+}
+
+/**
+ * Handles the biometric sign in via the 'get credentials' flow.
+ * If any expected value is not present, the flow is considered unreachable and we will fallback
+ * to the original selector.
+ */
+private fun getBiometricDisplayValues(
+ getProviderInfoList: List<ProviderInfo>,
+ context: Context,
+ getRequestDisplayInfo: RequestDisplayInfo,
+ selectedEntry: CredentialEntryInfo,
+ getProviderDisplayInfo: ProviderDisplayInfo,
+): BiometricDisplayInfo? {
+ var icon: Bitmap? = null
+ var providerName: String? = null
+ var displayTitleText: String? = null
+ var descriptionText: String? = null
+ val primaryAccountsProviderInfo = retrievePrimaryAccountProviderInfo(selectedEntry.providerId,
+ getProviderInfoList)
+ icon = primaryAccountsProviderInfo?.icon?.toBitmap()
+ providerName = primaryAccountsProviderInfo?.displayName
+ if (icon == null || providerName == null) {
+ Log.d(TAG, "Unexpectedly found invalid provider information.")
+ return null
+ }
+ val singleEntryType = selectedEntry.credentialType
+ val username = selectedEntry.userName
+ displayTitleText = context.getString(
+ generateDisplayTitleTextResCode(singleEntryType),
+ getRequestDisplayInfo.appName
+ )
+ descriptionText = context.getString(
+ R.string.get_dialog_title_single_tap_for,
+ getRequestDisplayInfo.appName,
+ username
+ )
+ return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
+ displayTitleText = displayTitleText, descriptionAboveBiometricButton = descriptionText)
+}
+
+/**
+ * Handles the biometric sign in via the 'create credentials' flow, or early validates this flow
+ * needs to fallback.
+ */
+private fun createFlowDisplayValues(): BiometricDisplayInfo? {
+ // TODO(b/326243754) : Create Flow to be implemented in follow up
+ return null
+}
+
+/**
+ * During a get flow with single tap sign in enabled, this will match the credentialEntry that
+ * will single tap with the correct provider info. Namely, it's the first provider info that
+ * contains a matching providerId to the selected entry.
+ */
+private fun retrievePrimaryAccountProviderInfo(
+ providerId: String,
+ getProviderInfoList: List<ProviderInfo>
+): ProviderInfo? {
+ var discoveredProviderInfo: ProviderInfo? = null
+ getProviderInfoList.forEach { provider ->
+ if (provider.id == providerId) {
+ discoveredProviderInfo = provider
+ return@forEach
+ }
+ }
+ return discoveredProviderInfo
+}
+
+const val TAG = "BiometricHandler"
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index b9c9d8994c45..efde4c3d0eb2 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -16,8 +16,11 @@
package com.android.credentialmanager.getflow
+import android.content.Context
+import android.credentials.flags.Flags.credmanBiometricApiEnabled
import android.credentials.flags.Flags.selectorUiImprovementsEnabled
import android.graphics.drawable.Drawable
+import android.hardware.biometrics.BiometricPrompt
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
@@ -41,6 +44,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextLayoutResult
@@ -49,30 +53,31 @@ import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.CredentialSelectorViewModel
import com.android.credentialmanager.R
-import com.android.credentialmanager.model.EntryInfo
-import com.android.credentialmanager.model.CredentialType
-import com.android.credentialmanager.model.get.ProviderInfo
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.ActionEntry
import com.android.credentialmanager.common.ui.ConfirmButton
import com.android.credentialmanager.common.ui.CredentialContainerCard
+import com.android.credentialmanager.common.ui.CredentialListSectionHeader
import com.android.credentialmanager.common.ui.CtaButtonRow
import com.android.credentialmanager.common.ui.Entry
+import com.android.credentialmanager.common.ui.HeadlineIcon
+import com.android.credentialmanager.common.ui.HeadlineText
+import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant
import com.android.credentialmanager.common.ui.ModalBottomSheet
import com.android.credentialmanager.common.ui.MoreOptionTopAppBar
import com.android.credentialmanager.common.ui.SheetContainerCard
-import com.android.credentialmanager.common.ui.SnackbarActionText
-import com.android.credentialmanager.common.ui.HeadlineText
-import com.android.credentialmanager.common.ui.CredentialListSectionHeader
-import com.android.credentialmanager.common.ui.HeadlineIcon
-import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant
import com.android.credentialmanager.common.ui.Snackbar
+import com.android.credentialmanager.common.ui.SnackbarActionText
import com.android.credentialmanager.logging.GetCredentialEvent
+import com.android.credentialmanager.model.CredentialType
+import com.android.credentialmanager.model.EntryInfo
import com.android.credentialmanager.model.get.ActionEntryInfo
import com.android.credentialmanager.model.get.AuthenticationEntryInfo
import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.model.get.ProviderInfo
import com.android.credentialmanager.model.get.RemoteEntryInfo
import com.android.credentialmanager.userAndDisplayNameForPasskey
import com.android.internal.logging.UiEventLogger.UiEventEnum
@@ -82,7 +87,7 @@ import kotlin.math.max
fun GetCredentialScreen(
viewModel: CredentialSelectorViewModel,
getCredentialUiState: GetCredentialUiState,
- providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
+ providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>,
) {
if (getCredentialUiState.currentScreenState == GetScreenState.REMOTE_ONLY) {
RemoteCredentialSnackBarScreen(
@@ -137,6 +142,22 @@ fun GetCredentialScreen(
}
viewModel.uiMetrics.log(GetCredentialEvent
.CREDMAN_GET_CRED_SCREEN_PRIMARY_SELECTION)
+ } 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
+ selectedEntry = getCredentialUiState.activeEntry,
+ onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected,
+ onCancelFlowAndFinish = viewModel::onIllegalUiState,
+ requestDisplayInfo = getCredentialUiState.requestDisplayInfo,
+ providerInfoList = getCredentialUiState.providerInfoList,
+ providerDisplayInfo = getCredentialUiState.providerDisplayInfo,
+ onBiometricEntrySelected =
+ viewModel::getFlowOnEntrySelected,
+ fallbackToOriginalFlow =
+ viewModel::getFlowOnBackToPrimarySelectionScreen,
+ )
} else {
AllSignInOptionCard(
providerInfoList = getCredentialUiState.providerInfoList,
@@ -189,6 +210,30 @@ fun GetCredentialScreen(
}
}
+@Composable
+internal fun BiometricSelectionPage(
+ selectedEntry: EntryInfo?,
+ onCancelFlowAndFinish: (String) -> Unit,
+ onMoreOptionSelected: () -> Unit,
+ requestDisplayInfo: RequestDisplayInfo,
+ providerInfoList: List<ProviderInfo>,
+ providerDisplayInfo: ProviderDisplayInfo,
+ onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult?) -> Unit,
+ fallbackToOriginalFlow: () -> Unit,
+) {
+ runBiometricFlow(
+ selectedEntry = selectedEntry,
+ context = LocalContext.current,
+ openMoreOptionsPage = onMoreOptionSelected,
+ sendDataToProvider = onBiometricEntrySelected,
+ onCancelFlowAndFinish = onCancelFlowAndFinish,
+ getRequestDisplayInfo = requestDisplayInfo,
+ getProviderInfoList = providerInfoList,
+ getProviderDisplayInfo = providerDisplayInfo,
+ onBiometricFailureFallback = fallbackToOriginalFlow
+ )
+}
+
/** Draws the primary credential selection page, used in Android U. */
// TODO(b/327518384) - remove after flag selectorUiImprovementsEnabled is enabled.
@Composable
@@ -256,13 +301,8 @@ fun PrimarySelectionCard(
if (hasSingleEntry) {
val singleEntryType = sortedUserNameToCredentialEntryList.firstOrNull()
?.sortedCredentialEntryList?.firstOrNull()?.credentialType
- if (singleEntryType == CredentialType.PASSKEY)
- R.string.get_dialog_title_use_passkey_for
- else if (singleEntryType == CredentialType.PASSWORD)
- R.string.get_dialog_title_use_password_for
- else if (authenticationEntryList.isNotEmpty())
- R.string.get_dialog_title_unlock_options_for
- else R.string.get_dialog_title_use_sign_in_for
+ generateDisplayTitleTextResCode(singleEntryType!!,
+ authenticationEntryList)
} else {
if (authenticationEntryList.isNotEmpty() ||
sortedUserNameToCredentialEntryList.any { perNameEntryList ->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index e35acae547a6..3e7004a63317 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -17,8 +17,11 @@
package com.android.credentialmanager.getflow
import android.credentials.flags.Flags.selectorUiImprovementsEnabled
+import android.credentials.flags.Flags.credmanBiometricApiEnabled
import android.graphics.drawable.Drawable
import androidx.credentials.PriorityHints
+import com.android.credentialmanager.R
+import com.android.credentialmanager.model.CredentialType
import com.android.credentialmanager.model.get.ProviderInfo
import com.android.credentialmanager.model.EntryInfo
import com.android.credentialmanager.model.get.AuthenticationEntryInfo
@@ -39,6 +42,56 @@ data class GetCredentialUiState(
val isNoAccount: Boolean = false,
)
+/**
+ * Checks if this get flow is a biometric selection flow by ensuring that the first account has a
+ * single credential entry to display. The presently agreed upon condition matches the auto
+ * select criteria, but with one additional requirement that the entry contains an extra parameter
+ * that indicates the biometric flow. The auto select flow is supported over the biometric flow
+ * at the moment in cases where they collide.
+ */
+internal fun isBiometricFlow(
+ providerDisplayInfo: ProviderDisplayInfo,
+ isAutoSelectFlow: Boolean
+): Boolean {
+ if (!credmanBiometricApiEnabled()) {
+ return false
+ }
+ if (isAutoSelectFlow) {
+ // For this to be true, it must be the case that there is a single entry and a single
+ // account. If that is the case, and auto-select is enabled along side the one-tap flow, we
+ // always favor that over the one tap flow.
+ return false
+ }
+ // The flow through an authentication entry, even if only a singular entry exists, is deemed
+ // as not being eligible for the single tap flow given that it adds any number of credentials
+ // once unlocked; essentially, this entry contains additional complexities behind it, making it
+ // invalid.
+ if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) {
+ return false
+ }
+ val singleAccountEntryList = getCredentialEntryListIffSingleAccount(
+ providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return false
+
+ // TODO(b/326243754) : Set this value from dynamic slice until structured jetpack object is used
+ return singleAccountEntryList.firstOrNull()?.isSupportingSingleTap ?: false
+}
+
+/**
+ * A utility method that will procure the credential entry list if and only if the credential entry
+ * list is for a singular account use case. This can be used for various flows that condition on
+ * a singular account.
+ */
+internal fun getCredentialEntryListIffSingleAccount(
+ sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>
+): List<CredentialEntryInfo>? {
+ if (sortedUserNameToCredentialEntryList.size != 1) {
+ return null
+ }
+ val entryList = sortedUserNameToCredentialEntryList.firstOrNull() ?: return null
+ val sortedEntryList = entryList.sortedCredentialEntryList
+ return sortedEntryList
+}
+
internal fun hasContentToDisplay(state: GetCredentialUiState): Boolean {
return state.providerDisplayInfo.sortedUserNameToCredentialEntryList.isNotEmpty() ||
state.providerDisplayInfo.authenticationEntryList.isNotEmpty() ||
@@ -50,15 +103,14 @@ internal fun findAutoSelectEntry(providerDisplayInfo: ProviderDisplayInfo): Cred
if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) {
return null
}
- if (providerDisplayInfo.sortedUserNameToCredentialEntryList.size == 1) {
- val entryList = providerDisplayInfo.sortedUserNameToCredentialEntryList.firstOrNull()
- ?: return null
- if (entryList.sortedCredentialEntryList.size == 1) {
- val entry = entryList.sortedCredentialEntryList.firstOrNull() ?: return null
- if (entry.isAutoSelectable) {
- return entry
- }
- }
+ val entryList = getCredentialEntryListIffSingleAccount(
+ providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null
+ if (entryList.size != 1) {
+ return null
+ }
+ val entry = entryList.firstOrNull() ?: return null
+ if (entry.isAutoSelectable) {
+ return entry
}
return null
}
@@ -105,6 +157,9 @@ enum class GetScreenState {
/** The primary credential selection page. */
PRIMARY_SELECTION,
+ /** The single tap biometric selection page. */
+ BIOMETRIC_SELECTION,
+
/** The secondary credential selection page, where all sign-in options are listed. */
ALL_SIGN_IN_OPTIONS,
@@ -177,6 +232,22 @@ 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*".
+ */
+internal fun generateDisplayTitleTextResCode(
+ singleEntryType: CredentialType,
+ authenticationEntryList: List<AuthenticationEntryInfo> = emptyList()
+): Int =
+ if (singleEntryType == CredentialType.PASSKEY)
+ R.string.get_dialog_title_use_passkey_for
+ else if (singleEntryType == CredentialType.PASSWORD)
+ R.string.get_dialog_title_use_password_for
+ else if (authenticationEntryList.isNotEmpty())
+ R.string.get_dialog_title_unlock_options_for
+ else R.string.get_dialog_title_use_sign_in_for
+
fun toActiveEntry(
providerDisplayInfo: ProviderDisplayInfo,
): EntryInfo? {
@@ -211,6 +282,9 @@ private fun toGetScreenState(
GetScreenState.REMOTE_ONLY
else if (isRequestForAllOptions)
GetScreenState.ALL_SIGN_IN_OPTIONS
+ else if (isBiometricFlow(providerDisplayInfo,
+ findAutoSelectEntry(providerDisplayInfo) != null))
+ GetScreenState.BIOMETRIC_SELECTION
else GetScreenState.PRIMARY_SELECTION
}
diff --git a/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt b/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt
index b8432137b35e..01fb1cc1fd67 100644
--- a/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt
+++ b/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt
@@ -152,6 +152,7 @@ class GetCredScreenshotTest(emulationSpec: DeviceEmulationSpec) {
isDefaultIconPreferredAsSingleProvider = false,
rawCredentialType = "unknown-type",
affiliatedDomain = null,
+ isSupportingSingleTap = false,
)
),
authenticationEntryList = emptyList(),