Merge "Fix the duplicate intent launching issue."

This commit is contained in:
Helen Qin
2023-01-24 20:19:00 +00:00
committed by Android (Google) Code Review
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
}
}