UI bug fixes.

1. As a temporary solution while jetpack isn't ready, use the agreed
   constant to directly pull the authentication title values.
2. Don't show the "more option" button when there's nothing to show.
3. Updated the test data based on new jetpack updates.
4. Removed/fixed some TODOs.

Bug: 268297826
Fix: 268208819
Test: manual
Change-Id: I28fa57d6db26406981ad99384e079287dd1af557
This commit is contained in:
Helen Qin
2023-02-08 08:53:57 +00:00
parent 8c60a525f1
commit cbac773c5e
8 changed files with 92 additions and 201 deletions

View File

@@ -32,7 +32,6 @@
android:supportsRtl="true"
android:theme="@style/Theme.CredentialSelector">
<!--TODO: make sure implementing singleTop on NewIntent-->
<activity
android:name=".CredentialSelectorActivity"
android:exported="true"

View File

@@ -49,7 +49,12 @@ import androidx.credentials.CreatePasswordRequest
import java.time.Instant
// Consider repo per screen, similar to view model?
/**
* Client for interacting with Credential Manager. Also holds data inputs from it.
*
* IMPORTANT: instantiation of the object can fail if the data inputs aren't valid. Callers need
* to be equipped to handle this gracefully.
*/
class CredentialManagerRepo(
private val context: Context,
intent: Intent,
@@ -81,7 +86,6 @@ class CredentialManagerRepo(
GetCredentialProviderData::class.java
) ?: testGetCredentialProviderList()
else -> {
// TODO: fail gracefully
throw IllegalStateException("Unrecognized request type: ${requestInfo.type}")
}
}
@@ -167,9 +171,9 @@ class CredentialManagerRepo(
)
}
// IMPORTANT: new invocation should be mindful that this method can throw.
private fun getCredentialInitialUiState(): GetCredentialUiState? {
val providerEnabledList = GetFlowUtils.toProviderList(
// TODO: handle runtime cast error
providerEnabledList as List<GetCredentialProviderData>, context
)
val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context)
@@ -179,9 +183,9 @@ class CredentialManagerRepo(
)
}
// IMPORTANT: new invocation should be mindful that this method can throw.
private fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
val providerEnabledList = CreateFlowUtils.toEnabledProviderList(
// Handle runtime cast error
providerEnabledList as List<CreateCredentialProviderData>, context
)
return providerEnabledList
@@ -266,7 +270,7 @@ class CredentialManagerRepo(
return listOf(
GetCredentialProviderData.Builder("io.enpass.app")
.setCredentialEntries(
listOf<Entry>(
listOf(
GetTestUtils.newPasswordEntry(
context, "key1", "subkey-1", "elisa.family@outlook.com", null,
Instant.ofEpochSecond(8000L)
@@ -285,9 +289,12 @@ class CredentialManagerRepo(
),
)
).setAuthenticationEntries(
listOf<Entry>(
GetTestUtils.newAuthenticationEntry(context, "key2", "subkey-1"),
)
listOf(
GetTestUtils.newAuthenticationEntry(
context, "key2", "subkey-1", "locked-user1@gmail.com"),
GetTestUtils.newAuthenticationEntry(
context, "key2", "subkey-2", "locked-user2@gmail.com"),
)
).setActionChips(
listOf(
GetTestUtils.newActionEntry(
@@ -315,9 +322,8 @@ class CredentialManagerRepo(
),
)
).setAuthenticationEntries(
listOf<Entry>(
GetTestUtils.newAuthenticationEntry(context, "key2", "subkey-1"),
)
listOf(GetTestUtils.newAuthenticationEntry(
context, "key2", "subkey-1", "foo@email.com"))
).setActionChips(
listOf(
GetTestUtils.newActionEntry(
@@ -388,7 +394,6 @@ class CredentialManagerRepo(
CreateCredentialRequest(
"androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL",
credentialData,
// TODO: populate with actual data
/*candidateQueryData=*/ Bundle(),
/*isSystemProviderRequired=*/ false
),

View File

@@ -92,13 +92,20 @@ class CredentialSelectorActivity : ComponentActivity() {
handleDialogState(viewModel.uiState.dialogState)
}
if (viewModel.uiState.createCredentialUiState != null) {
val createCredentialUiState = viewModel.uiState.createCredentialUiState
val getCredentialUiState = viewModel.uiState.getCredentialUiState
if (createCredentialUiState != null) {
CreateCredentialScreen(
viewModel = viewModel,
createCredentialUiState = createCredentialUiState,
providerActivityLauncher = launcher
)
} else if (getCredentialUiState != null) {
GetCredentialScreen(
viewModel = viewModel,
getCredentialUiState = getCredentialUiState,
providerActivityLauncher = launcher
)
} else if (viewModel.uiState.getCredentialUiState != null) {
GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
} else {
Log.d(Constants.LOG_TAG, "UI wasn't able to render neither get nor create flow")
reportInstantiationErrorAndFinishActivity(credManRepo)

View File

@@ -255,14 +255,14 @@ class CredentialSelectorViewModel(
}
fun createFlowOnEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) {
val providerId = activeEntry.activeProvider.id
createFlowOnDefaultChanged(providerId)
uiState = uiState.copy(
createCredentialUiState = uiState.createCredentialUiState?.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
activeEntry = activeEntry
)
)
val providerId = uiState.createCredentialUiState?.activeEntry?.activeProvider?.id
createFlowOnDefaultChanged(providerId)
}
fun createFlowOnDisabledProvidersSelected() {

View File

@@ -17,6 +17,7 @@
package com.android.credentialmanager
import android.app.slice.Slice
import android.app.slice.SliceItem
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
@@ -176,7 +177,9 @@ class GetFlowUtils {
}
/* From service data structure to UI credential entry list representation. */
/**
* Note: caller required handle empty list due to parsing error.
*/
private fun getCredentialOptionInfoList(
providerId: String,
credentialEntries: List<Entry>,
@@ -255,6 +258,9 @@ class GetFlowUtils {
}
}
/**
* Note: caller required handle empty list due to parsing error.
*/
private fun getAuthenticationEntryList(
providerId: String,
providerDisplayName: String,
@@ -262,16 +268,24 @@ class GetFlowUtils {
authEntryList: List<Entry>,
): List<AuthenticationEntryInfo> {
val result: MutableList<AuthenticationEntryInfo> = mutableListOf()
authEntryList.forEach {
authEntryList.forEach { entry ->
val structuredAuthEntry =
AuthenticationAction.fromSlice(it.slice) ?: return@forEach
AuthenticationAction.fromSlice(entry.slice) ?: return@forEach
// TODO: replace with official jetpack code.
val titleItem: SliceItem? = entry.slice.items.firstOrNull {
it.hasHint(
"androidx.credentials.provider.authenticationAction.SLICE_HINT_TITLE")
}
val title: String = titleItem?.text?.toString() ?: providerDisplayName
result.add(AuthenticationEntryInfo(
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
entryKey = entry.key,
entrySubkey = entry.subkey,
pendingIntent = structuredAuthEntry.pendingIntent,
fillInIntent = it.frameworkExtrasIntent,
title = providerDisplayName,
fillInIntent = entry.frameworkExtrasIntent,
title = title,
icon = providerIcon,
))
}
@@ -279,7 +293,6 @@ class GetFlowUtils {
}
private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? {
// TODO: should also call fromSlice after getting the official jetpack code.
if (remoteEntry == null) {
return null
}
@@ -294,6 +307,9 @@ class GetFlowUtils {
)
}
/**
* Note: caller required handle empty list due to parsing error.
*/
private fun getActionEntryList(
providerId: String,
actionEntries: List<Entry>,
@@ -321,7 +337,9 @@ class GetFlowUtils {
class CreateFlowUtils {
companion object {
// Returns the list (potentially empty) of enabled provider.
/**
* Note: caller required handle empty list due to parsing error.
*/
fun toEnabledProviderList(
providerDataList: List<CreateCredentialProviderData>,
context: Context,
@@ -346,7 +364,9 @@ class CreateFlowUtils {
return providerList
}
// Returns the list (potentially empty) of disabled provider.
/**
* Note: caller required handle empty list due to parsing error.
*/
fun toDisabledProviderList(
providerDataList: List<DisabledProviderData>?,
context: Context,
@@ -532,6 +552,9 @@ class CreateFlowUtils {
} else null
}
/**
* Note: caller required handle empty list due to parsing error.
*/
private fun toCreationOptionInfoList(
providerId: String,
creationEntries: List<Entry>,

View File

@@ -23,10 +23,11 @@ import android.content.Context
import android.content.Intent
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.credentials.ui.Entry
import android.graphics.drawable.Icon
import android.net.Uri
import android.provider.Settings
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry
import java.time.Instant
@@ -37,6 +38,7 @@ class GetTestUtils {
context: Context,
key: String,
subkey: String,
title: String,
): Entry {
val slice = Slice.Builder(
Uri.EMPTY, SliceSpec("AuthenticationAction", 0)
@@ -52,6 +54,11 @@ class GetTestUtils {
.build(),
/*subType=*/null
)
slice.addText(
title,
null,
listOf("androidx.credentials.provider.authenticationAction.SLICE_HINT_TITLE")
)
return Entry(
key,
subkey,
@@ -94,23 +101,6 @@ class GetTestUtils {
)
}
private const val SLICE_HINT_TYPE_DISPLAY_NAME =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
private const val SLICE_HINT_TITLE =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_USER_NAME"
private const val SLICE_HINT_SUBTITLE =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
private const val SLICE_HINT_ICON =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_PROFILE_ICON"
private const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_PENDING_INTENT"
private const val SLICE_HINT_AUTO_ALLOWED =
"androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_AUTO_ALLOWED"
private const val AUTO_SELECT_TRUE_STRING = "true"
private const val AUTO_SELECT_FALSE_STRING = "false"
internal fun newPasswordEntry(
context: Context,
key: String,
@@ -125,90 +115,13 @@ class GetTestUtils {
val pendingIntent = PendingIntent.getActivity(
context, 1,
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_ONE_SHOT)
)
return Entry(
key,
subkey,
toPasswordSlice(userName, userDisplayName, pendingIntent, lastUsedTime),
Intent()
or PendingIntent.FLAG_ONE_SHOT)
)
val passwordEntry = PasswordCredentialEntry.Builder(context, userName, pendingIntent)
.setDisplayName(userDisplayName).setLastUsedTime(lastUsedTime).build()
return Entry(key, subkey, passwordEntry.slice, Intent())
}
private fun toPasswordSlice(
title: CharSequence,
subTitle: CharSequence?,
pendingIntent: PendingIntent,
lastUsedTime: Instant?,
icon: Icon? = null,
isAutoSelectAllowed: Boolean = true
): Slice {
val type = TYPE_PASSWORD_CREDENTIAL
val autoSelectAllowed = if (isAutoSelectAllowed) {
AUTO_SELECT_TRUE_STRING
} else {
AUTO_SELECT_FALSE_STRING
}
val sliceBuilder = Slice.Builder(
Uri.EMPTY, SliceSpec(
type, 1
)
)
.addText(
"Password", /*subType=*/null,
listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
)
.addText(
title, /*subType=*/null,
listOf(SLICE_HINT_TITLE)
)
.addText(
subTitle, /*subType=*/null,
listOf(SLICE_HINT_SUBTITLE)
)
.addText(
autoSelectAllowed, /*subType=*/null,
listOf(SLICE_HINT_AUTO_ALLOWED)
)
if (lastUsedTime != null) {
sliceBuilder.addLong(
lastUsedTime.toEpochMilli(),
/*subType=*/null,
listOf(SLICE_HINT_LAST_USED_TIME_MILLIS)
)
}
if (icon != null) {
sliceBuilder.addIcon(
icon, /*subType=*/null,
listOf(SLICE_HINT_ICON)
)
}
sliceBuilder.addAction(
pendingIntent,
Slice.Builder(sliceBuilder)
.addHints(listOf(SLICE_HINT_PENDING_INTENT))
.build(),
/*subType=*/null
)
return sliceBuilder.build()
}
private const val PASSKEY_SLICE_HINT_TYPE_DISPLAY_NAME =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
private const val PASSKEY_SLICE_HINT_TITLE =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_USER_NAME"
private const val PASSKEY_SLICE_HINT_SUBTITLE =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
private const val PASSKEY_SLICE_HINT_LAST_USED_TIME_MILLIS =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
private const val PASSKEY_SLICE_HINT_ICON =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_PROFILE_ICON"
private const val PASSKEY_SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_PENDING_INTENT"
private const val PASSKEY_SLICE_HINT_AUTO_ALLOWED =
"androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_AUTO_ALLOWED"
internal fun newPasskeyEntry(
context: Context,
key: String,
@@ -223,72 +136,11 @@ class GetTestUtils {
val pendingIntent = PendingIntent.getActivity(
context, 1,
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_ONE_SHOT)
or PendingIntent.FLAG_ONE_SHOT)
)
return Entry(
key, subkey, toPasskeySlice(
userName, userDisplayName, pendingIntent, lastUsedTime
),
Intent()
)
}
private fun toPasskeySlice(
title: CharSequence,
subTitle: CharSequence?,
pendingIntent: PendingIntent,
lastUsedTime: Instant?,
icon: Icon? = null,
isAutoSelectAllowed: Boolean = true
): Slice {
val type = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
val autoSelectAllowed = if (isAutoSelectAllowed) {
AUTO_SELECT_TRUE_STRING
} else {
AUTO_SELECT_FALSE_STRING
}
val sliceBuilder = Slice.Builder(
Uri.EMPTY, SliceSpec(
type, 1
)
)
.addText(
"Passkey", /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_TYPE_DISPLAY_NAME)
)
.addText(
title, /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_TITLE)
)
.addText(
subTitle, /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_SUBTITLE)
)
.addText(
autoSelectAllowed, /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_AUTO_ALLOWED)
)
if (lastUsedTime != null) {
sliceBuilder.addLong(
lastUsedTime.toEpochMilli(),
/*subType=*/null,
listOf(PASSKEY_SLICE_HINT_LAST_USED_TIME_MILLIS)
)
}
if (icon != null) {
sliceBuilder.addIcon(
icon, /*subType=*/null,
listOf(PASSKEY_SLICE_HINT_ICON)
)
}
sliceBuilder.addAction(
pendingIntent,
Slice.Builder(sliceBuilder)
.addHints(listOf(PASSKEY_SLICE_HINT_PENDING_INTENT))
.build(),
/*subType=*/null
)
return sliceBuilder.build()
val passkeyEntry = PublicKeyCredentialEntry.Builder(context, userName, pendingIntent)
.setDisplayName(userDisplayName).setLastUsedTime(lastUsedTime).build()
return Entry(key, subkey, passkeyEntry.slice, Intent())
}
}
}
@@ -326,7 +178,7 @@ class CreateTestUtils {
val pendingIntent = PendingIntent.getActivity(
context, 1,
intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_ONE_SHOT)
or PendingIntent.FLAG_ONE_SHOT)
)
val credCountMap = mutableMapOf<String, Int>()
passwordCount?.let { credCountMap.put(TYPE_PASSWORD_CREDENTIAL, it) }

View File

@@ -62,9 +62,9 @@ import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@Composable
fun CreateCredentialScreen(
viewModel: CredentialSelectorViewModel,
createCredentialUiState: CreateCredentialUiState,
providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val createCredentialUiState = viewModel.uiState.createCredentialUiState ?: return
ModalBottomSheet(
sheetContent = {
// Hide the sheet content as opposed to the whole bottom sheet to maintain the scrim
@@ -94,6 +94,7 @@ fun CreateCredentialScreen(
requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
enabledProviderList = createCredentialUiState.enabledProviders,
providerInfo = createCredentialUiState.activeEntry?.activeProvider!!,
hasDefaultProvider = createCredentialUiState.hasDefaultProvider,
createOptionInfo =
createCredentialUiState.activeEntry.activeEntryInfo
as CreateOptionInfo,
@@ -264,7 +265,6 @@ fun ConfirmationCard(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProviderSelectionCard(
requestDisplayInfo: RequestDisplayInfo,
@@ -351,7 +351,6 @@ fun ProviderSelectionCard(
thickness = 24.dp,
color = Color.Transparent
)
// TODO: handle the error situation that if multiple remoteInfos exists
enabledProviderList.forEach { enabledProvider ->
if (enabledProvider.remoteEntry != null) {
Row(
@@ -363,6 +362,7 @@ fun ProviderSelectionCard(
onMoreOptionsSelected
)
}
return@forEach
}
}
Divider(
@@ -463,7 +463,6 @@ fun MoreOptionsSelectionCard(
)
}
}
// TODO: handle the error situation that if multiple remoteInfos exists
enabledProviderList.forEach {
if (it.remoteEntry != null) {
item {
@@ -472,6 +471,7 @@ fun MoreOptionsSelectionCard(
onRemoteEntrySelected = onRemoteEntrySelected,
)
}
return@forEach
}
}
}
@@ -549,6 +549,7 @@ fun CreationSelectionCard(
onOptionSelected: (BaseEntry) -> Unit,
onConfirm: () -> Unit,
onMoreOptionsSelected: () -> Unit,
hasDefaultProvider: Boolean,
) {
ContainerCard() {
Column() {
@@ -601,7 +602,6 @@ fun CreationSelectionCard(
onOptionSelected = onOptionSelected
)
}
var shouldShowMoreOptionsButton = false
var createOptionsSize = 0
var remoteEntry: RemoteInfo? = null
enabledProviderList.forEach { enabledProvider ->
@@ -610,8 +610,13 @@ fun CreationSelectionCard(
}
createOptionsSize += enabledProvider.createOptions.size
}
if (createOptionsSize > 1 || remoteEntry != null) {
shouldShowMoreOptionsButton = true
val shouldShowMoreOptionsButton = if (!hasDefaultProvider) {
// User has already been presented with all options on the default provider
// selection screen. Don't show them again. Therefore, only show the more option
// button if remote option is present.
remoteEntry != null
} else {
createOptionsSize > 1 || remoteEntry != null
}
Row(
horizontalArrangement =

View File

@@ -76,9 +76,9 @@ import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
@Composable
fun GetCredentialScreen(
viewModel: CredentialSelectorViewModel,
getCredentialUiState: GetCredentialUiState,
providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val getCredentialUiState = viewModel.uiState.getCredentialUiState ?: return
if (getCredentialUiState.currentScreenState != GetScreenState.REMOTE_ONLY) {
ModalBottomSheet(
sheetContent = {