summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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(),