Fix the duplicate intent launching issue.

This fixed unintended duplicate intent launching issue for
1. action chips for get flow
2. settings for create flow.
Also improved the data model to better fit our practical purposes.

Bug: 264943119
Fix: 264943119
Test: local deployment
Change-Id: I57be60060012465655306da83e8c2972b37c0b8b
This commit is contained in:
Helen Qin
2023-01-24 03:39:58 +00:00
parent cfcf9f6501
commit c9a80cdbcb
8 changed files with 603 additions and 558 deletions

View File

@@ -17,6 +17,7 @@
package com.android.credentialmanager
import android.content.Intent
import android.credentials.ui.RequestInfo
import android.os.Bundle
import android.provider.Settings
import android.util.Log
@@ -26,89 +27,83 @@ import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.credentialmanager.common.DialogType
import com.android.credentialmanager.common.DialogResult
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.ResultState
import com.android.credentialmanager.createflow.CreateCredentialScreen
import com.android.credentialmanager.createflow.CreateCredentialViewModel
import com.android.credentialmanager.getflow.GetCredentialScreen
import com.android.credentialmanager.getflow.GetCredentialViewModel
import com.android.credentialmanager.ui.theme.CredentialSelectorTheme
import kotlinx.coroutines.launch
@ExperimentalMaterialApi
class CredentialSelectorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val credManRepo = CredentialManagerRepo(this, intent)
UserConfigRepo.setup(this)
val requestInfo = credManRepo.requestInfo
setContent {
CredentialSelectorTheme {
CredentialManagerBottomSheet(DialogType.toDialogType(requestInfo.type), credManRepo)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val credManRepo = CredentialManagerRepo(this, intent)
UserConfigRepo.setup(this)
val requestInfo = credManRepo.requestInfo
setContent {
CredentialSelectorTheme {
CredentialManagerBottomSheet(requestInfo.type, credManRepo)
}
}
}
}
@ExperimentalMaterialApi
@Composable
fun CredentialManagerBottomSheet(dialogType: DialogType, credManRepo: CredentialManagerRepo) {
val providerActivityResult = remember { mutableStateOf<ProviderActivityResult?>(null) }
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) {
providerActivityResult.value = ProviderActivityResult(it.resultCode, it.data)
@ExperimentalMaterialApi
@Composable
fun CredentialManagerBottomSheet(requestType: String, credManRepo: CredentialManagerRepo) {
val providerActivityResult = remember { mutableStateOf<ProviderActivityResult?>(null) }
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) {
providerActivityResult.value = ProviderActivityResult(it.resultCode, it.data)
}
when (requestType) {
RequestInfo.TYPE_CREATE -> {
val viewModel: CreateCredentialViewModel = viewModel {
CreateCredentialViewModel(credManRepo)
}
LaunchedEffect(viewModel.uiState.dialogState) {
handleDialogState(viewModel.uiState.dialogState)
}
providerActivityResult.value?.let {
viewModel.onProviderActivityResult(it)
providerActivityResult.value = null
}
CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
}
RequestInfo.TYPE_GET -> {
val viewModel: GetCredentialViewModel = viewModel {
GetCredentialViewModel(credManRepo)
}
LaunchedEffect(viewModel.uiState.dialogState) {
handleDialogState(viewModel.uiState.dialogState)
}
providerActivityResult.value?.let {
viewModel.onProviderActivityResult(it)
providerActivityResult.value = null
}
GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
}
else -> {
Log.w("AccountSelector", "Unknown type, not rendering any UI")
this.finish()
}
}
}
when (dialogType) {
DialogType.CREATE_PASSKEY -> {
val viewModel: CreateCredentialViewModel = viewModel{
CreateCredentialViewModel(credManRepo)
}
lifecycleScope.launch {
viewModel.observeDialogResult().collect{ dialogResult ->
onCancel(dialogResult)
}
}
providerActivityResult.value?.let {
viewModel.onProviderActivityResult(it)
providerActivityResult.value = null
}
CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
}
DialogType.GET_CREDENTIALS -> {
val viewModel: GetCredentialViewModel = viewModel{
GetCredentialViewModel(credManRepo)
}
lifecycleScope.launch {
viewModel.observeDialogResult().collect{ dialogResult ->
onCancel(dialogResult)
}
}
providerActivityResult.value?.let {
viewModel.onProviderActivityResult(it)
providerActivityResult.value = null
}
GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
}
else -> {
Log.w("AccountSelector", "Unknown type, not rendering any UI")
this.finish()
}
}
}
private fun onCancel(dialogResut: DialogResult) {
if (dialogResut.resultState == ResultState
.COMPLETE || dialogResut.resultState == ResultState.NORMAL_CANCELED) {
this@CredentialSelectorActivity.finish()
} else if (dialogResut.resultState == ResultState.LAUNCH_SETTING_CANCELED) {
this@CredentialSelectorActivity.startActivity(Intent(Settings.ACTION_SYNC_SETTINGS))
this@CredentialSelectorActivity.finish()
private fun handleDialogState(dialogState: DialogState) {
if (dialogState == DialogState.COMPLETE) {
Log.i("AccountSelector", "Received signal to finish the activity.")
this@CredentialSelectorActivity.finish()
} else if (dialogState == DialogState.CANCELED_FOR_SETTINGS) {
Log.i("AccountSelector", "Received signal to finish the activity and launch settings.")
this@CredentialSelectorActivity.startActivity(Intent(Settings.ACTION_SYNC_SETTINGS))
this@CredentialSelectorActivity.finish()
}
}
}
}

