diff --git a/packages/CredentialManager/res/drawable/ic_face.xml b/packages/CredentialManager/res/drawable/ic_face.xml
new file mode 100644
index 0000000000000..16fe14495e9d1
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_face.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_manage_accounts.xml b/packages/CredentialManager/res/drawable/ic_manage_accounts.xml
new file mode 100644
index 0000000000000..adad2f105d55b
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_manage_accounts.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index d47e3f49242c2..2f6d1b4bea02e 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -54,6 +54,12 @@
" - "
Sign-in options
-
+
For %1$s
+
+ Locked password managers
+
+ Tap to unlock
+
+ Manage sign-ins
\ 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 0d5009a3bf800..8bd7cf03008b3 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -209,7 +209,7 @@ class CredentialManagerRepo(
private fun testGetCredentialProviderList(): List {
return listOf(
- GetCredentialProviderData.Builder("com.google/com.google.CredentialManagerService")
+ GetCredentialProviderData.Builder("io.enpass.app")
.setCredentialEntries(
listOf(
newGetEntry(
@@ -225,8 +225,23 @@ class CredentialManagerRepo(
"elisa.family@outlook.com", null, 100L
),
)
+ ).setAuthenticationEntry(
+ newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+ ).setActionChips(
+ listOf(
+ newActionEntry(
+ "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
+ Icon.createWithResource(context, R.drawable.ic_manage_accounts),
+ "Open Google Password Manager", "elisa.beckett@gmail.com"
+ ),
+ newActionEntry(
+ "key3", "subkey-2", TYPE_PASSWORD_CREDENTIAL,
+ Icon.createWithResource(context, R.drawable.ic_manage_accounts),
+ "Open Google Password Manager", "beckett-family@gmail.com"
+ ),
+ )
).build(),
- GetCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService")
+ GetCredentialProviderData.Builder("com.dashlane")
.setCredentialEntries(
listOf(
newGetEntry(
@@ -238,10 +253,58 @@ class CredentialManagerRepo(
"elisa.family@outlook.com", null, 100L
),
)
+ ).setAuthenticationEntry(
+ newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+ ).setActionChips(
+ listOf(
+ newActionEntry(
+ "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
+ Icon.createWithResource(context, R.drawable.ic_face),
+ "Open Enpass"
+ ),
+ )
).build(),
)
}
+ private fun newActionEntry(
+ key: String,
+ subkey: String,
+ credentialType: String,
+ icon: Icon,
+ text: String,
+ subtext: String? = null,
+ ): Entry {
+ val slice = Slice.Builder(
+ Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
+ ).addText(
+ text, null, listOf(Entry.HINT_ACTION_TITLE)
+ ).addIcon(icon, null, listOf(Entry.HINT_ACTION_ICON))
+ if (subtext != null) {
+ slice.addText(subtext, null, listOf(Entry.HINT_ACTION_SUBTEXT))
+ }
+ return Entry(
+ key,
+ subkey,
+ slice.build()
+ )
+ }
+
+ private fun newAuthenticationEntry(
+ key: String,
+ subkey: String,
+ credentialType: String,
+ ): Entry {
+ val slice = Slice.Builder(
+ Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
+ )
+ return Entry(
+ key,
+ subkey,
+ slice.build()
+ )
+ }
+
private fun newGetEntry(
key: String,
subkey: String,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 1e43d541ac131..33fb154f44f0d 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -22,12 +22,14 @@ import android.credentials.ui.Entry
import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.CreateCredentialProviderData
import android.credentials.ui.DisabledProviderData
+import android.graphics.drawable.Drawable
import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.createflow.RemoteInfo
import com.android.credentialmanager.getflow.ActionEntryInfo
import com.android.credentialmanager.getflow.AuthenticationEntryInfo
import com.android.credentialmanager.getflow.CredentialEntryInfo
import com.android.credentialmanager.getflow.ProviderInfo
+import com.android.credentialmanager.jetpack.provider.ActionUi
import com.android.credentialmanager.jetpack.provider.CredentialEntryUi
import com.android.credentialmanager.jetpack.provider.SaveEntryUi
@@ -39,17 +41,27 @@ class GetFlowUtils {
providerDataList: List,
context: Context,
): List {
+ val packageManager = context.packageManager
return providerDataList.map {
+ // TODO: get from the actual service info
+ val pkgInfo = packageManager
+ .getPackageInfo(it.providerFlattenedComponentName,
+ PackageManager.PackageInfoFlags.of(0))
+ val providerDisplayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString()
+ // TODO: decide what to do when failed to load a provider icon
+ val providerIcon = pkgInfo.applicationInfo.loadIcon(packageManager)!!
ProviderInfo(
id = it.providerFlattenedComponentName,
- // TODO: replace to extract from the service data structure when available
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- // TODO: get the service display name and icon from the component name.
- displayName = it.providerFlattenedComponentName,
+ // TODO: decide what to do when failed to load a provider icon
+ icon = providerIcon,
+ displayName = providerDisplayName,
credentialEntryList = getCredentialOptionInfoList(
it.providerFlattenedComponentName, it.credentialEntries, context),
authenticationEntry = getAuthenticationEntry(
- it.providerFlattenedComponentName, it.authenticationEntry, context),
+ it.providerFlattenedComponentName,
+ providerDisplayName,
+ providerIcon,
+ it.authenticationEntry),
actionEntryList = getActionEntryList(
it.providerFlattenedComponentName, it.actionChips, context),
)
@@ -85,11 +97,22 @@ class GetFlowUtils {
private fun getAuthenticationEntry(
providerId: String,
+ providerDisplayName: String,
+ providerIcon: Drawable,
authEntry: Entry?,
- context: Context,
): AuthenticationEntryInfo? {
- // TODO: implement
- return null
+ // TODO: should also call fromSlice after getting the official jetpack code.
+
+ if (authEntry == null) {
+ return null
+ }
+ return AuthenticationEntryInfo(
+ providerId = providerId,
+ entryKey = authEntry.key,
+ entrySubkey = authEntry.subkey,
+ title = providerDisplayName,
+ icon = providerIcon,
+ )
}
private fun getActionEntryList(
@@ -97,8 +120,19 @@ class GetFlowUtils {
actionEntries: List,
context: Context,
): List {
- // TODO: implement
- return emptyList()
+ return actionEntries.map {
+ val actionEntryUi = ActionUi.fromSlice(it.slice)
+
+ return@map ActionEntryInfo(
+ providerId = providerId,
+ entryKey = it.key,
+ entrySubkey = it.subkey,
+ title = actionEntryUi.text.toString(),
+ // TODO: gracefully fail
+ icon = actionEntryUi.icon.loadDrawable(context)!!,
+ subTitle = actionEntryUi.subtext?.toString(),
+ )
+ }
}
}
}
@@ -110,12 +144,14 @@ class CreateFlowUtils {
providerDataList: List,
context: Context,
): List {
+ // TODO: get from the actual service info
val packageManager = context.packageManager
return providerDataList.map {
val pkgInfo = packageManager
.getPackageInfo(it.providerFlattenedComponentName,
PackageManager.PackageInfoFlags.of(0))
com.android.credentialmanager.createflow.EnabledProviderInfo(
+ // TODO: decide what to do when failed to load a provider icon
icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!,
name = it.providerFlattenedComponentName,
displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
@@ -130,6 +166,7 @@ class CreateFlowUtils {
providerDataList: List,
context: Context,
): List {
+ // TODO: get from the actual service info
val packageManager = context.packageManager
return providerDataList.map {
val pkgInfo = packageManager
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 23592c370c53e..dcdd71a283a8c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -55,8 +55,6 @@ import com.android.credentialmanager.common.material.rememberModalBottomSheetSta
import com.android.credentialmanager.common.ui.CancelButton
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
-
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GetCredentialScreen(
viewModel: GetCredentialViewModel,
@@ -72,13 +70,14 @@ fun GetCredentialScreen(
when (uiState.currentScreenState) {
GetScreenState.PRIMARY_SELECTION -> PrimarySelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
- sortedUserNameToCredentialEntryList = uiState.sortedUserNameToCredentialEntryList,
+ providerDisplayInfo = uiState.providerDisplayInfo,
onEntrySelected = viewModel::onEntrySelected,
onCancel = viewModel::onCancel,
onMoreOptionSelected = viewModel::onMoreOptionSelected,
)
GetScreenState.ALL_SIGN_IN_OPTIONS -> AllSignInOptionCard(
- sortedUserNameToCredentialEntryList = uiState.sortedUserNameToCredentialEntryList,
+ providerInfoList = uiState.providerInfoList,
+ providerDisplayInfo = uiState.providerDisplayInfo,
onEntrySelected = viewModel::onEntrySelected,
onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
)
@@ -95,15 +94,16 @@ fun GetCredentialScreen(
}
/** Draws the primary credential selection page. */
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PrimarySelectionCard(
requestDisplayInfo: RequestDisplayInfo,
- sortedUserNameToCredentialEntryList: List,
+ providerDisplayInfo: ProviderDisplayInfo,
onEntrySelected: (EntryInfo) -> Unit,
onCancel: () -> Unit,
onMoreOptionSelected: () -> Unit,
) {
+ val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList
+ val authenticationEntryList = providerDisplayInfo.authenticationEntryList
Card() {
Column() {
Text(
@@ -133,7 +133,13 @@ fun PrimarySelectionCard(
items(sortedUserNameToCredentialEntryList) {
CredentialEntryRow(
credentialEntryInfo = it.sortedCredentialEntryList.first(),
- onEntrySelected = onEntrySelected
+ onEntrySelected = onEntrySelected,
+ )
+ }
+ items(authenticationEntryList) {
+ AuthenticationEntryRow(
+ authenticationEntryInfo = it,
+ onEntrySelected = onEntrySelected,
)
}
item {
@@ -164,10 +170,13 @@ fun PrimarySelectionCard(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AllSignInOptionCard(
- sortedUserNameToCredentialEntryList: List,
+ providerInfoList: List,
+ providerDisplayInfo: ProviderDisplayInfo,
onEntrySelected: (EntryInfo) -> Unit,
onBackButtonClicked: () -> Unit,
) {
+ val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList
+ val authenticationEntryList = providerDisplayInfo.authenticationEntryList
Card() {
Column() {
TopAppBar(
@@ -199,19 +208,74 @@ fun AllSignInOptionCard(
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
+ // For username
items(sortedUserNameToCredentialEntryList) { item ->
PerUserNameCredentials(
perUserNameCredentialEntryList = item,
- onEntrySelected = onEntrySelected
+ onEntrySelected = onEntrySelected,
)
}
+ // Locked password manager
+ item {
+ if (!authenticationEntryList.isEmpty()) {
+ LockedCredentials(
+ authenticationEntryList = authenticationEntryList,
+ onEntrySelected = onEntrySelected,
+ )
+ }
+ }
+ // TODO: Remote action
+ // Manage sign-ins
+ item {
+ ActionChips(providerInfoList = providerInfoList, onEntrySelected = onEntrySelected)
+ }
}
}
}
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+// TODO: create separate rows for primary and secondary pages.
+// TODO: reuse rows and columns across types.
+
+@Composable
+fun ActionChips(
+ providerInfoList: List,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ val actionChips = providerInfoList.flatMap { it.actionEntryList }
+ if (actionChips.isEmpty()) {
+ return
+ }
+
+ Text(
+ text = stringResource(R.string.get_dialog_heading_manage_sign_ins),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ // TODO: tweak padding.
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ actionChips.forEach {
+ ActionEntryRow(it, onEntrySelected)
+ }
+ }
+}
+
+@Composable
+fun LockedCredentials(
+ authenticationEntryList: List,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ Text(
+ text = stringResource(R.string.get_dialog_heading_locked_password_managers),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ authenticationEntryList.forEach {
+ AuthenticationEntryRow(it, onEntrySelected)
+ }
+}
+
@Composable
fun PerUserNameCredentials(
perUserNameCredentialEntryList: PerUserNameCredentialEntryList,
@@ -268,6 +332,73 @@ fun CredentialEntryRow(
)
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AuthenticationEntryRow(
+ authenticationEntryInfo: AuthenticationEntryInfo,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ SuggestionChip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {onEntrySelected(authenticationEntryInfo)},
+ icon = {
+ Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
+ bitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(),
+ // TODO: add description.
+ contentDescription = "")
+ },
+ shape = MaterialTheme.shapes.large,
+ label = {
+ Column() {
+ // TODO: fix the text values.
+ Text(
+ text = authenticationEntryInfo.title,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(top = 16.dp)
+ )
+ Text(
+ text = stringResource(R.string.locked_credential_entry_label_subtext),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ActionEntryRow(
+ actionEntryInfo: ActionEntryInfo,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ SuggestionChip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { onEntrySelected(actionEntryInfo) },
+ icon = {
+ Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
+ bitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(),
+ // TODO: add description.
+ contentDescription = "")
+ },
+ shape = MaterialTheme.shapes.large,
+ label = {
+ Column() {
+ Text(
+ text = actionEntryInfo.title,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ if (actionEntryInfo.subTitle != null) {
+ Text(
+ text = actionEntryInfo.subTitle,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+ }
+ )
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SignInAnotherWayRow(onSelect: () -> Unit) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index f44927482fed5..f78456aab332c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -32,12 +32,7 @@ data class GetCredentialUiState(
val providerInfoList: List,
val currentScreenState: GetScreenState,
val requestDisplayInfo: RequestDisplayInfo,
- /**
- * The credential entries grouped by userName, derived from all entries of the [providerInfoList].
- * Note that the list order matters to the display order.
- */
- val sortedUserNameToCredentialEntryList: List =
- createSortedUserNameToCredentialEntryList(providerInfoList),
+ val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
)
class GetCredentialViewModel(
@@ -85,14 +80,19 @@ class GetCredentialViewModel(
}
}
-internal fun createSortedUserNameToCredentialEntryList(
+private fun toProviderDisplayInfo(
providerInfoList: List
-): List {
- // Group by username
- val userNameToEntryMap = mutableMapOf>()
+): ProviderDisplayInfo {
+
+ val userNameToCredentialEntryMap = mutableMapOf>()
+ val authenticationEntryList = mutableListOf()
providerInfoList.forEach { providerInfo ->
+ if (providerInfo.authenticationEntry != null) {
+ authenticationEntryList.add(providerInfo.authenticationEntry)
+ }
+
providerInfo.credentialEntryList.forEach {
- userNameToEntryMap.compute(
+ userNameToCredentialEntryMap.compute(
it.userName
) {
_, v ->
@@ -105,17 +105,24 @@ internal fun createSortedUserNameToCredentialEntryList(
}
}
}
+
+ // Compose sortedUserNameToCredentialEntryList
val comparator = CredentialEntryInfoComparator()
// Sort per username
- userNameToEntryMap.values.forEach {
+ userNameToCredentialEntryMap.values.forEach {
it.sortWith(comparator)
}
// Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
- return userNameToEntryMap.map {
+ val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
PerUserNameCredentialEntryList(it.key, it.value)
}.sortedWith(
compareBy(comparator) { it.sortedCredentialEntryList.first() }
)
+
+ return ProviderDisplayInfo(
+ sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
+ authenticationEntryList = authenticationEntryList,
+ )
}
internal class CredentialEntryInfoComparator : Comparator {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 84009b1eb4415..c1d9ea9b91882 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -32,6 +32,17 @@ data class ProviderInfo(
// TODO: add remote entry
)
+/** Display-centric data structure derived from the [ProviderInfo]. This abstraction is not grouping
+ * by the provider id but instead focuses on structures convenient for display purposes. */
+data class ProviderDisplayInfo(
+ /**
+ * The credential entries grouped by userName, derived from all entries of the [providerInfoList].
+ * Note that the list order matters to the display order.
+ */
+ val sortedUserNameToCredentialEntryList: List,
+ val authenticationEntryList: List,
+)
+
abstract class EntryInfo (
/** Unique id combination of this entry. Not for display purpose. */
val providerId: String,
@@ -59,6 +70,8 @@ class AuthenticationEntryInfo(
providerId: String,
entryKey: String,
entrySubkey: String,
+ val title: String,
+ val icon: Drawable,
) : EntryInfo(providerId, entryKey, entrySubkey)
class ActionEntryInfo(
@@ -66,6 +79,7 @@ class ActionEntryInfo(
entryKey: String,
entrySubkey: String,
val title: String,
+ val icon: Drawable,
val subTitle: String?,
) : EntryInfo(providerId, entryKey, entrySubkey)