Loading packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt +5 −5 Original line number Diff line number Diff line Loading @@ -142,7 +142,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), biometricRequest = predetermineAndValidateBiometricFlow(it, biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) Loading Loading @@ -172,7 +172,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), biometricRequest = predetermineAndValidateBiometricFlow(it, biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) Loading Loading @@ -201,7 +201,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), biometricRequest = predetermineAndValidateBiometricFlow(it, biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) Loading @@ -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 Loading @@ -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? { Loading packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +56 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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) Loading packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +9 −3 Original line number Diff line number Diff line Loading @@ -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( Loading Loading @@ -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, Loading Loading @@ -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), ) ) Loading packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt→packages/CredentialManager/src/com/android/credentialmanager/common/BiometricFlowType.kt +1 −1 Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ package com.android.credentialmanager.common enum class FlowType { enum class BiometricFlowType { GET, CREATE } No newline at end of file packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt +112 −52 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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 ) /** Loading Loading @@ -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( if (getBiometricPromptState() != BiometricPromptState.INACTIVE) { // Screen is already up, do not re-launch return } onBiometricPromptStateChange(BiometricPromptState.PENDING) val biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo( createRequestDisplayInfo, createProviderInfo, context, biometricEntry) } 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 } Loading @@ -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() Loading @@ -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 Loading Loading @@ -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 " + Loading @@ -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 Loading @@ -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 Loading @@ -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?, Loading @@ -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 } Loading @@ -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() Loading Loading @@ -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, Loading Loading @@ -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) Loading Loading
packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt +5 −5 Original line number Diff line number Diff line Loading @@ -142,7 +142,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), biometricRequest = predetermineAndValidateBiometricFlow(it, biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) Loading Loading @@ -172,7 +172,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), biometricRequest = predetermineAndValidateBiometricFlow(it, biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) Loading Loading @@ -201,7 +201,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), biometricRequest = predetermineAndValidateBiometricFlow(it, biometricRequest = retrieveEntryBiometricRequest(it, CREDENTIAL_ENTRY_PREFIX), ) ) Loading @@ -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 Loading @@ -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? { Loading
packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +56 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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) Loading
packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +9 −3 Original line number Diff line number Diff line Loading @@ -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( Loading Loading @@ -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, Loading Loading @@ -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), ) ) Loading
packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt→packages/CredentialManager/src/com/android/credentialmanager/common/BiometricFlowType.kt +1 −1 Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ package com.android.credentialmanager.common enum class FlowType { enum class BiometricFlowType { GET, CREATE } No newline at end of file
packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt +112 −52 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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 ) /** Loading Loading @@ -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( if (getBiometricPromptState() != BiometricPromptState.INACTIVE) { // Screen is already up, do not re-launch return } onBiometricPromptStateChange(BiometricPromptState.PENDING) val biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo( createRequestDisplayInfo, createProviderInfo, context, biometricEntry) } 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 } Loading @@ -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() Loading @@ -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 Loading Loading @@ -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 " + Loading @@ -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 Loading @@ -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 Loading @@ -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?, Loading @@ -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 } Loading @@ -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() Loading Loading @@ -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, Loading Loading @@ -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) Loading