From c9a80cdbcbbacb630cfeb2860b21a1f093e3b59a Mon Sep 17 00:00:00 2001 From: Helen Qin Date: Tue, 24 Jan 2023 03:39:58 +0000 Subject: [PATCH] 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 --- .../CredentialSelectorActivity.kt | 131 +++--- .../credentialmanager/common/DialogResult.kt | 6 + .../credentialmanager/common/DialogType.kt | 5 +- .../common/ProviderActivityState.kt | 27 ++ .../createflow/CreateCredentialComponents.kt | 137 +++--- .../createflow/CreateCredentialViewModel.kt | 389 +++++++++-------- .../getflow/GetCredentialComponents.kt | 71 ++-- .../getflow/GetCredentialViewModel.kt | 395 +++++++++--------- 8 files changed, 603 insertions(+), 558 deletions(-) create mode 100644 packages/CredentialManager/src/com/android/credentialmanager/common/ProviderActivityState.kt diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 0620f9aa8c828..df8574560e2ca 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -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(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(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() + } } - } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/DialogResult.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/DialogResult.kt index 743f49b18c3f4..6d07df70e7bf6 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/DialogResult.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/DialogResult.kt @@ -16,6 +16,12 @@ package com.android.credentialmanager.common +enum class DialogState { + ACTIVE, + COMPLETE, + CANCELED_FOR_SETTINGS, +} + enum class ResultState { COMPLETE, NORMAL_CANCELED, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/DialogType.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/DialogType.kt index b5e9fd00bb228..f40dc7e548704 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/DialogType.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/DialogType.kt @@ -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 } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ProviderActivityState.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ProviderActivityState.kt new file mode 100644 index 0000000000000..b47d0e514c014 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ProviderActivityState.kt @@ -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, +} \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 1e1c833ed6851..5e432b9a5edfe 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -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), ) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt index a9b1d3328f38b..d3cf241240354 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt @@ -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, - val disabledProviders: List? = null, - val currentScreenState: CreateScreenState, - val requestDisplayInfo: RequestDisplayInfo, - val sortedCreateOptionsPairs: List>, - // 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, + val disabledProviders: List? = null, + val currentScreenState: CreateScreenState, + val requestDisplayInfo: RequestDisplayInfo, + val sortedCreateOptionsPairs: List>, + // 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 = - MutableSharedFlow(replay = 0, extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST) - - fun observeDialogResult(): SharedFlow { - 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 - ) { - 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 + ) { + 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) + } } - } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 420b4d5796978..b30d1ec24d784 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -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( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt index c72ae726ff202..065a2deb31035 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt @@ -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, - 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, + 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 = - MutableSharedFlow(replay = 0, extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST) - - fun observeDialogResult(): SharedFlow { - 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 - ) { - 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 + ) { + 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 + providerInfoList: List ): ProviderDisplayInfo { - val userNameToCredentialEntryMap = mutableMapOf>() - val authenticationEntryList = mutableListOf() - val remoteEntryList = mutableListOf() - 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>() + val authenticationEntryList = mutableListOf() + val remoteEntryList = mutableListOf() + 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 + providerInfoList: List ): 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 { - 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 - } } \ No newline at end of file