1 /* 2 * 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 174 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 358 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 423 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 437 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 461 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 487 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 509 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 533 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( 544 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 572 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 597 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 610 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 634 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 }