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