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 }