summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Arpan Kaphle <akaphle@google.com> 2024-03-21 19:14:55 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-03-21 19:14:55 +0000
commitd458675f649ab745b53c27b60d08864cdb286052 (patch)
treea3b37c91666ffe996ac2fd95d1c77ae70cfb0c7d
parentb231a189b970b9c180b02f956a867a8a7dda26cc (diff)
parent19033e9530e89e7c2ac32172e603718b194cd694 (diff)
Merge changes from topic "getFlow_bio" into main
* changes: Communicate Get Flow Biometric Success to Provider Get Flow States, Conversion, and Biometric
-rw-r--r--packages/CredentialManager/AndroidManifest.xml1
-rw-r--r--packages/CredentialManager/res/values/strings.xml8
-rw-r--r--packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt35
-rw-r--r--packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt28
-rw-r--r--packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt2
-rw-r--r--packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt2
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt10
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt27
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt2
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt374
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt2
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt75
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt101
13 files changed, 636 insertions, 31 deletions
diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml
index a5ccdb6575bb..7a8c25bd12ab 100644
--- a/packages/CredentialManager/AndroidManifest.xml
+++ b/packages/CredentialManager/AndroidManifest.xml
@@ -23,6 +23,7 @@
<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.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 12cb7ffddd5d..f2c252ec6422 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
@@ -48,6 +48,7 @@ import com.android.credentialmanager.model.get.ProviderInfo
import com.android.credentialmanager.model.get.RemoteEntryInfo
import com.android.credentialmanager.shared.R
import com.android.credentialmanager.TAG
+import com.android.credentialmanager.model.BiometricRequestInfo
import com.android.credentialmanager.model.EntryInfo
fun EntryInfo.getIntentSenderRequest(
@@ -139,6 +140,7 @@ private fun getCredentialOptionInfoList(
isDefaultIconPreferredAsSingleProvider =
credentialEntry.isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
+ biometricRequest = predetermineAndValidateBiometricFlow(it),
)
)
}
@@ -167,6 +169,7 @@ private fun getCredentialOptionInfoList(
isDefaultIconPreferredAsSingleProvider =
credentialEntry.isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
+ biometricRequest = predetermineAndValidateBiometricFlow(it),
)
)
}
@@ -194,6 +197,7 @@ private fun getCredentialOptionInfoList(
isDefaultIconPreferredAsSingleProvider =
credentialEntry.isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
+ biometricRequest = predetermineAndValidateBiometricFlow(it),
)
)
}
@@ -205,6 +209,36 @@ private fun getCredentialOptionInfoList(
}
return result
}
+
+/**
+ * This validates if this is a biometric flow or not, and if it is, this returns the expected
+ * [BiometricRequestInfo]. Namely, the biometric flow must have at least the
+ * ALLOWED_AUTHENTICATORS bit passed from Jetpack.
+ * 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.
+ * // 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
+): 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")
+ }?.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")
+ }?.int
+ if (allowedAuthenticators != null) {
+ return BiometricRequestInfo(opId = opId, allowedAuthenticators = allowedAuthenticators)
+ }
+ return null
+}
+
val Slice.credentialEntry: CredentialEntry?
get() =
try {
@@ -221,7 +255,6 @@ val Slice.credentialEntry: CredentialEntry?
CustomCredentialEntry.fromSlice(this)
}
-
/**
* Note: caller required handle empty list due to parsing error.
*/
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt
new file mode 100644
index 000000000000..486cfe7123dd
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.model
+
+/**
+ * This allows reading the data from the request, and holding that state around the framework.
+ * The [opId] bit is required for some authentication flows where CryptoObjects are used.
+ * The [allowedAuthenticators] is needed for all flows, and our flow ensures this value is never
+ * null.
+ */
+data class BiometricRequestInfo(
+ val opId: Int? = null,
+ val allowedAuthenticators: Int
+) \ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt
index d6189eb15ff3..fe02e5ba026d 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt
@@ -19,6 +19,7 @@ package com.android.credentialmanager.model.creation
import android.app.PendingIntent
import android.content.Intent
import android.graphics.drawable.Drawable
+import com.android.credentialmanager.model.BiometricRequestInfo
import com.android.credentialmanager.model.EntryInfo
import java.time.Instant
@@ -36,6 +37,7 @@ class CreateOptionInfo(
val lastUsedTime: Instant,
val footerDescription: String?,
val allowAutoSelect: Boolean,
+ val biometricRequest: BiometricRequestInfo? = null,
) : EntryInfo(
providerId,
entryKey,
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..8913397db072 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
@@ -19,6 +19,7 @@ package com.android.credentialmanager.model.get
import android.app.PendingIntent
import android.content.Intent
import android.graphics.drawable.Drawable
+import com.android.credentialmanager.model.BiometricRequestInfo
import com.android.credentialmanager.model.CredentialType
import com.android.credentialmanager.model.EntryInfo
import java.time.Instant
@@ -49,6 +50,7 @@ class CredentialEntryInfo(
// "For <value-of-entryGroupId>" on the more-option screen.
val isDefaultIconPreferredAsSingleProvider: Boolean,
val affiliatedDomain: String?,
+ val biometricRequest: BiometricRequestInfo? = null,
) : EntryInfo(
providerId,
entryKey,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 879d64c761ec..b17a98b30eee 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -40,6 +40,7 @@ import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.findAutoSelectEntry
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.createflow.isFlowAutoSelectable
+import com.android.credentialmanager.getflow.findBiometricFlowEntry
/**
* Client for interacting with Credential Manager. Also holds data inputs from it.
@@ -148,10 +149,17 @@ class CredentialManagerRepo(
)
}
RequestInfo.TYPE_GET -> {
- val getCredentialInitialUiState = getCredentialInitialUiState(originName,
+ var getCredentialInitialUiState = getCredentialInitialUiState(originName,
isReqForAllOptions)!!
val autoSelectEntry =
findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo)
+ val biometricEntry = findBiometricFlowEntry(
+ getCredentialInitialUiState.providerDisplayInfo,
+ autoSelectEntry != null)
+ if (biometricEntry != null) {
+ getCredentialInitialUiState = getCredentialInitialUiState.copy(
+ activeEntry = biometricEntry)
+ }
UiState(
createCredentialUiState = null,
getCredentialUiState = getCredentialInitialUiState,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 1f2fa200e43d..28c40479962e 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,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.common.BiometricResult
+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 +57,7 @@ data class UiState(
val isAutoSelectFlow: Boolean = false,
val cancelRequestState: CancelUiRequestState?,
val isInitialRender: Boolean,
+ val biometricState: BiometricState = BiometricState()
)
data class CancelUiRequestState(
@@ -113,12 +117,21 @@ class CredentialSelectorViewModel(
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val entry = uiState.selectedEntry
+ val biometricState = uiState.biometricState
val pendingIntent = entry?.pendingIntent
if (pendingIntent != null) {
Log.d(Constants.LOG_TAG, "Launching provider activity")
uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING)
val entryIntent = entry.fillInIntent
entryIntent?.putExtra(Constants.IS_AUTO_SELECTED_KEY, uiState.isAutoSelectFlow)
+ if (biometricState.biometricResult != null) {
+ if (uiState.isAutoSelectFlow) {
+ Log.w(Constants.LOG_TAG, "Unexpected biometric result exists when " +
+ "autoSelect is preferred.")
+ }
+ entryIntent?.putExtra(Constants.BIOMETRIC_AUTH_TYPE,
+ biometricState.biometricResult.biometricAuthenticationResult.authenticationType)
+ }
val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent)
.setFillInIntent(entryIntent).build()
try {
@@ -200,13 +213,20 @@ 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 = if (authResult == null) uiState.biometricState else uiState
+ .biometricState.copy(biometricResult = BiometricResult(
+ biometricAuthenticationResult = authResult)
+ )
)
} else {
credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey)
@@ -347,4 +367,9 @@ 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 6a1998a5e24e..fd6fc6a44c7c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -503,6 +503,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
)
)
}
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..db5ab569535f
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
@@ -0,0 +1,374 @@
+/*
+ * 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.BiometricManager
+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.BiometricRequestInfo
+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(
+ val providerIcon: Bitmap,
+ val providerName: String,
+ val displayTitleText: String,
+ val descriptionAboveBiometricButton: String,
+ val biometricRequestInfo: BiometricRequestInfo,
+)
+
+/**
+ * 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 biometricResult: BiometricResult? = null,
+ val biometricError: BiometricError? = null,
+ val biometricHelp: BiometricHelp? = null,
+ val biometricAcquireInfo: Int? = null,
+)
+
+/**
+ * When a result exists, it must be retrievable. This encapsulates the result
+ * so that should this object exist, the result will be retrievable.
+ */
+data class BiometricResult(
+ val biometricAuthenticationResult: BiometricPrompt.AuthenticationResult
+)
+
+/**
+ * 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(
+ biometricEntry: 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,
+) {
+ var biometricDisplayInfo: BiometricDisplayInfo? = null
+ 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(
+ createRequestDisplayInfo,
+ createProviderInfo
+ )
+ }
+
+ if (biometricDisplayInfo == null) {
+ onBiometricFailureFallback()
+ return
+ }
+
+ val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage,
+ biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators)
+
+ val callback: BiometricPrompt.AuthenticationCallback =
+ setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry,
+ 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.
+ * // 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,
+): 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),
+ getMainExecutor(context)) { _, _ ->
+ openMoreOptionsPage()
+ }
+ .setAllowedAuthenticators(finalAuthenticators)
+ .setConfirmationRequired(true)
+ // TODO(b/326243754) : Add logo back once new permission privileges sorted out
+ .setDescription(biometricDisplayInfo.descriptionAboveBiometricButton)
+ .build()
+
+ return biometricPrompt
+}
+
+// TODO(b/326243754) : Remove after larger level alignments made on fallback negative button
+// For the time being, we do not support the pin fallback until UX is decided.
+private fun removeDeviceCredential(requestAllowedAuthenticators: Int): Int {
+ var finalAuthenticators = requestAllowedAuthenticators
+
+ if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL or
+ BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
+ finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+ }
+
+ if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL or
+ BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
+ finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+ }
+
+ if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
+ finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+ }
+
+ return finalAuthenticators
+}
+
+/**
+ * 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. Note that while this method's main purpose is to retrieve the info
+ * required to display the biometric prompt, it acts as a secondary validator to handle any null
+ * checks at the beginning of the biometric flow and supports a quick fallback.
+ * While it's not expected for the flow to be triggered if values are
+ * missing, some values are by default nullable when they are pulled, such as entries. Thus, this
+ * acts as a final validation failsafe, without requiring null checks or null forcing around the
+ * codebase.
+ */
+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)
+ }
+ return null
+}
+
+/**
+ * Creates the [BiometricDisplayInfo] for create flows, and early handles conditional
+ * 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(
+ createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo?,
+ createProviderInfo: EnabledProviderInfo?,
+): 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()
+ }
+ 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. Note that these redundant checks are just failsafe; the original
+ * flow should never reach here with invalid params.
+ */
+private fun getBiometricDisplayValues(
+ getProviderInfoList: List<ProviderInfo>,
+ context: Context,
+ getRequestDisplayInfo: RequestDisplayInfo,
+ selectedEntry: CredentialEntryInfo,
+): 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
+ }
+ if (selectedEntry.biometricRequest == null) {
+ Log.d(TAG, "Unexpectedly in biometric flow without a biometric request.")
+ 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,
+ biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo)
+}
+
+/**
+ * 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/common/Constants.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt
index 51ca5971cec4..7e7a74fd3107 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt
@@ -22,5 +22,7 @@ class Constants {
const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
"androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED"
const val IS_AUTO_SELECTED_KEY = "IS_AUTO_SELECTED"
+ const val BIOMETRIC_AUTH_TYPE = "BIOMETRIC_AUTH_TYPE"
+ const val BIOMETRIC_AUTH_FAILURE = "BIOMETRIC_AUTH_FAILURE"
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index b9c9d8994c45..b59ccc264630 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -16,8 +16,10 @@
package com.android.credentialmanager.getflow
+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 +43,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 +52,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 +86,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 +141,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
+ biometricEntry = 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 +209,34 @@ fun GetCredentialScreen(
}
}
+@Composable
+internal fun BiometricSelectionPage(
+ biometricEntry: EntryInfo?,
+ onCancelFlowAndFinish: (String) -> Unit,
+ onMoreOptionSelected: () -> Unit,
+ requestDisplayInfo: RequestDisplayInfo,
+ providerInfoList: List<ProviderInfo>,
+ providerDisplayInfo: ProviderDisplayInfo,
+ onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult?) -> Unit,
+ fallbackToOriginalFlow: () -> Unit,
+) {
+ if (biometricEntry == null) {
+ fallbackToOriginalFlow()
+ return
+ }
+ runBiometricFlow(
+ biometricEntry = biometricEntry,
+ 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 +304,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..6d5b52a7a5f9 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,59 @@ 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 validates this flow for
+ * a single account. In the case when there's a single credential, this flow matches the auto
+ * select criteria, but with the possibility that the two flows (autoselect and biometric) may
+ * collide. In those collision cases, the auto select flow is supported over the biometric flow.
+ * If there is a single account but more than one credential, and the first ranked credential has
+ * the biometric bit flipped on, we will use the biometric flow. If all conditions are valid, this
+ * responds with the entry utilized by the biometricFlow, or null otherwise.
+ */
+internal fun findBiometricFlowEntry(
+ providerDisplayInfo: ProviderDisplayInfo,
+ isAutoSelectFlow: Boolean
+): CredentialEntryInfo? {
+ if (!credmanBiometricApiEnabled()) {
+ return null
+ }
+ 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 null
+ }
+ // 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 null
+ }
+ val singleAccountEntryList = getCredentialEntryListIffSingleAccount(
+ providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null
+
+ val firstEntry = singleAccountEntryList.firstOrNull()
+ return if (firstEntry?.biometricRequest != null) firstEntry else null
+}
+
+/**
+ * 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 +106,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 +160,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 +235,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,9 +285,18 @@ private fun toGetScreenState(
GetScreenState.REMOTE_ONLY
else if (isRequestForAllOptions)
GetScreenState.ALL_SIGN_IN_OPTIONS
+ else if (isBiometricFlow(providerDisplayInfo))
+ GetScreenState.BIOMETRIC_SELECTION
else GetScreenState.PRIMARY_SELECTION
}
+/**
+ * Determines if the flow is a biometric flow by taking into account autoselect criteria.
+ */
+internal fun isBiometricFlow(providerDisplayInfo: ProviderDisplayInfo) =
+ findBiometricFlowEntry(providerDisplayInfo,
+ findAutoSelectEntry(providerDisplayInfo) != null) != null
+
internal class CredentialEntryInfoComparatorByTypeThenTimestamp(
val typePriorityMap: Map<String, Int>,
) : Comparator<CredentialEntryInfo> {