View File

@@ -16,6 +16,12 @@
package com.android.credentialmanager.common
enum class DialogState {
ACTIVE,
COMPLETE,
CANCELED_FOR_SETTINGS,
}
enum class ResultState {
COMPLETE,
NORMAL_CANCELED,

View File

@@ -19,16 +19,15 @@ package com.android.credentialmanager.common
import android.credentials.ui.RequestInfo
enum class DialogType {
CREATE_PASSKEY,
CREATE_CREDENTIAL,
GET_CREDENTIALS,
CREATE_PASSWORD,
UNKNOWN;
companion object {
fun toDialogType(value: String): DialogType {
return when (value) {
RequestInfo.TYPE_GET -> GET_CREDENTIALS
RequestInfo.TYPE_CREATE -> CREATE_PASSKEY
RequestInfo.TYPE_CREATE -> CREATE_CREDENTIAL
else -> UNKNOWN
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2023 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
enum class ProviderActivityState {
/** No provider activity is active nor is any ready for launch, */
NOT_APPLICABLE,
/** Ready to launch the provider activity. */
READY_TO_LAUNCH,
/** The provider activity is launched and we are waiting for its result. We should hide our UI
* content when this happens. */
PENDING,
}

View File

@@ -37,6 +37,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.R
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
import com.android.credentialmanager.common.material.rememberModalBottomSheetState
@@ -66,63 +67,83 @@ fun CreateCredentialScreen(
sheetState = state,
sheetContent = {
val uiState = viewModel.uiState
if (!uiState.hidden) {
when (uiState.currentScreenState) {
CreateScreenState.PASSKEY_INTRO -> ConfirmationCard(
onConfirm = viewModel::onConfirmIntro,
onLearnMore = viewModel::onLearnMore,
)
CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
disabledProviderList = uiState.disabledProviders,
sortedCreateOptionsPairs = uiState.sortedCreateOptionsPairs,
onOptionSelected = viewModel::onEntrySelectedFromFirstUseScreen,
onDisabledProvidersSelected = viewModel::onDisabledProvidersSelected,
onMoreOptionsSelected = viewModel::onMoreOptionsSelectedOnProviderSelection,
)
CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
providerInfo = uiState.activeEntry?.activeProvider!!,
createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo,
onOptionSelected = viewModel::onEntrySelected,
onConfirm = viewModel::onConfirmEntrySelected,
onMoreOptionsSelected = viewModel::onMoreOptionsSelectedOnCreationSelection,
)
CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
disabledProviderList = uiState.disabledProviders,
sortedCreateOptionsPairs = uiState.sortedCreateOptionsPairs,
hasDefaultProvider = uiState.hasDefaultProvider,
isFromProviderSelection = uiState.isFromProviderSelection!!,
onBackProviderSelectionButtonSelected =
viewModel::onBackProviderSelectionButtonSelected,
onBackCreationSelectionButtonSelected =
viewModel::onBackCreationSelectionButtonSelected,
onOptionSelected = viewModel::onEntrySelectedFromMoreOptionScreen,
onDisabledProvidersSelected = viewModel::onDisabledProvidersSelected,
onRemoteEntrySelected = viewModel::onEntrySelected,
)
CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
providerInfo = uiState.activeEntry?.activeProvider!!,
onChangeDefaultSelected = viewModel::onChangeDefaultSelected,
onUseOnceSelected = viewModel::onUseOnceSelected,
)
CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
activeRemoteEntry = uiState.activeEntry?.activeEntryInfo!!,
onOptionSelected = viewModel::onEntrySelected,
onConfirm = viewModel::onConfirmEntrySelected,
)
CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO -> MoreAboutPasskeysIntroCard(
onBackPasskeyIntroButtonSelected =
viewModel::onBackPasskeyIntroButtonSelected,
)
// Hide the sheet content as opposed to the whole bottom sheet to maintain the scrim
// background color even when the content should be hidden while waiting for
// results from the provider app.
when (uiState.providerActivityState) {
ProviderActivityState.NOT_APPLICABLE -> {
when (uiState.currentScreenState) {
CreateScreenState.PASSKEY_INTRO -> ConfirmationCard(
onConfirm = viewModel::onConfirmIntro,
onLearnMore = viewModel::onLearnMore,
)
CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
disabledProviderList = uiState.disabledProviders,
sortedCreateOptionsPairs = uiState.sortedCreateOptionsPairs,
onOptionSelected = viewModel::onEntrySelectedFromFirstUseScreen,
onDisabledProvidersSelected =
viewModel::onDisabledProvidersSelected,
onMoreOptionsSelected =
viewModel::onMoreOptionsSelectedOnProviderSelection,
)
CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
providerInfo = uiState.activeEntry?.activeProvider!!,
createOptionInfo =
uiState.activeEntry.activeEntryInfo as CreateOptionInfo,
onOptionSelected = viewModel::onEntrySelected,
onConfirm = viewModel::onConfirmEntrySelected,
onMoreOptionsSelected =
viewModel::onMoreOptionsSelectedOnCreationSelection,
)
CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
disabledProviderList = uiState.disabledProviders,
sortedCreateOptionsPairs = uiState.sortedCreateOptionsPairs,
hasDefaultProvider = uiState.hasDefaultProvider,
isFromProviderSelection = uiState.isFromProviderSelection!!,
onBackProviderSelectionButtonSelected =
viewModel::onBackProviderSelectionButtonSelected,
onBackCreationSelectionButtonSelected =
viewModel::onBackCreationSelectionButtonSelected,
onOptionSelected =
viewModel::onEntrySelectedFromMoreOptionScreen,
onDisabledProvidersSelected =
viewModel::onDisabledProvidersSelected,
onRemoteEntrySelected = viewModel::onEntrySelected,
)
CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
providerInfo = uiState.activeEntry?.activeProvider!!,
onChangeDefaultSelected = viewModel::onChangeDefaultSelected,
onUseOnceSelected = viewModel::onUseOnceSelected,
)
CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
activeRemoteEntry = uiState.activeEntry?.activeEntryInfo!!,
onOptionSelected = viewModel::onEntrySelected,
onConfirm = viewModel::onConfirmEntrySelected,
)
CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO ->
MoreAboutPasskeysIntroCard(
onBackPasskeyIntroButtonSelected =
viewModel::onBackPasskeyIntroButtonSelected,
)
}
}
ProviderActivityState.READY_TO_LAUNCH -> {
// Launch only once per providerActivityState change so that the provider
// UI will not be accidentally launched twice.
LaunchedEffect(uiState.providerActivityState) {
viewModel.launchProviderUi(providerActivityLauncher)
}
}
ProviderActivityState.PENDING -> {
// Hide our content when the provider activity is active.
}
} else if (uiState.selectedEntry != null && !uiState.providerActivityPending) {
viewModel.launchProviderUi(providerActivityLauncher)
}
},
scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.8f),
@@ -986,7 +1007,9 @@ fun MoreOptionsDisabledProvidersRow(
)
// TODO: Update the subtitle once design is confirmed
TextSecondary(
text = disabledProviders.joinToString(separator = " • ") { it.displayName },
text = disabledProviders.joinToString(separator = " • ") {
it.displayName
},
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
)

