diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java
index 33427d373219c..6fa331bbdc545 100644
--- a/core/java/android/credentials/ui/Entry.java
+++ b/core/java/android/credentials/ui/Entry.java
@@ -53,7 +53,8 @@ public class Entry implements Parcelable {
/** Below are only available for get flows. */
public static final String HINT_NOTE = "HINT_NOTE";
public static final String HINT_USER_NAME = "HINT_USER_NAME";
- public static final String HINT_CREDENTIAL_TYPE = "HINT_CREDENTIAL_TYPE";
+ public static final String HINT_CREDENTIAL_TYPE_DISPLAY_NAME =
+ "HINT_CREDENTIAL_TYPE_DISPLAY_NAME";
public static final String HINT_PASSKEY_USER_DISPLAY_NAME = "HINT_PASSKEY_USER_DISPLAY_NAME";
public static final String HINT_PASSWORD_VALUE = "HINT_PASSWORD_VALUE";
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 6178efcf86c01..7a4b7cb0288d4 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -29,4 +29,26 @@
CreateOptionInfo credentialType icon
"Close sheet"
+
+ "Go back to the previous page"
+
+
+
+ Use your saved passkey for %1$s?
+
+ Use your saved sign-in for %1$s?
+
+ Choose a saved sign-in for %1$s
+
+ Sign in another way
+
+ No thanks
+
+ Continue
+
+ " - "
+
+ Sign-in options
+
+ For %1$s
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.kt
deleted file mode 100644
index ee4f4ca9c832f..0000000000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.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 credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-class CredentialEntryUi(
- val userName: CharSequence,
- val displayName: CharSequence?,
- val icon: Icon?,
- val usageData: CharSequence?,
- // TODO: add last used.
-) {
- companion object {
- fun fromSlice(slice: Slice): CredentialEntryUi {
- 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 CredentialEntryUi(title!!, subTitle, icon, usageData)
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 33bc79c0a4e8a..2099a235a3e88 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -16,11 +16,14 @@
package com.android.credentialmanager
+import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.content.Context
import android.content.Intent
import android.credentials.CreateCredentialRequest
+import android.credentials.GetCredentialOption
+import android.credentials.GetCredentialRequest
import android.credentials.ui.Constants
import android.credentials.ui.Entry
import android.credentials.ui.CreateCredentialProviderData
@@ -40,7 +43,7 @@ import com.android.credentialmanager.createflow.ProviderInfo
import com.android.credentialmanager.createflow.RequestDisplayInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.GetScreenState
-import com.android.credentialmanager.jetpack.provider.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
// Consider repo per screen, similar to view model?
class CredentialManagerRepo(
@@ -56,7 +59,7 @@ class CredentialManagerRepo(
requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
- ) ?: testRequestInfo()
+ ) ?: testCreateRequestInfo()
providerList = when (requestInfo.type) {
RequestInfo.TYPE_CREATE ->
@@ -101,19 +104,14 @@ class CredentialManagerRepo(
fun getCredentialInitialUiState(): GetCredentialUiState {
val providerList = GetFlowUtils.toProviderList(
- // TODO: handle runtime cast error
- providerList as List, context)
+ // TODO: handle runtime cast error
+ providerList as List, context)
// TODO: covert from real requestInfo
- val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo(
- "Elisa Beckett",
- "beckett-bakert@gmail.com",
- TYPE_PUBLIC_KEY_CREDENTIAL,
- "tribank")
+ val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo("tribank")
return GetCredentialUiState(
providerList,
- GetScreenState.CREDENTIAL_SELECTION,
+ GetScreenState.PRIMARY_SELECTION,
requestDisplayInfo,
- providerList.first()
)
}
@@ -165,9 +163,9 @@ class CredentialManagerRepo(
.Builder("com.google/com.google.CredentialManagerService")
.setSaveEntries(
listOf(
- newEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
+ newCreateEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
20, 7, 27, 10000),
- newEntry("key1", "subkey-2", "elisa.work@google.com",
+ newCreateEntry("key1", "subkey-2", "elisa.work@google.com",
20, 7, 27, 11000),
)
)
@@ -177,9 +175,9 @@ class CredentialManagerRepo(
.Builder("com.dashlane/com.dashlane.CredentialManagerService")
.setSaveEntries(
listOf(
- newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
+ newCreateEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
20, 7, 27, 30000),
- newEntry("key1", "subkey-4", "elisa.work@dashlane.com",
+ newCreateEntry("key1", "subkey-4", "elisa.work@dashlane.com",
20, 7, 27, 31000),
)
)
@@ -192,37 +190,69 @@ class CredentialManagerRepo(
GetCredentialProviderData.Builder("com.google/com.google.CredentialManagerService")
.setCredentialEntries(
listOf(
- newEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
- 20, 7, 27, 10000),
- newEntry("key1", "subkey-2", "elisa.work@google.com",
- 20, 7, 27, 11000),
+ newGetEntry(
+ "key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
+ "elisa.bakery@gmail.com", "Elisa Beckett", 300L
+ ),
+ newGetEntry(
+ "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
+ "elisa.bakery@gmail.com", null, 300L
+ ),
+ newGetEntry(
+ "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password",
+ "elisa.family@outlook.com", null, 100L
+ ),
)
- ).setActionChips(
- listOf(
- 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",
- 20, 7, 27, 30000),
- newEntry("key1", "subkey-4", "elisa.work@dashlane.com",
- 20, 7, 27, 31000),
+ newGetEntry(
+ "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password",
+ "elisa.family@outlook.com", null, 600L
+ ),
+ newGetEntry(
+ "key1", "subkey-2", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
+ "elisa.family@outlook.com", null, 100L
+ ),
)
- ).setActionChips(
- listOf(
- newEntry("key2", "subkey-3", "Manage Accounts",
- 20, 7, 27, 40000),
- ),
).build(),
)
}
- private fun newEntry(
+ private fun newGetEntry(
+ key: String,
+ subkey: String,
+ credentialType: String,
+ credentialTypeDisplayName: String,
+ userName: String,
+ userDisplayName: String?,
+ lastUsedTimeMillis: Long?,
+ ): Entry {
+ val slice = Slice.Builder(
+ Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
+ ).addText(
+ credentialTypeDisplayName, null, listOf(Entry.HINT_CREDENTIAL_TYPE_DISPLAY_NAME)
+ ).addText(
+ userName, null, listOf(Entry.HINT_USER_NAME)
+ ).addIcon(
+ Icon.createWithResource(context, R.drawable.ic_passkey),
+ null,
+ listOf(Entry.HINT_PROFILE_ICON))
+ if (userDisplayName != null) {
+ slice.addText(userDisplayName, null, listOf(Entry.HINT_PASSKEY_USER_DISPLAY_NAME))
+ }
+ if (lastUsedTimeMillis != null) {
+ slice.addLong(lastUsedTimeMillis, null, listOf(Entry.HINT_LAST_USED_TIME_MILLIS))
+ }
+ return Entry(
+ key,
+ subkey,
+ slice.build()
+ )
+ }
+
+ private fun newCreateEntry(
key: String,
subkey: String,
providerDisplayName: String,
@@ -259,12 +289,11 @@ class CredentialManagerRepo(
)
}
- private fun testRequestInfo(): RequestInfo {
+ private fun testCreateRequestInfo(): RequestInfo {
val data = Bundle()
return RequestInfo.newCreateRequestInfo(
Binder(),
CreateCredentialRequest(
- // TODO: use the jetpack type and utils once defined.
TYPE_PUBLIC_KEY_CREDENTIAL,
data
),
@@ -272,4 +301,18 @@ class CredentialManagerRepo(
"tribank.us"
)
}
+
+ private fun testGetRequestInfo(): RequestInfo {
+ val data = Bundle()
+ return RequestInfo.newGetRequestInfo(
+ Binder(),
+ GetCredentialRequest.Builder()
+ .addGetCredentialOption(
+ GetCredentialOption(TYPE_PUBLIC_KEY_CREDENTIAL, Bundle())
+ )
+ .build(),
+ /*isFirstUsage=*/false,
+ "tribank.us"
+ )
+ }
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 3a8e975ea0c1c..5c79564234690 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -21,8 +21,11 @@ import android.credentials.ui.Entry
import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.CreateCredentialProviderData
import com.android.credentialmanager.createflow.CreateOptionInfo
-import com.android.credentialmanager.getflow.CredentialOptionInfo
+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.CredentialEntryUi
import com.android.credentialmanager.jetpack.provider.SaveEntryUi
/** Utility functions for converting CredentialManager data structures to or from UI formats. */
@@ -35,37 +38,65 @@ class GetFlowUtils {
): List {
return providerDataList.map {
ProviderInfo(
+ id = it.providerFlattenedComponentName,
// 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)!!,
- credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context),
+ credentialEntryList = getCredentialOptionInfoList(
+ it.providerFlattenedComponentName, it.credentialEntries, context),
+ authenticationEntry = getAuthenticationEntry(
+ it.providerFlattenedComponentName, it.authenticationEntry, context),
+ actionEntryList = getActionEntryList(
+ it.providerFlattenedComponentName, it.actionChips, context),
)
}
}
/* From service data structure to UI credential entry list representation. */
- private fun toCredentialOptionInfoList(
+ private fun getCredentialOptionInfoList(
+ providerId: String,
credentialEntries: List,
context: Context,
- ): List {
+ ): List {
return credentialEntries.map {
val credentialEntryUi = CredentialEntryUi.fromSlice(it.slice)
// Consider directly move the UI object into the class.
- return@map CredentialOptionInfo(
- // TODO: remove fallbacks
- icon = credentialEntryUi.icon?.loadDrawable(context)
- ?: context.getDrawable(R.drawable.ic_passkey)!!,
+ return@map CredentialEntryInfo(
+ providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
- usageData = credentialEntryUi.usageData?.toString() ?: "Unknown usageData",
+ credentialType = credentialEntryUi.credentialType.toString(),
+ credentialTypeDisplayName = credentialEntryUi.credentialTypeDisplayName.toString(),
+ userName = credentialEntryUi.userName.toString(),
+ displayName = credentialEntryUi.userDisplayName?.toString(),
+ // TODO: proper fallback
+ icon = credentialEntryUi.entryIcon.loadDrawable(context)
+ ?: context.getDrawable(R.drawable.ic_passkey)!!,
+ lastUsedTimeMillis = credentialEntryUi.lastUsedTimeMillis,
)
}
}
+
+ private fun getAuthenticationEntry(
+ providerId: String,
+ authEntry: Entry?,
+ context: Context,
+ ): AuthenticationEntryInfo? {
+ // TODO: implement
+ return null
+ }
+
+ private fun getActionEntryList(
+ providerId: String,
+ actionEntries: List,
+ context: Context,
+ ): List {
+ // TODO: implement
+ return emptyList()
+ }
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt
new file mode 100644
index 0000000000000..177d0e0288c7d
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.common.ui
+
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+
+@Composable
+fun CancelButton(text: String, onClick: () -> Unit) {
+ TextButton(onClick = onClick) {
+ Text(text = text)
+ }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ConfirmButton.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ConfirmButton.kt
new file mode 100644
index 0000000000000..b2b0bdcdf3ce6
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ConfirmButton.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.common.ui
+
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+
+@Composable
+fun ConfirmButton(text: String, onClick: () -> Unit) {
+ FilledTonalButton(onClick = onClick) {
+ Text(text = text)
+ }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index 278b8352f8d12..2344847ff28e0 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -1,5 +1,6 @@
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
@@ -11,7 +12,6 @@ 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.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -36,8 +36,9 @@ 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.jetpack.provider.CredentialEntryUi.Companion.TYPE_PASSWORD_CREDENTIAL
-import com.android.credentialmanager.jetpack.provider.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
+import com.android.credentialmanager.common.ui.CancelButton
+import com.android.credentialmanager.common.ui.ConfirmButton
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -226,7 +227,7 @@ fun MoreOptionsSelectionCard(
IconButton(onClick = onBackButtonSelected) {
Icon(
Icons.Filled.ArrowBack,
- "backIcon")
+ stringResource(R.string.accessibility_back_arrow_button))
}
}
)
@@ -332,20 +333,6 @@ fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit
)
}
-@Composable
-fun CancelButton(text: String, onClick: () -> Unit) {
- TextButton(onClick = onClick) {
- Text(text = text)
- }
-}
-
-@Composable
-fun ConfirmButton(text: String, onClick: () -> Unit) {
- FilledTonalButton(onClick = onClick) {
- Text(text = text)
- }
-}
-
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreationSelectionCard(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 76350218ca0bd..23592c370c53e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -16,22 +16,29 @@
package com.android.credentialmanager.getflow
+import android.text.TextUtils
+
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
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.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@@ -45,7 +52,9 @@ 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.createflow.CancelButton
+import com.android.credentialmanager.common.ui.CancelButton
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -61,14 +70,18 @@ fun GetCredentialScreen(
sheetContent = {
val uiState = viewModel.uiState
when (uiState.currentScreenState) {
- GetScreenState.CREDENTIAL_SELECTION -> CredentialSelectionCard(
+ GetScreenState.PRIMARY_SELECTION -> PrimarySelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
- providerInfo = uiState.selectedProvider!!,
+ sortedUserNameToCredentialEntryList = uiState.sortedUserNameToCredentialEntryList,
+ onEntrySelected = viewModel::onEntrySelected,
onCancel = viewModel::onCancel,
- onOptionSelected = viewModel::onCredentailSelected,
- multiProvider = uiState.providers.size > 1,
onMoreOptionSelected = viewModel::onMoreOptionSelected,
)
+ GetScreenState.ALL_SIGN_IN_OPTIONS -> AllSignInOptionCard(
+ sortedUserNameToCredentialEntryList = uiState.sortedUserNameToCredentialEntryList,
+ onEntrySelected = viewModel::onEntrySelected,
+ onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
+ )
}
},
scrimColor = Color.Transparent,
@@ -81,42 +94,35 @@ fun GetCredentialScreen(
}
}
+/** Draws the primary credential selection page. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun CredentialSelectionCard(
+fun PrimarySelectionCard(
requestDisplayInfo: RequestDisplayInfo,
- providerInfo: ProviderInfo,
- onOptionSelected: (String, String) -> Unit,
+ sortedUserNameToCredentialEntryList: List,
+ onEntrySelected: (EntryInfo) -> Unit,
onCancel: () -> Unit,
- multiProvider: Boolean,
onMoreOptionSelected: () -> Unit,
) {
Card() {
Column() {
- Icon(
- bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(),
- contentDescription = null,
- tint = Color.Unspecified,
- modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
- )
Text(
- text = stringResource(R.string.choose_sign_in_title),
+ text = stringResource(
+ if (sortedUserNameToCredentialEntryList.size == 1) {
+ if (sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList
+ .first().credentialType == PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
+ )
+ R.string.get_dialog_title_use_passkey_for
+ else R.string.get_dialog_title_use_sign_in_for
+ } else R.string.get_dialog_title_choose_sign_in_for,
+ requestDisplayInfo.appDomainName
+ ),
style = MaterialTheme.typography.titleMedium,
- modifier = Modifier
- .padding(all = 24.dp)
- .align(alignment = Alignment.CenterHorizontally)
- )
- Text(
- text = requestDisplayInfo.appDomainName,
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(horizontal = 28.dp)
- )
- Divider(
- thickness = 24.dp,
- color = Color.Transparent
+ modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
)
+
Card(
- shape = MaterialTheme.shapes.medium,
+ shape = MaterialTheme.shapes.large,
modifier = Modifier
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally)
@@ -124,15 +130,14 @@ fun CredentialSelectionCard(
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
- providerInfo.credentialOptions.forEach {
- item {
- CredentialOptionRow(credentialOptionInfo = it, onOptionSelected = onOptionSelected)
- }
+ items(sortedUserNameToCredentialEntryList) {
+ CredentialEntryRow(
+ credentialEntryInfo = it.sortedCredentialEntryList.first(),
+ onEntrySelected = onEntrySelected
+ )
}
- if (multiProvider) {
- item {
- MoreOptionRow(onSelect = onMoreOptionSelected)
- }
+ item {
+ SignInAnotherWayRow(onSelect = onMoreOptionSelected)
}
}
}
@@ -155,32 +160,106 @@ fun CredentialSelectionCard(
}
}
+/** Draws the secondary credential selection page, where all sign-in options are listed. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun CredentialOptionRow(
- credentialOptionInfo: CredentialOptionInfo,
- onOptionSelected: (String, String) -> Unit,
+fun AllSignInOptionCard(
+ sortedUserNameToCredentialEntryList: List,
+ onEntrySelected: (EntryInfo) -> Unit,
+ onBackButtonClicked: () -> Unit,
+) {
+ Card() {
+ Column() {
+ TopAppBar(
+ colors = TopAppBarDefaults.smallTopAppBarColors(
+ containerColor = Color.Transparent,
+ ),
+ title = {
+ Text(
+ text = stringResource(R.string.get_dialog_title_sign_in_options),
+ style = MaterialTheme.typography.titleMedium
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBackButtonClicked) {
+ Icon(
+ Icons.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.accessibility_back_arrow_button))
+ }
+ },
+ modifier = Modifier.padding(top = 12.dp)
+ )
+
+ Card(
+ shape = MaterialTheme.shapes.large,
+ modifier = Modifier
+ .padding(start = 24.dp, end = 24.dp, bottom = 24.dp)
+ .align(alignment = Alignment.CenterHorizontally)
+ ) {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(sortedUserNameToCredentialEntryList) { item ->
+ PerUserNameCredentials(
+ perUserNameCredentialEntryList = item,
+ onEntrySelected = onEntrySelected
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PerUserNameCredentials(
+ perUserNameCredentialEntryList: PerUserNameCredentialEntryList,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ Text(
+ text = stringResource(
+ R.string.get_dialog_heading_for_username, perUserNameCredentialEntryList.userName),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ perUserNameCredentialEntryList.sortedCredentialEntryList.forEach {
+ CredentialEntryRow(it, onEntrySelected)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CredentialEntryRow(
+ credentialEntryInfo: CredentialEntryInfo,
+ onEntrySelected: (EntryInfo) -> Unit,
) {
SuggestionChip(
modifier = Modifier.fillMaxWidth(),
- onClick = {onOptionSelected(credentialOptionInfo.entryKey, credentialOptionInfo.entrySubkey)},
+ onClick = {onEntrySelected(credentialEntryInfo)},
icon = {
Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
- bitmap = credentialOptionInfo.icon.toBitmap().asImageBitmap(),
+ bitmap = credentialEntryInfo.icon.toBitmap().asImageBitmap(),
// TODO: add description.
- contentDescription = "")
+ contentDescription = "")
},
shape = MaterialTheme.shapes.large,
label = {
Column() {
// TODO: fix the text values.
Text(
- text = credentialOptionInfo.entryKey,
+ text = credentialEntryInfo.userName,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp)
)
Text(
- text = credentialOptionInfo.entrySubkey,
+ text =
+ if (TextUtils.isEmpty(credentialEntryInfo.displayName))
+ credentialEntryInfo.credentialTypeDisplayName
+ else
+ credentialEntryInfo.credentialTypeDisplayName +
+ stringResource(R.string.get_dialog_sign_in_type_username_separator) +
+ credentialEntryInfo.displayName,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
@@ -191,15 +270,16 @@ fun CredentialOptionRow(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun MoreOptionRow(onSelect: () -> Unit) {
+fun SignInAnotherWayRow(onSelect: () -> Unit) {
SuggestionChip(
- modifier = Modifier.fillMaxWidth().height(52.dp),
+ modifier = Modifier.fillMaxWidth(),
onClick = onSelect,
shape = MaterialTheme.shapes.large,
label = {
Text(
- text = stringResource(R.string.string_more_options),
+ text = stringResource(R.string.get_dialog_use_saved_passkey_for),
style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(vertical = 16.dp)
)
}
)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index 7b6c30aca89e4..f44927482fed5 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -26,12 +26,18 @@ import androidx.lifecycle.ViewModel
import com.android.credentialmanager.CredentialManagerRepo
import com.android.credentialmanager.common.DialogResult
import com.android.credentialmanager.common.ResultState
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
data class GetCredentialUiState(
- val providers: List,
+ val providerInfoList: List,
val currentScreenState: GetScreenState,
val requestDisplayInfo: RequestDisplayInfo,
- val selectedProvider: ProviderInfo? = null,
+ /**
+ * 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),
)
class GetCredentialViewModel(
@@ -49,20 +55,28 @@ class GetCredentialViewModel(
return dialogResult
}
- fun onCredentailSelected(entryKey: String, entrySubkey: String) {
- Log.d("Account Selector", "credential selected: {key=$entryKey,subkey=$entrySubkey}")
+ fun onEntrySelected(entry: EntryInfo) {
+ Log.d("Account Selector", "credential selected:" +
+ " {provider=${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
CredentialManagerRepo.getInstance().onOptionSelected(
- uiState.selectedProvider!!.name,
- entryKey,
- entrySubkey
- )
- dialogResult.value = DialogResult(
- ResultState.COMPLETE,
+ entry.providerId,
+ entry.entryKey,
+ entry.entrySubkey
)
+ dialogResult.value = DialogResult(ResultState.COMPLETE)
}
fun onMoreOptionSelected() {
Log.d("Account Selector", "More Option selected")
+ uiState = uiState.copy(
+ currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS
+ )
+ }
+
+ fun onBackToPrimarySelectionScreen() {
+ uiState = uiState.copy(
+ currentScreenState = GetScreenState.PRIMARY_SELECTION
+ )
}
fun onCancel() {
@@ -70,3 +84,63 @@ class GetCredentialViewModel(
dialogResult.value = DialogResult(ResultState.CANCELED)
}
}
+
+internal fun createSortedUserNameToCredentialEntryList(
+ providerInfoList: List
+): List {
+ // Group by username
+ val userNameToEntryMap = mutableMapOf>()
+ providerInfoList.forEach { providerInfo ->
+ providerInfo.credentialEntryList.forEach {
+ userNameToEntryMap.compute(
+ it.userName
+ ) {
+ _, v ->
+ if (v == null) {
+ mutableListOf(it)
+ } else {
+ v.add(it)
+ v
+ }
+ }
+ }
+ }
+ val comparator = CredentialEntryInfoComparator()
+ // Sort per username
+ userNameToEntryMap.values.forEach {
+ it.sortWith(comparator)
+ }
+ // Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
+ return userNameToEntryMap.map {
+ PerUserNameCredentialEntryList(it.key, it.value)
+ }.sortedWith(
+ compareBy(comparator) { it.sortedCredentialEntryList.first() }
+ )
+}
+
+internal class CredentialEntryInfoComparator : Comparator {
+ override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
+ // First order by last used timestamp
+ if (p0.lastUsedTimeMillis != null && p1.lastUsedTimeMillis != null) {
+ if (p0.lastUsedTimeMillis < p1.lastUsedTimeMillis) {
+ return 1
+ } else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
+ return -1
+ }
+ } else if (p0.lastUsedTimeMillis != null && p0.lastUsedTimeMillis > 0) {
+ return -1
+ } else if (p1.lastUsedTimeMillis != null && p1.lastUsedTimeMillis > 0) {
+ return 1
+ }
+
+ // Then prefer passkey type for its security benefits
+ if (p0.credentialType != p1.credentialType) {
+ if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p0.credentialType) {
+ return -1
+ } else if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p1.credentialType) {
+ return 1
+ }
+ }
+ return 0
+ }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index b427de61505fa..84009b1eb4415 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -19,34 +19,74 @@ package com.android.credentialmanager.getflow
import android.graphics.drawable.Drawable
data class ProviderInfo(
+ /**
+ * Unique id (component name) of this provider.
+ * Not for display purpose - [displayName] should be used for ui rendering.
+ */
+ val id: String,
val icon: Drawable,
- val name: String,
val displayName: String,
- val credentialTypeIcon: Drawable,
- val credentialOptions: List,
- // TODO: Add the authenticationOption
+ val credentialEntryList: List,
+ val authenticationEntry: AuthenticationEntryInfo?,
+ val actionEntryList: List,
+ // TODO: add remote entry
)
-open class EntryInfo (
+abstract class EntryInfo (
+ /** Unique id combination of this entry. Not for display purpose. */
+ val providerId: String,
val entryKey: String,
val entrySubkey: String,
)
-class CredentialOptionInfo(
+class CredentialEntryInfo(
+ providerId: String,
entryKey: String,
entrySubkey: String,
+ /** Type of this credential used for sorting. Not localized so must not be directly displayed. */
+ val credentialType: String,
+ /** Localized type value of this credential used for display purpose. */
+ val credentialTypeDisplayName: String,
+ val userName: String,
+ val displayName: String?,
val icon: Drawable,
- val usageData: String,
-) : EntryInfo(entryKey, entrySubkey)
+ val lastUsedTimeMillis: Long?,
+) : EntryInfo(providerId, entryKey, entrySubkey)
+
+// TODO: handle sub credential type values like password obfuscation.
+
+class AuthenticationEntryInfo(
+ providerId: String,
+ entryKey: String,
+ entrySubkey: String,
+) : EntryInfo(providerId, entryKey, entrySubkey)
+
+class ActionEntryInfo(
+ providerId: String,
+ entryKey: String,
+ entrySubkey: String,
+ val title: String,
+ val subTitle: String?,
+) : EntryInfo(providerId, entryKey, entrySubkey)
data class RequestDisplayInfo(
- val userName: String,
- val displayName: String,
- val type: String,
val appDomainName: String,
)
+/**
+ * @property userName the userName that groups all the entries in this list
+ * @property sortedCredentialEntryList the credential entries associated with the [userName] sorted
+ * by last used timestamps and then by credential types
+ */
+data class PerUserNameCredentialEntryList(
+ val userName: String,
+ val sortedCredentialEntryList: List,
+)
+
/** The name of the current screen. */
enum class GetScreenState {
- CREDENTIAL_SELECTION,
+ /** The primary credential selection page. */
+ PRIMARY_SELECTION,
+ /** The secondary credential selection page, where all sign-in options are listed. */
+ ALL_SIGN_IN_OPTIONS,
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt
index 12ab436e1507a..dfbcae119fa07 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt
@@ -17,6 +17,7 @@
package com.android.credentialmanager.jetpack.provider
import android.app.slice.Slice
+import android.credentials.ui.Entry
import android.graphics.drawable.Icon
/**
@@ -24,23 +25,46 @@ import android.graphics.drawable.Icon
*
* TODO: move to jetpack.
*/
-abstract class CredentialEntryUi(
- val credentialTypeIcon: Icon,
- val profileIcon: Icon?,
+class CredentialEntryUi(
+ val credentialType: CharSequence,
+ val credentialTypeDisplayName: CharSequence,
+ val userName: CharSequence,
+ val userDisplayName: CharSequence?,
+ val entryIcon: Icon,
val lastUsedTimeMillis: Long?,
val note: CharSequence?,
) {
companion object {
fun fromSlice(slice: Slice): CredentialEntryUi {
- return when (slice.spec?.type) {
- TYPE_PUBLIC_KEY_CREDENTIAL -> PasskeyCredentialEntryUi.fromSlice(slice)
- TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntryUi.fromSlice(slice)
- else -> throw IllegalArgumentException("Unexpected type: ${slice.spec?.type}")
- }
- }
+ var credentialType = slice.spec!!.type
+ var credentialTypeDisplayName: CharSequence? = null
+ var userName: CharSequence? = null
+ var userDisplayName: CharSequence? = null
+ var entryIcon: Icon? = null
+ var lastUsedTimeMillis: Long? = null
+ var note: CharSequence? = null
- const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
- "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
- const val TYPE_PASSWORD_CREDENTIAL: String = "androidx.credentials.TYPE_PASSWORD"
+ val items = slice.items
+ items.forEach {
+ if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_DISPLAY_NAME)) {
+ credentialTypeDisplayName = it.text
+ } else if (it.hasHint(Entry.HINT_USER_NAME)) {
+ userName = it.text
+ } else if (it.hasHint(Entry.HINT_PASSKEY_USER_DISPLAY_NAME)) {
+ userDisplayName = it.text
+ } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+ entryIcon = it.icon
+ } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+ lastUsedTimeMillis = it.long
+ } else if (it.hasHint(Entry.HINT_NOTE)) {
+ note = it.text
+ }
+ }
+
+ return CredentialEntryUi(
+ credentialType, credentialTypeDisplayName!!, userName!!, userDisplayName, entryIcon!!,
+ lastUsedTimeMillis, note,
+ )
+ }
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasskeyCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasskeyCredentialEntryUi.kt
deleted file mode 100644
index c5dbe66e8dbbf..0000000000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasskeyCredentialEntryUi.kt
+++ /dev/null
@@ -1,63 +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.jetpack.provider
-
-import android.app.slice.Slice
-import android.credentials.ui.Entry
-import android.graphics.drawable.Icon
-
-class PasskeyCredentialEntryUi(
- val userName: CharSequence,
- val userDisplayName: CharSequence?,
- credentialTypeIcon: Icon,
- profileIcon: Icon?,
- lastUsedTimeMillis: Long?,
- note: CharSequence?,
-) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) {
- companion object {
- fun fromSlice(slice: Slice): CredentialEntryUi {
- var userName: CharSequence? = null
- var userDisplayName: CharSequence? = null
- var credentialTypeIcon: Icon? = null
- var profileIcon: Icon? = null
- var lastUsedTimeMillis: Long? = null
- var note: CharSequence? = null
-
- val items = slice.items
- items.forEach {
- if (it.hasHint(Entry.HINT_USER_NAME)) {
- userName = it.text
- } else if (it.hasHint(Entry.HINT_PASSKEY_USER_DISPLAY_NAME)) {
- userDisplayName = it.text
- } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
- credentialTypeIcon = it.icon
- } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
- profileIcon = it.icon
- } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
- lastUsedTimeMillis = it.long
- } else if (it.hasHint(Entry.HINT_NOTE)) {
- note = it.text
- }
- }
- // TODO: fail NPE more elegantly.
- return PasskeyCredentialEntryUi(
- userName!!, userDisplayName, credentialTypeIcon!!,
- profileIcon, lastUsedTimeMillis, note,
- )
- }
- }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasswordCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasswordCredentialEntryUi.kt
deleted file mode 100644
index 5049503b32c1d..0000000000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/PasswordCredentialEntryUi.kt
+++ /dev/null
@@ -1,68 +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.jetpack.provider
-
-import android.app.slice.Slice
-import android.credentials.ui.Entry
-import android.graphics.drawable.Icon
-
-/**
- * UI representation for a password credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-class PasswordCredentialEntryUi(
- val userName: CharSequence,
- val password: CharSequence,
- credentialTypeIcon: Icon,
- profileIcon: Icon?,
- lastUsedTimeMillis: Long?,
- note: CharSequence?,
-) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) {
- companion object {
- fun fromSlice(slice: Slice): CredentialEntryUi {
- var userName: CharSequence? = null
- var password: CharSequence? = null
- var credentialTypeIcon: Icon? = null
- var profileIcon: Icon? = null
- var lastUsedTimeMillis: Long? = null
- var note: CharSequence? = null
-
- val items = slice.items
- items.forEach {
- if (it.hasHint(Entry.HINT_USER_NAME)) {
- userName = it.text
- } else if (it.hasHint(Entry.HINT_PASSWORD_VALUE)) {
- password = it.text
- } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
- credentialTypeIcon = it.icon
- } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
- profileIcon = it.icon
- } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
- lastUsedTimeMillis = it.long
- } else if (it.hasHint(Entry.HINT_NOTE)) {
- note = it.text
- }
- }
- // TODO: fail NPE more elegantly.
- return PasswordCredentialEntryUi(
- userName!!, password!!, credentialTypeIcon!!,
- profileIcon, lastUsedTimeMillis, note,
- )
- }
- }
-}