diff --git a/packages/CredentialManager/res/drawable/ic_profile.xml b/packages/CredentialManager/res/drawable/ic_profile.xml new file mode 100644 index 0000000000000..ae65940f72e54 --- /dev/null +++ b/packages/CredentialManager/res/drawable/ic_profile.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index c6779fa39473b..08ab1b4000f8f 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -22,4 +22,9 @@ You can use saved %1$s on any device. It will be saved to %2$s for %3$s %1$s for %2$s %1$s + %1$s passwords and %2$s passkeys saved + passkeys + passwords + sign-ins + CreateOptionInfo credentialType icon \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 01348e47932cc..3d1fc926ddf96 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -33,8 +33,10 @@ import android.graphics.drawable.Icon 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.CreateScreenState +import com.android.credentialmanager.createflow.ProviderInfo import com.android.credentialmanager.createflow.RequestDisplayInfo import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState @@ -119,6 +121,10 @@ class CredentialManagerRepo( val providerList = CreateFlowUtils.toProviderList( // Handle runtime cast error providerList as List, context) + var hasDefault = false + var defaultProvider: ProviderInfo = providerList.first() + providerList.forEach{providerInfo -> + if (providerInfo.isDefault) {hasDefault = true; defaultProvider = providerInfo} } // TODO: covert from real requestInfo val requestDisplayInfo = RequestDisplayInfo( "Elisa Beckett", @@ -127,8 +133,12 @@ class CredentialManagerRepo( "tribank") return CreatePasskeyUiState( providers = providerList, - currentScreenState = CreateScreenState.PASSKEY_INTRO, + if (hasDefault) + {CreateScreenState.CREATION_OPTION_SELECTION} else {CreateScreenState.PASSKEY_INTRO}, requestDisplayInfo, + if (hasDefault) { + ActiveEntry(defaultProvider, defaultProvider.createOptions.first()) + } else null ) } @@ -150,38 +160,39 @@ class CredentialManagerRepo( // TODO: below are prototype functionalities. To be removed for productionization. private fun testCreateCredentialProviderList(): List { return listOf( - CreateCredentialProviderData.Builder("com.google/com.google.CredentialManagerService") + CreateCredentialProviderData + .Builder("com.google/com.google.CredentialManagerService") .setSaveEntries( listOf( newEntry("key1", "subkey-1", "elisa.beckett@gmail.com", - "Elisa Backett", "20 passwords and 7 passkeys saved"), + 20, 7, 27, 10000), newEntry("key1", "subkey-2", "elisa.work@google.com", - "Elisa Backett Work", "20 passwords and 7 passkeys saved"), + 20, 7, 27, 11000), ) ) .setActionChips( listOf( - newEntry("key2", "subkey-1", "Go to Settings", "", - "20 passwords and 7 passkeys saved"), - newEntry("key2", "subkey-2", "Switch Account", "", - "20 passwords and 7 passkeys saved"), + newEntry("key2", "subkey-1", "Go to Settings", + 20, 7, 27, 20000), + newEntry("key2", "subkey-2", "Switch Account", + 20, 7, 27, 21000), ), ) - .setIsDefaultProvider(true) + .setIsDefaultProvider(false) .build(), - CreateCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService") + CreateCredentialProviderData + .Builder("com.dashlane/com.dashlane.CredentialManagerService") .setSaveEntries( listOf( newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com", - "Elisa Backett", "20 passwords and 7 passkeys saved"), + 20, 7, 27, 30000), newEntry("key1", "subkey-4", "elisa.work@dashlane.com", - "Elisa Backett Work", "20 passwords and 7 passkeys saved"), + 20, 7, 27, 31000), ) ).setActionChips( listOf( newEntry("key2", "subkey-3", "Manage Accounts", - "Manage your accounts in the dashlane app", - "20 passwords and 7 passkeys saved"), + 20, 7, 27, 32000), ), ).build(), ) @@ -193,31 +204,30 @@ class CredentialManagerRepo( .setCredentialEntries( listOf( newEntry("key1", "subkey-1", "elisa.beckett@gmail.com", - "Elisa Backett", "20 passwords and 7 passkeys saved"), + 20, 7, 27, 10000), newEntry("key1", "subkey-2", "elisa.work@google.com", - "Elisa Backett Work", "20 passwords and 7 passkeys saved"), + 20, 7, 27, 11000), ) ).setActionChips( listOf( - newEntry("key2", "subkey-1", "Go to Settings", "", - "20 passwords and 7 passkeys saved"), - newEntry("key2", "subkey-2", "Switch Account", "", - "20 passwords and 7 passkeys saved"), + newEntry("key2", "subkey-1", "Go to Settings", + 20, 7, 27, 20000), + newEntry("key2", "subkey-2", "Switch Account", + 20, 7, 27, 21000), ), ).build(), GetCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService") .setCredentialEntries( listOf( newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com", - "Elisa Backett", "20 passwords and 7 passkeys saved"), + 20, 7, 27, 30000), newEntry("key1", "subkey-4", "elisa.work@dashlane.com", - "Elisa Backett Work", "20 passwords and 7 passkeys saved"), + 20, 7, 27, 31000), ) ).setActionChips( listOf( newEntry("key2", "subkey-3", "Manage Accounts", - "Manage your accounts in the dashlane app", - "20 passwords and 7 passkeys saved"), + 20, 7, 27, 40000), ), ).build(), ) @@ -226,20 +236,32 @@ class CredentialManagerRepo( private fun newEntry( key: String, subkey: String, - title: String, - subtitle: String, - usageData: String + providerDisplayName: String, + passwordCount: Int, + passkeyCount: Int, + totalCredentialCount: Int, + lastUsedTimeMillis: Long, ): Entry { val slice = Slice.Builder( Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1) ) - .addText(title, null, listOf(Entry.HINT_TITLE)) - .addText(subtitle, null, listOf(Entry.HINT_SUBTITLE)) + .addText( + providerDisplayName, null, listOf(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME)) .addIcon( Icon.createWithResource(context, R.drawable.ic_passkey), null, - listOf(Entry.HINT_ICON)) - .addText(usageData, Slice.SUBTYPE_MESSAGE, listOf(Entry.HINT_SUBTITLE)) + listOf(Entry.HINT_CREDENTIAL_TYPE_ICON)) + .addIcon( + Icon.createWithResource(context, R.drawable.ic_profile), + null, + listOf(Entry.HINT_PROFILE_ICON)) + .addInt( + passwordCount, null, listOf(Entry.HINT_PASSWORD_COUNT)) + .addInt( + passkeyCount, null, listOf(Entry.HINT_PASSKEY_COUNT)) + .addInt( + totalCredentialCount, null, listOf(Entry.HINT_TOTAL_CREDENTIAL_COUNT)) + .addLong(lastUsedTimeMillis, null, listOf(Entry.HINT_LAST_USED_TIME_MILLIS)) .build() return Entry( key, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index bf0dba23cb647..3a8e975ea0c1c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -23,6 +23,7 @@ import android.credentials.ui.CreateCredentialProviderData import com.android.credentialmanager.createflow.CreateOptionInfo import com.android.credentialmanager.getflow.CredentialOptionInfo import com.android.credentialmanager.getflow.ProviderInfo +import com.android.credentialmanager.jetpack.provider.SaveEntryUi /** Utility functions for converting CredentialManager data structures to or from UI formats. */ class GetFlowUtils { @@ -59,8 +60,6 @@ class GetFlowUtils { // TODO: remove fallbacks icon = credentialEntryUi.icon?.loadDrawable(context) ?: context.getDrawable(R.drawable.ic_passkey)!!, - title = credentialEntryUi.userName.toString(), - subtitle = credentialEntryUi.displayName?.toString() ?: "Unknown display name", entryKey = it.key, entrySubkey = it.subkey, usageData = credentialEntryUi.usageData?.toString() ?: "Unknown usageData", @@ -82,9 +81,7 @@ class CreateFlowUtils { // TODO: replace to extract from the service data structure when available icon = context.getDrawable(R.drawable.ic_passkey)!!, name = it.providerFlattenedComponentName, - // TODO: get the service display name and icon from the component name. displayName = it.providerFlattenedComponentName, - credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, createOptions = toCreationOptionInfoList(it.saveEntries, context), isDefault = it.isDefaultProvider, ) @@ -100,13 +97,17 @@ class CreateFlowUtils { return@map CreateOptionInfo( // TODO: remove fallbacks - icon = saveEntryUi.icon?.loadDrawable(context) - ?: context.getDrawable(R.drawable.ic_passkey)!!, - title = saveEntryUi.title.toString(), - subtitle = saveEntryUi.subTitle?.toString() ?: "Unknown subtitle", entryKey = it.key, entrySubkey = it.subkey, - usageData = saveEntryUi.usageData?.toString() ?: "Unknown usageData", + userProviderDisplayName = saveEntryUi.userProviderAccountName as String, + credentialTypeIcon = saveEntryUi.credentialTypeIcon?.loadDrawable(context) + ?: context.getDrawable(R.drawable.ic_passkey)!!, + profileIcon = saveEntryUi.profileIcon?.loadDrawable(context) + ?: context.getDrawable(R.drawable.ic_profile)!!, + passwordCount = saveEntryUi.passwordCount ?: 0, + passkeyCount = saveEntryUi.passkeyCount ?: 0, + totalCredentialCount = saveEntryUi.totalCredentialCount ?: 0, + lastUsedTimeMillis = saveEntryUi.lastUsedTimeMillis ?: 0, ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt deleted file mode 100644 index cd52197f92574..0000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt +++ /dev/null @@ -1,59 +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 - -import android.app.slice.Slice -import android.credentials.ui.Entry -import android.graphics.drawable.Icon - -/** - * UI representation for a save entry used during the create credential flow. - * - * TODO: move to jetpack. - */ -class SaveEntryUi( - val title: CharSequence, - val subTitle: CharSequence?, - val icon: Icon?, - val usageData: CharSequence?, - // TODO: add -) { - companion object { - fun fromSlice(slice: Slice): SaveEntryUi { - val items = slice.items - - var title: String? = null - var subTitle: String? = null - var icon: Icon? = null - var usageData: String? = null - - items.forEach { - if (it.hasHint(Entry.HINT_ICON)) { - icon = it.icon - } else if (it.hasHint(Entry.HINT_SUBTITLE) && it.subType == null) { - subTitle = it.text.toString() - } else if (it.hasHint(Entry.HINT_TITLE)) { - title = it.text.toString() - } else if (it.hasHint(Entry.HINT_SUBTITLE) && it.subType == Slice.SUBTYPE_MESSAGE) { - usageData = it.text.toString() - } - } - // TODO: fail NPE more elegantly. - return SaveEntryUi(title!!, subTitle, icon, usageData) - } - } -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index db0f337e9a2b3..d7e5ee8c721e8 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -22,20 +22,27 @@ data class ProviderInfo( val icon: Drawable, val name: String, val displayName: String, - val credentialTypeIcon: Drawable, val createOptions: List, val isDefault: Boolean, ) -data class CreateOptionInfo( - val icon: Drawable, - val title: String, - val subtitle: String, +open class EntryInfo ( val entryKey: String, val entrySubkey: String, - val usageData: String ) +class CreateOptionInfo( + entryKey: String, + entrySubkey: String, + val userProviderDisplayName: String, + val credentialTypeIcon: Drawable, + val profileIcon: Drawable, + val passwordCount: Int, + val passkeyCount: Int, + val totalCredentialCount: Int, + val lastUsedTimeMillis: Long?, +) : EntryInfo(entryKey, entrySubkey) + data class RequestDisplayInfo( val userName: String, val displayName: String, @@ -49,7 +56,7 @@ data class RequestDisplayInfo( */ data class ActiveEntry ( val activeProvider: ProviderInfo, - val activeCreateOptionInfo: CreateOptionInfo, + val activeEntryInfo: EntryInfo, ) /** The name of the current screen. */ diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt index aeea46a85caf4..13a892fa3592c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt @@ -74,7 +74,7 @@ fun CreatePasskeyScreen( CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard( requestDisplayInfo = uiState.requestDisplayInfo, providerInfo = uiState.activeEntry?.activeProvider!!, - createOptionInfo = uiState.activeEntry.activeCreateOptionInfo, + createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo, onOptionSelected = viewModel::onPrimaryCreateOptionInfoSelected, onConfirm = viewModel::onPrimaryCreateOptionInfoSelected, onCancel = viewModel::onCancel, @@ -408,7 +408,7 @@ fun CreationSelectionCard( ) { Column() { Icon( - bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(), + bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), contentDescription = null, tint = Color.Unspecified, modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp) @@ -436,12 +436,12 @@ fun CreationSelectionCard( text = stringResource( R.string.choose_create_option_description, when (requestDisplayInfo.type) { - TYPE_PUBLIC_KEY_CREDENTIAL -> "passkeys" - TYPE_PASSWORD_CREDENTIAL -> "passwords" - else -> "sign-ins" + TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.passkeys) + TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.passwords) + else -> stringResource(R.string.sign_ins) }, providerInfo.displayName, - createOptionInfo.title), + createOptionInfo.userProviderDisplayName), style = Typography.body1, modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) ) @@ -455,8 +455,11 @@ fun CreationSelectionCard( verticalArrangement = Arrangement.spacedBy(2.dp) ) { item { - PrimaryCreateOptionRow(requestDisplayInfo = requestDisplayInfo, - onOptionSelected = onOptionSelected) + PrimaryCreateOptionRow( + requestDisplayInfo = requestDisplayInfo, + createOptionInfo = createOptionInfo, + onOptionSelected = onOptionSelected + ) } } } @@ -506,12 +509,17 @@ fun CreationSelectionCard( @Composable fun PrimaryCreateOptionRow( requestDisplayInfo: RequestDisplayInfo, + createOptionInfo: CreateOptionInfo, onOptionSelected: () -> Unit ) { Chip( modifier = Modifier.fillMaxWidth(), onClick = onOptionSelected, - // TODO: Add an icon generated by provider according to requestDisplayInfo type + leadingIcon = { + Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), + contentDescription = stringResource(R.string.createOptionInfo_icon_description)) + }, colors = ChipDefaults.chipColors( backgroundColor = Grey100, leadingIconContentColor = Grey100 @@ -545,7 +553,7 @@ fun MoreOptionsInfoRow( onClick = onOptionSelected, leadingIcon = { Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), - bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(), + bitmap = createOptionInfo.profileIcon.toBitmap().asImageBitmap(), // painter = painterResource(R.drawable.ic_passkey), // TODO: add description. contentDescription = "") @@ -561,14 +569,15 @@ fun MoreOptionsInfoRow( text = if (providerInfo.createOptions.size > 1) {stringResource(R.string.more_options_title_multiple_options, - providerInfo.displayName, createOptionInfo.title)} else { + providerInfo.displayName, createOptionInfo.userProviderDisplayName)} else { stringResource(R.string.more_options_title_one_option, providerInfo.displayName)}, style = Typography.h6, modifier = Modifier.padding(top = 16.dp) ) Text( - text = createOptionInfo.usageData, + text = stringResource(R.string.more_options_usage_data, + createOptionInfo.passwordCount, createOptionInfo.passkeyCount), style = Typography.body2, modifier = Modifier.padding(bottom = 16.dp) ) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt index 615da4e9de0aa..2e9758aece33d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt @@ -73,21 +73,6 @@ class CreatePasskeyViewModel( ) } - fun onCreateOptionSelected(entryKey: String, entrySubkey: String) { - Log.d( - "Account Selector", - "Option selected for creation: {key = $entryKey, subkey = $entrySubkey}" - ) - CredentialManagerRepo.getInstance().onOptionSelected( - uiState.activeEntry?.activeProvider!!.name, - entryKey, - entrySubkey - ) - dialogResult.value = DialogResult( - ResultState.COMPLETE, - ) - } - fun getProviderInfoByName(providerName: String): ProviderInfo { return uiState.providers.single { it.name.equals(providerName) @@ -126,18 +111,18 @@ class CreatePasskeyViewModel( } fun onPrimaryCreateOptionInfoSelected() { - var createOptionEntryKey = uiState.activeEntry?.activeCreateOptionInfo?.entryKey - var createOptionEntrySubkey = uiState.activeEntry?.activeCreateOptionInfo?.entrySubkey + val entryKey = uiState.activeEntry?.activeEntryInfo?.entryKey + val entrySubkey = uiState.activeEntry?.activeEntryInfo?.entrySubkey Log.d( "Account Selector", "Option selected for creation: " + - "{key = $createOptionEntryKey, subkey = $createOptionEntrySubkey}" + "{key = $entryKey, subkey = $entrySubkey}" ) - if (createOptionEntryKey != null && createOptionEntrySubkey != null) { + if (entryKey != null && entrySubkey != null) { CredentialManagerRepo.getInstance().onOptionSelected( uiState.activeEntry?.activeProvider!!.name, - createOptionEntryKey, - createOptionEntrySubkey + entryKey, + entrySubkey ) } else { TODO("Gracefully handle illegal state.") diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index e3398c0d6fde8..8b8108319b89f 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -184,12 +184,12 @@ fun CredentialOptionRow( ) { Column() { Text( - text = credentialOptionInfo.title, + text = credentialOptionInfo.entryKey, style = Typography.h6, modifier = Modifier.padding(top = 16.dp) ) Text( - text = credentialOptionInfo.subtitle, + text = credentialOptionInfo.entrySubkey, style = Typography.body2, modifier = Modifier.padding(bottom = 16.dp) ) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index b6ecd371e6582..b427de61505fa 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -24,17 +24,21 @@ data class ProviderInfo( val displayName: String, val credentialTypeIcon: Drawable, val credentialOptions: List, + // TODO: Add the authenticationOption ) -data class CredentialOptionInfo( - val icon: Drawable, - val title: String, - val subtitle: String, +open class EntryInfo ( val entryKey: String, val entrySubkey: String, - val usageData: String ) +class CredentialOptionInfo( + entryKey: String, + entrySubkey: String, + val icon: Drawable, + val usageData: String, +) : EntryInfo(entryKey, entrySubkey) + data class RequestDisplayInfo( val userName: String, val displayName: String,