Merge "Support locked entries and action chips for the get flow."
This commit is contained in:
30
packages/CredentialManager/res/drawable/ic_face.xml
Normal file
30
packages/CredentialManager/res/drawable/ic_face.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<!--TODO: Testing only icon. Remove later. -->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="VectorPath"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M9.025,14.275Q8.5,14.275 8.125,13.9Q7.75,13.525 7.75,13Q7.75,12.475 8.125,12.1Q8.5,11.725 9.025,11.725Q9.575,11.725 9.938,12.1Q10.3,12.475 10.3,13Q10.3,13.525 9.938,13.9Q9.575,14.275 9.025,14.275ZM14.975,14.275Q14.425,14.275 14.062,13.9Q13.7,13.525 13.7,13Q13.7,12.475 14.062,12.1Q14.425,11.725 14.975,11.725Q15.5,11.725 15.875,12.1Q16.25,12.475 16.25,13Q16.25,13.525 15.875,13.9Q15.5,14.275 14.975,14.275ZM12,19.925Q15.325,19.925 17.625,17.625Q19.925,15.325 19.925,12Q19.925,11.4 19.85,10.85Q19.775,10.3 19.575,9.775Q19.05,9.9 18.538,9.962Q18.025,10.025 17.45,10.025Q15.2,10.025 13.188,9.062Q11.175,8.1 9.775,6.375Q8.975,8.3 7.5,9.712Q6.025,11.125 4.075,11.85Q4.075,11.9 4.075,11.925Q4.075,11.95 4.075,12Q4.075,15.325 6.375,17.625Q8.675,19.925 12,19.925ZM12,22.2Q9.9,22.2 8.038,21.4Q6.175,20.6 4.788,19.225Q3.4,17.85 2.6,15.988Q1.8,14.125 1.8,12Q1.8,9.875 2.6,8.012Q3.4,6.15 4.788,4.775Q6.175,3.4 8.038,2.6Q9.9,1.8 12,1.8Q14.125,1.8 15.988,2.6Q17.85,3.4 19.225,4.775Q20.6,6.15 21.4,8.012Q22.2,9.875 22.2,12Q22.2,14.125 21.4,15.988Q20.6,17.85 19.225,19.225Q17.85,20.6 15.988,21.4Q14.125,22.2 12,22.2Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<!--TODO: Testing only icon. Remove later. -->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="VectorPath"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M16.1,21.2 L15.775,19.675Q15.5,19.55 15.25,19.425Q15,19.3 14.75,19.1L13.275,19.575L12.2,17.75L13.375,16.725Q13.325,16.4 13.325,16.112Q13.325,15.825 13.375,15.5L12.2,14.475L13.275,12.65L14.75,13.1Q15,12.925 15.25,12.787Q15.5,12.65 15.775,12.55L16.1,11.025H18.25L18.55,12.55Q18.825,12.65 19.075,12.8Q19.325,12.95 19.575,13.15L21.05,12.65L22.125,14.525L20.95,15.55Q21.025,15.825 21.013,16.137Q21,16.45 20.95,16.725L22.125,17.75L21.05,19.575L19.575,19.1Q19.325,19.3 19.075,19.425Q18.825,19.55 18.55,19.675L18.25,21.2ZM1.8,20.3V17.3Q1.8,16.375 2.275,15.613Q2.75,14.85 3.5,14.475Q4.775,13.825 6.425,13.362Q8.075,12.9 10,12.9Q10.2,12.9 10.4,12.9Q10.6,12.9 10.775,12.95Q9.925,14.85 10.062,16.738Q10.2,18.625 11.4,20.3ZM17.175,18.075Q17.975,18.075 18.55,17.487Q19.125,16.9 19.125,16.1Q19.125,15.3 18.55,14.725Q17.975,14.15 17.175,14.15Q16.375,14.15 15.788,14.725Q15.2,15.3 15.2,16.1Q15.2,16.9 15.788,17.487Q16.375,18.075 17.175,18.075ZM10,11.9Q8.25,11.9 7.025,10.662Q5.8,9.425 5.8,7.7Q5.8,5.95 7.025,4.725Q8.25,3.5 10,3.5Q11.75,3.5 12.975,4.725Q14.2,5.95 14.2,7.7Q14.2,9.425 12.975,10.662Q11.75,11.9 10,11.9Z"/>
|
||||
</vector>
|
||||
@@ -54,6 +54,12 @@
|
||||
<string name="get_dialog_sign_in_type_username_separator" translatable="false">" - "</string>
|
||||
<!-- Modal bottom sheet title for displaying all the available sign-in options. [CHAR LIMIT=80] -->
|
||||
<string name="get_dialog_title_sign_in_options">Sign-in options</string>
|
||||
<!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=20] -->
|
||||
<!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=80] -->
|
||||
<string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string>
|
||||
<!-- Column heading for displaying locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-ins. [CHAR LIMIT=80] -->
|
||||
<string name="get_dialog_heading_locked_password_managers">Locked password managers</string>
|
||||
<!-- Explanatory sub/body text for an option entry to use a locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-in. [CHAR LIMIT=120] -->
|
||||
<string name="locked_credential_entry_label_subtext">Tap to unlock</string>
|
||||
<!-- Column heading for displaying action chips for managing sign-ins from each credential provider. [CHAR LIMIT=80] -->
|
||||
<string name="get_dialog_heading_manage_sign_ins">Manage sign-ins</string>
|
||||
</resources>
|
||||
@@ -209,7 +209,7 @@ class CredentialManagerRepo(
|
||||
|
||||
private fun testGetCredentialProviderList(): List<GetCredentialProviderData> {
|
||||
return listOf(
|
||||
GetCredentialProviderData.Builder("com.google/com.google.CredentialManagerService")
|
||||
GetCredentialProviderData.Builder("io.enpass.app")
|
||||
.setCredentialEntries(
|
||||
listOf<Entry>(
|
||||
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<Entry>(
|
||||
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,
|
||||
|
||||
@@ -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<GetCredentialProviderData>,
|
||||
context: Context,
|
||||
): List<ProviderInfo> {
|
||||
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<Entry>,
|
||||
context: Context,
|
||||
): List<ActionEntryInfo> {
|
||||
// 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<CreateCredentialProviderData>,
|
||||
context: Context,
|
||||
): List<com.android.credentialmanager.createflow.EnabledProviderInfo> {
|
||||
// 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<DisabledProviderData>,
|
||||
context: Context,
|
||||
): List<com.android.credentialmanager.createflow.DisabledProviderInfo> {
|
||||
// TODO: get from the actual service info
|
||||
val packageManager = context.packageManager
|
||||
return providerDataList.map {
|
||||
val pkgInfo = packageManager
|
||||
|
||||
@@ -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<PerUserNameCredentialEntryList>,
|
||||
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<PerUserNameCredentialEntryList>,
|
||||
providerInfoList: List<ProviderInfo>,
|
||||
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<ProviderInfo>,
|
||||
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<AuthenticationEntryInfo>,
|
||||
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) {
|
||||
|
||||
@@ -32,12 +32,7 @@ data class GetCredentialUiState(
|
||||
val providerInfoList: List<ProviderInfo>,
|
||||
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<PerUserNameCredentialEntryList> =
|
||||
createSortedUserNameToCredentialEntryList(providerInfoList),
|
||||
val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
|
||||
)
|
||||
|
||||
class GetCredentialViewModel(
|
||||
@@ -85,14 +80,19 @@ class GetCredentialViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createSortedUserNameToCredentialEntryList(
|
||||
private fun toProviderDisplayInfo(
|
||||
providerInfoList: List<ProviderInfo>
|
||||
): List<PerUserNameCredentialEntryList> {
|
||||
// Group by username
|
||||
val userNameToEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
|
||||
): ProviderDisplayInfo {
|
||||
|
||||
val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
|
||||
val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
|
||||
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<CredentialEntryInfo> {
|
||||
|
||||
@@ -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<PerUserNameCredentialEntryList>,
|
||||
val authenticationEntryList: List<AuthenticationEntryInfo>,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user