From 71e87df405da1657d57f6fa5ccaf928295b6491c Mon Sep 17 00:00:00 2001 From: Qinmei Du Date: Wed, 16 Nov 2022 18:40:27 +0000 Subject: Convert the create password requestInfo from real requestInfo and display the email and password screenshot:https://screenshot.googleplex.com/BmjfD7JRC6Zsv6Q Test: deployed locally Bug: 253157211 Change-Id: Ief8fd7a8173b9494216ae5cd49702b1338314ef0 --- .../credentialmanager/CredentialManagerRepo.kt | 28 +- .../CredentialSelectorActivity.kt | 8 +- .../createflow/CreateCredentialComponents.kt | 670 +++++++++++++++++++++ .../createflow/CreateCredentialViewModel.kt | 143 +++++ .../credentialmanager/createflow/CreateModel.kt | 4 +- .../createflow/CreatePasskeyComponents.kt | 660 -------------------- .../createflow/CreatePasskeyViewModel.kt | 143 ----- 7 files changed, 839 insertions(+), 817 deletions(-) create mode 100644 packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt create mode 100644 packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt delete mode 100644 packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt delete mode 100644 packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 8bd7cf03008b..b848a47f37de 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -38,12 +38,15 @@ import android.os.Binder import android.os.Bundle import android.os.ResultReceiver import com.android.credentialmanager.createflow.ActiveEntry -import com.android.credentialmanager.createflow.CreatePasskeyUiState +import com.android.credentialmanager.createflow.CreateCredentialUiState import com.android.credentialmanager.createflow.CreateScreenState import com.android.credentialmanager.createflow.EnabledProviderInfo import com.android.credentialmanager.createflow.RequestDisplayInfo import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState +import com.android.credentialmanager.jetpack.developer.CreateCredentialRequest.Companion.createFrom +import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest +import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL // Consider repo per screen, similar to view model? @@ -123,7 +126,7 @@ class CredentialManagerRepo( ) } - fun createPasskeyInitialUiState(): CreatePasskeyUiState { + fun createCredentialInitialUiState(): CreateCredentialUiState { val providerEnabledList = CreateFlowUtils.toEnabledProviderList( // Handle runtime cast error providerEnabledList as List, context) @@ -135,13 +138,22 @@ class CredentialManagerRepo( providerEnabledList.forEach{providerInfo -> providerInfo.createOptions = providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed() if (providerInfo.isDefault) {hasDefault = true; defaultProvider = providerInfo} } - // TODO: covert from real requestInfo - val requestDisplayInfo = RequestDisplayInfo( + // TODO: covert from real requestInfo for create passkey + var requestDisplayInfo = RequestDisplayInfo( "Elisa Beckett", "beckett-bakert@gmail.com", TYPE_PUBLIC_KEY_CREDENTIAL, "tribank") - return CreatePasskeyUiState( + val createCredentialRequest = requestInfo.createCredentialRequest + val createCredentialRequestJetpack = createCredentialRequest?.let { createFrom(it) } + if (createCredentialRequestJetpack is CreatePasswordRequest) { + requestDisplayInfo = RequestDisplayInfo( + createCredentialRequestJetpack.id, + createCredentialRequestJetpack.password, + TYPE_PASSWORD_CREDENTIAL, + "tribank") + } + return CreateCredentialUiState( enabledProviders = providerEnabledList, disabledProviders = providerDisabledList, if (hasDefault) @@ -388,15 +400,15 @@ class CredentialManagerRepo( } private fun testCreateRequestInfo(): RequestInfo { - val data = Bundle() + val data = toBundle("beckett-bakert@gmail.com", "password123") return RequestInfo.newCreateRequestInfo( Binder(), CreateCredentialRequest( - TYPE_PUBLIC_KEY_CREDENTIAL, + TYPE_PASSWORD_CREDENTIAL, data ), /*isFirstUsage=*/false, - "tribank.us" + "tribank" ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 78edaa936bcd..1041a33333b3 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -28,8 +28,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.android.credentialmanager.common.DialogType import com.android.credentialmanager.common.DialogResult import com.android.credentialmanager.common.ResultState -import com.android.credentialmanager.createflow.CreatePasskeyScreen -import com.android.credentialmanager.createflow.CreatePasskeyViewModel +import com.android.credentialmanager.createflow.CreateCredentialScreen +import com.android.credentialmanager.createflow.CreateCredentialViewModel import com.android.credentialmanager.getflow.GetCredentialScreen import com.android.credentialmanager.getflow.GetCredentialViewModel import com.android.credentialmanager.ui.theme.CredentialSelectorTheme @@ -63,12 +63,12 @@ class CredentialSelectorActivity : ComponentActivity() { val dialogType = DialogType.toDialogType(operationType) when (dialogType) { DialogType.CREATE_PASSKEY -> { - val viewModel: CreatePasskeyViewModel = viewModel() + val viewModel: CreateCredentialViewModel = viewModel() viewModel.observeDialogResult().observe( this@CredentialSelectorActivity, onCancel ) - CreatePasskeyScreen(viewModel = viewModel) + CreateCredentialScreen(viewModel = viewModel) } DialogType.GET_CREDENTIALS -> { val viewModel: GetCredentialViewModel = viewModel() diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt new file mode 100644 index 000000000000..dbb33c8233dd --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -0,0 +1,670 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.android.credentialmanager.createflow + +import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL +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.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.outlined.NewReleases +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import com.android.credentialmanager.R +import com.android.credentialmanager.common.material.ModalBottomSheetLayout +import com.android.credentialmanager.common.material.ModalBottomSheetValue +import com.android.credentialmanager.common.material.rememberModalBottomSheetState +import com.android.credentialmanager.common.ui.CancelButton +import com.android.credentialmanager.common.ui.ConfirmButton +import com.android.credentialmanager.ui.theme.EntryShape +import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateCredentialScreen( + viewModel: CreateCredentialViewModel, +) { + val state = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Expanded, + skipHalfExpanded = true + ) + ModalBottomSheetLayout( + sheetState = state, + sheetContent = { + val uiState = viewModel.uiState + when (uiState.currentScreenState) { + CreateScreenState.PASSKEY_INTRO -> ConfirmationCard( + onConfirm = viewModel::onConfirmIntro, + onCancel = viewModel::onCancel, + ) + CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard( + enabledProviderList = uiState.enabledProviders, + onCancel = viewModel::onCancel, + onProviderSelected = viewModel::onProviderSelected + ) + CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard( + requestDisplayInfo = uiState.requestDisplayInfo, + providerInfo = uiState.activeEntry?.activeProvider!!, + createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo, + onOptionSelected = viewModel::onPrimaryCreateOptionInfoSelected, + onConfirm = viewModel::onPrimaryCreateOptionInfoSelected, + onCancel = viewModel::onCancel, + multiProvider = uiState.enabledProviders.size > 1, + onMoreOptionsSelected = viewModel::onMoreOptionsSelected + ) + CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( + requestDisplayInfo = uiState.requestDisplayInfo, + enabledProviderList = uiState.enabledProviders, + disabledProviderList = uiState.disabledProviders, + onBackButtonSelected = viewModel::onBackButtonSelected, + onOptionSelected = viewModel::onMoreOptionsRowSelected, + onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected, + onRemoteEntrySelected = viewModel::onRemoteEntrySelected + ) + CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard( + providerInfo = uiState.activeEntry?.activeProvider!!, + onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected + ) + } + }, + scrimColor = MaterialTheme.colorScheme.scrim, + sheetShape = EntryShape.TopRoundedCorner, + ) {} + LaunchedEffect(state.currentValue) { + if (state.currentValue == ModalBottomSheetValue.Hidden) { + viewModel.onCancel() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfirmationCard( + onConfirm: () -> Unit, + onCancel: () -> Unit, +) { + Card() { + Column() { + Icon( + painter = painterResource(R.drawable.ic_passkey), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp) + ) + Text( + text = stringResource(R.string.passkey_creation_intro_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally) + ) + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Text( + text = stringResource(R.string.passkey_creation_intro_body), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(horizontal = 28.dp) + ) + Divider( + thickness = 48.dp, + color = Color.Transparent + ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton( + stringResource(R.string.string_cancel), + onClick = onCancel + ) + ConfirmButton( + stringResource(R.string.string_continue), + onClick = onConfirm + ) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProviderSelectionCard( + enabledProviderList: List, + onProviderSelected: (String) -> Unit, + onCancel: () -> Unit +) { + Card() { + Column() { + Text( + text = stringResource(R.string.choose_provider_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) + ) + Text( + text = stringResource(R.string.choose_provider_body), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(horizontal = 28.dp) + ) + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Card( + shape = EntryShape.FullRoundedCorner, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally), + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + enabledProviderList.forEach { + item { + ProviderRow(providerInfo = it, onProviderSelected = onProviderSelected) + } + } + } + } + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Row( + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton(stringResource(R.string.string_cancel), onCancel) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MoreOptionsSelectionCard( + requestDisplayInfo: RequestDisplayInfo, + enabledProviderList: List, + disabledProviderList: List?, + onBackButtonSelected: () -> Unit, + onOptionSelected: (ActiveEntry) -> Unit, + onDisabledPasswordManagerSelected: () -> Unit, + onRemoteEntrySelected: () -> Unit, +) { + Card() { + Column() { + TopAppBar( + title = { + Text( + text = when (requestDisplayInfo.type) { + TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.create_passkey_in) + TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.save_password_to) + else -> stringResource(R.string.save_sign_in_to) + }, + style = MaterialTheme.typography.titleMedium + ) + }, + navigationIcon = { + IconButton(onClick = onBackButtonSelected) { + Icon( + Icons.Filled.ArrowBack, + stringResource(R.string.accessibility_back_arrow_button)) + } + }, + colors = TopAppBarDefaults.smallTopAppBarColors + (containerColor = Color.Transparent), + ) + Divider( + thickness = 8.dp, + color = Color.Transparent + ) + Card( + shape = EntryShape.FullRoundedCorner, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally) + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + enabledProviderList.forEach { enabledProviderInfo -> + enabledProviderInfo.createOptions.forEach { createOptionInfo -> + item { + MoreOptionsInfoRow( + providerInfo = enabledProviderInfo, + createOptionInfo = createOptionInfo, + onOptionSelected = { + onOptionSelected(ActiveEntry(enabledProviderInfo, createOptionInfo)) + }) + } + } + } + if (disabledProviderList != null) { + item { + MoreOptionsDisabledProvidersRow( + disabledProviders = disabledProviderList, + onDisabledPasswordManagerSelected = onDisabledPasswordManagerSelected, + ) + } + } + var hasRemoteInfo = false + enabledProviderList.forEach { + if (it.remoteEntry != null) { + hasRemoteInfo = true + } + } + if (hasRemoteInfo) { + item { + RemoteEntryRow( + onRemoteEntrySelected = onRemoteEntrySelected, + ) + } + } + } + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 40.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MoreOptionsRowIntroCard( + providerInfo: EnabledProviderInfo, + onDefaultOrNotSelected: () -> Unit, +) { + Card() { + Column() { + Icon( + Icons.Outlined.NewReleases, + contentDescription = null, + modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp) + ) + Text( + text = stringResource(R.string.use_provider_for_all_title, providerInfo.displayName), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(R.string.confirm_default_or_use_once_description), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) + ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton( + stringResource(R.string.use_once), + onClick = onDefaultOrNotSelected + ) + ConfirmButton( + stringResource(R.string.set_as_default), + onClick = onDefaultOrNotSelected + ) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 40.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) { + SuggestionChip( + modifier = Modifier.fillMaxWidth(), + onClick = {onProviderSelected(providerInfo.name)}, + icon = { + Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + bitmap = providerInfo.icon.toBitmap().asImageBitmap(), + // painter = painterResource(R.drawable.ic_passkey), + // TODO: add description. + contentDescription = "") + }, + shape = EntryShape.FullRoundedCorner, + label = { + Text( + text = providerInfo.displayName, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(vertical = 18.dp) + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreationSelectionCard( + requestDisplayInfo: RequestDisplayInfo, + providerInfo: ProviderInfo, + createOptionInfo: CreateOptionInfo, + onOptionSelected: () -> Unit, + onConfirm: () -> Unit, + onCancel: () -> Unit, + multiProvider: Boolean, + onMoreOptionsSelected: () -> Unit, +) { + Card() { + Column() { + Icon( + bitmap = providerInfo.icon.toBitmap().asImageBitmap(), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.align(alignment = Alignment.CenterHorizontally) + .padding(all = 24.dp).size(32.dp) + ) + Text( + text = when (requestDisplayInfo.type) { + TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.choose_create_option_passkey_title, + providerInfo.displayName) + TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.choose_create_option_password_title, + providerInfo.displayName) + else -> stringResource(R.string.choose_create_option_sign_in_title, + providerInfo.displayName) + }, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + ) + if (createOptionInfo.userProviderDisplayName != null) { + Text( + text = stringResource( + R.string.choose_create_option_description, + requestDisplayInfo.appDomainName, + when (requestDisplayInfo.type) { + TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.passkey) + TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.password) + else -> stringResource(R.string.sign_ins) + }, + providerInfo.displayName, + createOptionInfo.userProviderDisplayName + ), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) + ) + } + Card( + shape = EntryShape.FullRoundedCorner, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally), + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + item { + PrimaryCreateOptionRow( + requestDisplayInfo = requestDisplayInfo, + createOptionInfo = createOptionInfo, + onOptionSelected = onOptionSelected + ) + } + } + } + if (multiProvider) { + TextButton( + onClick = onMoreOptionsSelected, + modifier = Modifier + .padding(horizontal = 24.dp) + .align(alignment = Alignment.CenterHorizontally)){ + Text( + text = + when (requestDisplayInfo.type) { + TYPE_PUBLIC_KEY_CREDENTIAL -> + stringResource(R.string.string_create_in_another_place) + else -> stringResource(R.string.string_save_to_another_place)}, + textAlign = TextAlign.Center, + ) + } + } + Divider( + thickness = 24.dp, + color = Color.Transparent + ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) + ) { + CancelButton( + stringResource(R.string.string_cancel), + onClick = onCancel + ) + ConfirmButton( + stringResource(R.string.string_continue), + onClick = onConfirm + ) + } + Divider( + thickness = 18.dp, + color = Color.Transparent, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrimaryCreateOptionRow( + requestDisplayInfo: RequestDisplayInfo, + createOptionInfo: CreateOptionInfo, + onOptionSelected: () -> Unit +) { + SuggestionChip( + modifier = Modifier.fillMaxWidth(), + onClick = onOptionSelected, + icon = { + Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), + contentDescription = null) + }, + shape = EntryShape.FullRoundedCorner, + label = { + Column() { + // TODO: Add the function to hide/view password when the type is create password + if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL || + requestDisplayInfo.type == TYPE_PASSWORD_CREDENTIAL) { + Text( + text = requestDisplayInfo.title, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp) + ) + Text( + text = requestDisplayInfo.subtitle, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + } else { + Text( + text = requestDisplayInfo.subtitle, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp, bottom = 16.dp) + ) + } + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MoreOptionsInfoRow( + providerInfo: EnabledProviderInfo, + createOptionInfo: CreateOptionInfo, + onOptionSelected: () -> Unit +) { + SuggestionChip( + modifier = Modifier.fillMaxWidth(), + onClick = onOptionSelected, + icon = { + Image(modifier = Modifier.size(32.dp, 32.dp).padding(start = 16.dp), + bitmap = providerInfo.icon.toBitmap().asImageBitmap(), + contentDescription = null) + }, + shape = EntryShape.FullRoundedCorner, + label = { + Column() { + Text( + text = providerInfo.displayName, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp, start = 16.dp) + ) + if (createOptionInfo.userProviderDisplayName != null) { + Text( + text = createOptionInfo.userProviderDisplayName, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 16.dp) + ) + } + if (createOptionInfo.passwordCount != null && createOptionInfo.passkeyCount != null) { + Text( + text = + stringResource( + R.string.more_options_usage_passwords_passkeys, + createOptionInfo.passwordCount, + createOptionInfo.passkeyCount + ), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) + ) + } else if (createOptionInfo.passwordCount != null) { + Text( + text = + stringResource( + R.string.more_options_usage_passwords, + createOptionInfo.passwordCount + ), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) + ) + } else if (createOptionInfo.passkeyCount != null) { + Text( + text = + stringResource( + R.string.more_options_usage_passkeys, + createOptionInfo.passkeyCount + ), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) + ) + } else if (createOptionInfo.totalCredentialCount != null) { + // TODO: Handle the case when there is total count + // but no passwords and passkeys after design is set + } + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MoreOptionsDisabledProvidersRow( + disabledProviders: List, + onDisabledPasswordManagerSelected: () -> Unit, +) { + SuggestionChip( + modifier = Modifier.fillMaxWidth(), + onClick = onDisabledPasswordManagerSelected, + icon = { + Icon( + Icons.Filled.Add, + contentDescription = null, + modifier = Modifier.padding(start = 16.dp) + ) + }, + shape = EntryShape.FullRoundedCorner, + label = { + Column() { + Text( + text = stringResource(R.string.other_password_manager), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp, start = 16.dp) + ) + Text( + text = disabledProviders.joinToString(separator = ", "){ it.displayName }, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) + ) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RemoteEntryRow( + onRemoteEntrySelected: () -> Unit, +) { + SuggestionChip( + modifier = Modifier.fillMaxWidth(), + onClick = onRemoteEntrySelected, + icon = { + Icon( + painter = painterResource(R.drawable.ic_other_devices), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.padding(start = 18.dp) + ) + }, + shape = EntryShape.FullRoundedCorner, + label = { + Column() { + Text( + text = stringResource(R.string.another_device), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(start = 16.dp, top = 18.dp, bottom = 18.dp) + .align(alignment = Alignment.CenterHorizontally) + ) + } + } + ) +} \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt new file mode 100644 index 000000000000..6be019fa0882 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.createflow + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.android.credentialmanager.CredentialManagerRepo +import com.android.credentialmanager.common.DialogResult +import com.android.credentialmanager.common.ResultState + +data class CreateCredentialUiState( + val enabledProviders: List, + val disabledProviders: List? = null, + val currentScreenState: CreateScreenState, + val requestDisplayInfo: RequestDisplayInfo, + val activeEntry: ActiveEntry? = null, +) + +class CreateCredentialViewModel( + credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance() +) : ViewModel() { + + var uiState by mutableStateOf(credManRepo.createCredentialInitialUiState()) + private set + + val dialogResult: MutableLiveData by lazy { + MutableLiveData() + } + + fun observeDialogResult(): LiveData { + return dialogResult + } + + fun onConfirmIntro() { + if (uiState.enabledProviders.size > 1) { + uiState = uiState.copy( + currentScreenState = CreateScreenState.PROVIDER_SELECTION + ) + } else if (uiState.enabledProviders.size == 1){ + uiState = uiState.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + activeEntry = ActiveEntry(uiState.enabledProviders.first(), + uiState.enabledProviders.first().createOptions.first()) + ) + } else { + throw java.lang.IllegalStateException("Empty provider list.") + } + } + + fun onProviderSelected(providerName: String) { + uiState = uiState.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + activeEntry = ActiveEntry(getProviderInfoByName(providerName), + getProviderInfoByName(providerName).createOptions.first()) + ) + } + + fun getProviderInfoByName(providerName: String): EnabledProviderInfo { + return uiState.enabledProviders.single { + it.name.equals(providerName) + } + } + + fun onMoreOptionsSelected() { + uiState = uiState.copy( + currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, + ) + } + + fun onBackButtonSelected() { + uiState = uiState.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + ) + } + + fun onMoreOptionsRowSelected(activeEntry: ActiveEntry) { + uiState = uiState.copy( + currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO, + activeEntry = activeEntry + ) + } + + fun onDisabledPasswordManagerSelected() { + // TODO: Complete this function + } + + fun onRemoteEntrySelected() { + // TODO: Complete this function + } + + fun onCancel() { + CredentialManagerRepo.getInstance().onCancel() + dialogResult.value = DialogResult(ResultState.CANCELED) + } + + fun onDefaultOrNotSelected() { + uiState = uiState.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + ) + // TODO: implement the if choose as default or not logic later + } + + fun onPrimaryCreateOptionInfoSelected() { + val entryKey = uiState.activeEntry?.activeEntryInfo?.entryKey + val entrySubkey = uiState.activeEntry?.activeEntryInfo?.entrySubkey + Log.d( + "Account Selector", + "Option selected for creation: " + + "{key = $entryKey, subkey = $entrySubkey}" + ) + if (entryKey != null && entrySubkey != null) { + CredentialManagerRepo.getInstance().onOptionSelected( + uiState.activeEntry?.activeProvider!!.name, + entryKey, + entrySubkey + ) + } else { + TODO("Gracefully handle illegal state.") + } + dialogResult.value = DialogResult( + ResultState.COMPLETE, + ) + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 123c3d454905..1ab234a0e0bc 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -62,8 +62,8 @@ class RemoteInfo( ) : EntryInfo(entryKey, entrySubkey) data class RequestDisplayInfo( - val userName: String, - val displayName: String, + val title: String, + val subtitle: String, val type: String, val appDomainName: String, ) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt deleted file mode 100644 index 8a1f83d2cb77..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt +++ /dev/null @@ -1,660 +0,0 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - -package com.android.credentialmanager.createflow - -import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL -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.lazy.LazyColumn -import androidx.compose.material3.Card -import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SuggestionChip -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.outlined.NewReleases -import androidx.compose.material.icons.filled.Add -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment -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.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.core.graphics.drawable.toBitmap -import com.android.credentialmanager.R -import com.android.credentialmanager.common.material.ModalBottomSheetLayout -import com.android.credentialmanager.common.material.ModalBottomSheetValue -import com.android.credentialmanager.common.material.rememberModalBottomSheetState -import com.android.credentialmanager.common.ui.CancelButton -import com.android.credentialmanager.common.ui.ConfirmButton -import com.android.credentialmanager.ui.theme.EntryShape -import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CreatePasskeyScreen( - viewModel: CreatePasskeyViewModel, -) { - val state = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Expanded, - skipHalfExpanded = true - ) - ModalBottomSheetLayout( - sheetState = state, - sheetContent = { - val uiState = viewModel.uiState - when (uiState.currentScreenState) { - CreateScreenState.PASSKEY_INTRO -> ConfirmationCard( - onConfirm = viewModel::onConfirmIntro, - onCancel = viewModel::onCancel, - ) - CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard( - enabledProviderList = uiState.enabledProviders, - onCancel = viewModel::onCancel, - onProviderSelected = viewModel::onProviderSelected - ) - CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard( - requestDisplayInfo = uiState.requestDisplayInfo, - providerInfo = uiState.activeEntry?.activeProvider!!, - createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo, - onOptionSelected = viewModel::onPrimaryCreateOptionInfoSelected, - onConfirm = viewModel::onPrimaryCreateOptionInfoSelected, - onCancel = viewModel::onCancel, - multiProvider = uiState.enabledProviders.size > 1, - onMoreOptionsSelected = viewModel::onMoreOptionsSelected - ) - CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( - requestDisplayInfo = uiState.requestDisplayInfo, - enabledProviderList = uiState.enabledProviders, - disabledProviderList = uiState.disabledProviders, - onBackButtonSelected = viewModel::onBackButtonSelected, - onOptionSelected = viewModel::onMoreOptionsRowSelected, - onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected, - onRemoteEntrySelected = viewModel::onRemoteEntrySelected - ) - CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard( - providerInfo = uiState.activeEntry?.activeProvider!!, - onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected - ) - } - }, - scrimColor = MaterialTheme.colorScheme.scrim, - sheetShape = EntryShape.TopRoundedCorner, - ) {} - LaunchedEffect(state.currentValue) { - if (state.currentValue == ModalBottomSheetValue.Hidden) { - viewModel.onCancel() - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ConfirmationCard( - onConfirm: () -> Unit, - onCancel: () -> Unit, -) { - Card() { - Column() { - Icon( - painter = painterResource(R.drawable.ic_passkey), - contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp) - ) - Text( - text = stringResource(R.string.passkey_creation_intro_title), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally) - ) - Divider( - thickness = 24.dp, - color = Color.Transparent - ) - Text( - text = stringResource(R.string.passkey_creation_intro_body), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(horizontal = 28.dp) - ) - Divider( - thickness = 48.dp, - color = Color.Transparent - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) - ) { - CancelButton( - stringResource(R.string.string_cancel), - onClick = onCancel - ) - ConfirmButton( - stringResource(R.string.string_continue), - onClick = onConfirm - ) - } - Divider( - thickness = 18.dp, - color = Color.Transparent, - modifier = Modifier.padding(bottom = 16.dp) - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ProviderSelectionCard( - enabledProviderList: List, - onProviderSelected: (String) -> Unit, - onCancel: () -> Unit -) { - Card() { - Column() { - Text( - text = stringResource(R.string.choose_provider_title), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) - ) - Text( - text = stringResource(R.string.choose_provider_body), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(horizontal = 28.dp) - ) - Divider( - thickness = 24.dp, - color = Color.Transparent - ) - Card( - shape = EntryShape.FullRoundedCorner, - modifier = Modifier - .padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally), - ) { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - enabledProviderList.forEach { - item { - ProviderRow(providerInfo = it, onProviderSelected = onProviderSelected) - } - } - } - } - Divider( - thickness = 24.dp, - color = Color.Transparent - ) - Row( - horizontalArrangement = Arrangement.Start, - modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) - ) { - CancelButton(stringResource(R.string.string_cancel), onCancel) - } - Divider( - thickness = 18.dp, - color = Color.Transparent, - modifier = Modifier.padding(bottom = 16.dp) - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MoreOptionsSelectionCard( - requestDisplayInfo: RequestDisplayInfo, - enabledProviderList: List, - disabledProviderList: List?, - onBackButtonSelected: () -> Unit, - onOptionSelected: (ActiveEntry) -> Unit, - onDisabledPasswordManagerSelected: () -> Unit, - onRemoteEntrySelected: () -> Unit, -) { - Card() { - Column() { - TopAppBar( - title = { - Text( - text = when (requestDisplayInfo.type) { - TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.create_passkey_in) - TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.save_password_to) - else -> stringResource(R.string.save_sign_in_to) - }, - style = MaterialTheme.typography.titleMedium - ) - }, - navigationIcon = { - IconButton(onClick = onBackButtonSelected) { - Icon( - Icons.Filled.ArrowBack, - stringResource(R.string.accessibility_back_arrow_button)) - } - }, - colors = TopAppBarDefaults.smallTopAppBarColors - (containerColor = Color.Transparent), - ) - Divider( - thickness = 8.dp, - color = Color.Transparent - ) - Card( - shape = EntryShape.FullRoundedCorner, - modifier = Modifier - .padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally) - ) { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - enabledProviderList.forEach { enabledProviderInfo -> - enabledProviderInfo.createOptions.forEach { createOptionInfo -> - item { - MoreOptionsInfoRow( - providerInfo = enabledProviderInfo, - createOptionInfo = createOptionInfo, - onOptionSelected = { - onOptionSelected(ActiveEntry(enabledProviderInfo, createOptionInfo)) - }) - } - } - } - if (disabledProviderList != null) { - item { - MoreOptionsDisabledProvidersRow( - disabledProviders = disabledProviderList, - onDisabledPasswordManagerSelected = onDisabledPasswordManagerSelected, - ) - } - } - var hasRemoteInfo = false - enabledProviderList.forEach { - if (it.remoteEntry != null) { - hasRemoteInfo = true - } - } - if (hasRemoteInfo) { - item { - RemoteEntryRow( - onRemoteEntrySelected = onRemoteEntrySelected, - ) - } - } - } - } - Divider( - thickness = 18.dp, - color = Color.Transparent, - modifier = Modifier.padding(bottom = 40.dp) - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MoreOptionsRowIntroCard( - providerInfo: EnabledProviderInfo, - onDefaultOrNotSelected: () -> Unit, -) { - Card() { - Column() { - Icon( - Icons.Outlined.NewReleases, - contentDescription = null, - modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp) - ) - Text( - text = stringResource(R.string.use_provider_for_all_title, providerInfo.displayName), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally), - textAlign = TextAlign.Center, - ) - Text( - text = stringResource(R.string.confirm_default_or_use_once_description), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) - ) { - CancelButton( - stringResource(R.string.use_once), - onClick = onDefaultOrNotSelected - ) - ConfirmButton( - stringResource(R.string.set_as_default), - onClick = onDefaultOrNotSelected - ) - } - Divider( - thickness = 18.dp, - color = Color.Transparent, - modifier = Modifier.padding(bottom = 40.dp) - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), - onClick = {onProviderSelected(providerInfo.name)}, - icon = { - Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), - bitmap = providerInfo.icon.toBitmap().asImageBitmap(), - // painter = painterResource(R.drawable.ic_passkey), - // TODO: add description. - contentDescription = "") - }, - shape = EntryShape.FullRoundedCorner, - label = { - Text( - text = providerInfo.displayName, - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(vertical = 18.dp) - ) - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CreationSelectionCard( - requestDisplayInfo: RequestDisplayInfo, - providerInfo: ProviderInfo, - createOptionInfo: CreateOptionInfo, - onOptionSelected: () -> Unit, - onConfirm: () -> Unit, - onCancel: () -> Unit, - multiProvider: Boolean, - onMoreOptionsSelected: () -> Unit, -) { - Card() { - Column() { - Icon( - bitmap = providerInfo.icon.toBitmap().asImageBitmap(), - contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier.align(alignment = Alignment.CenterHorizontally) - .padding(all = 24.dp).size(32.dp) - ) - Text( - text = when (requestDisplayInfo.type) { - TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.choose_create_option_passkey_title, - providerInfo.displayName) - TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.choose_create_option_password_title, - providerInfo.displayName) - else -> stringResource(R.string.choose_create_option_sign_in_title, - providerInfo.displayName) - }, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally), - textAlign = TextAlign.Center, - ) - if (createOptionInfo.userProviderDisplayName != null) { - Text( - text = stringResource( - R.string.choose_create_option_description, - requestDisplayInfo.appDomainName, - when (requestDisplayInfo.type) { - TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.passkey) - TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.password) - else -> stringResource(R.string.sign_ins) - }, - providerInfo.displayName, - createOptionInfo.userProviderDisplayName - ), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) - ) - } - Card( - shape = EntryShape.FullRoundedCorner, - modifier = Modifier - .padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally), - ) { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - item { - PrimaryCreateOptionRow( - requestDisplayInfo = requestDisplayInfo, - createOptionInfo = createOptionInfo, - onOptionSelected = onOptionSelected - ) - } - } - } - if (multiProvider) { - TextButton( - onClick = onMoreOptionsSelected, - modifier = Modifier - .padding(horizontal = 24.dp) - .align(alignment = Alignment.CenterHorizontally)){ - Text( - text = - when (requestDisplayInfo.type) { - TYPE_PUBLIC_KEY_CREDENTIAL -> - stringResource(R.string.string_create_in_another_place) - else -> stringResource(R.string.string_save_to_another_place)}, - textAlign = TextAlign.Center, - ) - } - } - Divider( - thickness = 24.dp, - color = Color.Transparent - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp) - ) { - CancelButton( - stringResource(R.string.string_cancel), - onClick = onCancel - ) - ConfirmButton( - stringResource(R.string.string_continue), - onClick = onConfirm - ) - } - Divider( - thickness = 18.dp, - color = Color.Transparent, - modifier = Modifier.padding(bottom = 16.dp) - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PrimaryCreateOptionRow( - requestDisplayInfo: RequestDisplayInfo, - createOptionInfo: CreateOptionInfo, - onOptionSelected: () -> Unit -) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), - onClick = onOptionSelected, - icon = { - Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), - bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), - contentDescription = null) - }, - shape = EntryShape.FullRoundedCorner, - label = { - Column() { - Text( - text = requestDisplayInfo.userName, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(top = 16.dp) - ) - Text( - text = requestDisplayInfo.displayName, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) - ) - } - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MoreOptionsInfoRow( - providerInfo: EnabledProviderInfo, - createOptionInfo: CreateOptionInfo, - onOptionSelected: () -> Unit -) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), - onClick = onOptionSelected, - icon = { - Image(modifier = Modifier.size(32.dp, 32.dp).padding(start = 16.dp), - bitmap = providerInfo.icon.toBitmap().asImageBitmap(), - contentDescription = null) - }, - shape = EntryShape.FullRoundedCorner, - label = { - Column() { - Text( - text = providerInfo.displayName, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(top = 16.dp, start = 16.dp) - ) - if (createOptionInfo.userProviderDisplayName != null) { - Text( - text = createOptionInfo.userProviderDisplayName, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(start = 16.dp) - ) - } - if (createOptionInfo.passwordCount != null && createOptionInfo.passkeyCount != null) { - Text( - text = - stringResource( - R.string.more_options_usage_passwords_passkeys, - createOptionInfo.passwordCount, - createOptionInfo.passkeyCount - ), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) - ) - } else if (createOptionInfo.passwordCount != null) { - Text( - text = - stringResource( - R.string.more_options_usage_passwords, - createOptionInfo.passwordCount - ), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) - ) - } else if (createOptionInfo.passkeyCount != null) { - Text( - text = - stringResource( - R.string.more_options_usage_passkeys, - createOptionInfo.passkeyCount - ), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) - ) - } else if (createOptionInfo.totalCredentialCount != null) { - // TODO: Handle the case when there is total count - // but no passwords and passkeys after design is set - } - } - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MoreOptionsDisabledProvidersRow( - disabledProviders: List, - onDisabledPasswordManagerSelected: () -> Unit, -) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), - onClick = onDisabledPasswordManagerSelected, - icon = { - Icon( - Icons.Filled.Add, - contentDescription = null, - modifier = Modifier.padding(start = 16.dp) - ) - }, - shape = EntryShape.FullRoundedCorner, - label = { - Column() { - Text( - text = stringResource(R.string.other_password_manager), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(top = 16.dp, start = 16.dp) - ) - Text( - text = disabledProviders.joinToString(separator = ", "){ it.displayName }, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp, start = 16.dp) - ) - } - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun RemoteEntryRow( - onRemoteEntrySelected: () -> Unit, -) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), - onClick = onRemoteEntrySelected, - icon = { - Icon( - painter = painterResource(R.drawable.ic_other_devices), - contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier.padding(start = 18.dp) - ) - }, - shape = EntryShape.FullRoundedCorner, - label = { - Column() { - Text( - text = stringResource(R.string.another_device), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(start = 16.dp, top = 18.dp, bottom = 18.dp) - .align(alignment = Alignment.CenterHorizontally) - ) - } - } - ) -} \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt deleted file mode 100644 index af74b8ea4de1..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.createflow - -import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.android.credentialmanager.CredentialManagerRepo -import com.android.credentialmanager.common.DialogResult -import com.android.credentialmanager.common.ResultState - -data class CreatePasskeyUiState( - val enabledProviders: List, - val disabledProviders: List? = null, - val currentScreenState: CreateScreenState, - val requestDisplayInfo: RequestDisplayInfo, - val activeEntry: ActiveEntry? = null, -) - -class CreatePasskeyViewModel( - credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance() -) : ViewModel() { - - var uiState by mutableStateOf(credManRepo.createPasskeyInitialUiState()) - private set - - val dialogResult: MutableLiveData by lazy { - MutableLiveData() - } - - fun observeDialogResult(): LiveData { - return dialogResult - } - - fun onConfirmIntro() { - if (uiState.enabledProviders.size > 1) { - uiState = uiState.copy( - currentScreenState = CreateScreenState.PROVIDER_SELECTION - ) - } else if (uiState.enabledProviders.size == 1){ - uiState = uiState.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - activeEntry = ActiveEntry(uiState.enabledProviders.first(), - uiState.enabledProviders.first().createOptions.first()) - ) - } else { - throw java.lang.IllegalStateException("Empty provider list.") - } - } - - fun onProviderSelected(providerName: String) { - uiState = uiState.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - activeEntry = ActiveEntry(getProviderInfoByName(providerName), - getProviderInfoByName(providerName).createOptions.first()) - ) - } - - fun getProviderInfoByName(providerName: String): EnabledProviderInfo { - return uiState.enabledProviders.single { - it.name.equals(providerName) - } - } - - fun onMoreOptionsSelected() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, - ) - } - - fun onBackButtonSelected() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - ) - } - - fun onMoreOptionsRowSelected(activeEntry: ActiveEntry) { - uiState = uiState.copy( - currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO, - activeEntry = activeEntry - ) - } - - fun onDisabledPasswordManagerSelected() { - // TODO: Complete this function - } - - fun onRemoteEntrySelected() { - // TODO: Complete this function - } - - fun onCancel() { - CredentialManagerRepo.getInstance().onCancel() - dialogResult.value = DialogResult(ResultState.CANCELED) - } - - fun onDefaultOrNotSelected() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - ) - // TODO: implement the if choose as default or not logic later - } - - fun onPrimaryCreateOptionInfoSelected() { - val entryKey = uiState.activeEntry?.activeEntryInfo?.entryKey - val entrySubkey = uiState.activeEntry?.activeEntryInfo?.entrySubkey - Log.d( - "Account Selector", - "Option selected for creation: " + - "{key = $entryKey, subkey = $entrySubkey}" - ) - if (entryKey != null && entrySubkey != null) { - CredentialManagerRepo.getInstance().onOptionSelected( - uiState.activeEntry?.activeProvider!!.name, - entryKey, - entrySubkey - ) - } else { - TODO("Gracefully handle illegal state.") - } - dialogResult.value = DialogResult( - ResultState.COMPLETE, - ) - } -} -- cgit v1.2.3-59-g8ed1b