View File

@@ -28,240 +28,231 @@ import androidx.lifecycle.ViewModel
import com.android.credentialmanager.CreateFlowUtils
import com.android.credentialmanager.CredentialManagerRepo
import com.android.credentialmanager.UserConfigRepo
import com.android.credentialmanager.common.DialogResult
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.ResultState
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import com.android.credentialmanager.common.ProviderActivityState
data class CreateCredentialUiState(
val enabledProviders: List<EnabledProviderInfo>,
val disabledProviders: List<DisabledProviderInfo>? = null,
val currentScreenState: CreateScreenState,
val requestDisplayInfo: RequestDisplayInfo,
val sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
// Should not change with the real time update of default provider, only determine whether we're
// showing provider selection page at the beginning
val hasDefaultProvider: Boolean,
val activeEntry: ActiveEntry? = null,
val selectedEntry: EntryInfo? = null,
val hidden: Boolean = false,
val providerActivityPending: Boolean = false,
val isFromProviderSelection: Boolean? = null,
val enabledProviders: List<EnabledProviderInfo>,
val disabledProviders: List<DisabledProviderInfo>? = null,
val currentScreenState: CreateScreenState,
val requestDisplayInfo: RequestDisplayInfo,
val sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
// Should not change with the real time update of default provider, only determine whether
// we're showing provider selection page at the beginning
val hasDefaultProvider: Boolean,
val activeEntry: ActiveEntry? = null,
val selectedEntry: EntryInfo? = null,
val providerActivityState: ProviderActivityState =
ProviderActivityState.NOT_APPLICABLE,
val isFromProviderSelection: Boolean? = null,
val dialogState: DialogState = DialogState.ACTIVE,
)
class CreateCredentialViewModel(
private val credManRepo: CredentialManagerRepo,
userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance(),
private val credManRepo: CredentialManagerRepo,
userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance(),
) : ViewModel() {
var providerEnableListUiState = credManRepo.getCreateProviderEnableListInitialUiState()
val providerEnableListUiState = credManRepo.getCreateProviderEnableListInitialUiState()
var providerDisableListUiState = credManRepo.getCreateProviderDisableListInitialUiState()
val providerDisableListUiState = credManRepo.getCreateProviderDisableListInitialUiState()
var requestDisplayInfoUiState = credManRepo.getCreateRequestDisplayInfoInitialUiState()
val requestDisplayInfoUiState = credManRepo.getCreateRequestDisplayInfoInitialUiState()
var defaultProviderId = userConfigRepo.getDefaultProviderId()
val defaultProviderId = userConfigRepo.getDefaultProviderId()
var isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse()
val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse()
var uiState by mutableStateOf(
CreateFlowUtils.toCreateCredentialUiState(
providerEnableListUiState,
providerDisableListUiState,
defaultProviderId,
requestDisplayInfoUiState,
false,
isPasskeyFirstUse))
private set
var uiState by mutableStateOf(
CreateFlowUtils.toCreateCredentialUiState(
providerEnableListUiState,
providerDisableListUiState,
defaultProviderId,
requestDisplayInfoUiState,
false,
isPasskeyFirstUse))
private set
val dialogResult: MutableSharedFlow<DialogResult> =
MutableSharedFlow(replay = 0, extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST)
fun observeDialogResult(): SharedFlow<DialogResult> {
return dialogResult
}
fun onConfirmIntro() {
uiState = CreateFlowUtils.toCreateCredentialUiState(
providerEnableListUiState, providerDisableListUiState, defaultProviderId,
requestDisplayInfoUiState, true, isPasskeyFirstUse)
UserConfigRepo.getInstance().setIsPasskeyFirstUse(false)
}
fun getProviderInfoByName(providerId: String): EnabledProviderInfo {
return uiState.enabledProviders.single {
it.id == providerId
fun onConfirmIntro() {
uiState = CreateFlowUtils.toCreateCredentialUiState(
providerEnableListUiState, providerDisableListUiState, defaultProviderId,
requestDisplayInfoUiState, true, isPasskeyFirstUse)
UserConfigRepo.getInstance().setIsPasskeyFirstUse(false)
}
}
fun onMoreOptionsSelectedOnProviderSelection() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
isFromProviderSelection = true
)
}
fun getProviderInfoByName(providerId: String): EnabledProviderInfo {
return uiState.enabledProviders.single {
it.id == providerId
}
}
fun onMoreOptionsSelectedOnCreationSelection() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
isFromProviderSelection = false
)
}
fun onMoreOptionsSelectedOnProviderSelection() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
isFromProviderSelection = true
)
}
fun onBackProviderSelectionButtonSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.PROVIDER_SELECTION,
)
}
fun onMoreOptionsSelectedOnCreationSelection() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
isFromProviderSelection = false
)
}
fun onBackCreationSelectionButtonSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
)
}
fun onBackProviderSelectionButtonSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.PROVIDER_SELECTION,
)
}
fun onBackPasskeyIntroButtonSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.PASSKEY_INTRO,
)
}
fun onBackCreationSelectionButtonSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
)
}
fun onEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) {
uiState = uiState.copy(
currentScreenState = if (
activeEntry.activeProvider.id == UserConfigRepo.getInstance().getDefaultProviderId()
) CreateScreenState.CREATION_OPTION_SELECTION else CreateScreenState.MORE_OPTIONS_ROW_INTRO,
activeEntry = activeEntry
)
}
fun onBackPasskeyIntroButtonSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.PASSKEY_INTRO,
)
}
fun onEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
activeEntry = activeEntry
)
val providerId = uiState.activeEntry?.activeProvider?.id
onDefaultChanged(providerId)
}
fun onEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) {
uiState = uiState.copy(
currentScreenState =
if (activeEntry.activeProvider.id ==
UserConfigRepo.getInstance().getDefaultProviderId())
CreateScreenState.CREATION_OPTION_SELECTION
else CreateScreenState.MORE_OPTIONS_ROW_INTRO,
activeEntry = activeEntry
)
}
fun onDisabledProvidersSelected() {
credManRepo.onSettingLaunchCancel()
dialogResult.tryEmit(DialogResult(ResultState.LAUNCH_SETTING_CANCELED))
}
fun onEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
activeEntry = activeEntry
)
val providerId = uiState.activeEntry?.activeProvider?.id
onDefaultChanged(providerId)
}
fun onCancel() {
credManRepo.onUserCancel()
dialogResult.tryEmit(DialogResult(ResultState.NORMAL_CANCELED))
}
fun onDisabledProvidersSelected() {
credManRepo.onSettingLaunchCancel()
uiState = uiState.copy(dialogState = DialogState.CANCELED_FOR_SETTINGS)
}
fun onLearnMore() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO,
)
}
fun onCancel() {
credManRepo.onUserCancel()
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
fun onChangeDefaultSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
)
val providerId = uiState.activeEntry?.activeProvider?.id
onDefaultChanged(providerId)
}
fun onLearnMore() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO,
)
}
fun onUseOnceSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
)
}
fun onChangeDefaultSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
)
val providerId = uiState.activeEntry?.activeProvider?.id
onDefaultChanged(providerId)
}
fun onDefaultChanged(providerId: String?) {
if (providerId != null) {
Log.d(
"Account Selector", "Default provider changed to: " +
fun onUseOnceSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
)
}
fun onDefaultChanged(providerId: String?) {
if (providerId != null) {
Log.d(
"Account Selector", "Default provider changed to: " +
" {provider=$providerId")
UserConfigRepo.getInstance().setDefaultProvider(providerId)
} else {
Log.w("Account Selector", "Null provider is being changed")
UserConfigRepo.getInstance().setDefaultProvider(providerId)
} else {
Log.w("Account Selector", "Null provider is being changed")
}
}
}
fun onEntrySelected(selectedEntry: EntryInfo) {
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,
hidden = true,
)
} else {
credManRepo.onOptionSelected(
providerId,
entryKey,
entrySubkey
)
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
fun onEntrySelected(selectedEntry: EntryInfo) {
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,
providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
)
} else {
credManRepo.onOptionSelected(
providerId,
entryKey,
entrySubkey
)
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
}
}
fun launchProviderUi(
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val entry = uiState.selectedEntry
if (entry != null && entry.pendingIntent != null) {
uiState = uiState.copy(
providerActivityPending = true,
)
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
.setFillInIntent(entry.fillInIntent).build()
launcher.launch(intentSenderRequest)
} else {
Log.w("Account Selector", "No provider UI to launch")
fun launchProviderUi(
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val entry = uiState.selectedEntry
if (entry != null && entry.pendingIntent != null) {
uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING)
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
.setFillInIntent(entry.fillInIntent).build()
launcher.launch(intentSenderRequest)
} else {
Log.w("Account Selector", "No provider UI to launch")
}
}
}
fun onConfirmEntrySelected() {
val selectedEntry = uiState.activeEntry?.activeEntryInfo
if (selectedEntry != null) {
onEntrySelected(selectedEntry)
} else {
Log.w("Account Selector",
"Illegal state: confirm is pressed but activeEntry isn't set.")
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
fun onConfirmEntrySelected() {
val selectedEntry = uiState.activeEntry?.activeEntryInfo
if (selectedEntry != null) {
onEntrySelected(selectedEntry)
} else {
Log.w("Account Selector",
"Illegal state: confirm is pressed but activeEntry isn't set.")
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
}
}
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
val entry = uiState.selectedEntry
val resultCode = providerActivityResult.resultCode
val resultData = providerActivityResult.data
if (resultCode == Activity.RESULT_CANCELED) {
// Re-display the CredMan UI if the user canceled from the provider UI.
uiState = uiState.copy(
selectedEntry = null,
hidden = false,
providerActivityPending = false,
)
} else {
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}"
)
credManRepo.onOptionSelected(
providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData,
)
} else {
Log.w("Account Selector",
"Illegal state: received a provider result but found no matching entry.")
}
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
val entry = uiState.selectedEntry
val resultCode = providerActivityResult.resultCode
val resultData = providerActivityResult.data
if (resultCode == Activity.RESULT_CANCELED) {
// Re-display the CredMan UI if the user canceled from the provider UI.
Log.d("Account Selector", "The provider activity was cancelled," +
" re-displaying our UI.")
uiState = uiState.copy(
selectedEntry = null,
providerActivityState = ProviderActivityState.NOT_APPLICABLE,
)
} else {
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}"
)
credManRepo.onOptionSelected(
providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData,
)
} else {
Log.w("Account Selector",
"Illegal state: received a provider result but found no matching entry.")
}
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
}
}
}

