1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.credentialmanager.common
18 
19 import android.content.Context
20 import android.content.DialogInterface
21 import android.graphics.Bitmap
22 import android.hardware.biometrics.BiometricManager
23 import android.hardware.biometrics.BiometricManager.Authenticators
24 import android.hardware.biometrics.BiometricPrompt
25 import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
26 import android.os.CancellationSignal
27 import android.util.Log
28 import androidx.core.content.ContextCompat.getMainExecutor
29 import androidx.core.graphics.drawable.toBitmap
30 import com.android.credentialmanager.R
31 import com.android.credentialmanager.createflow.EnabledProviderInfo
32 import com.android.credentialmanager.createflow.getCreateTitleResCode
33 import com.android.credentialmanager.getflow.ProviderDisplayInfo
34 import com.android.credentialmanager.getflow.RequestDisplayInfo
35 import com.android.credentialmanager.getflow.generateDisplayTitleTextResCode
36 import com.android.credentialmanager.model.BiometricRequestInfo
37 import com.android.credentialmanager.model.CredentialType
38 import com.android.credentialmanager.model.EntryInfo
39 import com.android.credentialmanager.model.creation.CreateOptionInfo
40 import com.android.credentialmanager.model.get.CredentialEntryInfo
41 import com.android.credentialmanager.model.get.ProviderInfo
42 
43 /**
44  * Aggregates common display information used for the Biometric Flow.
45  * Namely, this adds the ability to encapsulate the [providerIcon], the providers icon, the
46  * [providerName], which represents the name of the provider, the [displayTitleText] which is
47  * the large text displaying the flow in progress, and the [descriptionForCredential], which
48  * describes details of where the credential is being saved, and how. [displaySubtitleText] is only expected
49  * to be used by the 'create' flow, optionally, and describes the saved name of the creating entity.
50  * (E.g. assume a hypothetical provider 'Any Provider' for *passkey* flows with Your@Email.com and
51  * name 'Your', and an rp called 'The App'):
52  *
53  * 'get' flow:
54  *     - [providerIcon] and [providerName] = 'Any Provider' (and it's icon)
55  *     - [displayTitleText] = "Use your saved passkey for The App?"
56  *     - [descriptionForCredential] = "Sign in to The App with your saved passkey for
57  *     Your@gmail.com"
58  *
59  * 'create' flow:
60  *     - [providerIcon] and [providerName] = 'Any Provider' (and it's icon)
61  *     - [displayTitleText] = "Create passkey to sign in to Any Provider?"
62  *     - [subtitle] = "Your"
63  *     - [descriptionForCredential] = "You can use your passkey on other devices. It is saved to
64  *  *     Google Password Manager for Your@gmail.com."
65  * ).
66  *
67  * The above are examples; the credential type can change depending on scenario.
68  */
69 data class BiometricDisplayInfo(
70     val providerIcon: Bitmap,
71     val providerName: String,
72     val displayTitleText: String,
73     val descriptionForCredential: String?,
74     val biometricRequestInfo: BiometricRequestInfo,
75     val displaySubtitleText: CharSequence? = null,
76 )
77 
78 /**
79  * Sets up generic state used by the create and get flows to hold the holistic states for the flow.
80  * These match all the present callback values from [BiometricPrompt], and may be extended to hold
81  * additional states that may improve the flow.
82  */
83 data class BiometricState(
84     val biometricResult: BiometricResult? = null,
85     val biometricError: BiometricError? = null,
86     val biometricStatus: BiometricPromptState = BiometricPromptState.INACTIVE,
87     val biometricCancellationSignal: CancellationSignal = CancellationSignal(),
88 )
89 
90 /**
91  * When a result exists, it must be retrievable. This encapsulates the result
92  * so that should this object exist, the result will be retrievable.
93  */
94 data class BiometricResult(
95     val biometricAuthenticationResult: BiometricPrompt.AuthenticationResult,
96 )
97 
98 /**
99  * Encapsulates the error callback results to easily manage biometric error states in the flow.
100  */
101 data class BiometricError(
102     val errorCode: Int,
103     val errorMessage: CharSequence? = null
104 )
105 
106 /**
107  * This is the entry point to start the integrated biometric prompt for 'get' flows. It captures
108  * information specific to the get flow, along with required shared callbacks and more general
109  * info across both flows, such as the tapped [EntryInfo] or [sendDataToProvider].
110  */
111 fun runBiometricFlowForGet(
112     biometricEntry: EntryInfo,
113     context: Context,
114     openMoreOptionsPage: () -> Unit,
115     sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult?, BiometricError?) -> Unit,
116     onCancelFlowAndFinish: () -> Unit,
117     onIllegalStateAndFinish: (String) -> Unit,
118     getBiometricPromptState: () -> BiometricPromptState,
119     onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
120     onBiometricFailureFallback: (BiometricFlowType) -> Unit,
121     getBiometricCancellationSignal: () -> CancellationSignal,
122     getRequestDisplayInfo: RequestDisplayInfo? = null,
123     getProviderInfoList: List<ProviderInfo>? = null,
124     getProviderDisplayInfo: ProviderDisplayInfo? = null
125 ): Boolean {
126     if (getBiometricPromptState() != BiometricPromptState.INACTIVE) {
127         // Screen is already up, do not re-launch
128         return false
129     }
130     onBiometricPromptStateChange(BiometricPromptState.PENDING)
131     val biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(
132         getRequestDisplayInfo,
133         getProviderInfoList,
134         getProviderDisplayInfo,
135         context, biometricEntry
136     )
137 
138     if (biometricDisplayInfo == null) {
139         onBiometricFailureFallback(BiometricFlowType.GET)
140         return false
141     }
142 
143     val callback: BiometricPrompt.AuthenticationCallback =
144         setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry,
145             onCancelFlowAndFinish, onIllegalStateAndFinish, onBiometricPromptStateChange,
146             getBiometricPromptState)
147 
148     Log.d(TAG, "The BiometricPrompt API call begins for Get.")
149     return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
150         onBiometricFailureFallback, BiometricFlowType.GET, onCancelFlowAndFinish,
151         getBiometricCancellationSignal)
152 }
153 
154 /**
155  * This is the entry point to start the integrated biometric prompt for 'create' flows. It captures
156  * information specific to the create flow, along with required shared callbacks and more general
157  * info across both flows, such as the tapped [EntryInfo] or [sendDataToProvider].
158  */
runBiometricFlowForCreatenull159 fun runBiometricFlowForCreate(
160     biometricEntry: EntryInfo,
161     context: Context,
162     openMoreOptionsPage: () -> Unit,
163     sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult?, BiometricError?) -> Unit,
164     onCancelFlowAndFinish: () -> Unit,
165     onIllegalStateAndFinish: (String) -> Unit,
166     getBiometricPromptState: () -> BiometricPromptState,
167     onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
168     onBiometricFailureFallback: (BiometricFlowType) -> Unit,
169     getBiometricCancellationSignal: () -> CancellationSignal,
170     createRequestDisplayInfo: com.android.credentialmanager.createflow
171     .RequestDisplayInfo? = null,
172     createProviderInfo: EnabledProviderInfo? = null
173 ): Boolean {
174     if (getBiometricPromptState() != BiometricPromptState.INACTIVE) {
175         // Screen is already up, do not re-launch
176         return false
177     }
178     onBiometricPromptStateChange(BiometricPromptState.PENDING)
179     val biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo(
180         createRequestDisplayInfo,
181         createProviderInfo,
182         context, biometricEntry
183     )
184 
185     if (biometricDisplayInfo == null) {
186         onBiometricFailureFallback(BiometricFlowType.CREATE)
187         return false
188     }
189 
190     val callback: BiometricPrompt.AuthenticationCallback =
191         setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry,
192             onCancelFlowAndFinish, onIllegalStateAndFinish, onBiometricPromptStateChange,
193             getBiometricPromptState)
194 
195     Log.d(TAG, "The BiometricPrompt API call begins for Create.")
196     return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
197         onBiometricFailureFallback, BiometricFlowType.CREATE, onCancelFlowAndFinish,
198         getBiometricCancellationSignal)
199 }
200 
201 /**
202  * This will handle the logic for integrating credential manager with the biometric prompt for the
203  * single account biometric experience. This simultaneously handles both the get and create flows,
204  * by retrieving all the data from credential manager, and properly parsing that data into the
205  * biometric prompt. It will fallback in cases where the biometric api cannot be called, or when
206  * only device credentials are requested.
207  */
runBiometricFlownull208 private fun runBiometricFlow(
209         context: Context,
210         biometricDisplayInfo: BiometricDisplayInfo,
211         callback: BiometricPrompt.AuthenticationCallback,
212         openMoreOptionsPage: () -> Unit,
213         onBiometricFailureFallback: (BiometricFlowType) -> Unit,
214         biometricFlowType: BiometricFlowType,
215         onCancelFlowAndFinish: () -> Unit,
216         getBiometricCancellationSignal: () -> CancellationSignal
217 ): Boolean {
218     try {
219         if (!canCallBiometricPrompt(biometricDisplayInfo, context)) {
220             onBiometricFailureFallback(biometricFlowType)
221             return false
222         }
223 
224         val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo,
225             openMoreOptionsPage, biometricDisplayInfo.biometricRequestInfo, onCancelFlowAndFinish)
226 
227         val cancellationSignal = getBiometricCancellationSignal()
228 
229         val executor = getMainExecutor(context)
230 
231         val cryptoOpId = getCryptoOpId(biometricDisplayInfo)
232         if (cryptoOpId != null) {
233             biometricPrompt.authenticate(
234                 BiometricPrompt.CryptoObject(cryptoOpId),
235                 cancellationSignal, executor, callback)
236         } else {
237             biometricPrompt.authenticate(cancellationSignal, executor, callback)
238         }
239     } catch (e: IllegalArgumentException) {
240         Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n")
241         onBiometricFailureFallback(biometricFlowType)
242         return false
243     }
244     return true
245 }
246 
getCryptoOpIdnull247 private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Long? {
248     return biometricDisplayInfo.biometricRequestInfo.opId
249 }
250 
251 /**
252  * Determines if, given the allowed authenticators, the flow should fallback early. This has
253  * consistency because for biometrics to exist, **device credentials must exist**. Thus, fallbacks
254  * occur if *only* device credentials are available, to avoid going right into the PIN screen.
255  * Note that if device credential is the only available modality but not requested, or if none
256  * of the requested modalities are available, we fallback to the normal flow to ensure a selector
257  * shows up.
258  */
canCallBiometricPromptnull259 private fun canCallBiometricPrompt(
260     biometricDisplayInfo: BiometricDisplayInfo,
261     context: Context
262 ): Boolean {
263     val allowedAuthenticators = biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators
264     if (allowedAuthenticators == BiometricManager.Authenticators.DEVICE_CREDENTIAL) {
265         return false
266     }
267 
268     val biometricManager = context.getSystemService(Context.BIOMETRIC_SERVICE) as BiometricManager
269 
270     if (biometricManager.canAuthenticate(allowedAuthenticators) !=
271         BiometricManager.BIOMETRIC_SUCCESS) {
272         return false
273     }
274 
275     if (onlySupportsAtMostDeviceCredentials(biometricManager)) return false
276 
277     return true
278 }
279 
onlySupportsAtMostDeviceCredentialsnull280 private fun onlySupportsAtMostDeviceCredentials(biometricManager: BiometricManager): Boolean {
281     if (biometricManager.canAuthenticate(Authenticators.BIOMETRIC_WEAK) !=
282         BiometricManager.BIOMETRIC_SUCCESS &&
283         biometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG) !=
284         BiometricManager.BIOMETRIC_SUCCESS
285     ) {
286         return true
287     }
288     return false
289 }
290 
containsBiometricAuthenticatorWithDeviceCredentialsnull291 private fun containsBiometricAuthenticatorWithDeviceCredentials(
292     allowedAuthenticators: Int
293 ): Boolean {
294     val allowedAuthContainsDeviceCredential = (allowedAuthenticators ==
295             Authenticators.BIOMETRIC_WEAK or Authenticators.DEVICE_CREDENTIAL) ||
296             (allowedAuthenticators ==
297                     Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL)
298     return allowedAuthContainsDeviceCredential
299 }
300 
301 /**
302  * Sets up the biometric prompt with the UI specific bits.
303  * // TODO(b/333445112) : Pass in opId once dependency is confirmed via CryptoObject
304  */
setupBiometricPromptnull305 private fun setupBiometricPrompt(
306     context: Context,
307     biometricDisplayInfo: BiometricDisplayInfo,
308     openMoreOptionsPage: () -> Unit,
309     biometricRequestInfo: BiometricRequestInfo,
310     onCancelFlowAndFinish: () -> Unit
311 ): BiometricPrompt {
312     val listener =
313         DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> openMoreOptionsPage() }
314 
315     val promptContentViewBuilder = PromptContentViewWithMoreOptionsButton.Builder()
316         .setMoreOptionsButtonListener(context.mainExecutor, listener)
317     biometricDisplayInfo.descriptionForCredential?.let {
318         promptContentViewBuilder.setDescription(it) }
319 
320     val biometricPromptBuilder = BiometricPrompt.Builder(context)
321         .setTitle(biometricDisplayInfo.displayTitleText)
322         .setAllowedAuthenticators(biometricRequestInfo.allowedAuthenticators)
323         .setConfirmationRequired(true)
324         .setLogoBitmap(biometricDisplayInfo.providerIcon)
325         .setLogoDescription(biometricDisplayInfo.providerName)
326         .setContentView(promptContentViewBuilder.build())
327 
328     if (!containsBiometricAuthenticatorWithDeviceCredentials(biometricDisplayInfo
329             .biometricRequestInfo.allowedAuthenticators)) {
330         biometricPromptBuilder.setNegativeButton(context.getString(R.string.string_cancel),
331             getMainExecutor(context)
332         ) { _: DialogInterface?, _: Int -> onCancelFlowAndFinish() }
333     }
334 
335     biometricDisplayInfo.displaySubtitleText?.let { biometricPromptBuilder.setSubtitle(it) }
336 
337     return biometricPromptBuilder.build()
338 }
339 
340 /**
341  * Sets up the biometric authentication callback.
342  */
setupBiometricAuthenticationCallbacknull343 private fun setupBiometricAuthenticationCallback(
344     sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult?, BiometricError?) -> Unit,
345     selectedEntry: EntryInfo,
346     onCancelFlowAndFinish: () -> Unit,
347     onIllegalStateAndFinish: (String) -> Unit,
348     onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
349     getBiometricPromptState: () -> BiometricPromptState,
350 ): BiometricPrompt.AuthenticationCallback {
351     val callback: BiometricPrompt.AuthenticationCallback =
352         object : BiometricPrompt.AuthenticationCallback() {
353             override fun onAuthenticationSucceeded(
354                 authResult: BiometricPrompt.AuthenticationResult?
355             ) {
356                 super.onAuthenticationSucceeded(authResult)
357                 try {
358                     if (authResult != null) {
359                         onBiometricPromptStateChange(BiometricPromptState.COMPLETE)
360                         sendDataToProvider(selectedEntry, authResult, /*authError=*/null)
361                     } else {
362                         onIllegalStateAndFinish("The biometric flow succeeded but unexpectedly " +
363                                 "returned a null value.")
364                     }
365                 } catch (e: Exception) {
366                     onIllegalStateAndFinish("The biometric flow succeeded but failed on handling " +
367                             "the result. See: \n$e\n")
368                 }
369             }
370 
371             override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) {
372                 super.onAuthenticationHelp(helpCode, helpString)
373                 Log.d(TAG, "Authentication help discovered: $helpCode and $helpString")
374             }
375 
376             override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
377                 super.onAuthenticationError(errorCode, errString)
378                 Log.d(TAG, "Authentication error-ed out: $errorCode and $errString")
379                 if (getBiometricPromptState() == BiometricPromptState.CANCELED && errorCode
380                     == BiometricPrompt.BIOMETRIC_ERROR_CANCELED) {
381                     Log.d(TAG, "Developer cancellation signal received. Nothing more to do.")
382                     // This unique edge case means a developer cancellation signal was sent.
383                     return
384                 }
385                 onBiometricPromptStateChange(BiometricPromptState.COMPLETE)
386                 if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED) {
387                     // Note that because the biometric prompt is imbued directly
388                     // into the selector, parity applies to the selector's cancellation instead
389                     // of the provider's biometric prompt cancellation.
390                     onCancelFlowAndFinish()
391                 } else {
392                     sendDataToProvider(selectedEntry, /*authResult=*/null, /*authError=*/
393                         BiometricError(errorCode, errString))
394                 }
395             }
396 
397             override fun onAuthenticationFailed() {
398                 super.onAuthenticationFailed()
399                 Log.d(TAG, "Authentication failed.")
400             }
401         }
402     return callback
403 }
404 
405 /**
406  * Creates the [BiometricDisplayInfo] for get flows, and early handles conditional
407  * checking between the two.  Note that while this method's main purpose is to retrieve the info
408  * required to display the biometric prompt, it acts as a secondary validator to handle any null
409  * checks at the beginning of the biometric flow and supports a quick fallback.
410  * While it's not expected for the flow to be triggered if values are
411  * missing, some values are by default nullable when they are pulled, such as entries. Thus, this
412  * acts as a final validation failsafe, without requiring null checks or null forcing around the
413  * codebase.
414  */
validateAndRetrieveBiometricGetDisplayInfonull415 private fun validateAndRetrieveBiometricGetDisplayInfo(
416     getRequestDisplayInfo: RequestDisplayInfo?,
417     getProviderInfoList: List<ProviderInfo>?,
418     getProviderDisplayInfo: ProviderDisplayInfo?,
419     context: Context,
420     selectedEntry: EntryInfo
421 ): BiometricDisplayInfo? {
422     if (getRequestDisplayInfo != null && getProviderInfoList != null &&
423         getProviderDisplayInfo != null) {
424         if (selectedEntry !is CredentialEntryInfo) { return null }
425         return retrieveBiometricGetDisplayValues(getProviderInfoList,
426             context, getRequestDisplayInfo, selectedEntry)
427     }
428     return null
429 }
430 
431 /**
432  * Creates the [BiometricDisplayInfo] for create flows, and early handles conditional
433  * checking between the two. The reason for this method matches the logic for the
434  * [validateAndRetrieveBiometricGetDisplayInfo] with the only difference being that this is for
435  * the create flow.
436  */
validateAndRetrieveBiometricCreateDisplayInfonull437 private fun validateAndRetrieveBiometricCreateDisplayInfo(
438     createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo?,
439     createProviderInfo: EnabledProviderInfo?,
440     context: Context,
441     selectedEntry: EntryInfo,
442 ): BiometricDisplayInfo? {
443     if (createRequestDisplayInfo != null && createProviderInfo != null) {
444         if (selectedEntry !is CreateOptionInfo) { return null }
445         return retrieveBiometricCreateDisplayValues(createRequestDisplayInfo, createProviderInfo,
446             context, selectedEntry)
447     }
448     return null
449 }
450 
451 /**
452  * Handles the biometric sign in via the 'get credentials' flow.
453  * If any expected value is not present, the flow is considered unreachable and we will fallback
454  * to the original selector. Note that these redundant checks are just failsafe; the original
455  * flow should never reach here with invalid params.
456  */
retrieveBiometricGetDisplayValuesnull457 private fun retrieveBiometricGetDisplayValues(
458     getProviderInfoList: List<ProviderInfo>,
459     context: Context,
460     getRequestDisplayInfo: RequestDisplayInfo,
461     selectedEntry: CredentialEntryInfo,
462 ): BiometricDisplayInfo? {
463     val icon: Bitmap?
464     val providerName: String?
465     val displayTitleText: String?
466     val descriptionText: String?
467     val primaryAccountsProviderInfo = retrievePrimaryAccountProviderInfo(selectedEntry.providerId,
468         getProviderInfoList)
469     icon = primaryAccountsProviderInfo?.icon?.toBitmap()
470     providerName = primaryAccountsProviderInfo?.displayName
471     if (icon == null || providerName == null) {
472         Log.d(TAG, "Unexpectedly found invalid provider information.")
473         return null
474     }
475     if (selectedEntry.biometricRequest == null) {
476         Log.d(TAG, "Unexpectedly in biometric flow without a biometric request.")
477         return null
478     }
479     val singleEntryType = selectedEntry.credentialType
480     val descriptionName = if (singleEntryType == CredentialType.PASSKEY &&
481         !selectedEntry.displayName.isNullOrBlank()) selectedEntry.displayName else
482         selectedEntry.userName
483 
484     // TODO(b/336362538) : In W, utilize updated localization strings
485     displayTitleText = context.getString(
486         generateDisplayTitleTextResCode(singleEntryType),
487         getRequestDisplayInfo.appName
488     )
489 
490     descriptionText = context.getString(
491         R.string.get_dialog_description_single_tap,
492         getRequestDisplayInfo.appName,
493         descriptionName
494     )
495 
496     return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
497         displayTitleText = displayTitleText, descriptionForCredential = descriptionText,
498         biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo)
499 }
500 
501 /**
502  * Handles the biometric sign in via the create credentials flow. Stricter in the get flow in that
503  * if this is called, a result is guaranteed. Specifically, this is guaranteed to return a non-null
504  * value unlike the get counterpart.
505  */
retrieveBiometricCreateDisplayValuesnull506 private fun retrieveBiometricCreateDisplayValues(
507     createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo,
508     createProviderInfo: EnabledProviderInfo,
509     context: Context,
510     selectedEntry: CreateOptionInfo,
511 ): BiometricDisplayInfo {
512     val icon: Bitmap?
513     val providerName: String?
514     val displayTitleText: String?
515     icon = createProviderInfo.icon.toBitmap()
516     providerName = createProviderInfo.displayName
517     displayTitleText = context.getString(
518         getCreateTitleResCode(createRequestDisplayInfo),
519         createRequestDisplayInfo.appName
520     )
521 
522     // TODO(b/330396140) : If footerDescription is null, determine if we need to fallback
523     return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
524         displayTitleText = displayTitleText, descriptionForCredential = selectedEntry
525             .footerDescription, biometricRequestInfo = selectedEntry.biometricRequest
526                 as BiometricRequestInfo, displaySubtitleText = createRequestDisplayInfo.title)
527 }
528 
529 /**
530  * During a get flow with single tap sign in enabled, this will match the credentialEntry that
531  * will single tap with the correct provider info. Namely, it's the first provider info that
532  * contains a matching providerId to the selected entry.
533  */
retrievePrimaryAccountProviderInfonull534 private fun retrievePrimaryAccountProviderInfo(
535     providerId: String,
536     getProviderInfoList: List<ProviderInfo>
537 ): ProviderInfo? {
538     var discoveredProviderInfo: ProviderInfo? = null
539     getProviderInfoList.forEach { provider ->
540         if (provider.id == providerId) {
541             discoveredProviderInfo = provider
542             return@forEach
543         }
544     }
545     return discoveredProviderInfo
546 }
547 
548 const val TAG = "BiometricHandler"
549