Add the only one createOption but has remoteEntry screen for create passkey

screenshot: https://screenshot.googleplex.com/TULWynJWxx4cAeP

Test: deployed locally

Bug: 253157211
Change-Id: I1865b89f71129ec94cac3ef0ea09a6b631eac65b
This commit is contained in:
Qinmei Du
2022-11-18 20:42:26 +00:00
parent b5c35c6564
commit 20c29c93df
6 changed files with 118 additions and 81 deletions

View File

@@ -6,6 +6,7 @@
<string name="string_more_options">More options</string>
<string name="string_create_in_another_place">Create in another place</string>
<string name="string_save_to_another_place">Save to another place</string>
<string name="string_use_another_device">Use another device</string>
<string name="string_no_thanks">No thanks</string>
<string name="passkey_creation_intro_title">A simple way to sign in safely</string>
<string name="passkey_creation_intro_body">Use your fingerprint, face or screen lock to sign in with a unique passkey that can’t be forgotten or stolen. Learn more</string>

View File

@@ -149,8 +149,8 @@ class CredentialManagerRepo(
if (providerInfo.isDefault) {hasDefault = true; defaultProvider = providerInfo} }
// TODO: covert from real requestInfo for create passkey
var requestDisplayInfo = RequestDisplayInfo(
"Elisa Beckett",
"beckett-bakert@gmail.com",
"Elisa Beckett",
TYPE_PUBLIC_KEY_CREDENTIAL,
"tribank")
val createCredentialRequest = requestInfo.createCredentialRequest
@@ -165,8 +165,12 @@ class CredentialManagerRepo(
return CreateCredentialUiState(
enabledProviders = providerEnabledList,
disabledProviders = providerDisabledList,
if (hasDefault)
{CreateScreenState.CREATION_OPTION_SELECTION} else {CreateScreenState.PASSKEY_INTRO},
// TODO: Add the screen when defaultProvider has no createOption but
// there's remoteInfo under other providers
if (!hasDefault || defaultProvider.createOptions.isEmpty()) {
if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL)
{CreateScreenState.PASSKEY_INTRO} else {CreateScreenState.PROVIDER_SELECTION}
} else {CreateScreenState.CREATION_OPTION_SELECTION},
requestDisplayInfo,
if (hasDefault) {
ActiveEntry(defaultProvider, defaultProvider.createOptions.first())
@@ -439,7 +443,7 @@ class CredentialManagerRepo(
return RequestInfo.newCreateRequestInfo(
Binder(),
CreateCredentialRequest(
TYPE_PASSWORD_CREDENTIAL,
TYPE_PUBLIC_KEY_CREDENTIAL,
data
),
/*isFirstUsage=*/false,

View File

@@ -193,9 +193,10 @@ class CreateFlowUtils {
icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!,
name = it.providerFlattenedComponentName,
displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
createOptions = toCreationOptionInfoList(it.saveEntries, context),
createOptions = toCreationOptionInfoList(
it.providerFlattenedComponentName, it.saveEntries, context),
isDefault = it.isDefaultProvider,
remoteEntry = toRemoteInfo(it.remoteEntry),
remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry),
)
}
}
@@ -219,6 +220,7 @@ class CreateFlowUtils {
}
private fun toCreationOptionInfoList(
providerId: String,
creationEntries: List<Entry>,
context: Context,
): List<CreateOptionInfo> {
@@ -227,6 +229,7 @@ class CreateFlowUtils {
return@map CreateOptionInfo(
// TODO: remove fallbacks
providerId = providerId,
entryKey = it.key,
entrySubkey = it.subkey,
pendingIntent = it.pendingIntent,
@@ -245,11 +248,13 @@ class CreateFlowUtils {
}
private fun toRemoteInfo(
providerId: String,
remoteEntry: Entry?,
): RemoteInfo? {
// TODO: should also call fromSlice after getting the official jetpack code.
return if (remoteEntry != null) {
RemoteInfo(
providerId = providerId,
entryKey = remoteEntry.key,
entrySubkey = remoteEntry.subkey,
pendingIntent = remoteEntry.pendingIntent,

View File

@@ -55,8 +55,11 @@ fun CreateCredentialScreen(
viewModel: CreateCredentialViewModel,
providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val primaryEntryCallback: () -> Unit = {
viewModel.onPrimaryCreateOptionInfoSelected(providerActivityLauncher)
val selectEntryCallback: (EntryInfo) -> Unit = {
viewModel.onEntrySelected(it, providerActivityLauncher)
}
val confirmEntryCallback: () -> Unit = {
viewModel.onConfirmCreationSelected(providerActivityLauncher)
}
val state = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded,
@@ -78,23 +81,23 @@ fun CreateCredentialScreen(
)
CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
providerInfo = uiState.activeEntry?.activeProvider!!,
createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo,
onOptionSelected = primaryEntryCallback,
onConfirm = primaryEntryCallback,
onOptionSelected = selectEntryCallback,
onConfirm = confirmEntryCallback,
onCancel = viewModel::onCancel,
multiProvider = uiState.enabledProviders.size > 1,
onMoreOptionsSelected = viewModel::onMoreOptionsSelected
onMoreOptionsSelected = viewModel::onMoreOptionsSelected,
)
CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
disabledProviderList = uiState.disabledProviders,
onBackButtonSelected = viewModel::onBackButtonSelected,
onOptionSelected = viewModel::onMoreOptionsRowSelected,
onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected,
onRemoteEntrySelected = viewModel::onRemoteEntrySelected
)
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
disabledProviderList = uiState.disabledProviders,
onBackButtonSelected = viewModel::onBackButtonSelected,
onOptionSelected = viewModel::onMoreOptionsRowSelected,
onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected,
onRemoteEntrySelected = selectEntryCallback,
)
CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
providerInfo = uiState.activeEntry?.activeProvider!!,
onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected
@@ -234,7 +237,7 @@ fun MoreOptionsSelectionCard(
onBackButtonSelected: () -> Unit,
onOptionSelected: (ActiveEntry) -> Unit,
onDisabledPasswordManagerSelected: () -> Unit,
onRemoteEntrySelected: () -> Unit,
onRemoteEntrySelected: (EntryInfo) -> Unit,
) {
Card() {
Column() {
@@ -292,17 +295,15 @@ fun MoreOptionsSelectionCard(
)
}
}
var hasRemoteInfo = false
// TODO: handle the error situation that if multiple remoteInfos exists
enabledProviderList.forEach {
if (it.remoteEntry != null) {
hasRemoteInfo = true
}
}
if (hasRemoteInfo) {
item {
RemoteEntryRow(
onRemoteEntrySelected = onRemoteEntrySelected,
)
item {
RemoteEntryRow(
remoteInfo = it.remoteEntry!!,
onRemoteEntrySelected = onRemoteEntrySelected,
)
}
}
}
}
@@ -388,12 +389,12 @@ fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit
@Composable
fun CreationSelectionCard(
requestDisplayInfo: RequestDisplayInfo,
providerInfo: ProviderInfo,
enabledProviderList: List<EnabledProviderInfo>,
providerInfo: EnabledProviderInfo,
createOptionInfo: CreateOptionInfo,
onOptionSelected: () -> Unit,
onOptionSelected: (EntryInfo) -> Unit,
onConfirm: () -> Unit,
onCancel: () -> Unit,
multiProvider: Boolean,
onMoreOptionsSelected: () -> Unit,
) {
Card() {
@@ -442,19 +443,16 @@ fun CreationSelectionCard(
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally),
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
item {
PrimaryCreateOptionRow(
requestDisplayInfo = requestDisplayInfo,
createOptionInfo = createOptionInfo,
onOptionSelected = onOptionSelected
)
}
}
PrimaryCreateOptionRow(
requestDisplayInfo = requestDisplayInfo,
createOptionInfo = createOptionInfo,
onOptionSelected = onOptionSelected
)
}
if (multiProvider) {
var createOptionsSize = 0
enabledProviderList.forEach{
enabledProvider -> createOptionsSize += enabledProvider.createOptions.size}
if (createOptionsSize > 1) {
TextButton(
onClick = onMoreOptionsSelected,
modifier = Modifier
@@ -469,6 +467,26 @@ fun CreationSelectionCard(
textAlign = TextAlign.Center,
)
}
} else if (
requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL
) {
// TODO: handle the error situation that if multiple remoteInfos exists
enabledProviderList.forEach { enabledProvider ->
if (enabledProvider.remoteEntry != null) {
TextButton(
onClick = {
onOptionSelected(enabledProvider.remoteEntry!!) },
modifier = Modifier
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally)
) {
Text(
text = stringResource(R.string.string_use_another_device),
textAlign = TextAlign.Center,
)
}
}
}
}
Divider(
thickness = 24.dp,
@@ -501,10 +519,10 @@ fun CreationSelectionCard(
fun PrimaryCreateOptionRow(
requestDisplayInfo: RequestDisplayInfo,
createOptionInfo: CreateOptionInfo,
onOptionSelected: () -> Unit
onOptionSelected: (EntryInfo) -> Unit
) {
Entry(
onClick = onOptionSelected,
onClick = {onOptionSelected(createOptionInfo)},
icon = {
Image(modifier = Modifier.size(32.dp).padding(start = 10.dp),
bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(),
@@ -527,7 +545,7 @@ fun PrimaryCreateOptionRow(
)
} else {
Text(
text = requestDisplayInfo.subtitle,
text = requestDisplayInfo.title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp, bottom = 16.dp)
)
@@ -640,10 +658,11 @@ fun MoreOptionsDisabledProvidersRow(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RemoteEntryRow(
onRemoteEntrySelected: () -> Unit,
remoteInfo: RemoteInfo,
onRemoteEntrySelected: (RemoteInfo) -> Unit,
) {
Entry(
onClick = onRemoteEntrySelected,
onClick = {onRemoteEntrySelected(remoteInfo)},
icon = {
Icon(
painter = painterResource(R.drawable.ic_other_devices),

View File

@@ -37,6 +37,7 @@ data class CreateCredentialUiState(
val currentScreenState: CreateScreenState,
val requestDisplayInfo: RequestDisplayInfo,
val activeEntry: ActiveEntry? = null,
val selectedEntry: EntryInfo? = null,
)
class CreateCredentialViewModel(
@@ -109,10 +110,6 @@ class CreateCredentialViewModel(
// TODO: Complete this function
}
fun onRemoteEntrySelected() {
// TODO: Complete this function
}
fun onCancel() {
CredentialManagerRepo.getInstance().onCancel()
dialogResult.value = DialogResult(ResultState.CANCELED)
@@ -125,46 +122,54 @@ class CreateCredentialViewModel(
// TODO: implement the if choose as default or not logic later
}
fun onPrimaryCreateOptionInfoSelected(
fun onEntrySelected(
selectedEntry: EntryInfo,
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val providerId = selectedEntry.providerId
val entryKey = selectedEntry.entryKey
val entrySubkey = selectedEntry.entrySubkey
Log.d(
"Account Selector", "Option selected for entry: " +
" {provider=$providerId, key=$entryKey, subkey=$entrySubkey")
if (selectedEntry.pendingIntent != null) {
uiState = uiState.copy(selectedEntry = selectedEntry)
val intentSenderRequest = IntentSenderRequest.Builder(selectedEntry.pendingIntent)
.setFillInIntent(selectedEntry.fillInIntent).build()
launcher.launch(intentSenderRequest)
} else {
CredentialManagerRepo.getInstance().onOptionSelected(
providerId,
entryKey,
entrySubkey
)
dialogResult.value = DialogResult(
ResultState.COMPLETE,
)
}
}
fun onConfirmCreationSelected(
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val selectedEntry = uiState.activeEntry?.activeEntryInfo
if (selectedEntry != null) {
val entryKey = selectedEntry.entryKey
val entrySubkey = selectedEntry.entrySubkey
Log.d(
"Account Selector",
"Option selected for creation: " +
"{key = $entryKey, subkey = $entrySubkey}"
)
if (selectedEntry.pendingIntent != null) {
val intentSenderRequest = IntentSenderRequest.Builder(selectedEntry.pendingIntent)
.setFillInIntent(selectedEntry.fillInIntent).build()
launcher.launch(intentSenderRequest)
} else {
CredentialManagerRepo.getInstance().onOptionSelected(
uiState.activeEntry?.activeProvider!!.name,
entryKey,
entrySubkey
)
dialogResult.value = DialogResult(
ResultState.COMPLETE,
)
}
onEntrySelected(selectedEntry, launcher)
} else {
Log.w("Account Selector",
"Illegal state: confirm is pressed but activeEntry isn't set.")
dialogResult.value = DialogResult(
ResultState.COMPLETE,
)
TODO("Gracefully handle illegal state.")
}
}
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
val entry = uiState.activeEntry?.activeEntryInfo
val entry = uiState.selectedEntry
val resultCode = providerActivityResult.resultCode
val resultData = providerActivityResult.data
val providerId = uiState.activeEntry?.activeProvider!!.name
if (entry != null) {
val providerId = entry.providerId
Log.d("Account Selector", "Got provider activity result: {provider=" +
"$providerId, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
"resultCode=$resultCode, resultData=$resultData}"

View File

@@ -42,6 +42,7 @@ class DisabledProviderInfo(
) : ProviderInfo(icon, name, displayName)
open class EntryInfo (
val providerId: String,
val entryKey: String,
val entrySubkey: String,
val pendingIntent: PendingIntent?,
@@ -49,6 +50,7 @@ open class EntryInfo (
)
class CreateOptionInfo(
providerId: String,
entryKey: String,
entrySubkey: String,
pendingIntent: PendingIntent?,
@@ -60,14 +62,15 @@ class CreateOptionInfo(
val passkeyCount: Int?,
val totalCredentialCount: Int?,
val lastUsedTimeMillis: Long?,
) : EntryInfo(entryKey, entrySubkey, pendingIntent, fillInIntent)
) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
class RemoteInfo(
providerId: String,
entryKey: String,
entrySubkey: String,
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
) : EntryInfo(entryKey, entrySubkey, pendingIntent, fillInIntent)
) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
data class RequestDisplayInfo(
val title: String,