• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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.getflow
18 
19 import android.graphics.drawable.Drawable
20 import android.text.TextUtils
21 import androidx.activity.compose.ManagedActivityResultLauncher
22 import androidx.activity.result.ActivityResult
23 import androidx.activity.result.IntentSenderRequest
24 import androidx.compose.foundation.layout.Arrangement
25 import androidx.compose.foundation.layout.Column
26 import androidx.compose.foundation.layout.PaddingValues
27 import androidx.compose.foundation.layout.fillMaxWidth
28 import androidx.compose.foundation.layout.heightIn
29 import androidx.compose.foundation.layout.padding
30 import androidx.compose.foundation.layout.wrapContentHeight
31 import androidx.compose.foundation.lazy.items
32 import androidx.compose.material.icons.Icons
33 import androidx.compose.material.icons.outlined.QrCodeScanner
34 import androidx.compose.material3.Divider
35 import androidx.compose.material3.TextButton
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.LaunchedEffect
38 import androidx.compose.runtime.mutableStateOf
39 import androidx.compose.runtime.remember
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.graphics.Color
42 import androidx.compose.ui.graphics.asImageBitmap
43 import androidx.compose.ui.res.painterResource
44 import androidx.compose.ui.res.stringResource
45 import androidx.compose.ui.text.TextLayoutResult
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.ActionEntry
57 import com.android.credentialmanager.common.ui.ConfirmButton
58 import com.android.credentialmanager.common.ui.CredentialContainerCard
59 import com.android.credentialmanager.common.ui.CtaButtonRow
60 import com.android.credentialmanager.common.ui.Entry
61 import com.android.credentialmanager.common.ui.ModalBottomSheet
62 import com.android.credentialmanager.common.ui.MoreOptionTopAppBar
63 import com.android.credentialmanager.common.ui.SheetContainerCard
64 import com.android.credentialmanager.common.ui.SnackbarActionText
65 import com.android.credentialmanager.common.ui.HeadlineText
66 import com.android.credentialmanager.common.ui.CredentialListSectionHeader
67 import com.android.credentialmanager.common.ui.HeadlineIcon
68 import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant
69 import com.android.credentialmanager.common.ui.Snackbar
70 import com.android.credentialmanager.logging.GetCredentialEvent
71 import com.android.credentialmanager.userAndDisplayNameForPasskey
72 import com.android.internal.logging.UiEventLogger.UiEventEnum
73 
74 @Composable
75 fun GetCredentialScreen(
76     viewModel: CredentialSelectorViewModel,
77     getCredentialUiState: GetCredentialUiState,
78     providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
79 ) {
80     if (getCredentialUiState.currentScreenState == GetScreenState.REMOTE_ONLY) {
81         RemoteCredentialSnackBarScreen(
82             onClick = viewModel::getFlowOnMoreOptionOnSnackBarSelected,
83             onCancel = viewModel::onUserCancel,
84             onLog = { viewModel.logUiEvent(it) },
85         )
86         viewModel.uiMetrics.log(GetCredentialEvent.CREDMAN_GET_CRED_SCREEN_REMOTE_ONLY)
87     } else if (getCredentialUiState.currentScreenState
88         == GetScreenState.UNLOCKED_AUTH_ENTRIES_ONLY) {
89         EmptyAuthEntrySnackBarScreen(
90             authenticationEntryList =
91             getCredentialUiState.providerDisplayInfo.authenticationEntryList,
92             onCancel = viewModel::silentlyFinishActivity,
93             onLastLockedAuthEntryNotFound = viewModel::onLastLockedAuthEntryNotFoundError,
94             onLog = { viewModel.logUiEvent(it) },
95         )
96         viewModel.uiMetrics.log(GetCredentialEvent
97                 .CREDMAN_GET_CRED_SCREEN_UNLOCKED_AUTH_ENTRIES_ONLY)
98     } else {
99         ModalBottomSheet(
100             sheetContent = {
101                 // Hide the sheet content as opposed to the whole bottom sheet to maintain the scrim
102                 // background color even when the content should be hidden while waiting for
103                 // results from the provider app.
104                 when (viewModel.uiState.providerActivityState) {
105                     ProviderActivityState.NOT_APPLICABLE -> {
106                         if (getCredentialUiState.currentScreenState
107                             == GetScreenState.PRIMARY_SELECTION) {
108                             PrimarySelectionCard(
109                                 requestDisplayInfo = getCredentialUiState.requestDisplayInfo,
110                                 providerDisplayInfo = getCredentialUiState.providerDisplayInfo,
111                                 providerInfoList = getCredentialUiState.providerInfoList,
112                                 activeEntry = getCredentialUiState.activeEntry,
113                                 onEntrySelected = viewModel::getFlowOnEntrySelected,
114                                 onConfirm = viewModel::getFlowOnConfirmEntrySelected,
115                                 onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected,
116                                 onLog = { viewModel.logUiEvent(it) },
117                             )
118                             viewModel.uiMetrics.log(GetCredentialEvent
119                                     .CREDMAN_GET_CRED_SCREEN_PRIMARY_SELECTION)
120                         } else {
121                             AllSignInOptionCard(
122                                 providerInfoList = getCredentialUiState.providerInfoList,
123                                 providerDisplayInfo = getCredentialUiState.providerDisplayInfo,
124                                 onEntrySelected = viewModel::getFlowOnEntrySelected,
125                                 onBackButtonClicked =
126                                 if (getCredentialUiState.isNoAccount)
127                                     viewModel::getFlowOnBackToHybridSnackBarScreen
128                                 else viewModel::getFlowOnBackToPrimarySelectionScreen,
129                                 onCancel = viewModel::onUserCancel,
130                                 onLog = { viewModel.logUiEvent(it) },
131                             )
132                             viewModel.uiMetrics.log(GetCredentialEvent
133                                     .CREDMAN_GET_CRED_SCREEN_ALL_SIGN_IN_OPTIONS)
134                         }
135                     }
136                     ProviderActivityState.READY_TO_LAUNCH -> {
137                         // This is a native bug from ModalBottomSheet. For now, use the temporary
138                         // solution of not having an empty state.
139                         if (viewModel.uiState.isAutoSelectFlow) {
140                             Divider(
141                                 thickness = Dp.Hairline, color = ModalBottomSheetDefaults.scrimColor
142                             )
143                         }
144                         // Launch only once per providerActivityState change so that the provider
145                         // UI will not be accidentally launched twice.
146                         LaunchedEffect(viewModel.uiState.providerActivityState) {
147                             viewModel.launchProviderUi(providerActivityLauncher)
148                         }
149                         viewModel.uiMetrics.log(GetCredentialEvent
150                                 .CREDMAN_GET_CRED_PROVIDER_ACTIVITY_READY_TO_LAUNCH)
151                     }
152                     ProviderActivityState.PENDING -> {
153                         if (viewModel.uiState.isAutoSelectFlow) {
154                             Divider(
155                                 thickness = Dp.Hairline, color = ModalBottomSheetDefaults.scrimColor
156                             )
157                         }
158                         // Hide our content when the provider activity is active.
159                         viewModel.uiMetrics.log(GetCredentialEvent
160                                 .CREDMAN_GET_CRED_PROVIDER_ACTIVITY_PENDING)
161                     }
162                 }
163             },
164             onDismiss = viewModel::onUserCancel,
165             isInitialRender = viewModel.uiState.isInitialRender,
166             isAutoSelectFlow = viewModel.uiState.isAutoSelectFlow,
167             onInitialRenderComplete = viewModel::onInitialRenderComplete,
168         )
169     }
170 }
171 
172 /** Draws the primary credential selection page. */
173 @Composable
PrimarySelectionCardnull174 fun PrimarySelectionCard(
175     requestDisplayInfo: RequestDisplayInfo,
176     providerDisplayInfo: ProviderDisplayInfo,
177     providerInfoList: List<ProviderInfo>,
178     activeEntry: BaseEntry?,
179     onEntrySelected: (BaseEntry) -> Unit,
180     onConfirm: () -> Unit,
181     onMoreOptionSelected: () -> Unit,
182     onLog: @Composable (UiEventEnum) -> Unit,
183 ) {
184     val showMoreForTruncatedEntry = remember { mutableStateOf(false) }
185     val sortedUserNameToCredentialEntryList =
186         providerDisplayInfo.sortedUserNameToCredentialEntryList
187     val authenticationEntryList = providerDisplayInfo.authenticationEntryList
188     SheetContainerCard {
189         val preferTopBrandingContent = requestDisplayInfo.preferTopBrandingContent
190         if (preferTopBrandingContent != null) {
191             item {
192                 HeadlineProviderIconAndName(
193                     preferTopBrandingContent.icon,
194                     preferTopBrandingContent.displayName
195                 )
196             }
197         } else {
198             // When only one provider (not counting the remote-only provider) exists, display that
199             // provider's icon + name up top.
200             val providersWithActualEntries = providerInfoList.filter {
201                 it.credentialEntryList.isNotEmpty() || it.authenticationEntryList.isNotEmpty()
202             }
203             if (providersWithActualEntries.size == 1) {
204                 // First should always work but just to be safe.
205                 val providerInfo = providersWithActualEntries.firstOrNull()
206                 if (providerInfo != null) {
207                     item {
208                         HeadlineProviderIconAndName(
209                             providerInfo.icon,
210                             providerInfo.displayName
211                         )
212                     }
213                 }
214             }
215         }
216 
217         val hasSingleEntry = (sortedUserNameToCredentialEntryList.size == 1 &&
218             authenticationEntryList.isEmpty()) || (sortedUserNameToCredentialEntryList.isEmpty() &&
219             authenticationEntryList.size == 1)
220         item {
221             if (requestDisplayInfo.preferIdentityDocUi) {
222                 HeadlineText(
223                     text = stringResource(
224                         if (hasSingleEntry) {
225                             R.string.get_dialog_title_use_info_on
226                         } else {
227                             R.string.get_dialog_title_choose_option_for
228                         },
229                         requestDisplayInfo.appName
230                     ),
231                 )
232             } else {
233                 HeadlineText(
234                     text = stringResource(
235                         if (hasSingleEntry) {
236                             val singleEntryType = sortedUserNameToCredentialEntryList.firstOrNull()
237                                 ?.sortedCredentialEntryList?.firstOrNull()?.credentialType
238                             if (singleEntryType == CredentialType.PASSKEY)
239                                 R.string.get_dialog_title_use_passkey_for
240                             else if (singleEntryType == CredentialType.PASSWORD)
241                                 R.string.get_dialog_title_use_password_for
242                             else if (authenticationEntryList.isNotEmpty())
243                                 R.string.get_dialog_title_unlock_options_for
244                             else R.string.get_dialog_title_use_sign_in_for
245                         } else {
246                             if (authenticationEntryList.isNotEmpty() ||
247                                 sortedUserNameToCredentialEntryList.any { perNameEntryList ->
248                                     perNameEntryList.sortedCredentialEntryList.any { entry ->
249                                         entry.credentialType != CredentialType.PASSWORD &&
250                                             entry.credentialType != CredentialType.PASSKEY
251                                     }
252                                 }
253                             )
254                                 R.string.get_dialog_title_choose_sign_in_for
255                             else
256                                 R.string.get_dialog_title_choose_saved_sign_in_for
257                         },
258                         requestDisplayInfo.appName
259                     ),
260                 )
261             }
262         }
263         item { Divider(thickness = 24.dp, color = Color.Transparent) }
264         item {
265             CredentialContainerCard {
266                 Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
267                     val usernameForCredentialSize = sortedUserNameToCredentialEntryList.size
268                     val authenticationEntrySize = authenticationEntryList.size
269                     // If true, render a view more button for the single truncated entry on the
270                     // front page.
271                     // Show max 4 entries in this primary page
272                     if (usernameForCredentialSize + authenticationEntrySize <= 4) {
273                         sortedUserNameToCredentialEntryList.forEach {
274                             CredentialEntryRow(
275                                 credentialEntryInfo = it.sortedCredentialEntryList.first(),
276                                 onEntrySelected = onEntrySelected,
277                                 enforceOneLine = true,
278                                 onTextLayout = {
279                                     showMoreForTruncatedEntry.value = it.hasVisualOverflow
280                                 }
281                             )
282                         }
283                         authenticationEntryList.forEach {
284                             AuthenticationEntryRow(
285                                 authenticationEntryInfo = it,
286                                 onEntrySelected = onEntrySelected,
287                                 enforceOneLine = true,
288                             )
289                         }
290                     } else if (usernameForCredentialSize < 4) {
291                         sortedUserNameToCredentialEntryList.forEach {
292                             CredentialEntryRow(
293                                 credentialEntryInfo = it.sortedCredentialEntryList.first(),
294                                 onEntrySelected = onEntrySelected,
295                                 enforceOneLine = true,
296                             )
297                         }
298                         authenticationEntryList.take(4 - usernameForCredentialSize).forEach {
299                             AuthenticationEntryRow(
300                                 authenticationEntryInfo = it,
301                                 onEntrySelected = onEntrySelected,
302                                 enforceOneLine = true,
303                             )
304                         }
305                     } else {
306                         sortedUserNameToCredentialEntryList.take(4).forEach {
307                             CredentialEntryRow(
308                                 credentialEntryInfo = it.sortedCredentialEntryList.first(),
309                                 onEntrySelected = onEntrySelected,
310                                 enforceOneLine = true,
311                             )
312                         }
313                     }
314                 }
315             }
316         }
317         item { Divider(thickness = 24.dp, color = Color.Transparent) }
318         var totalEntriesCount = sortedUserNameToCredentialEntryList
319             .flatMap { it.sortedCredentialEntryList }.size + authenticationEntryList
320             .size + providerInfoList.flatMap { it.actionEntryList }.size
321         if (providerDisplayInfo.remoteEntry != null) totalEntriesCount += 1
322         // Row horizontalArrangement differs on only one actionButton(should place on most
323         // left)/only one confirmButton(should place on most right)/two buttons exist the same
324         // time(should be one on the left, one on the right)
325         item {
326             CtaButtonRow(
327                 leftButton = if (totalEntriesCount > 1) {
328                     {
329                         ActionButton(
330                             stringResource(R.string.get_dialog_title_sign_in_options),
331                             onMoreOptionSelected
332                         )
333                     }
334                 } else if (showMoreForTruncatedEntry.value) {
335                     {
336                         ActionButton(
337                             stringResource(R.string.button_label_view_more),
338                             onMoreOptionSelected
339                         )
340                     }
341                 } else null,
342                 rightButton = if (activeEntry != null) { // Only one sign-in options exist
343                     {
344                         ConfirmButton(
345                             stringResource(R.string.string_continue),
346                             onClick = onConfirm
347                         )
348                     }
349                 } else null,
350             )
351         }
352     }
353     onLog(GetCredentialEvent.CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD)
354 }
355 
356 /** Draws the secondary credential selection page, where all sign-in options are listed. */
357 @Composable
AllSignInOptionCardnull358 fun AllSignInOptionCard(
359     providerInfoList: List<ProviderInfo>,
360     providerDisplayInfo: ProviderDisplayInfo,
361     onEntrySelected: (BaseEntry) -> Unit,
362     onBackButtonClicked: () -> Unit,
363     onCancel: () -> Unit,
364     onLog: @Composable (UiEventEnum) -> Unit,
365 ) {
366     val sortedUserNameToCredentialEntryList =
367         providerDisplayInfo.sortedUserNameToCredentialEntryList
368     val authenticationEntryList = providerDisplayInfo.authenticationEntryList
369     SheetContainerCard(topAppBar = {
370         MoreOptionTopAppBar(
371             text = stringResource(R.string.get_dialog_title_sign_in_options),
372             onNavigationIconClicked = onBackButtonClicked,
373             bottomPadding = 0.dp,
374         )
375     }) {
376         var isFirstSection = true
377         // For username
378         items(sortedUserNameToCredentialEntryList) { item ->
379             PerUserNameCredentials(
380                 perUserNameCredentialEntryList = item,
381                 onEntrySelected = onEntrySelected,
382                 isFirstSection = isFirstSection,
383             )
384             isFirstSection = false
385         }
386         // Locked password manager
387         if (authenticationEntryList.isNotEmpty()) {
388             item {
389                 LockedCredentials(
390                     authenticationEntryList = authenticationEntryList,
391                     onEntrySelected = onEntrySelected,
392                     isFirstSection = isFirstSection,
393                 )
394                 isFirstSection = false
395             }
396         }
397         // From another device
398         val remoteEntry = providerDisplayInfo.remoteEntry
399         if (remoteEntry != null) {
400             item {
401                 RemoteEntryCard(
402                     remoteEntry = remoteEntry,
403                     onEntrySelected = onEntrySelected,
404                     isFirstSection = isFirstSection,
405                 )
406                 isFirstSection = false
407             }
408         }
409         // Manage sign-ins (action chips)
410         item {
411             ActionChips(
412                 providerInfoList = providerInfoList,
413                 onEntrySelected = onEntrySelected,
414                 isFirstSection = isFirstSection,
415             )
416             isFirstSection = false
417         }
418     }
419     onLog(GetCredentialEvent.CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD)
420 }
421 
422 @Composable
HeadlineProviderIconAndNamenull423 fun HeadlineProviderIconAndName(
424     icon: Drawable,
425     name: String,
426 ) {
427     HeadlineIcon(
428         bitmap = icon.toBitmap().asImageBitmap(),
429         tint = Color.Unspecified,
430     )
431     Divider(thickness = 4.dp, color = Color.Transparent)
432     LargeLabelTextOnSurfaceVariant(text = name)
433     Divider(thickness = 16.dp, color = Color.Transparent)
434 }
435 
436 @Composable
ActionChipsnull437 fun ActionChips(
438     providerInfoList: List<ProviderInfo>,
439     onEntrySelected: (BaseEntry) -> Unit,
440     isFirstSection: Boolean,
441 ) {
442     val actionChips = providerInfoList.flatMap { it.actionEntryList }
443     if (actionChips.isEmpty()) {
444         return
445     }
446 
447     CredentialListSectionHeader(
448         text = stringResource(R.string.get_dialog_heading_manage_sign_ins),
449         isFirstSection = isFirstSection,
450     )
451     CredentialContainerCard {
452         Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
453             actionChips.forEach {
454                 ActionEntryRow(it, onEntrySelected)
455             }
456         }
457     }
458 }
459 
460 @Composable
RemoteEntryCardnull461 fun RemoteEntryCard(
462     remoteEntry: RemoteEntryInfo,
463     onEntrySelected: (BaseEntry) -> Unit,
464     isFirstSection: Boolean,
465 ) {
466     CredentialListSectionHeader(
467         text = stringResource(R.string.get_dialog_heading_from_another_device),
468         isFirstSection = isFirstSection,
469     )
470     CredentialContainerCard {
471         Column(
472             modifier = Modifier.fillMaxWidth().wrapContentHeight(),
473             verticalArrangement = Arrangement.spacedBy(2.dp),
474         ) {
475             Entry(
476                 onClick = { onEntrySelected(remoteEntry) },
477                 iconImageVector = Icons.Outlined.QrCodeScanner,
478                 entryHeadlineText = stringResource(
479                     R.string.get_dialog_option_headline_use_a_different_device
480                 ),
481             )
482         }
483     }
484 }
485 
486 @Composable
LockedCredentialsnull487 fun LockedCredentials(
488     authenticationEntryList: List<AuthenticationEntryInfo>,
489     onEntrySelected: (BaseEntry) -> Unit,
490     isFirstSection: Boolean,
491 ) {
492     CredentialListSectionHeader(
493         text = stringResource(R.string.get_dialog_heading_locked_password_managers),
494         isFirstSection = isFirstSection,
495     )
496     CredentialContainerCard {
497         Column(
498             modifier = Modifier.fillMaxWidth().wrapContentHeight(),
499             verticalArrangement = Arrangement.spacedBy(2.dp),
500         ) {
501             authenticationEntryList.forEach {
502                 AuthenticationEntryRow(it, onEntrySelected)
503             }
504         }
505     }
506 }
507 
508 @Composable
PerUserNameCredentialsnull509 fun PerUserNameCredentials(
510     perUserNameCredentialEntryList: PerUserNameCredentialEntryList,
511     onEntrySelected: (BaseEntry) -> Unit,
512     isFirstSection: Boolean,
513 ) {
514     CredentialListSectionHeader(
515         text = stringResource(
516             R.string.get_dialog_heading_for_username, perUserNameCredentialEntryList.userName
517         ),
518         isFirstSection = isFirstSection,
519     )
520     CredentialContainerCard {
521         Column(
522             modifier = Modifier.fillMaxWidth().wrapContentHeight(),
523             verticalArrangement = Arrangement.spacedBy(2.dp),
524         ) {
525             perUserNameCredentialEntryList.sortedCredentialEntryList.forEach {
526                 CredentialEntryRow(it, onEntrySelected)
527             }
528         }
529     }
530 }
531 
532 @Composable
CredentialEntryRownull533 fun CredentialEntryRow(
534     credentialEntryInfo: CredentialEntryInfo,
535     onEntrySelected: (BaseEntry) -> Unit,
536     enforceOneLine: Boolean = false,
537     onTextLayout: (TextLayoutResult) -> Unit = {},
538 ) {
539     val (username, displayName) = if (credentialEntryInfo.credentialType == CredentialType.PASSKEY)
540         userAndDisplayNameForPasskey(
541             credentialEntryInfo.userName, credentialEntryInfo.displayName ?: "")
542     else Pair(credentialEntryInfo.userName, credentialEntryInfo.displayName)
543     Entry(
<lambda>null544         onClick = { onEntrySelected(credentialEntryInfo) },
545         iconImageBitmap = credentialEntryInfo.icon?.toBitmap()?.asImageBitmap(),
546         shouldApplyIconImageBitmapTint = credentialEntryInfo.shouldTintIcon,
547         // Fall back to iconPainter if iconImageBitmap isn't available
548         iconPainter =
549         if (credentialEntryInfo.icon == null) painterResource(R.drawable.ic_other_sign_in_24)
550         else null,
551         entryHeadlineText = username,
552         entrySecondLineText = if (
553             credentialEntryInfo.credentialType == CredentialType.PASSWORD) {
554             "••••••••••••"
555         } else {
556             val itemsToDisplay = listOf(
557                 displayName,
558                 credentialEntryInfo.credentialTypeDisplayName,
559                 credentialEntryInfo.providerDisplayName
560             ).filterNot(TextUtils::isEmpty)
561             if (itemsToDisplay.isEmpty()) null
562             else itemsToDisplay.joinToString(
563                 separator = stringResource(R.string.get_dialog_sign_in_type_username_separator)
564             )
565         },
566         enforceOneLine = enforceOneLine,
567         onTextLayout = onTextLayout,
568     )
569 }
570 
571 @Composable
AuthenticationEntryRownull572 fun AuthenticationEntryRow(
573     authenticationEntryInfo: AuthenticationEntryInfo,
574     onEntrySelected: (BaseEntry) -> Unit,
575     enforceOneLine: Boolean = false,
576 ) {
577     Entry(
578         onClick = if (authenticationEntryInfo.isUnlockedAndEmpty) {
579             {}
580         } // No-op
581         else {
582             { onEntrySelected(authenticationEntryInfo) }
583         },
584         iconImageBitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(),
585         entryHeadlineText = authenticationEntryInfo.title,
586         entrySecondLineText = stringResource(
587             if (authenticationEntryInfo.isUnlockedAndEmpty)
588                 R.string.locked_credential_entry_label_subtext_no_sign_in
589             else R.string.locked_credential_entry_label_subtext_tap_to_unlock
590         ),
591         isLockedAuthEntry = !authenticationEntryInfo.isUnlockedAndEmpty,
592         enforceOneLine = enforceOneLine,
593     )
594 }
595 
596 @Composable
ActionEntryRownull597 fun ActionEntryRow(
598     actionEntryInfo: ActionEntryInfo,
599     onEntrySelected: (BaseEntry) -> Unit,
600 ) {
601     ActionEntry(
602         iconImageBitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(),
603         entryHeadlineText = actionEntryInfo.title,
604         entrySecondLineText = actionEntryInfo.subTitle,
605         onClick = { onEntrySelected(actionEntryInfo) },
606     )
607 }
608 
609 @Composable
RemoteCredentialSnackBarScreennull610 fun RemoteCredentialSnackBarScreen(
611     onClick: (Boolean) -> Unit,
612     onCancel: () -> Unit,
613     onLog: @Composable (UiEventEnum) -> Unit,
614 ) {
615     Snackbar(
616         action = {
617             TextButton(
618                 modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, start = 16.dp)
619                     .heightIn(min = 32.dp),
620                 onClick = { onClick(true) },
621                 contentPadding =
622                 PaddingValues(start = 0.dp, top = 6.dp, end = 0.dp, bottom = 6.dp),
623             ) {
624                 SnackbarActionText(text = stringResource(R.string.snackbar_action))
625             }
626         },
627         onDismiss = onCancel,
628         contentText = stringResource(R.string.get_dialog_use_saved_passkey_for),
629     )
630     onLog(GetCredentialEvent.CREDMAN_GET_CRED_REMOTE_CRED_SNACKBAR_SCREEN)
631 }
632 
633 @Composable
EmptyAuthEntrySnackBarScreennull634 fun EmptyAuthEntrySnackBarScreen(
635     authenticationEntryList: List<AuthenticationEntryInfo>,
636     onCancel: () -> Unit,
637     onLastLockedAuthEntryNotFound: () -> Unit,
638     onLog: @Composable (UiEventEnum) -> Unit,
639 ) {
640     val lastLocked = authenticationEntryList.firstOrNull({ it.isLastUnlocked })
641     if (lastLocked == null) {
642         onLastLockedAuthEntryNotFound()
643         return
644     }
645 
646     Snackbar(
647         onDismiss = onCancel,
648         contentText = stringResource(R.string.no_sign_in_info_in, lastLocked.providerDisplayName),
649     )
650     onLog(GetCredentialEvent.CREDMAN_GET_CRED_SCREEN_EMPTY_AUTH_SNACKBAR_SCREEN)
651 }