| @file:OptIn(ExperimentalMaterial3Api::class) |
| |
| package com.android.credentialmanager.createflow |
| |
| import androidx.activity.compose.ManagedActivityResultLauncher |
| import androidx.activity.result.ActivityResult |
| import androidx.activity.result.IntentSenderRequest |
| import androidx.compose.foundation.isSystemInDarkTheme |
| import androidx.compose.foundation.Image |
| import androidx.compose.foundation.layout.Arrangement |
| import androidx.compose.foundation.layout.Column |
| import androidx.compose.foundation.layout.Row |
| import androidx.compose.foundation.layout.fillMaxWidth |
| import androidx.compose.foundation.layout.padding |
| import androidx.compose.foundation.layout.size |
| import androidx.compose.foundation.layout.wrapContentHeight |
| import androidx.compose.material3.Divider |
| import androidx.compose.material3.ExperimentalMaterial3Api |
| import androidx.compose.material3.MaterialTheme |
| import androidx.compose.material.icons.Icons |
| import androidx.compose.material.icons.outlined.NewReleases |
| import androidx.compose.material.icons.filled.Add |
| import androidx.compose.material.icons.outlined.QrCodeScanner |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.LaunchedEffect |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.remember |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.graphics.asImageBitmap |
| import androidx.compose.ui.res.painterResource |
| import androidx.compose.ui.res.stringResource |
| import androidx.compose.ui.unit.dp |
| import androidx.core.graphics.drawable.toBitmap |
| import com.android.compose.rememberSystemUiController |
| import com.android.credentialmanager.CredentialSelectorViewModel |
| import com.android.credentialmanager.R |
| import com.android.credentialmanager.common.BaseEntry |
| import com.android.credentialmanager.common.CredentialType |
| import com.android.credentialmanager.common.ProviderActivityState |
| import com.android.credentialmanager.common.ui.ActionButton |
| import com.android.credentialmanager.common.ui.BodyMediumText |
| import com.android.credentialmanager.common.ui.BodySmallText |
| import com.android.credentialmanager.common.ui.ConfirmButton |
| import com.android.credentialmanager.common.ui.CredentialContainerCard |
| import com.android.credentialmanager.common.ui.CtaButtonRow |
| import com.android.credentialmanager.common.ui.Entry |
| import com.android.credentialmanager.common.ui.HeadlineIcon |
| import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant |
| import com.android.credentialmanager.common.ui.ModalBottomSheet |
| import com.android.credentialmanager.common.ui.MoreAboutPasskeySectionHeader |
| import com.android.credentialmanager.common.ui.MoreOptionTopAppBar |
| import com.android.credentialmanager.common.ui.SheetContainerCard |
| import com.android.credentialmanager.common.ui.PasskeyBenefitRow |
| import com.android.credentialmanager.common.ui.HeadlineText |
| import com.android.credentialmanager.common.ui.setBottomSheetSystemBarsColor |
| |
| @Composable |
| fun CreateCredentialScreen( |
| viewModel: CredentialSelectorViewModel, |
| createCredentialUiState: CreateCredentialUiState, |
| providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult> |
| ) { |
| val sysUiController = rememberSystemUiController() |
| setBottomSheetSystemBarsColor(sysUiController) |
| ModalBottomSheet( |
| sheetContent = { |
| // Hide the sheet content as opposed to the whole bottom sheet to maintain the scrim |
| // background color even when the content should be hidden while waiting for |
| // results from the provider app. |
| when (viewModel.uiState.providerActivityState) { |
| ProviderActivityState.NOT_APPLICABLE -> { |
| when (createCredentialUiState.currentScreenState) { |
| CreateScreenState.PASSKEY_INTRO -> PasskeyIntroCard( |
| onConfirm = viewModel::createFlowOnConfirmIntro, |
| onLearnMore = viewModel::createFlowOnLearnMore, |
| ) |
| CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard( |
| requestDisplayInfo = createCredentialUiState.requestDisplayInfo, |
| disabledProviderList = createCredentialUiState.disabledProviders, |
| sortedCreateOptionsPairs = |
| createCredentialUiState.sortedCreateOptionsPairs, |
| hasRemoteEntry = createCredentialUiState.remoteEntry != null, |
| onOptionSelected = |
| viewModel::createFlowOnEntrySelectedFromFirstUseScreen, |
| onDisabledProvidersSelected = |
| viewModel::createFlowOnDisabledProvidersSelected, |
| onMoreOptionsSelected = |
| viewModel::createFlowOnMoreOptionsSelectedOnProviderSelection, |
| ) |
| CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard( |
| requestDisplayInfo = createCredentialUiState.requestDisplayInfo, |
| enabledProviderList = createCredentialUiState.enabledProviders, |
| providerInfo = createCredentialUiState.activeEntry?.activeProvider!!, |
| hasDefaultProvider = createCredentialUiState.hasDefaultProvider, |
| createOptionInfo = |
| createCredentialUiState.activeEntry.activeEntryInfo |
| as CreateOptionInfo, |
| onOptionSelected = viewModel::createFlowOnEntrySelected, |
| onConfirm = viewModel::createFlowOnConfirmEntrySelected, |
| onMoreOptionsSelected = |
| viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection, |
| ) |
| CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( |
| requestDisplayInfo = createCredentialUiState.requestDisplayInfo, |
| enabledProviderList = createCredentialUiState.enabledProviders, |
| disabledProviderList = createCredentialUiState.disabledProviders, |
| sortedCreateOptionsPairs = |
| createCredentialUiState.sortedCreateOptionsPairs, |
| hasDefaultProvider = createCredentialUiState.hasDefaultProvider, |
| isFromProviderSelection = |
| createCredentialUiState.isFromProviderSelection!!, |
| onBackProviderSelectionButtonSelected = |
| viewModel::createFlowOnBackProviderSelectionButtonSelected, |
| onBackCreationSelectionButtonSelected = |
| viewModel::createFlowOnBackCreationSelectionButtonSelected, |
| onOptionSelected = |
| viewModel::createFlowOnEntrySelectedFromMoreOptionScreen, |
| onDisabledProvidersSelected = |
| viewModel::createFlowOnDisabledProvidersSelected, |
| onRemoteEntrySelected = viewModel::createFlowOnEntrySelected, |
| ) |
| CreateScreenState.MORE_OPTIONS_ROW_INTRO -> { |
| if (createCredentialUiState.activeEntry == null) { |
| viewModel.onIllegalUiState("Expect active entry to be non-null" + |
| " upon default provider dialog.") |
| } else { |
| MoreOptionsRowIntroCard( |
| selectedEntry = createCredentialUiState.activeEntry, |
| onIllegalScreenState = viewModel::onIllegalUiState, |
| onChangeDefaultSelected = |
| viewModel::createFlowOnChangeDefaultSelected, |
| onUseOnceSelected = viewModel::createFlowOnUseOnceSelected, |
| ) |
| } |
| } |
| CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard( |
| requestDisplayInfo = createCredentialUiState.requestDisplayInfo, |
| activeRemoteEntry = |
| createCredentialUiState.activeEntry?.activeEntryInfo!!, |
| onOptionSelected = viewModel::createFlowOnEntrySelected, |
| onConfirm = viewModel::createFlowOnConfirmEntrySelected, |
| ) |
| CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO -> |
| MoreAboutPasskeysIntroCard( |
| onBackPasskeyIntroButtonSelected = |
| viewModel::createFlowOnBackPasskeyIntroButtonSelected, |
| ) |
| } |
| } |
| ProviderActivityState.READY_TO_LAUNCH -> { |
| // Launch only once per providerActivityState change so that the provider |
| // UI will not be accidentally launched twice. |
| LaunchedEffect(viewModel.uiState.providerActivityState) { |
| viewModel.launchProviderUi(providerActivityLauncher) |
| } |
| } |
| ProviderActivityState.PENDING -> { |
| // Hide our content when the provider activity is active. |
| } |
| } |
| }, |
| onDismiss = viewModel::onUserCancel |
| ) |
| } |
| |
| @Composable |
| fun PasskeyIntroCard( |
| onConfirm: () -> Unit, |
| onLearnMore: () -> Unit, |
| ) { |
| SheetContainerCard { |
| item { |
| val onboardingImageResource = remember { |
| mutableStateOf(R.drawable.ic_passkeys_onboarding) |
| } |
| if (isSystemInDarkTheme()) { |
| onboardingImageResource.value = R.drawable.ic_passkeys_onboarding_dark |
| } else { |
| onboardingImageResource.value = R.drawable.ic_passkeys_onboarding |
| } |
| Row( |
| modifier = Modifier.wrapContentHeight().fillMaxWidth(), |
| horizontalArrangement = Arrangement.Center, |
| ) { |
| Image( |
| painter = painterResource(onboardingImageResource.value), |
| contentDescription = null, |
| modifier = Modifier.size(316.dp, 168.dp) |
| ) |
| } |
| } |
| item { Divider(thickness = 16.dp, color = Color.Transparent) } |
| item { HeadlineText(text = stringResource(R.string.passkey_creation_intro_title)) } |
| item { Divider(thickness = 16.dp, color = Color.Transparent) } |
| item { |
| PasskeyBenefitRow( |
| leadingIconPainter = painterResource(R.drawable.ic_passkeys_onboarding_password), |
| text = stringResource(R.string.passkey_creation_intro_body_password), |
| ) |
| } |
| item { Divider(thickness = 16.dp, color = Color.Transparent) } |
| item { |
| PasskeyBenefitRow( |
| leadingIconPainter = painterResource(R.drawable.ic_passkeys_onboarding_fingerprint), |
| text = stringResource(R.string.passkey_creation_intro_body_fingerprint), |
| ) |
| } |
| item { Divider(thickness = 16.dp, color = Color.Transparent) } |
| item { |
| PasskeyBenefitRow( |
| leadingIconPainter = painterResource(R.drawable.ic_passkeys_onboarding_device), |
| text = stringResource(R.string.passkey_creation_intro_body_device), |
| ) |
| } |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| |
| item { |
| CtaButtonRow( |
| leftButton = { |
| ActionButton( |
| stringResource(R.string.string_learn_more), |
| onClick = onLearnMore |
| ) |
| }, |
| rightButton = { |
| ConfirmButton( |
| stringResource(R.string.string_continue), |
| onClick = onConfirm |
| ) |
| }, |
| ) |
| } |
| } |
| } |
| |
| @Composable |
| fun ProviderSelectionCard( |
| requestDisplayInfo: RequestDisplayInfo, |
| disabledProviderList: List<DisabledProviderInfo>?, |
| sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, |
| hasRemoteEntry: Boolean, |
| onOptionSelected: (ActiveEntry) -> Unit, |
| onDisabledProvidersSelected: () -> Unit, |
| onMoreOptionsSelected: () -> Unit, |
| ) { |
| SheetContainerCard { |
| item { HeadlineIcon(bitmap = requestDisplayInfo.typeIcon.toBitmap().asImageBitmap()) } |
| item { Divider(thickness = 16.dp, color = Color.Transparent) } |
| item { |
| HeadlineText( |
| text = stringResource( |
| R.string.choose_provider_title, |
| when (requestDisplayInfo.type) { |
| CredentialType.PASSKEY -> |
| stringResource(R.string.passkeys) |
| CredentialType.PASSWORD -> |
| stringResource(R.string.passwords) |
| CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info) |
| } |
| ) |
| ) |
| } |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| |
| item { BodyMediumText(text = stringResource(R.string.choose_provider_body)) } |
| item { Divider(thickness = 16.dp, color = Color.Transparent) } |
| item { |
| CredentialContainerCard { |
| Column( |
| verticalArrangement = Arrangement.spacedBy(2.dp) |
| ) { |
| sortedCreateOptionsPairs.forEach { entry -> |
| MoreOptionsInfoRow( |
| requestDisplayInfo = requestDisplayInfo, |
| providerInfo = entry.second, |
| createOptionInfo = entry.first, |
| onOptionSelected = { |
| onOptionSelected( |
| ActiveEntry( |
| entry.second, |
| entry.first |
| ) |
| ) |
| } |
| ) |
| } |
| MoreOptionsDisabledProvidersRow( |
| disabledProviders = disabledProviderList, |
| onDisabledProvidersSelected = onDisabledProvidersSelected, |
| ) |
| } |
| } |
| } |
| if (hasRemoteEntry) { |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| item { |
| CtaButtonRow( |
| leftButton = { |
| ActionButton( |
| stringResource(R.string.string_more_options), |
| onMoreOptionsSelected |
| ) |
| } |
| ) |
| } |
| } |
| } |
| } |
| |
| @Composable |
| fun MoreOptionsSelectionCard( |
| requestDisplayInfo: RequestDisplayInfo, |
| enabledProviderList: List<EnabledProviderInfo>, |
| disabledProviderList: List<DisabledProviderInfo>?, |
| sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, |
| hasDefaultProvider: Boolean, |
| isFromProviderSelection: Boolean, |
| onBackProviderSelectionButtonSelected: () -> Unit, |
| onBackCreationSelectionButtonSelected: () -> Unit, |
| onOptionSelected: (ActiveEntry) -> Unit, |
| onDisabledProvidersSelected: () -> Unit, |
| onRemoteEntrySelected: (BaseEntry) -> Unit, |
| ) { |
| SheetContainerCard(topAppBar = { |
| MoreOptionTopAppBar( |
| text = stringResource( |
| R.string.save_credential_to_title, |
| when (requestDisplayInfo.type) { |
| CredentialType.PASSKEY -> |
| stringResource(R.string.passkey) |
| CredentialType.PASSWORD -> |
| stringResource(R.string.password) |
| CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info) |
| } |
| ), |
| onNavigationIconClicked = |
| if (isFromProviderSelection) onBackProviderSelectionButtonSelected |
| else onBackCreationSelectionButtonSelected, |
| ) |
| }) { |
| item { Divider(thickness = 8.dp, color = Color.Transparent) } // Top app bar has a 8dp |
| // bottom padding already |
| item { |
| CredentialContainerCard { |
| Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { |
| // Only in the flows with default provider(not first time use) we can show the |
| // createOptions here, or they will be shown on ProviderSelectionCard |
| if (hasDefaultProvider) { |
| sortedCreateOptionsPairs.forEach { entry -> |
| MoreOptionsInfoRow( |
| requestDisplayInfo = requestDisplayInfo, |
| providerInfo = entry.second, |
| createOptionInfo = entry.first, |
| onOptionSelected = { |
| onOptionSelected( |
| ActiveEntry( |
| entry.second, |
| entry.first |
| ) |
| ) |
| } |
| ) |
| } |
| MoreOptionsDisabledProvidersRow( |
| disabledProviders = disabledProviderList, |
| onDisabledProvidersSelected = |
| onDisabledProvidersSelected, |
| ) |
| } |
| enabledProviderList.forEach { |
| if (it.remoteEntry != null) { |
| RemoteEntryRow( |
| remoteInfo = it.remoteEntry!!, |
| onRemoteEntrySelected = onRemoteEntrySelected, |
| ) |
| return@forEach |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @Composable |
| fun MoreOptionsRowIntroCard( |
| selectedEntry: ActiveEntry, |
| onIllegalScreenState: (String) -> Unit, |
| onChangeDefaultSelected: () -> Unit, |
| onUseOnceSelected: () -> Unit, |
| ) { |
| val entryInfo = selectedEntry.activeEntryInfo |
| if (entryInfo !is CreateOptionInfo) { |
| onIllegalScreenState("Encountered unexpected type of entry during the default provider" + |
| " dialog: ${entryInfo::class}") |
| return |
| } |
| SheetContainerCard { |
| item { HeadlineIcon(imageVector = Icons.Outlined.NewReleases) } |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| item { |
| HeadlineText( |
| text = stringResource( |
| R.string.use_provider_for_all_title, selectedEntry.activeProvider.displayName) |
| ) |
| } |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| item { |
| BodyMediumText(text = stringResource( |
| R.string.use_provider_for_all_description, entryInfo.userProviderDisplayName)) |
| } |
| item { |
| CtaButtonRow( |
| leftButton = { |
| ActionButton( |
| stringResource(R.string.use_once), |
| onClick = onUseOnceSelected |
| ) |
| }, |
| rightButton = { |
| ConfirmButton( |
| stringResource(R.string.set_as_default), |
| onClick = onChangeDefaultSelected |
| ) |
| }, |
| ) |
| } |
| } |
| } |
| |
| @Composable |
| fun CreationSelectionCard( |
| requestDisplayInfo: RequestDisplayInfo, |
| enabledProviderList: List<EnabledProviderInfo>, |
| providerInfo: EnabledProviderInfo, |
| createOptionInfo: CreateOptionInfo, |
| onOptionSelected: (BaseEntry) -> Unit, |
| onConfirm: () -> Unit, |
| onMoreOptionsSelected: () -> Unit, |
| hasDefaultProvider: Boolean, |
| ) { |
| SheetContainerCard { |
| item { |
| HeadlineIcon( |
| bitmap = providerInfo.icon.toBitmap().asImageBitmap(), |
| tint = Color.Unspecified, |
| ) |
| } |
| item { Divider(thickness = 4.dp, color = Color.Transparent) } |
| item { LargeLabelTextOnSurfaceVariant(text = providerInfo.displayName) } |
| item { Divider(thickness = 16.dp, color = Color.Transparent) } |
| item { |
| HeadlineText( |
| text = when (requestDisplayInfo.type) { |
| CredentialType.PASSKEY -> stringResource( |
| R.string.choose_create_option_passkey_title, |
| requestDisplayInfo.appName |
| ) |
| CredentialType.PASSWORD -> stringResource( |
| R.string.choose_create_option_password_title, |
| requestDisplayInfo.appName |
| ) |
| CredentialType.UNKNOWN -> stringResource( |
| R.string.choose_create_option_sign_in_title, |
| requestDisplayInfo.appName |
| ) |
| } |
| ) |
| } |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| item { |
| CredentialContainerCard { |
| PrimaryCreateOptionRow( |
| requestDisplayInfo = requestDisplayInfo, |
| entryInfo = createOptionInfo, |
| onOptionSelected = onOptionSelected |
| ) |
| } |
| } |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| var createOptionsSize = 0 |
| var remoteEntry: RemoteInfo? = null |
| enabledProviderList.forEach { enabledProvider -> |
| if (enabledProvider.remoteEntry != null) { |
| remoteEntry = enabledProvider.remoteEntry |
| } |
| createOptionsSize += enabledProvider.createOptions.size |
| } |
| val shouldShowMoreOptionsButton = if (!hasDefaultProvider) { |
| // User has already been presented with all options on the default provider |
| // selection screen. Don't show them again. Therefore, only show the more option |
| // button if remote option is present. |
| remoteEntry != null |
| } else { |
| createOptionsSize > 1 || remoteEntry != null |
| } |
| item { |
| CtaButtonRow( |
| leftButton = if (shouldShowMoreOptionsButton) { |
| { |
| ActionButton( |
| stringResource(R.string.string_more_options), |
| onMoreOptionsSelected |
| ) |
| } |
| } else null, |
| rightButton = { |
| ConfirmButton( |
| stringResource(R.string.string_continue), |
| onClick = onConfirm |
| ) |
| }, |
| ) |
| } |
| if (createOptionInfo.footerDescription != null) { |
| item { |
| Divider( |
| thickness = 1.dp, |
| color = MaterialTheme.colorScheme.outlineVariant, |
| modifier = Modifier.padding(vertical = 16.dp) |
| ) |
| } |
| item { BodySmallText(text = createOptionInfo.footerDescription) } |
| } |
| } |
| } |
| |
| @Composable |
| fun ExternalOnlySelectionCard( |
| requestDisplayInfo: RequestDisplayInfo, |
| activeRemoteEntry: BaseEntry, |
| onOptionSelected: (BaseEntry) -> Unit, |
| onConfirm: () -> Unit, |
| ) { |
| SheetContainerCard { |
| item { HeadlineIcon(imageVector = Icons.Outlined.QrCodeScanner) } |
| item { Divider(thickness = 16.dp, color = Color.Transparent) } |
| item { HeadlineText(text = stringResource(R.string.create_passkey_in_other_device_title)) } |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| item { |
| CredentialContainerCard { |
| PrimaryCreateOptionRow( |
| requestDisplayInfo = requestDisplayInfo, |
| entryInfo = activeRemoteEntry, |
| onOptionSelected = onOptionSelected |
| ) |
| } |
| } |
| item { Divider(thickness = 24.dp, color = Color.Transparent) } |
| item { |
| CtaButtonRow( |
| rightButton = { |
| ConfirmButton( |
| stringResource(R.string.string_continue), |
| onClick = onConfirm |
| ) |
| }, |
| ) |
| } |
| } |
| } |
| |
| @Composable |
| fun MoreAboutPasskeysIntroCard( |
| onBackPasskeyIntroButtonSelected: () -> Unit, |
| ) { |
| SheetContainerCard( |
| topAppBar = { |
| MoreOptionTopAppBar( |
| text = stringResource(R.string.more_about_passkeys_title), |
| onNavigationIconClicked = onBackPasskeyIntroButtonSelected, |
| ) |
| }, |
| contentVerticalArrangement = Arrangement.spacedBy(8.dp) |
| ) { |
| item { |
| MoreAboutPasskeySectionHeader( |
| text = stringResource(R.string.passwordless_technology_title) |
| ) |
| BodyMediumText(text = stringResource(R.string.passwordless_technology_detail)) |
| } |
| item { |
| MoreAboutPasskeySectionHeader( |
| text = stringResource(R.string.public_key_cryptography_title) |
| ) |
| BodyMediumText(text = stringResource(R.string.public_key_cryptography_detail)) |
| } |
| item { |
| MoreAboutPasskeySectionHeader( |
| text = stringResource(R.string.improved_account_security_title) |
| ) |
| BodyMediumText(text = stringResource(R.string.improved_account_security_detail)) |
| } |
| item { |
| MoreAboutPasskeySectionHeader( |
| text = stringResource(R.string.seamless_transition_title) |
| ) |
| BodyMediumText(text = stringResource(R.string.seamless_transition_detail)) |
| } |
| } |
| } |
| |
| @Composable |
| fun PrimaryCreateOptionRow( |
| requestDisplayInfo: RequestDisplayInfo, |
| entryInfo: BaseEntry, |
| onOptionSelected: (BaseEntry) -> Unit |
| ) { |
| Entry( |
| onClick = { onOptionSelected(entryInfo) }, |
| iconImageBitmap = |
| if (entryInfo is CreateOptionInfo && entryInfo.profileIcon != null) { |
| entryInfo.profileIcon.toBitmap().asImageBitmap() |
| } else { |
| requestDisplayInfo.typeIcon.toBitmap().asImageBitmap() |
| }, |
| shouldApplyIconImageBitmapTint = !(entryInfo is CreateOptionInfo && |
| entryInfo.profileIcon != null), |
| entryHeadlineText = requestDisplayInfo.title, |
| entrySecondLineText = when (requestDisplayInfo.type) { |
| CredentialType.PASSKEY -> { |
| if (requestDisplayInfo.subtitle != null) { |
| requestDisplayInfo.subtitle + " • " + stringResource( |
| R.string.passkey_before_subtitle |
| ) |
| } else { |
| stringResource(R.string.passkey_before_subtitle) |
| } |
| } |
| // Set passwordValue instead |
| CredentialType.PASSWORD -> null |
| CredentialType.UNKNOWN -> requestDisplayInfo.subtitle |
| }, |
| passwordValue = |
| if (requestDisplayInfo.type == CredentialType.PASSWORD) |
| // This subtitle would never be null for create password |
| requestDisplayInfo.subtitle ?: "" |
| else null, |
| enforceOneLine = true, |
| ) |
| } |
| |
| @Composable |
| fun MoreOptionsInfoRow( |
| requestDisplayInfo: RequestDisplayInfo, |
| providerInfo: EnabledProviderInfo, |
| createOptionInfo: CreateOptionInfo, |
| onOptionSelected: () -> Unit |
| ) { |
| Entry( |
| onClick = onOptionSelected, |
| iconImageBitmap = providerInfo.icon.toBitmap().asImageBitmap(), |
| entryHeadlineText = providerInfo.displayName, |
| entrySecondLineText = createOptionInfo.userProviderDisplayName, |
| entryThirdLineText = |
| if (requestDisplayInfo.type == CredentialType.PASSKEY || |
| requestDisplayInfo.type == CredentialType.PASSWORD) { |
| if (createOptionInfo.passwordCount != null && |
| createOptionInfo.passkeyCount != null |
| ) { |
| stringResource( |
| R.string.more_options_usage_passwords_passkeys, |
| createOptionInfo.passwordCount, |
| createOptionInfo.passkeyCount |
| ) |
| } else if (createOptionInfo.passwordCount != null) { |
| stringResource( |
| R.string.more_options_usage_passwords, |
| createOptionInfo.passwordCount |
| ) |
| } else if (createOptionInfo.passkeyCount != null) { |
| stringResource( |
| R.string.more_options_usage_passkeys, |
| createOptionInfo.passkeyCount |
| ) |
| } else { |
| null |
| } |
| } else { |
| if (createOptionInfo.totalCredentialCount != null) { |
| stringResource( |
| R.string.more_options_usage_credentials, |
| createOptionInfo.totalCredentialCount |
| ) |
| } else { |
| null |
| } |
| }, |
| ) |
| } |
| |
| @Composable |
| fun MoreOptionsDisabledProvidersRow( |
| disabledProviders: List<ProviderInfo>?, |
| onDisabledProvidersSelected: () -> Unit, |
| ) { |
| if (disabledProviders != null && disabledProviders.isNotEmpty()) { |
| Entry( |
| onClick = onDisabledProvidersSelected, |
| iconImageVector = Icons.Filled.Add, |
| entryHeadlineText = stringResource(R.string.other_password_manager), |
| entrySecondLineText = disabledProviders.joinToString(separator = " • ") { |
| it.displayName |
| }, |
| ) |
| } |
| } |
| |
| @Composable |
| fun RemoteEntryRow( |
| remoteInfo: RemoteInfo, |
| onRemoteEntrySelected: (RemoteInfo) -> Unit, |
| ) { |
| Entry( |
| onClick = { onRemoteEntrySelected(remoteInfo) }, |
| iconImageVector = Icons.Outlined.QrCodeScanner, |
| entryHeadlineText = stringResource(R.string.another_device), |
| ) |
| } |