Merge "Get flow fundamental E2E."
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -29,4 +29,26 @@
|
||||
<string name="createOptionInfo_icon_description">CreateOptionInfo credentialType icon</string>
|
||||
<!-- Spoken content description of an element which will close the sheet when clicked. -->
|
||||
<string name="close_sheet">"Close sheet"</string>
|
||||
<!-- Spoken content description of the back arrow button. -->
|
||||
<string name="accessibility_back_arrow_button">"Go back to the previous page"</string>
|
||||
|
||||
<!-- Strings for the get flow. -->
|
||||
<!-- This appears as the title of the modal bottom sheet asking for user confirmation to use the single previously saved passkey to sign in to the app. [CHAR LIMIT=200] -->
|
||||
<string name="get_dialog_title_use_passkey_for">Use your saved passkey for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
|
||||
<!-- This appears as the title of the dialog asking for user confirmation to use the single previously saved credential to sign in to the app. [CHAR LIMIT=200] -->
|
||||
<string name="get_dialog_title_use_sign_in_for">Use your saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
|
||||
<!-- This appears as the title of the dialog asking for user to make a choice from various previously saved credentials to sign in to the app. [CHAR LIMIT=200] -->
|
||||
<string name="get_dialog_title_choose_sign_in_for">Choose a saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g></string>
|
||||
<!-- Appears as an option row for viewing all the available sign-in options. [CHAR LIMIT=80] -->
|
||||
<string name="get_dialog_use_saved_passkey_for">Sign in another way</string>
|
||||
<!-- Button label to close the dialog when the user does not want to use any sign-in. [CHAR LIMIT=40] -->
|
||||
<string name="get_dialog_button_label_no_thanks">No thanks</string>
|
||||
<!-- Button label to continue with the selected sign-in. [CHAR LIMIT=40] -->
|
||||
<string name="get_dialog_button_label_continue">Continue</string>
|
||||
<!-- Separator for sign-in type and username in a sign-in entry. -->
|
||||
<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] -->
|
||||
<string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string>
|
||||
</resources>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GetCredentialProviderData>, context)
|
||||
// TODO: handle runtime cast error
|
||||
providerList as List<GetCredentialProviderData>, 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<Entry>(
|
||||
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<Entry>(
|
||||
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<Entry>(
|
||||
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<Entry>(
|
||||
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<Entry>(
|
||||
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<Entry>(
|
||||
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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ProviderInfo> {
|
||||
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<Entry>,
|
||||
context: Context,
|
||||
): List<CredentialOptionInfo> {
|
||||
): List<CredentialEntryInfo> {
|
||||
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<Entry>,
|
||||
context: Context,
|
||||
): List<ActionEntryInfo> {
|
||||
// TODO: implement
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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<PerUserNameCredentialEntryList>,
|
||||
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<PerUserNameCredentialEntryList>,
|
||||
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)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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<ProviderInfo>,
|
||||
val providerInfoList: List<ProviderInfo>,
|
||||
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<PerUserNameCredentialEntryList> =
|
||||
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<ProviderInfo>
|
||||
): List<PerUserNameCredentialEntryList> {
|
||||
// Group by username
|
||||
val userNameToEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
|
||||
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<CredentialEntryInfo> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<CredentialOptionInfo>,
|
||||
// TODO: Add the authenticationOption
|
||||
val credentialEntryList: List<CredentialEntryInfo>,
|
||||
val authenticationEntry: AuthenticationEntryInfo?,
|
||||
val actionEntryList: List<ActionEntryInfo>,
|
||||
// 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<CredentialEntryInfo>,
|
||||
)
|
||||
|
||||
/** 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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user