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