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