summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Arpan <akaphle@google.com> 2024-03-13 20:47:47 +0000
committer Arpan <akaphle@google.com> 2024-03-21 17:09:50 +0000
commit19033e9530e89e7c2ac32172e603718b194cd694 (patch)
tree32d31a05424c831255bfbe9dc139df1fec2efa72
parent58e33aa480bbcb3d753f9ef820f14552de0901ed (diff)
Communicate Get Flow Biometric Success to Provider
This sets up the biometric flow to communicate the results to the provider for the get flow. This is to finalize the biometric API call so that the end result of the authentication can be communicated to the provider, completed the E2E Get Happy Path. This specifies the success case, which is the pivotal case to complete the flow. Other cases, such as failures, or other callback values (help/confirm/etc..) are presently logged and will be productionized as soon as possible. However, this should have the success flow checked in with ways to identify failure cases from the non triggerable flows presently in the framework. To make this full E2E, this also ensures that the initial input is completely allowed as expected by Jetpack, though everything is being tested. Therefore, it is protected from being reachable by flags. The Create version will follow up with the create changes, and the required exhaustive when statement has been left with a 'TODO' for the create case; that case will be tested to not break any E2E existing flows. Bug: 327620327 Test: Visual, Build, and Unit in Progress: see b/329874867 Change-Id: Icf0d8459d784880a2d307c4997103451ca5b29fb
-rw-r--r--packages/CredentialManager/AndroidManifest.xml1
-rw-r--r--packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt40
-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.kt3
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt10
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt22
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt2
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt104
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt2
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt11
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt37
-rw-r--r--packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt1
13 files changed, 206 insertions, 57 deletions
diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml
index ab7f419216c0..7a8c25bd12ab 100644
--- a/packages/CredentialManager/AndroidManifest.xml
+++ b/packages/CredentialManager/AndroidManifest.xml
@@ -22,7 +22,6 @@
<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" />
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 e0ddb125bef7..7ad339252eed 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.CredentialType
import com.android.credentialmanager.model.get.ProviderInfo
import com.android.credentialmanager.model.get.RemoteEntryInfo
import com.android.credentialmanager.TAG
+import com.android.credentialmanager.model.BiometricRequestInfo
import com.android.credentialmanager.model.EntryInfo
fun EntryInfo.getIntentSenderRequest(
@@ -139,7 +140,7 @@ private fun getCredentialOptionInfoList(
isDefaultIconPreferredAsSingleProvider =
credentialEntry.isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
- isSupportingSingleTap = false, // TODO(b/326243754) : Fill in as product built
+ biometricRequest = predetermineAndValidateBiometricFlow(it),
)
)
}
@@ -168,7 +169,7 @@ private fun getCredentialOptionInfoList(
isDefaultIconPreferredAsSingleProvider =
credentialEntry.isDefaultIconPreferredAsSingleProvider,
affiliatedDomain = credentialEntry.affiliatedDomain?.toString(),
- isSupportingSingleTap = false, // TODO(b/326243754) : Fill in as product built
+ biometricRequest = predetermineAndValidateBiometricFlow(it),
)
)
}
@@ -196,9 +197,7 @@ 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.
+ biometricRequest = predetermineAndValidateBiometricFlow(it),
)
)
}
@@ -210,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 {
@@ -226,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 49c71f17839f..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,7 +50,7 @@ class CredentialEntryInfo(
// "For <value-of-entryGroupId>" on the more-option screen.
val isDefaultIconPreferredAsSingleProvider: Boolean,
val affiliatedDomain: String?,
- val isSupportingSingleTap: Boolean,
+ 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 c05544e537d8..28c40479962e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -29,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.BiometricResult
import com.android.credentialmanager.common.BiometricState
import com.android.credentialmanager.model.EntryInfo
import com.android.credentialmanager.common.Constants
@@ -56,7 +57,7 @@ data class UiState(
val isAutoSelectFlow: Boolean = false,
val cancelRequestState: CancelUiRequestState?,
val isInitialRender: Boolean,
- val biometricState: BiometricState? = null
+ val biometricState: BiometricState = BiometricState()
)
data class CancelUiRequestState(
@@ -116,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 {
@@ -213,8 +223,9 @@ class CredentialSelectorViewModel(
uiState.copy(
selectedEntry = entry,
providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
- biometricState = uiState.biometricState?.copy(
- biometricAuthenticationResult = authResult
+ biometricState = if (authResult == null) uiState.biometricState else uiState
+ .biometricState.copy(biometricResult = BiometricResult(
+ biometricAuthenticationResult = authResult)
)
)
} else {
@@ -356,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
index 5bba688e7b8c..db5ab569535f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
@@ -18,6 +18,7 @@ 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
@@ -28,6 +29,7 @@ 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
@@ -41,10 +43,11 @@ import java.lang.Exception
* 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
+ val providerIcon: Bitmap,
+ val providerName: String,
+ val displayTitleText: String,
+ val descriptionAboveBiometricButton: String,
+ val biometricRequestInfo: BiometricRequestInfo,
)
/**
@@ -53,10 +56,18 @@ data class BiometricDisplayInfo(
* additional states that may improve the flow.
*/
data class BiometricState(
- val biometricAuthenticationResult: BiometricPrompt.AuthenticationResult? = null,
+ val biometricResult: BiometricResult? = null,
val biometricError: BiometricError? = null,
val biometricHelp: BiometricHelp? = null,
- val biometricAcquireInfo: Int? = 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
)
/**
@@ -83,7 +94,7 @@ data class BiometricHelp(
* biometric prompt.
*/
fun runBiometricFlow(
- selectedEntry: EntryInfo?,
+ biometricEntry: EntryInfo,
context: Context,
openMoreOptionsPage: () -> Unit,
sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit,
@@ -96,16 +107,12 @@ fun runBiometricFlow(
.RequestDisplayInfo? = null,
createProviderInfo: EnabledProviderInfo? = null,
) {
- if (selectedEntry == null) {
- onBiometricFailureFallback()
- return
- }
var biometricDisplayInfo: BiometricDisplayInfo? = null
if (getRequestDisplayInfo != null) {
biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(getRequestDisplayInfo,
getProviderInfoList,
getProviderDisplayInfo,
- context, selectedEntry)
+ context, biometricEntry)
} else if (createRequestDisplayInfo != null) {
// TODO(b/326243754) : Create Flow to be implemented in follow up
biometricDisplayInfo = validateBiometricCreateFlow(
@@ -119,10 +126,11 @@ fun runBiometricFlow(
return
}
- val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage)
+ val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage,
+ biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators)
val callback: BiometricPrompt.AuthenticationCallback =
- setupBiometricAuthenticationCallback(sendDataToProvider, selectedEntry,
+ setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry,
onCancelFlowAndFinish)
val cancellationSignal = CancellationSignal()
@@ -144,12 +152,20 @@ fun runBiometricFlow(
/**
* Sets up the biometric prompt with the UI specific bits.
+ * // TODO(b/326243754) : Pass in opId once dependency is confirmed via CryptoObject
+ * // TODO(b/326243754) : Given fallbacks aren't allowed, for now we validate that device creds
+ * // are NOT allowed to be passed in to avoid throwing an error. Later, however, once target
+ * // alignments occur, we should add the bit back properly.
*/
private fun setupBiometricPrompt(
context: Context,
biometricDisplayInfo: BiometricDisplayInfo,
- openMoreOptionsPage: () -> Unit
-): BiometricPrompt = BiometricPrompt.Builder(context)
+ 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
@@ -157,13 +173,37 @@ private fun setupBiometricPrompt(
getMainExecutor(context)) { _, _ ->
openMoreOptionsPage()
}
+ .setAllowedAuthenticators(finalAuthenticators)
.setConfirmationRequired(true)
- .setLogoBitmap(biometricDisplayInfo.providerIcon)
- .setLogoDescription(biometricDisplayInfo
- .providerName)
+ // 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.
*/
@@ -218,7 +258,13 @@ private fun setupBiometricAuthenticationCallback(
/**
* Creates the [BiometricDisplayInfo] for get flows, and early handles conditional
- * checking between the two.
+ * 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?,
@@ -231,21 +277,22 @@ private fun validateAndRetrieveBiometricGetDisplayInfo(
getProviderDisplayInfo != null) {
if (selectedEntry !is CredentialEntryInfo) { return null }
return getBiometricDisplayValues(getProviderInfoList,
- context,
- getRequestDisplayInfo, selectedEntry, getProviderDisplayInfo)
+ context, getRequestDisplayInfo, selectedEntry)
}
return null
}
/**
* Creates the [BiometricDisplayInfo] for create flows, and early handles conditional
- * checking between the two.
+ * 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()
}
@@ -255,14 +302,14 @@ private fun validateBiometricCreateFlow(
/**
* 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.
+ * 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,
- getProviderDisplayInfo: ProviderDisplayInfo,
): BiometricDisplayInfo? {
var icon: Bitmap? = null
var providerName: String? = null
@@ -276,6 +323,10 @@ private fun getBiometricDisplayValues(
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(
@@ -288,7 +339,8 @@ private fun getBiometricDisplayValues(
username
)
return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
- displayTitleText = displayTitleText, descriptionAboveBiometricButton = descriptionText)
+ displayTitleText = displayTitleText, descriptionAboveBiometricButton = descriptionText,
+ biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo)
}
/**
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 efde4c3d0eb2..b59ccc264630 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -16,7 +16,6 @@
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
@@ -147,7 +146,7 @@ fun GetCredentialScreen(
BiometricSelectionPage(
// TODO(b/326243754) : Utilize expected entry for this flow, confirm
// activeEntry will always be what represents the single tap flow
- selectedEntry = getCredentialUiState.activeEntry,
+ biometricEntry = getCredentialUiState.activeEntry,
onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected,
onCancelFlowAndFinish = viewModel::onIllegalUiState,
requestDisplayInfo = getCredentialUiState.requestDisplayInfo,
@@ -212,7 +211,7 @@ fun GetCredentialScreen(
@Composable
internal fun BiometricSelectionPage(
- selectedEntry: EntryInfo?,
+ biometricEntry: EntryInfo?,
onCancelFlowAndFinish: (String) -> Unit,
onMoreOptionSelected: () -> Unit,
requestDisplayInfo: RequestDisplayInfo,
@@ -221,8 +220,12 @@ internal fun BiometricSelectionPage(
onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult?) -> Unit,
fallbackToOriginalFlow: () -> Unit,
) {
+ if (biometricEntry == null) {
+ fallbackToOriginalFlow()
+ return
+ }
runBiometricFlow(
- selectedEntry = selectedEntry,
+ biometricEntry = biometricEntry,
context = LocalContext.current,
openMoreOptionsPage = onMoreOptionSelected,
sendDataToProvider = onBiometricEntrySelected,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 3e7004a63317..6d5b52a7a5f9 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -44,36 +44,39 @@ data class GetCredentialUiState(
/**
* 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.
+ * 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 isBiometricFlow(
+internal fun findBiometricFlowEntry(
providerDisplayInfo: ProviderDisplayInfo,
isAutoSelectFlow: Boolean
-): Boolean {
+): CredentialEntryInfo? {
if (!credmanBiometricApiEnabled()) {
- return false
+ 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 false
+ 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 false
+ return null
}
val singleAccountEntryList = getCredentialEntryListIffSingleAccount(
- providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return false
+ providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null
- // TODO(b/326243754) : Set this value from dynamic slice until structured jetpack object is used
- return singleAccountEntryList.firstOrNull()?.isSupportingSingleTap ?: false
+ val firstEntry = singleAccountEntryList.firstOrNull()
+ return if (firstEntry?.biometricRequest != null) firstEntry else null
}
/**
@@ -282,12 +285,18 @@ private fun toGetScreenState(
GetScreenState.REMOTE_ONLY
else if (isRequestForAllOptions)
GetScreenState.ALL_SIGN_IN_OPTIONS
- else if (isBiometricFlow(providerDisplayInfo,
- findAutoSelectEntry(providerDisplayInfo) != null))
+ 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> {
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 01fb1cc1fd67..b8432137b35e 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,7 +152,6 @@ class GetCredScreenshotTest(emulationSpec: DeviceEmulationSpec) {
isDefaultIconPreferredAsSingleProvider = false,
rawCredentialType = "unknown-type",
affiliatedDomain = null,
- isSupportingSingleTap = false,
)
),
authenticationEntryList = emptyList(),