diff options
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(), |