View File

@@ -59,6 +59,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import com.android.credentialmanager.R
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
import com.android.credentialmanager.common.material.rememberModalBottomSheetState
@@ -90,30 +91,42 @@ fun GetCredentialScreen(
modifier = Modifier.background(Color.Transparent),
sheetState = state,
sheetContent = {
// TODO: hide UI at top level
if (!uiState.hidden) {
if (uiState.currentScreenState == GetScreenState.PRIMARY_SELECTION) {
PrimarySelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
providerDisplayInfo = uiState.providerDisplayInfo,
providerInfoList = uiState.providerInfoList,
activeEntry = uiState.activeEntry,
onEntrySelected = viewModel::onEntrySelected,
onConfirm = viewModel::onConfirmEntrySelected,
onMoreOptionSelected = viewModel::onMoreOptionSelected,
)
} else {
AllSignInOptionCard(
providerInfoList = uiState.providerInfoList,
providerDisplayInfo = uiState.providerDisplayInfo,
onEntrySelected = viewModel::onEntrySelected,
onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
onCancel = viewModel::onCancel,
isNoAccount = uiState.isNoAccount,
)
// Hide the sheet content as opposed to the whole bottom sheet to maintain the scrim
// background color even when the content should be hidden while waiting for
// results from the provider app.
when (uiState.providerActivityState) {
ProviderActivityState.NOT_APPLICABLE -> {
if (uiState.currentScreenState == GetScreenState.PRIMARY_SELECTION) {
PrimarySelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
providerDisplayInfo = uiState.providerDisplayInfo,
providerInfoList = uiState.providerInfoList,
activeEntry = uiState.activeEntry,
onEntrySelected = viewModel::onEntrySelected,
onConfirm = viewModel::onConfirmEntrySelected,
onMoreOptionSelected = viewModel::onMoreOptionSelected,
)
} else {
AllSignInOptionCard(
providerInfoList = uiState.providerInfoList,
providerDisplayInfo = uiState.providerDisplayInfo,
onEntrySelected = viewModel::onEntrySelected,
onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
onCancel = viewModel::onCancel,
isNoAccount = uiState.isNoAccount,
)
}
}
ProviderActivityState.READY_TO_LAUNCH -> {
// Launch only once per providerActivityState change so that the provider
// UI will not be accidentally launched twice.
LaunchedEffect(uiState.providerActivityState) {
viewModel.launchProviderUi(providerActivityLauncher)
}
}
ProviderActivityState.PENDING -> {
// Hide our content when the provider activity is active.
}
} else if (uiState.selectedEntry != null && !uiState.providerActivityPending) {
viewModel.launchProviderUi(providerActivityLauncher)
}
},
scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.8f),
@@ -225,7 +238,7 @@ fun PrimarySelectionCard(
color = Color.Transparent
)
var totalEntriesCount = sortedUserNameToCredentialEntryList
.flatMap{ it.sortedCredentialEntryList}.size + authenticationEntryList
.flatMap { it.sortedCredentialEntryList }.size + authenticationEntryList
.size + providerInfoList.flatMap { it.actionEntryList }.size
if (providerDisplayInfo.remoteEntry != null) totalEntriesCount += 1
// Row horizontalArrangement differs on only one actionButton(should place on most
@@ -528,10 +541,10 @@ fun CredentialEntryRow(
credentialEntryInfo.credentialTypeDisplayName
else
credentialEntryInfo.credentialTypeDisplayName +
stringResource(
R.string.get_dialog_sign_in_type_username_separator
) +
credentialEntryInfo.displayName
stringResource(
R.string.get_dialog_sign_in_type_username_separator
) +
credentialEntryInfo.displayName
},
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp)
@@ -561,7 +574,7 @@ fun AuthenticationEntryRow(
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
) {
) {
Column() {
// TODO: fix the text values.
TextOnSurfaceVariant(

View File

@@ -26,251 +26,242 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.android.credentialmanager.CredentialManagerRepo
import com.android.credentialmanager.common.DialogResult
import com.android.credentialmanager.common.DialogState
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.ResultState
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
import com.android.internal.util.Preconditions
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
data class GetCredentialUiState(
val providerInfoList: List<ProviderInfo>,
val requestDisplayInfo: RequestDisplayInfo,
val currentScreenState: GetScreenState = toGetScreenState(providerInfoList),
val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
val selectedEntry: EntryInfo? = null,
val activeEntry: EntryInfo? = toActiveEntry(providerDisplayInfo),
val hidden: Boolean = false,
val providerActivityPending: Boolean = false,
val isNoAccount: Boolean = false,
val providerInfoList: List<ProviderInfo>,
val requestDisplayInfo: RequestDisplayInfo,
val currentScreenState: GetScreenState = toGetScreenState(providerInfoList),
val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
val selectedEntry: EntryInfo? = null,
val activeEntry: EntryInfo? = toActiveEntry(providerDisplayInfo),
val providerActivityState: ProviderActivityState =
ProviderActivityState.NOT_APPLICABLE,
val isNoAccount: Boolean = false,
val dialogState: DialogState = DialogState.ACTIVE,
)
class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : ViewModel() {
var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState())
private set
var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState())
private set
val dialogResult: MutableSharedFlow<DialogResult> =
MutableSharedFlow(replay = 0, extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST)
fun observeDialogResult(): SharedFlow<DialogResult> {
return dialogResult
}
fun onEntrySelected(entry: EntryInfo) {
Log.d("Account Selector", "credential selected:" +
" {provider=${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
if (entry.pendingIntent != null) {
uiState = uiState.copy(
selectedEntry = entry,
hidden = true,
)
} else {
credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey)
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
fun onEntrySelected(entry: EntryInfo) {
Log.d("Account Selector", "credential selected: {provider=${entry.providerId}" +
", key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
if (entry.pendingIntent != null) {
uiState = uiState.copy(
selectedEntry = entry,
providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
)
} else {
credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey)
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
}
}
fun onConfirmEntrySelected() {
val activeEntry = uiState.activeEntry
if (activeEntry != null) {
onEntrySelected(activeEntry)
} else {
Log.w("Account Selector",
"Illegal state: confirm is pressed but activeEntry isn't set.")
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
fun onConfirmEntrySelected() {
val activeEntry = uiState.activeEntry
if (activeEntry != null) {
onEntrySelected(activeEntry)
} else {
Log.w("Account Selector",
"Illegal state: confirm is pressed but activeEntry isn't set.")
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
}
}
fun launchProviderUi(
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val entry = uiState.selectedEntry
if (entry != null && entry.pendingIntent != null) {
uiState = uiState.copy(
providerActivityPending = true,
)
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
.setFillInIntent(entry.fillInIntent).build()
launcher.launch(intentSenderRequest)
} else {
Log.w("Account Selector", "No provider UI to launch")
fun launchProviderUi(
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val entry = uiState.selectedEntry
if (entry != null && entry.pendingIntent != null) {
Log.d("credentials", "Launching provider activity")
uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING)
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
.setFillInIntent(entry.fillInIntent).build()
launcher.launch(intentSenderRequest)
} else {
Log.w("Account Selector", "No provider UI to launch")
}
}
}
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
val entry = uiState.selectedEntry
val resultCode = providerActivityResult.resultCode
val resultData = providerActivityResult.data
if (resultCode == Activity.RESULT_CANCELED) {
// Re-display the CredMan UI if the user canceled from the provider UI.
uiState = uiState.copy(
selectedEntry = null,
hidden = false,
providerActivityPending = false,
)
} else {
if (entry != null) {
Log.d("Account Selector", "Got provider activity result: {provider=" +
"${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
"resultCode=$resultCode, resultData=$resultData}"
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
val entry = uiState.selectedEntry
val resultCode = providerActivityResult.resultCode
val resultData = providerActivityResult.data
if (resultCode == Activity.RESULT_CANCELED) {
// Re-display the CredMan UI if the user canceled from the provider UI.
Log.d("Account Selector", "The provider activity was cancelled," +
" re-displaying our UI.")
uiState = uiState.copy(
selectedEntry = null,
providerActivityState = ProviderActivityState.NOT_APPLICABLE,
)
} else {
if (entry != null) {
Log.d("Account Selector", "Got provider activity result: {provider=" +
"${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}" +
", resultCode=$resultCode, resultData=$resultData}"
)
credManRepo.onOptionSelected(
entry.providerId, entry.entryKey, entry.entrySubkey,
resultCode, resultData,
)
} else {
Log.w("Account Selector",
"Illegal state: received a provider result but found no matching entry.")
}
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
}
fun onMoreOptionSelected() {
Log.d("Account Selector", "More Option selected")
uiState = uiState.copy(
currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS
)
credManRepo.onOptionSelected(
entry.providerId, entry.entryKey, entry.entrySubkey,
resultCode, resultData,
)
} else {
Log.w("Account Selector",
"Illegal state: received a provider result but found no matching entry.")
}
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
}
}
fun onMoreOptionSelected() {
Log.d("Account Selector", "More Option selected")
uiState = uiState.copy(
currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS
)
}
fun onMoreOptionOnSnackBarSelected(isNoAccount: Boolean) {
Log.d("Account Selector", "More Option on snackBar selected")
uiState = uiState.copy(
currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS,
isNoAccount = isNoAccount,
)
}
fun onMoreOptionOnSnackBarSelected(isNoAccount: Boolean) {
Log.d("Account Selector", "More Option on snackBar selected")
uiState = uiState.copy(
currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS,
isNoAccount = isNoAccount,
)
}
fun onBackToPrimarySelectionScreen() {
uiState = uiState.copy(
currentScreenState = GetScreenState.PRIMARY_SELECTION
)
}
fun onBackToPrimarySelectionScreen() {
uiState = uiState.copy(
currentScreenState = GetScreenState.PRIMARY_SELECTION
)
}
fun onCancel() {
credManRepo.onUserCancel()
dialogResult.tryEmit(DialogResult(ResultState.NORMAL_CANCELED))
}
fun onCancel() {
credManRepo.onUserCancel()
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
}
}
private fun toProviderDisplayInfo(
providerInfoList: List<ProviderInfo>
providerInfoList: List<ProviderInfo>
): ProviderDisplayInfo {
val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
val remoteEntryList = mutableListOf<RemoteEntryInfo>()
providerInfoList.forEach { providerInfo ->
if (providerInfo.authenticationEntry != null) {
authenticationEntryList.add(providerInfo.authenticationEntry)
}
if (providerInfo.remoteEntry != null) {
remoteEntryList.add(providerInfo.remoteEntry)
}
providerInfo.credentialEntryList.forEach {
userNameToCredentialEntryMap.compute(
it.userName
) {
_, v ->
if (v == null) {
mutableListOf(it)
} else {
v.add(it)
v
val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
val remoteEntryList = mutableListOf<RemoteEntryInfo>()
providerInfoList.forEach { providerInfo ->
if (providerInfo.authenticationEntry != null) {
authenticationEntryList.add(providerInfo.authenticationEntry)
}
if (providerInfo.remoteEntry != null) {
remoteEntryList.add(providerInfo.remoteEntry)
}
providerInfo.credentialEntryList.forEach {
userNameToCredentialEntryMap.compute(
it.userName
) { _, v ->
if (v == null) {
mutableListOf(it)
} else {
v.add(it)
v
}
}
}
}
}
}
// There can only be at most one remote entry
// TODO: fail elegantly
Preconditions.checkState(remoteEntryList.size <= 1)
// There can only be at most one remote entry
// TODO: fail elegantly
Preconditions.checkState(remoteEntryList.size <= 1)
// Compose sortedUserNameToCredentialEntryList
val comparator = CredentialEntryInfoComparatorByTypeThenTimestamp()
// Sort per username
userNameToCredentialEntryMap.values.forEach {
it.sortWith(comparator)
}
// Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
PerUserNameCredentialEntryList(it.key, it.value)
}.sortedWith(
compareByDescending{ it.sortedCredentialEntryList.first().lastUsedTimeMillis }
)
// Compose sortedUserNameToCredentialEntryList
val comparator = CredentialEntryInfoComparatorByTypeThenTimestamp()
// Sort per username
userNameToCredentialEntryMap.values.forEach {
it.sortWith(comparator)
}
// Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
PerUserNameCredentialEntryList(it.key, it.value)
}.sortedWith(
compareByDescending { it.sortedCredentialEntryList.first().lastUsedTimeMillis }
)
return ProviderDisplayInfo(
sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
authenticationEntryList = authenticationEntryList,
remoteEntry = remoteEntryList.getOrNull(0),
)
return ProviderDisplayInfo(
sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
authenticationEntryList = authenticationEntryList,
remoteEntry = remoteEntryList.getOrNull(0),
)
}
private fun toActiveEntry(
providerDisplayInfo: ProviderDisplayInfo,
providerDisplayInfo: ProviderDisplayInfo,
): EntryInfo? {
val sortedUserNameToCredentialEntryList =
providerDisplayInfo.sortedUserNameToCredentialEntryList
val authenticationEntryList = providerDisplayInfo.authenticationEntryList
var activeEntry: EntryInfo? = null
if (sortedUserNameToCredentialEntryList
.size == 1 && authenticationEntryList.isEmpty()
) {
activeEntry = sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList.first()
} else if (
sortedUserNameToCredentialEntryList
.isEmpty() && authenticationEntryList.size == 1
) {
activeEntry = authenticationEntryList.first()
}
return activeEntry
val sortedUserNameToCredentialEntryList =
providerDisplayInfo.sortedUserNameToCredentialEntryList
val authenticationEntryList = providerDisplayInfo.authenticationEntryList
var activeEntry: EntryInfo? = null
if (sortedUserNameToCredentialEntryList
.size == 1 && authenticationEntryList.isEmpty()
) {
activeEntry = sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList.first()
} else if (
sortedUserNameToCredentialEntryList
.isEmpty() && authenticationEntryList.size == 1
) {
activeEntry = authenticationEntryList.first()
}
return activeEntry
}
private fun toGetScreenState(
providerInfoList: List<ProviderInfo>
providerInfoList: List<ProviderInfo>
): GetScreenState {
var noLocalAccount = true
var remoteInfo: RemoteEntryInfo? = null
providerInfoList.forEach{providerInfo -> if (
providerInfo.credentialEntryList.isNotEmpty() || providerInfo.authenticationEntry != null
) { noLocalAccount = false }
// TODO: handle the error situation that if multiple remoteInfos exists
if (providerInfo.remoteEntry != null) {
remoteInfo = providerInfo.remoteEntry
var noLocalAccount = true
var remoteInfo: RemoteEntryInfo? = null
providerInfoList.forEach { providerInfo ->
if (providerInfo.credentialEntryList.isNotEmpty() ||
providerInfo.authenticationEntry != null) {
noLocalAccount = false
}
// TODO: handle the error situation that if multiple remoteInfos exists
if (providerInfo.remoteEntry != null) {
remoteInfo = providerInfo.remoteEntry
}
}
}
return if (noLocalAccount && remoteInfo != null)
GetScreenState.REMOTE_ONLY else GetScreenState.PRIMARY_SELECTION
return if (noLocalAccount && remoteInfo != null)
GetScreenState.REMOTE_ONLY else GetScreenState.PRIMARY_SELECTION
}
internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator<CredentialEntryInfo> {
override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
// First 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
}
}
override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
// First 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
}
}
// Then 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 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
}
return 0
}
return 0
}
}