1 /*
<lambda>null2  * Copyright (C) 2022 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.createflow
18 
19 import android.credentials.flags.Flags.selectorUiImprovementsEnabled
20 import android.hardware.biometrics.BiometricPrompt
21 import android.os.CancellationSignal
22 import android.text.TextUtils
23 import androidx.activity.compose.ManagedActivityResultLauncher
24 import androidx.activity.result.ActivityResult
25 import androidx.activity.result.IntentSenderRequest
26 import androidx.compose.foundation.layout.Arrangement
27 import androidx.compose.foundation.layout.Column
28 import androidx.compose.foundation.layout.Row
29 import androidx.compose.foundation.layout.fillMaxWidth
30 import androidx.compose.foundation.layout.padding
31 import androidx.compose.foundation.layout.wrapContentHeight
32 import androidx.compose.material3.Divider
33 import androidx.compose.material.icons.Icons
34 import androidx.compose.material.icons.outlined.NewReleases
35 import androidx.compose.material.icons.filled.Add
36 import androidx.compose.material.icons.filled.ArrowBack
37 import androidx.compose.material.icons.filled.Close
38 import androidx.compose.material.icons.outlined.QrCodeScanner
39 import androidx.compose.runtime.Composable
40 import androidx.compose.runtime.LaunchedEffect
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.graphics.Color
43 import androidx.compose.ui.graphics.asImageBitmap
44 import androidx.compose.ui.platform.LocalContext
45 import androidx.compose.ui.res.stringResource
46 import androidx.compose.ui.unit.Dp
47 import androidx.compose.ui.unit.dp
48 import androidx.core.graphics.drawable.toBitmap
49 import com.android.compose.theme.LocalAndroidColorScheme
50 import com.android.credentialmanager.CredentialSelectorViewModel
51 import com.android.credentialmanager.R
52 import com.android.credentialmanager.common.BiometricError
53 import com.android.credentialmanager.common.BiometricFlowType
54 import com.android.credentialmanager.common.BiometricPromptState
55 import com.android.credentialmanager.model.EntryInfo
56 import com.android.credentialmanager.model.CredentialType
57 import com.android.credentialmanager.common.ProviderActivityState
58 import com.android.credentialmanager.common.material.ModalBottomSheetDefaults
59 import com.android.credentialmanager.common.runBiometricFlowForCreate
60 import com.android.credentialmanager.common.ui.ActionButton
61 import com.android.credentialmanager.common.ui.BodyMediumText
62 import com.android.credentialmanager.common.ui.BodySmallText
63 import com.android.credentialmanager.common.ui.ConfirmButton
64 import com.android.credentialmanager.common.ui.CredentialContainerCard
65 import com.android.credentialmanager.common.ui.CtaButtonRow
66 import com.android.credentialmanager.common.ui.Entry
67 import com.android.credentialmanager.common.ui.HeadlineIcon
68 import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant
69 import com.android.credentialmanager.common.ui.ModalBottomSheet
70 import com.android.credentialmanager.common.ui.MoreOptionTopAppBar
71 import com.android.credentialmanager.common.ui.SheetContainerCard
72 import com.android.credentialmanager.common.ui.HeadlineText
73 import com.android.credentialmanager.logging.CreateCredentialEvent
74 import com.android.credentialmanager.model.creation.CreateOptionInfo
75 import com.android.credentialmanager.model.creation.RemoteInfo
76 import com.android.internal.logging.UiEventLogger.UiEventEnum
77 
78 @Composable
79 fun CreateCredentialScreen(
80     viewModel: CredentialSelectorViewModel,
81     createCredentialUiState: CreateCredentialUiState,
82     providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
83 ) {
84     ModalBottomSheet(
85         sheetContent = {
86             // Hide the sheet content as opposed to the whole bottom sheet to maintain the scrim
87             // background color even when the content should be hidden while waiting for
88             // results from the provider app.
89             when (viewModel.uiState.providerActivityState) {
90                 ProviderActivityState.NOT_APPLICABLE -> {
91                     when (createCredentialUiState.currentScreenState) {
92                         CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
93                                 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
94                                 enabledProviderList = createCredentialUiState.enabledProviders,
95                                 providerInfo = createCredentialUiState
96                                         .activeEntry?.activeProvider!!,
97                                 createOptionInfo =
98                                 createCredentialUiState.activeEntry.activeEntryInfo
99                                         as CreateOptionInfo,
100                                 onOptionSelected = viewModel::createFlowOnEntrySelected,
101                                 onConfirm = viewModel::createFlowOnConfirmEntrySelected,
102                                 onMoreOptionsSelected =
103                                 viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection,
104                                 onLog = { viewModel.logUiEvent(it) },
105                         )
106                         CreateScreenState.BIOMETRIC_SELECTION ->
107                             BiometricSelectionPage(
108                                 biometricEntry = createCredentialUiState
109                                     .activeEntry?.activeEntryInfo,
110                                 onCancelFlowAndFinish = viewModel::onUserCancel,
111                                 onIllegalScreenStateAndFinish = viewModel::onIllegalUiState,
112                                 onMoreOptionSelected =
113                                 viewModel::createFlowOnMoreOptionsOnlySelectedOnCreationSelection,
114                                 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
115                                 enabledProviderInfo = createCredentialUiState
116                                         .activeEntry?.activeProvider!!,
117                                 onBiometricEntrySelected =
118                                 viewModel::createFlowOnEntrySelected,
119                                 fallbackToOriginalFlow =
120                                 viewModel::fallbackFromBiometricToNormalFlow,
121                                 getBiometricPromptState =
122                                 viewModel::getBiometricPromptStateStatus,
123                                 onBiometricPromptStateChange =
124                                 viewModel::onBiometricPromptStateChange,
125                                 getBiometricCancellationSignal =
126                                 viewModel::getBiometricCancellationSignal,
127                                 onLog = { viewModel.logUiEvent(it) },
128                             )
129                         CreateScreenState.MORE_OPTIONS_SELECTION_ONLY -> MoreOptionsSelectionCard(
130                                 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
131                                 enabledProviderList = createCredentialUiState.enabledProviders,
132                                 disabledProviderList = createCredentialUiState.disabledProviders,
133                                 sortedCreateOptionsPairs =
134                                 createCredentialUiState.sortedCreateOptionsPairs,
135                                 onBackCreationSelectionButtonSelected =
136                                 viewModel::createFlowOnBackCreationSelectionButtonSelected,
137                                 onOptionSelected =
138                                 viewModel::createFlowOnEntrySelectedFromMoreOptionScreen,
139                                 onDisabledProvidersSelected =
140                                 viewModel::createFlowOnLaunchSettings,
141                                 onRemoteEntrySelected = viewModel::createFlowOnEntrySelected,
142                                 onLog = { viewModel.logUiEvent(it) },
143                                 customTopAppBar = { MoreOptionTopAppBar(
144                                         text = stringResource(
145                                                 R.string.save_credential_to_title,
146                                                 when (createCredentialUiState.requestDisplayInfo
147                                                         .type) {
148                                                     CredentialType.PASSKEY ->
149                                                         stringResource(R.string.passkey)
150                                                     CredentialType.PASSWORD ->
151                                                         stringResource(R.string.password)
152                                                     CredentialType.UNKNOWN -> stringResource(
153                                                             R.string.sign_in_info)
154                                                 }
155                                         ),
156                                         onNavigationIconClicked = viewModel::onUserCancel,
157                                         bottomPadding = 16.dp,
158                                         navigationIcon = Icons.Filled.Close,
159                                         navigationIconContentDescription = stringResource(
160                                                 R.string.accessibility_close_button
161                                         )
162                                 )}
163                         )
164                         CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
165                                 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
166                                 enabledProviderList = createCredentialUiState.enabledProviders,
167                                 disabledProviderList = createCredentialUiState.disabledProviders,
168                                 sortedCreateOptionsPairs =
169                                 createCredentialUiState.sortedCreateOptionsPairs,
170                                 onBackCreationSelectionButtonSelected =
171                                 viewModel::createFlowOnBackCreationSelectionButtonSelected,
172                                 onOptionSelected =
173                                 viewModel::createFlowOnEntrySelectedFromMoreOptionScreen,
174                                 onDisabledProvidersSelected =
175                                 viewModel::createFlowOnLaunchSettings,
176                                 onRemoteEntrySelected = viewModel::createFlowOnEntrySelected,
177                                 onLog = { viewModel.logUiEvent(it) },
178                         )
179                         CreateScreenState.DEFAULT_PROVIDER_CONFIRMATION -> {
180                             if (createCredentialUiState.activeEntry == null) {
181                                 viewModel.onIllegalUiState("Expect active entry to be non-null" +
182                                         " upon default provider dialog.")
183                             } else {
184                                 NonDefaultUsageConfirmationCard(
185                                         selectedEntry = createCredentialUiState.activeEntry,
186                                         onIllegalScreenState = viewModel::onIllegalUiState,
187                                         onLaunchSettings =
188                                         viewModel::createFlowOnLaunchSettings,
189                                         onUseOnceSelected = viewModel::createFlowOnUseOnceSelected,
190                                         onLog = { viewModel.logUiEvent(it) },
191                                 )
192                             }
193                         }
194                         CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard(
195                                 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
196                                 activeRemoteEntry =
197                                 createCredentialUiState.activeEntry?.activeEntryInfo!!,
198                                 onOptionSelected = viewModel::createFlowOnEntrySelected,
199                                 onConfirm = viewModel::createFlowOnConfirmEntrySelected,
200                                 onLog = { viewModel.logUiEvent(it) },
201                         )
202                     }
203                 }
204                 ProviderActivityState.READY_TO_LAUNCH -> {
205                     // This is a native bug from ModalBottomSheet. For now, use the temporary
206                     // solution of not having an empty state.
207                     if (viewModel.uiState.isAutoSelectFlow) {
208                         Divider(
209                             thickness = Dp.Hairline, color = ModalBottomSheetDefaults.scrimColor
210                         )
211                     }
212                     // Launch only once per providerActivityState change so that the provider
213                     // UI will not be accidentally launched twice.
214                     LaunchedEffect(viewModel.uiState.providerActivityState) {
215                         viewModel.launchProviderUi(providerActivityLauncher)
216                     }
217                     viewModel.uiMetrics.log(
218                             CreateCredentialEvent
219                                     .CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_READY_TO_LAUNCH)
220                 }
221                 ProviderActivityState.PENDING -> {
222                     if (viewModel.uiState.isAutoSelectFlow) {
223                         Divider(
224                             thickness = Dp.Hairline, color = ModalBottomSheetDefaults.scrimColor
225                         )
226                     }
227                     // Hide our content when the provider activity is active.
228                     viewModel.uiMetrics.log(
229                             CreateCredentialEvent.CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_PENDING)
230                 }
231             }
232         },
233         onDismiss = viewModel::onUserCancel,
234         isInitialRender = viewModel.uiState.isInitialRender,
235         isAutoSelectFlow = viewModel.uiState.isAutoSelectFlow,
236         onInitialRenderComplete = viewModel::onInitialRenderComplete,
237     )
238 }
239 
240 @Composable
MoreOptionsSelectionCardnull241 fun MoreOptionsSelectionCard(
242     requestDisplayInfo: RequestDisplayInfo,
243     enabledProviderList: List<EnabledProviderInfo>,
244     disabledProviderList: List<DisabledProviderInfo>?,
245     sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
246     onBackCreationSelectionButtonSelected: () -> Unit,
247     onOptionSelected: (ActiveEntry) -> Unit,
248     onDisabledProvidersSelected: () -> Unit,
249     onRemoteEntrySelected: (EntryInfo) -> Unit,
250     onLog: @Composable (UiEventEnum) -> Unit,
251     customTopAppBar: (@Composable() () -> Unit)? = null
252 ) {
253     SheetContainerCard(topAppBar = {
254         if (customTopAppBar != null) {
255             customTopAppBar()
256         } else {
257             MoreOptionTopAppBar(
258                     text = stringResource(
259                             R.string.save_credential_to_title,
260                             when (requestDisplayInfo.type) {
261                                 CredentialType.PASSKEY ->
262                                     stringResource(R.string.passkey)
263                                 CredentialType.PASSWORD ->
264                                     stringResource(R.string.password)
265                                 CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info)
266                             }
267                     ),
268                     onNavigationIconClicked = onBackCreationSelectionButtonSelected,
269                     bottomPadding = 16.dp,
270                     navigationIcon = Icons.Filled.ArrowBack,
271                     navigationIconContentDescription = stringResource(
272                             R.string.accessibility_back_arrow_button
273                     )
274             )
275         }
276     }) {
277         // bottom padding already
278         item {
279             CredentialContainerCard {
280                 Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
281                     sortedCreateOptionsPairs.forEach { entry ->
282                         MoreOptionsInfoRow(
283                             requestDisplayInfo = requestDisplayInfo,
284                             providerInfo = entry.second,
285                             createOptionInfo = entry.first,
286                             onOptionSelected = {
287                                 onOptionSelected(
288                                     ActiveEntry(
289                                         entry.second,
290                                         entry.first
291                                     )
292                                 )
293                             }
294                         )
295                     }
296                     MoreOptionsDisabledProvidersRow(
297                         disabledProviders = disabledProviderList,
298                         onDisabledProvidersSelected =
299                         onDisabledProvidersSelected,
300                     )
301                     enabledProviderList.forEach {
302                         if (it.remoteEntry != null) {
303                             RemoteEntryRow(
304                                 remoteInfo = it.remoteEntry!!,
305                                 onRemoteEntrySelected = onRemoteEntrySelected,
306                             )
307                             return@forEach
308                         }
309                     }
310                 }
311             }
312         }
313     }
314     onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_MORE_OPTIONS_SELECTION)
315 }
316 
317 @Composable
NonDefaultUsageConfirmationCardnull318 fun NonDefaultUsageConfirmationCard(
319         selectedEntry: ActiveEntry,
320         onIllegalScreenState: (String) -> Unit,
321         onLaunchSettings: () -> Unit,
322         onUseOnceSelected: () -> Unit,
323         onLog: @Composable (UiEventEnum) -> Unit,
324 ) {
325     val entryInfo = selectedEntry.activeEntryInfo
326     if (entryInfo !is CreateOptionInfo) {
327         onIllegalScreenState("Encountered unexpected type of entry during the default provider" +
328             " dialog: ${entryInfo::class}")
329         return
330     }
331     SheetContainerCard {
332         item { HeadlineIcon(imageVector = Icons.Outlined.NewReleases) }
333         item { Divider(thickness = 16.dp, color = Color.Transparent) }
334         item {
335             HeadlineText(
336                 text = stringResource(
337                     R.string.use_provider_for_all_title, selectedEntry.activeProvider.displayName)
338             )
339         }
340         item { Divider(thickness = 24.dp, color = Color.Transparent) }
341         item {
342             Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
343                 BodyMediumText(text = stringResource(
344                     R.string.use_provider_for_all_description, entryInfo.userProviderDisplayName))
345             }
346         }
347         item { Divider(thickness = 24.dp, color = Color.Transparent) }
348         item {
349             CtaButtonRow(
350                 leftButton = {
351                     ActionButton(
352                         stringResource(R.string.settings),
353                         onClick = onLaunchSettings,
354                     )
355                 },
356                 rightButton = {
357                     ConfirmButton(
358                         stringResource(R.string.use_once),
359                         onClick = onUseOnceSelected,
360                     )
361                 },
362             )
363         }
364     }
365     onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_MORE_OPTIONS_ROW_INTRO)
366 }
367 
368 @Composable
CreationSelectionCardnull369 fun CreationSelectionCard(
370     requestDisplayInfo: RequestDisplayInfo,
371     enabledProviderList: List<EnabledProviderInfo>,
372     providerInfo: EnabledProviderInfo,
373     createOptionInfo: CreateOptionInfo,
374     onOptionSelected: (EntryInfo) -> Unit,
375     onConfirm: () -> Unit,
376     onMoreOptionsSelected: () -> Unit,
377     onLog: @Composable (UiEventEnum) -> Unit,
378 ) {
379     SheetContainerCard {
380         item {
381             HeadlineIcon(
382                 bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
383                 tint = Color.Unspecified,
384             )
385         }
386         item { Divider(thickness = 4.dp, color = Color.Transparent) }
387         item { LargeLabelTextOnSurfaceVariant(text = providerInfo.displayName) }
388         item { Divider(thickness = 16.dp, color = Color.Transparent) }
389         item {
390             HeadlineText(
391                 text = stringResource(
392                     getCreateTitleResCode(requestDisplayInfo),
393                     requestDisplayInfo.appName)
394             )
395         }
396         item { Divider(thickness = 24.dp, color = Color.Transparent) }
397 
398         val footerDescription = createOptionInfo.footerDescription
399         if (selectorUiImprovementsEnabled()) {
400             if (!footerDescription.isNullOrBlank()) {
401                 item {
402                     Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
403                         BodyMediumText(text = footerDescription)
404                     }
405                 }
406                 item { Divider(thickness = 24.dp, color = Color.Transparent) }
407             }
408         }
409         item {
410             CredentialContainerCard {
411                 PrimaryCreateOptionRow(
412                     requestDisplayInfo = requestDisplayInfo,
413                     entryInfo = createOptionInfo,
414                     onOptionSelected = onOptionSelected
415                 )
416             }
417         }
418         item { Divider(thickness = 24.dp, color = Color.Transparent) }
419         var createOptionsSize = 0
420         var remoteEntry: RemoteInfo? = null
421         enabledProviderList.forEach { enabledProvider ->
422             if (enabledProvider.remoteEntry != null) {
423                 remoteEntry = enabledProvider.remoteEntry
424             }
425             createOptionsSize += enabledProvider.sortedCreateOptions.size
426         }
427         val shouldShowMoreOptionsButton = createOptionsSize > 1 || remoteEntry != null
428         item {
429             CtaButtonRow(
430                 leftButton = if (shouldShowMoreOptionsButton) {
431                     {
432                         ActionButton(
433                             stringResource(R.string.string_more_options),
434                             onMoreOptionsSelected
435                         )
436                     }
437                 } else null,
438                 rightButton = {
439                     ConfirmButton(
440                         stringResource(R.string.string_continue),
441                         onClick = onConfirm
442                     )
443                 },
444             )
445         }
446         if (!selectorUiImprovementsEnabled()) {
447             if (footerDescription != null && footerDescription.length > 0) {
448                 item {
449                     Divider(
450                         thickness = 1.dp,
451                         color = LocalAndroidColorScheme.current.outlineVariant,
452                         modifier = Modifier.padding(vertical = 16.dp)
453                     )
454                 }
455                 item {
456                     Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
457                         BodySmallText(text = footerDescription)
458                     }
459                 }
460             }
461         }
462     }
463     onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_CREATION_OPTION_SELECTION)
464 }
465 
466 @Composable
ExternalOnlySelectionCardnull467 fun ExternalOnlySelectionCard(
468     requestDisplayInfo: RequestDisplayInfo,
469     activeRemoteEntry: EntryInfo,
470     onOptionSelected: (EntryInfo) -> Unit,
471     onConfirm: () -> Unit,
472     onLog: @Composable (UiEventEnum) -> Unit,
473 ) {
474     SheetContainerCard {
475         item { HeadlineIcon(imageVector = Icons.Outlined.QrCodeScanner) }
476         item { Divider(thickness = 16.dp, color = Color.Transparent) }
477         item {
478             HeadlineText(
479                 text = stringResource(
480                     when (requestDisplayInfo.type) {
481                         CredentialType.PASSKEY -> R.string.create_passkey_in_other_device_title
482                         CredentialType.PASSWORD -> R.string.save_password_on_other_device_title
483                         else -> R.string.save_sign_in_on_other_device_title
484                     }
485                 )
486             )
487         }
488         item { Divider(thickness = 24.dp, color = Color.Transparent) }
489         item {
490             CredentialContainerCard {
491                 PrimaryCreateOptionRow(
492                     requestDisplayInfo = requestDisplayInfo,
493                     entryInfo = activeRemoteEntry,
494                     onOptionSelected = onOptionSelected
495                 )
496             }
497         }
498         item { Divider(thickness = 24.dp, color = Color.Transparent) }
499         item {
500             CtaButtonRow(
501                 rightButton = {
502                     ConfirmButton(
503                         stringResource(R.string.string_continue),
504                         onClick = onConfirm
505                     )
506                 },
507             )
508         }
509     }
510     onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION)
511 }
512 
513 @Composable
PrimaryCreateOptionRownull514 fun PrimaryCreateOptionRow(
515     requestDisplayInfo: RequestDisplayInfo,
516     entryInfo: EntryInfo,
517     onOptionSelected: (EntryInfo) -> Unit
518 ) {
519     Entry(
520         onClick = { onOptionSelected(entryInfo) },
521         iconImageBitmap = ((entryInfo as? CreateOptionInfo)?.profileIcon
522             ?: requestDisplayInfo.typeIcon)
523             .toBitmap().asImageBitmap(),
524         shouldApplyIconImageBitmapTint = !(entryInfo is CreateOptionInfo &&
525             entryInfo.profileIcon != null),
526         entryHeadlineText = requestDisplayInfo.title,
527         entrySecondLineText = when (requestDisplayInfo.type) {
528             CredentialType.PASSKEY -> {
529                 if (!TextUtils.isEmpty(requestDisplayInfo.subtitle)) {
530                     requestDisplayInfo.subtitle + " • " + stringResource(
531                         R.string.passkey_before_subtitle
532                     )
533                 } else {
534                     stringResource(R.string.passkey_before_subtitle)
535                 }
536             }
537             // Set passwordValue instead
538             CredentialType.PASSWORD -> null
539             CredentialType.UNKNOWN -> requestDisplayInfo.subtitle
540         },
541         passwordValue =
542         if (requestDisplayInfo.type == CredentialType.PASSWORD)
543         // This subtitle would never be null for create password
544             requestDisplayInfo.subtitle ?: ""
545         else null,
546         enforceOneLine = true,
547     )
548 }
549 
550 @Composable
MoreOptionsInfoRownull551 fun MoreOptionsInfoRow(
552     requestDisplayInfo: RequestDisplayInfo,
553     providerInfo: EnabledProviderInfo,
554     createOptionInfo: CreateOptionInfo,
555     onOptionSelected: () -> Unit
556 ) {
557     Entry(
558         onClick = onOptionSelected,
559         iconImageBitmap = providerInfo.icon.toBitmap().asImageBitmap(),
560         entryHeadlineText = providerInfo.displayName,
561         entrySecondLineText = createOptionInfo.userProviderDisplayName,
562         entryThirdLineText =
563         if (requestDisplayInfo.type == CredentialType.PASSKEY ||
564             requestDisplayInfo.type == CredentialType.PASSWORD) {
565             val passwordCount = createOptionInfo.passwordCount
566             val passkeyCount = createOptionInfo.passkeyCount
567             if (passwordCount != null && passkeyCount != null) {
568                 stringResource(
569                     R.string.more_options_usage_passwords_passkeys,
570                     passwordCount,
571                     passkeyCount
572                 )
573             } else if (passwordCount != null) {
574                 stringResource(
575                     R.string.more_options_usage_passwords,
576                     passwordCount
577                 )
578             } else if (passkeyCount != null) {
579                 stringResource(
580                     R.string.more_options_usage_passkeys,
581                     passkeyCount
582                 )
583             } else {
584                 null
585             }
586         } else {
587             val totalCredentialCount = createOptionInfo.totalCredentialCount
588             if (totalCredentialCount != null) {
589                 stringResource(
590                     R.string.more_options_usage_credentials,
591                     totalCredentialCount
592                 )
593             } else {
594                 null
595             }
596         },
597     )
598 }
599 
600 @Composable
MoreOptionsDisabledProvidersRownull601 fun MoreOptionsDisabledProvidersRow(
602     disabledProviders: List<ProviderInfo>?,
603     onDisabledProvidersSelected: () -> Unit,
604 ) {
605     if (disabledProviders != null && disabledProviders.isNotEmpty()) {
606         Entry(
607             onClick = onDisabledProvidersSelected,
608             iconImageVector = Icons.Filled.Add,
609             entryHeadlineText = stringResource(R.string.other_password_manager),
610             entrySecondLineText = disabledProviders.joinToString(separator = " • ") {
611                 it.displayName
612             },
613         )
614     }
615 }
616 
617 @Composable
RemoteEntryRownull618 fun RemoteEntryRow(
619     remoteInfo: RemoteInfo,
620     onRemoteEntrySelected: (RemoteInfo) -> Unit,
621 ) {
622     Entry(
623         onClick = { onRemoteEntrySelected(remoteInfo) },
624         iconImageVector = Icons.Outlined.QrCodeScanner,
625         entryHeadlineText = stringResource(R.string.another_device),
626     )
627 }
628 
629 @Composable
630 internal fun BiometricSelectionPage(
631     biometricEntry: EntryInfo?,
632     onMoreOptionSelected: () -> Unit,
633     requestDisplayInfo: RequestDisplayInfo,
634     enabledProviderInfo: EnabledProviderInfo,
635     onBiometricEntrySelected: (
636         EntryInfo,
637         BiometricPrompt.AuthenticationResult?,
638         BiometricError?
639     ) -> Unit,
640     onCancelFlowAndFinish: () -> Unit,
641     onIllegalScreenStateAndFinish: (String) -> Unit,
642     fallbackToOriginalFlow: (BiometricFlowType) -> Unit,
643     getBiometricPromptState: () -> BiometricPromptState,
644     onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
645     getBiometricCancellationSignal: () -> CancellationSignal,
646     onLog: @Composable (UiEventEnum) -> Unit
647 ) {
648     if (biometricEntry == null) {
649         fallbackToOriginalFlow(BiometricFlowType.CREATE)
650         return
651     }
652     val biometricFlowCalled = runBiometricFlowForCreate(
653         biometricEntry = biometricEntry,
654         context = LocalContext.current,
655         openMoreOptionsPage = onMoreOptionSelected,
656         sendDataToProvider = onBiometricEntrySelected,
657         onCancelFlowAndFinish = onCancelFlowAndFinish,
658         getBiometricPromptState = getBiometricPromptState,
659         onBiometricPromptStateChange = onBiometricPromptStateChange,
660         createRequestDisplayInfo = requestDisplayInfo,
661         createProviderInfo = enabledProviderInfo,
662         onBiometricFailureFallback = fallbackToOriginalFlow,
663         onIllegalStateAndFinish = onIllegalScreenStateAndFinish,
664         getBiometricCancellationSignal = getBiometricCancellationSignal
665     )
666     if (biometricFlowCalled) {
667         onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED)
668     }
669 }
670