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