diff options
author | 2024-04-02 22:15:13 +0000 | |
---|---|---|
committer | 2024-04-09 20:20:04 +0000 | |
commit | e7f4a7ab18ee75dfc370335a357372ff5960b315 (patch) | |
tree | 3c6fded768f1437e6f93ed91e43dbc47b1d47f79 | |
parent | 37ee1886cd3cad64b4fca9ec4045c84db024f78e (diff) |
Fixing Rotation Bug for BiometricCredMan
This fixes the rotation bug for credential manager by creating a
[BiometricFlowState] object. This object is used throughout the flow to
ensure that on re-compose, the state does not violate any constraints.
Namely, if a rotation or other similar configuration change happens that
causes a recompose, the Ui has consistent state.
Bug: 330396089
Test: Visual/Build Test updated to bug
Change-Id: Ic8a1a09647999abc9bf935d143c7006e0f3860e7
10 files changed, 250 insertions, 86 deletions
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 b408c1553d94..9c8ec3b56813 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt @@ -142,7 +142,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), - biometricRequest = predetermineAndValidateBiometricFlow(it, + biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) @@ -172,7 +172,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), - biometricRequest = predetermineAndValidateBiometricFlow(it, + biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) @@ -201,7 +201,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), - biometricRequest = predetermineAndValidateBiometricFlow(it, + biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) @@ -216,7 +216,7 @@ private fun getCredentialOptionInfoList( } /** - * This validates if this is a biometric flow or not, and if it is, this returns the expected + * This validates if the entry calling this method contains biometric info, and if so, returns a * [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 @@ -230,7 +230,7 @@ private fun getCredentialOptionInfoList( * // 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. */ -fun predetermineAndValidateBiometricFlow( +fun retrieveEntryBiometricRequest( entry: Entry, hintPrefix: String, ): BiometricRequestInfo? { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index a03975375e8a..888777e81d65 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -30,6 +30,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.android.credentialmanager.common.BiometricFlowType +import com.android.credentialmanager.common.BiometricPromptState import com.android.credentialmanager.common.BiometricResult import com.android.credentialmanager.common.BiometricState import com.android.credentialmanager.model.EntryInfo @@ -40,7 +42,7 @@ import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.createflow.ActiveEntry import com.android.credentialmanager.createflow.CreateCredentialUiState import com.android.credentialmanager.createflow.CreateScreenState -import com.android.credentialmanager.createflow.findBiometricFlowEntry +import com.android.credentialmanager.createflow.isBiometricFlow import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState import com.android.credentialmanager.logging.LifecycleEvent @@ -303,13 +305,23 @@ class CredentialSelectorViewModel( } fun createFlowOnEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) { + val isBiometricFlow = isBiometricFlow(activeEntry = activeEntry, isAutoSelectFlow = false) + if (isBiometricFlow) { + // This atomically ensures that the only edge case that *restarts* the biometric flow + // doesn't risk a configuration change bug on the more options page during create. + // Namely, it's atomic in that it happens only on a tap, and it is not possible to + // reproduce a tap and a rotation at the same time. However, even if it were, it would + // just be an alternate way to jump back into the biometric selection flow after this + // reset, and thus, the state machine is maintained. + onBiometricPromptStateChange(BiometricPromptState.INACTIVE) + } uiState = uiState.copy( createCredentialUiState = uiState.createCredentialUiState?.copy( currentScreenState = // An autoselect flow never makes it to the more options screen - if (findBiometricFlowEntry(activeEntry = activeEntry, - isAutoSelectFlow = false) != null) CreateScreenState.BIOMETRIC_SELECTION - else if ( + if (isBiometricFlow) { + CreateScreenState.BIOMETRIC_SELECTION + } else if ( uiState.createCredentialUiState?.requestDisplayInfo?.userSetDefaultProviderIds ?.contains(activeEntry.activeProvider.id) ?: true || !(uiState.createCredentialUiState?.foundCandidateFromUserDefaultProvider @@ -375,6 +387,46 @@ class CredentialSelectorViewModel( } } + /**************************************************************************/ + /***** Biometric Flow Callbacks *****/ + /**************************************************************************/ + + /** + * This allows falling back from the biometric prompt screen to the normal get flow by applying + * a reset to all necessary states involved in the fallback. + */ + fun fallbackFromBiometricToNormalFlow(biometricFlowType: BiometricFlowType) { + onBiometricPromptStateChange(BiometricPromptState.INACTIVE) + when (biometricFlowType) { + BiometricFlowType.GET -> getFlowOnBackToPrimarySelectionScreen() + BiometricFlowType.CREATE -> createFlowOnUseOnceSelected() + } + } + + /** + * This method can be used to change the [BiometricPromptState] according to the necessity. + * For example, if resetting, one might use [BiometricPromptState.INACTIVE], but if the flow + * has just launched, to avoid configuration errors, one can use + * [BiometricPromptState.PENDING]. + */ + fun onBiometricPromptStateChange(biometricPromptState: BiometricPromptState) { + uiState = uiState.copy( + biometricState = uiState.biometricState.copy( + biometricStatus = biometricPromptState + ) + ) + } + + /** + * This returns the present biometric state. + */ + fun getBiometricPromptState(): BiometricPromptState = + uiState.biometricState.biometricStatus + + /**************************************************************************/ + /***** Misc. Callbacks/Logs *****/ + /**************************************************************************/ + @Composable fun logUiEvent(uiEventEnum: UiEventEnum) { this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.packageName) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 358ebfa1ec90..c477f30a1d2f 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -53,8 +53,9 @@ import androidx.credentials.provider.RemoteEntry import org.json.JSONObject import android.credentials.flags.Flags import com.android.credentialmanager.createflow.isBiometricFlow +import com.android.credentialmanager.createflow.isFlowAutoSelectable import com.android.credentialmanager.getflow.TopBrandingContent -import com.android.credentialmanager.ktx.predetermineAndValidateBiometricFlow +import com.android.credentialmanager.ktx.retrieveEntryBiometricRequest import java.time.Instant fun getAppLabel( @@ -431,7 +432,12 @@ class CreateFlowUtils { remoteEntryProvider = remoteEntryProvider, ) val isBiometricFlow = if (activeEntry == null) false else isBiometricFlow(activeEntry, - sortedCreateOptionsPairs, requestDisplayInfo) + isFlowAutoSelectable( + requestDisplayInfo = requestDisplayInfo, + activeEntry = activeEntry, + sortedCreateOptionsPairs = sortedCreateOptionsPairs + ) + ) val initialScreenState = toCreateScreenState( createOptionSize = createOptionsPairs.size, remoteEntry = remoteEntry, @@ -514,7 +520,7 @@ class CreateFlowUtils { it.hasHint("androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_" + "SELECT_ALLOWED") }?.text == "true", - biometricRequest = predetermineAndValidateBiometricFlow(it, + biometricRequest = retrieveEntryBiometricRequest(it, CREATE_ENTRY_PREFIX), ) ) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricFlowType.kt index f6140f51b7b5..263cfd574d73 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricFlowType.kt @@ -16,7 +16,7 @@ package com.android.credentialmanager.common -enum class FlowType { +enum class BiometricFlowType { GET, CREATE }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt index fa177351be30..be3e0437a83e 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt @@ -59,7 +59,7 @@ import java.lang.Exception * ). * * The above are examples; the credential type can change depending on scenario. - * // TODO(b/326243891) : Finalize once all the strings and create flow is iterated to completion + * // TODO(b/333445112) : Finalize once all the strings and create flow is iterated to completion */ data class BiometricDisplayInfo( val providerIcon: Bitmap, @@ -75,7 +75,8 @@ data class BiometricDisplayInfo( * additional states that may improve the flow. */ data class BiometricState( - val biometricResult: BiometricResult? = null + val biometricResult: BiometricResult? = null, + val biometricStatus: BiometricPromptState = BiometricPromptState.INACTIVE ) /** @@ -104,58 +105,115 @@ data class BiometricHelp( ) /** - * This will handle the logic for integrating credential manager with the biometric prompt for the - * single account biometric experience. This simultaneously handles both the get and create flows, - * by retrieving all the data from credential manager, and properly parsing that data into the - * biometric prompt. + * This is the entry point to start the integrated biometric prompt for 'get' flows. It captures + * information specific to the get flow, along with required shared callbacks and more general + * info across both flows, such as the tapped [EntryInfo] or [sendDataToProvider]. */ -fun runBiometricFlow( +fun runBiometricFlowForGet( biometricEntry: EntryInfo, context: Context, openMoreOptionsPage: () -> Unit, sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, onCancelFlowAndFinish: () -> Unit, onIllegalStateAndFinish: (String) -> Unit, + getBiometricPromptState: () -> BiometricPromptState, + onBiometricPromptStateChange: (BiometricPromptState) -> Unit, + onBiometricFailureFallback: (BiometricFlowType) -> Unit, getRequestDisplayInfo: RequestDisplayInfo? = null, getProviderInfoList: List<ProviderInfo>? = null, getProviderDisplayInfo: ProviderDisplayInfo? = null, - onBiometricFailureFallback: () -> Unit, +) { + if (getBiometricPromptState() != BiometricPromptState.INACTIVE) { + // Screen is already up, do not re-launch + return + } + onBiometricPromptStateChange(BiometricPromptState.PENDING) + val biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo( + getRequestDisplayInfo, + getProviderInfoList, + getProviderDisplayInfo, + context, biometricEntry + ) + + if (biometricDisplayInfo == null) { + onBiometricFailureFallback(BiometricFlowType.GET) + return + } + + val callback: BiometricPrompt.AuthenticationCallback = + setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry, + onCancelFlowAndFinish, onIllegalStateAndFinish, onBiometricPromptStateChange) + + Log.d(TAG, "The BiometricPrompt API call begins.") + runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, + onBiometricFailureFallback, BiometricFlowType.GET) +} + +/** + * This is the entry point to start the integrated biometric prompt for 'create' flows. It captures + * information specific to the create flow, along with required shared callbacks and more general + * info across both flows, such as the tapped [EntryInfo] or [sendDataToProvider]. + */ +fun runBiometricFlowForCreate( + biometricEntry: EntryInfo, + context: Context, + openMoreOptionsPage: () -> Unit, + sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, + onCancelFlowAndFinish: () -> Unit, + onIllegalStateAndFinish: (String) -> Unit, + getBiometricPromptState: () -> BiometricPromptState, + onBiometricPromptStateChange: (BiometricPromptState) -> Unit, + onBiometricFailureFallback: (BiometricFlowType) -> Unit, createRequestDisplayInfo: com.android.credentialmanager.createflow .RequestDisplayInfo? = null, createProviderInfo: EnabledProviderInfo? = null, ) { - // TODO(b/330396089) : Add rotation configuration fix with state machine - var biometricDisplayInfo: BiometricDisplayInfo? = null - var flowType = FlowType.GET - if (getRequestDisplayInfo != null) { - biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(getRequestDisplayInfo, - getProviderInfoList, - getProviderDisplayInfo, - context, biometricEntry) - } else if (createRequestDisplayInfo != null) { - flowType = FlowType.CREATE - biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo( - createRequestDisplayInfo, - createProviderInfo, - context, biometricEntry) + if (getBiometricPromptState() != BiometricPromptState.INACTIVE) { + // Screen is already up, do not re-launch + return } + onBiometricPromptStateChange(BiometricPromptState.PENDING) + val biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo( + createRequestDisplayInfo, + createProviderInfo, + context, biometricEntry + ) if (biometricDisplayInfo == null) { - onBiometricFailureFallback() + onBiometricFailureFallback(BiometricFlowType.CREATE) return } - val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage, - biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators, flowType) - val callback: BiometricPrompt.AuthenticationCallback = setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry, - onCancelFlowAndFinish, onIllegalStateAndFinish) + onCancelFlowAndFinish, onIllegalStateAndFinish, onBiometricPromptStateChange) + + Log.d(TAG, "The BiometricPrompt API call begins.") + runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, + onBiometricFailureFallback, BiometricFlowType.CREATE) +} + +/** + * This will handle the logic for integrating credential manager with the biometric prompt for the + * single account biometric experience. This simultaneously handles both the get and create flows, + * by retrieving all the data from credential manager, and properly parsing that data into the + * biometric prompt. + */ +private fun runBiometricFlow( + context: Context, + biometricDisplayInfo: BiometricDisplayInfo, + callback: BiometricPrompt.AuthenticationCallback, + openMoreOptionsPage: () -> Unit, + onBiometricFailureFallback: (BiometricFlowType) -> Unit, + biometricFlowType: BiometricFlowType +) { + val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage, + biometricDisplayInfo.biometricRequestInfo, biometricFlowType) val cancellationSignal = CancellationSignal() cancellationSignal.setOnCancelListener { Log.d(TAG, "Your cancellation signal was called.") - // TODO(b/326243754) : Migrate towards passing along the developer cancellation signal + // TODO(b/333445112) : Migrate towards passing along the developer cancellation signal // or validate the necessity for this } @@ -165,27 +223,28 @@ fun runBiometricFlow( biometricPrompt.authenticate(cancellationSignal, executor, callback) } catch (e: IllegalArgumentException) { Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n") - onBiometricFailureFallback() + onBiometricFailureFallback(biometricFlowType) } } /** * Sets up the biometric prompt with the UI specific bits. - * // TODO(b/326243754) : Pass in opId once dependency is confirmed via CryptoObject + * // TODO(b/333445112) : Pass in opId once dependency is confirmed via CryptoObject */ private fun setupBiometricPrompt( context: Context, biometricDisplayInfo: BiometricDisplayInfo, openMoreOptionsPage: () -> Unit, - requestAllowedAuthenticators: Int, - flowType: FlowType, + biometricRequestInfo: BiometricRequestInfo, + biometricFlowType: BiometricFlowType, ): BiometricPrompt { - val finalAuthenticators = removeDeviceCredential(requestAllowedAuthenticators) + val finalAuthenticators = removeDeviceCredential(biometricRequestInfo.allowedAuthenticators) val biometricPrompt = BiometricPrompt.Builder(context) .setTitle(biometricDisplayInfo.displayTitleText) - // TODO(b/326243754) : Migrate to using new methods recently aligned upon - .setNegativeButton(context.getString(if (flowType == FlowType.GET) R.string + // TODO(b/333445112) : Migrate to using new methods and strings recently aligned upon + .setNegativeButton(context.getString(if (biometricFlowType == BiometricFlowType.GET) + R.string .dropdown_presentation_more_sign_in_options_text else R.string.string_more_options), getMainExecutor(context)) { _, _ -> openMoreOptionsPage() @@ -200,7 +259,7 @@ private fun setupBiometricPrompt( return biometricPrompt } -// TODO(b/326243754) : Remove after larger level alignments made on fallback negative button +// TODO(b/333445112) : 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 @@ -230,16 +289,18 @@ private fun setupBiometricAuthenticationCallback( selectedEntry: EntryInfo, onCancelFlowAndFinish: () -> Unit, onIllegalStateAndFinish: (String) -> Unit, + onBiometricPromptStateChange: (BiometricPromptState) -> Unit ): BiometricPrompt.AuthenticationCallback { val callback: BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { - // TODO(b/326243754) : Validate remaining callbacks + // TODO(b/333445772) : Validate remaining callbacks override fun onAuthenticationSucceeded( authResult: BiometricPrompt.AuthenticationResult? ) { super.onAuthenticationSucceeded(authResult) try { if (authResult != null) { + onBiometricPromptStateChange(BiometricPromptState.COMPLETE) sendDataToProvider(selectedEntry, authResult) } else { onIllegalStateAndFinish("The biometric flow succeeded but unexpectedly " + @@ -254,26 +315,24 @@ private fun setupBiometricAuthenticationCallback( override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) { super.onAuthenticationHelp(helpCode, helpString) Log.d(TAG, "Authentication help discovered: $helpCode and $helpString") - // TODO(b/326243754) : Decide on strategy with provider (a simple log probably - // suffices here) } override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { super.onAuthenticationError(errorCode, errString) Log.d(TAG, "Authentication error-ed out: $errorCode and $errString") + onBiometricPromptStateChange(BiometricPromptState.COMPLETE) if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED) { // Note that because the biometric prompt is imbued directly // into the selector, parity applies to the selector's cancellation instead // of the provider's biometric prompt cancellation. onCancelFlowAndFinish() } - // TODO(b/326243754) : Propagate to provider + // TODO(b/333445772) : Propagate to provider } override fun onAuthenticationFailed() { super.onAuthenticationFailed() Log.d(TAG, "Authentication failed.") - // TODO(b/326243754) : Propagate to provider } } return callback @@ -299,7 +358,7 @@ private fun validateAndRetrieveBiometricGetDisplayInfo( if (getRequestDisplayInfo != null && getProviderInfoList != null && getProviderDisplayInfo != null) { if (selectedEntry !is CredentialEntryInfo) { return null } - return getBiometricDisplayValues(getProviderInfoList, + return retrieveBiometricGetDisplayValues(getProviderInfoList, context, getRequestDisplayInfo, selectedEntry) } return null @@ -308,7 +367,8 @@ private fun validateAndRetrieveBiometricGetDisplayInfo( /** * Creates the [BiometricDisplayInfo] for create flows, and early handles conditional * checking between the two. The reason for this method matches the logic for the - * [validateBiometricGetFlow] with the only difference being that this is for the create flow. + * [validateAndRetrieveBiometricGetDisplayInfo] with the only difference being that this is for + * the create flow. */ private fun validateAndRetrieveBiometricCreateDisplayInfo( createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo?, @@ -318,8 +378,8 @@ private fun validateAndRetrieveBiometricCreateDisplayInfo( ): BiometricDisplayInfo? { if (createRequestDisplayInfo != null && createProviderInfo != null) { if (selectedEntry !is CreateOptionInfo) { return null } - return createBiometricDisplayValues(createRequestDisplayInfo, createProviderInfo, context, - selectedEntry) + return retrieveBiometricCreateDisplayValues(createRequestDisplayInfo, createProviderInfo, + context, selectedEntry) } return null } @@ -330,16 +390,16 @@ private fun validateAndRetrieveBiometricCreateDisplayInfo( * 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( +private fun retrieveBiometricGetDisplayValues( getProviderInfoList: List<ProviderInfo>, context: Context, getRequestDisplayInfo: RequestDisplayInfo, selectedEntry: CredentialEntryInfo, ): BiometricDisplayInfo? { - var icon: Bitmap? = null - var providerName: String? = null - var displayTitleText: String? = null - var descriptionText: String? = null + val icon: Bitmap? + val providerName: String? + val displayTitleText: String? + val descriptionText: String? val primaryAccountsProviderInfo = retrievePrimaryAccountProviderInfo(selectedEntry.providerId, getProviderInfoList) icon = primaryAccountsProviderInfo?.icon?.toBitmap() @@ -373,7 +433,7 @@ private fun getBiometricDisplayValues( * if this is called, a result is guaranteed. Specifically, this is guaranteed to return a non-null * value unlike the get counterpart. */ -private fun createBiometricDisplayValues( +private fun retrieveBiometricCreateDisplayValues( createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo, createProviderInfo: EnabledProviderInfo, context: Context, @@ -401,7 +461,7 @@ private fun createBiometricDisplayValues( }, createRequestDisplayInfo.appName, ) - // TODO(b/327620327) : Add a subtitle and any other recently aligned ideas + // TODO(b/333445112) : Add a subtitle and any other recently aligned ideas return BiometricDisplayInfo(providerIcon = icon, providerName = providerName, displayTitleText = displayTitleText, descriptionForCredential = descriptionText, biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricPromptState.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricPromptState.kt new file mode 100644 index 000000000000..e1aa0418e7a0 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricPromptState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.common + +enum class BiometricPromptState { + /** The biometric prompt hasn't been activated. */ + INACTIVE, + /** The biometric prompt is active but data hasn't been returned yet. */ + PENDING, + /** The biometric prompt has closed and returned data we then send to the provider activity. */ + COMPLETE +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 25fb477cbf38..122b8964dc96 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -46,11 +46,13 @@ import androidx.core.graphics.drawable.toBitmap import com.android.compose.theme.LocalAndroidColorScheme import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.R +import com.android.credentialmanager.common.BiometricFlowType +import com.android.credentialmanager.common.BiometricPromptState import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.common.material.ModalBottomSheetDefaults -import com.android.credentialmanager.common.runBiometricFlow +import com.android.credentialmanager.common.runBiometricFlowForCreate import com.android.credentialmanager.common.ui.ActionButton import com.android.credentialmanager.common.ui.BodyMediumText import com.android.credentialmanager.common.ui.BodySmallText @@ -111,7 +113,11 @@ fun CreateCredentialScreen( onBiometricEntrySelected = viewModel::createFlowOnEntrySelected, fallbackToOriginalFlow = - viewModel::getFlowOnBackToPrimarySelectionScreen, + viewModel::fallbackFromBiometricToNormalFlow, + getBiometricPromptState = + viewModel::getBiometricPromptState, + onBiometricPromptStateChange = + viewModel::onBiometricPromptStateChange ) CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( requestDisplayInfo = createCredentialUiState.requestDisplayInfo, @@ -578,18 +584,22 @@ internal fun BiometricSelectionPage( onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, onCancelFlowAndFinish: () -> Unit, onIllegalScreenStateAndFinish: (String) -> Unit, - fallbackToOriginalFlow: () -> Unit, + fallbackToOriginalFlow: (BiometricFlowType) -> Unit, + getBiometricPromptState: () -> BiometricPromptState, + onBiometricPromptStateChange: (BiometricPromptState) -> Unit, ) { if (biometricEntry == null) { - fallbackToOriginalFlow() + fallbackToOriginalFlow(BiometricFlowType.CREATE) return } - runBiometricFlow( + runBiometricFlowForCreate( biometricEntry = biometricEntry, context = LocalContext.current, openMoreOptionsPage = onMoreOptionSelected, sendDataToProvider = onBiometricEntrySelected, onCancelFlowAndFinish = onCancelFlowAndFinish, + getBiometricPromptState = getBiometricPromptState, + onBiometricPromptStateChange = onBiometricPromptStateChange, createRequestDisplayInfo = requestDisplayInfo, createProviderInfo = enabledProviderInfo, onBiometricFailureFallback = fallbackToOriginalFlow, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 1d262ba5261a..ddd4139b65b6 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -78,13 +78,8 @@ internal fun getCreateEntry( */ internal fun isBiometricFlow( activeEntry: ActiveEntry, - sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, - requestDisplayInfo: RequestDisplayInfo, -) = findBiometricFlowEntry(activeEntry, isFlowAutoSelectable( - requestDisplayInfo = requestDisplayInfo, - activeEntry = activeEntry, - sortedCreateOptionsPairs = sortedCreateOptionsPairs -)) != null + isAutoSelectFlow: Boolean, +) = findBiometricFlowEntry(activeEntry, isAutoSelectFlow) != null /** * This utility presents the correct resource string for the create flows title conditionally. diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 6d1a3dd98210..72b7814a791a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -52,9 +52,11 @@ import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.R +import com.android.credentialmanager.common.BiometricFlowType +import com.android.credentialmanager.common.BiometricPromptState import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.common.material.ModalBottomSheetDefaults -import com.android.credentialmanager.common.runBiometricFlow +import com.android.credentialmanager.common.runBiometricFlowForGet import com.android.credentialmanager.common.ui.ActionButton import com.android.credentialmanager.common.ui.ActionEntry import com.android.credentialmanager.common.ui.ConfirmButton @@ -154,7 +156,11 @@ fun GetCredentialScreen( onBiometricEntrySelected = viewModel::getFlowOnEntrySelected, fallbackToOriginalFlow = - viewModel::getFlowOnBackToPrimarySelectionScreen, + viewModel::fallbackFromBiometricToNormalFlow, + getBiometricPromptState = + viewModel::getBiometricPromptState, + onBiometricPromptStateChange = + viewModel::onBiometricPromptStateChange ) } else { AllSignInOptionCard( @@ -218,19 +224,23 @@ internal fun BiometricSelectionPage( providerInfoList: List<ProviderInfo>, providerDisplayInfo: ProviderDisplayInfo, onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult?) -> Unit, - fallbackToOriginalFlow: () -> Unit, + fallbackToOriginalFlow: (BiometricFlowType) -> Unit, + getBiometricPromptState: () -> BiometricPromptState, + onBiometricPromptStateChange: (BiometricPromptState) -> Unit, ) { if (biometricEntry == null) { - fallbackToOriginalFlow() + fallbackToOriginalFlow(BiometricFlowType.GET) return } - runBiometricFlow( + runBiometricFlowForGet( biometricEntry = biometricEntry, context = LocalContext.current, openMoreOptionsPage = onMoreOptionSelected, sendDataToProvider = onBiometricEntrySelected, onCancelFlowAndFinish = onCancelFlowAndFinish, onIllegalStateAndFinish = onIllegalStateAndFinish, + getBiometricPromptState = getBiometricPromptState, + onBiometricPromptStateChange = onBiometricPromptStateChange, getRequestDisplayInfo = requestDisplayInfo, getProviderInfoList = providerInfoList, getProviderDisplayInfo = providerDisplayInfo, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index ac776af4f627..b03407b9ebea 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -286,7 +286,7 @@ private fun toGetScreenState( GetScreenState.REMOTE_ONLY else if (isRequestForAllOptions) GetScreenState.ALL_SIGN_IN_OPTIONS - else if (isBiometricFlow(providerDisplayInfo)) + else if (isBiometricFlow(providerDisplayInfo, isFlowAutoSelectable(providerDisplayInfo))) GetScreenState.BIOMETRIC_SELECTION else GetScreenState.PRIMARY_SELECTION } @@ -294,9 +294,14 @@ private fun toGetScreenState( /** * 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 fun isBiometricFlow(providerDisplayInfo: ProviderDisplayInfo, isAutoSelectFlow: Boolean) = + findBiometricFlowEntry(providerDisplayInfo, isAutoSelectFlow) != null + +/** + * Determines if the flow is an autoselect flow. + */ +internal fun isFlowAutoSelectable(providerDisplayInfo: ProviderDisplayInfo) = + findAutoSelectEntry(providerDisplayInfo) != null internal class CredentialEntryInfoComparatorByTypeThenTimestamp( val typePriorityMap: Map<String, Int>, |