Merge "UI bug fixes."
diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml
index 5a4d256..499d130 100644
--- a/packages/CredentialManager/AndroidManifest.xml
+++ b/packages/CredentialManager/AndroidManifest.xml
@@ -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"
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 8c50271..51dc233 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -49,7 +49,12 @@
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 @@
GetCredentialProviderData::class.java
) ?: testGetCredentialProviderList()
else -> {
- // TODO: fail gracefully
throw IllegalStateException("Unrecognized request type: ${requestInfo.type}")
}
}
@@ -167,9 +171,9 @@
)
}
+ // 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 @@
)
}
+ // 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 @@
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 @@
),
)
).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 @@
),
)
).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 @@
CreateCredentialRequest(
"androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL",
credentialData,
- // TODO: populate with actual data
/*candidateQueryData=*/ Bundle(),
/*isSystemProviderRequired=*/ false
),
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 5136f04..bf69ef4 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -92,13 +92,20 @@
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 (viewModel.uiState.getCredentialUiState != null) {
- GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
+ } else if (getCredentialUiState != null) {
+ GetCredentialScreen(
+ viewModel = viewModel,
+ getCredentialUiState = getCredentialUiState,
+ providerActivityLauncher = launcher
+ )
} else {
Log.d(Constants.LOG_TAG, "UI wasn't able to render neither get nor create flow")
reportInstantiationErrorAndFinishActivity(credManRepo)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 6bf1513..30b4b86 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -255,14 +255,14 @@
}
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() {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 167b956..ae87d95 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -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 @@
}
- /* 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 @@
}
}
+ /**
+ * Note: caller required handle empty list due to parsing error.
+ */
private fun getAuthenticationEntryList(
providerId: String,
providerDisplayName: String,
@@ -262,16 +268,24 @@
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 @@
}
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 @@
)
}
+ /**
+ * Note: caller required handle empty list due to parsing error.
+ */
private fun getActionEntryList(
providerId: String,
actionEntries: List<Entry>,
@@ -321,7 +337,9 @@
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 @@
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 @@
} else null
}
+ /**
+ * Note: caller required handle empty list due to parsing error.
+ */
private fun toCreationOptionInfoList(
providerId: String,
creationEntries: List<Entry>,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt
index e3bbaeb..a580c6c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt
@@ -23,10 +23,11 @@
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 @@
context: Context,
key: String,
subkey: String,
+ title: String,
): Entry {
val slice = Slice.Builder(
Uri.EMPTY, SliceSpec("AuthenticationAction", 0)
@@ -52,6 +54,11 @@
.build(),
/*subType=*/null
)
+ slice.addText(
+ title,
+ null,
+ listOf("androidx.credentials.provider.authenticationAction.SLICE_HINT_TITLE")
+ )
return Entry(
key,
subkey,
@@ -94,23 +101,6 @@
)
}
- 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 @@
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,
- toPasswordSlice(userName, userDisplayName, pendingIntent, lastUsedTime),
- Intent()
- )
+ 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 @@
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 @@
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) }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index adb5467..558c229 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -62,9 +62,9 @@
@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 @@
requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
enabledProviderList = createCredentialUiState.enabledProviders,
providerInfo = createCredentialUiState.activeEntry?.activeProvider!!,
+ hasDefaultProvider = createCredentialUiState.hasDefaultProvider,
createOptionInfo =
createCredentialUiState.activeEntry.activeEntryInfo
as CreateOptionInfo,
@@ -264,7 +265,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProviderSelectionCard(
requestDisplayInfo: RequestDisplayInfo,
@@ -351,7 +351,6 @@
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 @@
onMoreOptionsSelected
)
}
+ return@forEach
}
}
Divider(
@@ -463,7 +463,6 @@
)
}
}
- // TODO: handle the error situation that if multiple remoteInfos exists
enabledProviderList.forEach {
if (it.remoteEntry != null) {
item {
@@ -472,6 +471,7 @@
onRemoteEntrySelected = onRemoteEntrySelected,
)
}
+ return@forEach
}
}
}
@@ -549,6 +549,7 @@
onOptionSelected: (BaseEntry) -> Unit,
onConfirm: () -> Unit,
onMoreOptionsSelected: () -> Unit,
+ hasDefaultProvider: Boolean,
) {
ContainerCard() {
Column() {
@@ -601,7 +602,6 @@
onOptionSelected = onOptionSelected
)
}
- var shouldShowMoreOptionsButton = false
var createOptionsSize = 0
var remoteEntry: RemoteInfo? = null
enabledProviderList.forEach { enabledProvider ->
@@ -610,8 +610,13 @@
}
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 =
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 8b311fe..48ee287 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -76,9 +76,9 @@
@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 = {