Fix the duplicate intent launching issue.
This fixed unintended duplicate intent launching issue for 1. action chips for get flow 2. settings for create flow. Also improved the data model to better fit our practical purposes. Bug: 264943119 Fix: 264943119 Test: local deployment Change-Id: I57be60060012465655306da83e8c2972b37c0b8b
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
package com.android.credentialmanager
|
||||
|
||||
import android.content.Intent
|
||||
import android.credentials.ui.RequestInfo
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
@@ -26,89 +27,83 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.android.credentialmanager.common.DialogType
|
||||
import com.android.credentialmanager.common.DialogResult
|
||||
import com.android.credentialmanager.common.DialogState
|
||||
import com.android.credentialmanager.common.ProviderActivityResult
|
||||
import com.android.credentialmanager.common.ResultState
|
||||
import com.android.credentialmanager.createflow.CreateCredentialScreen
|
||||
import com.android.credentialmanager.createflow.CreateCredentialViewModel
|
||||
import com.android.credentialmanager.getflow.GetCredentialScreen
|
||||
import com.android.credentialmanager.getflow.GetCredentialViewModel
|
||||
import com.android.credentialmanager.ui.theme.CredentialSelectorTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalMaterialApi
|
||||
class CredentialSelectorActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val credManRepo = CredentialManagerRepo(this, intent)
|
||||
UserConfigRepo.setup(this)
|
||||
val requestInfo = credManRepo.requestInfo
|
||||
setContent {
|
||||
CredentialSelectorTheme {
|
||||
CredentialManagerBottomSheet(DialogType.toDialogType(requestInfo.type), credManRepo)
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val credManRepo = CredentialManagerRepo(this, intent)
|
||||
UserConfigRepo.setup(this)
|
||||
val requestInfo = credManRepo.requestInfo
|
||||
setContent {
|
||||
CredentialSelectorTheme {
|
||||
CredentialManagerBottomSheet(requestInfo.type, credManRepo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun CredentialManagerBottomSheet(dialogType: DialogType, credManRepo: CredentialManagerRepo) {
|
||||
val providerActivityResult = remember { mutableStateOf<ProviderActivityResult?>(null) }
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) {
|
||||
providerActivityResult.value = ProviderActivityResult(it.resultCode, it.data)
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun CredentialManagerBottomSheet(requestType: String, credManRepo: CredentialManagerRepo) {
|
||||
val providerActivityResult = remember { mutableStateOf<ProviderActivityResult?>(null) }
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) {
|
||||
providerActivityResult.value = ProviderActivityResult(it.resultCode, it.data)
|
||||
}
|
||||
when (requestType) {
|
||||
RequestInfo.TYPE_CREATE -> {
|
||||
val viewModel: CreateCredentialViewModel = viewModel {
|
||||
CreateCredentialViewModel(credManRepo)
|
||||
}
|
||||
LaunchedEffect(viewModel.uiState.dialogState) {
|
||||
handleDialogState(viewModel.uiState.dialogState)
|
||||
}
|
||||
providerActivityResult.value?.let {
|
||||
viewModel.onProviderActivityResult(it)
|
||||
providerActivityResult.value = null
|
||||
}
|
||||
CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
|
||||
}
|
||||
RequestInfo.TYPE_GET -> {
|
||||
val viewModel: GetCredentialViewModel = viewModel {
|
||||
GetCredentialViewModel(credManRepo)
|
||||
}
|
||||
LaunchedEffect(viewModel.uiState.dialogState) {
|
||||
handleDialogState(viewModel.uiState.dialogState)
|
||||
}
|
||||
providerActivityResult.value?.let {
|
||||
viewModel.onProviderActivityResult(it)
|
||||
providerActivityResult.value = null
|
||||
}
|
||||
GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
|
||||
}
|
||||
else -> {
|
||||
Log.w("AccountSelector", "Unknown type, not rendering any UI")
|
||||
this.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
when (dialogType) {
|
||||
DialogType.CREATE_PASSKEY -> {
|
||||
val viewModel: CreateCredentialViewModel = viewModel{
|
||||
CreateCredentialViewModel(credManRepo)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
viewModel.observeDialogResult().collect{ dialogResult ->
|
||||
onCancel(dialogResult)
|
||||
}
|
||||
}
|
||||
providerActivityResult.value?.let {
|
||||
viewModel.onProviderActivityResult(it)
|
||||
providerActivityResult.value = null
|
||||
}
|
||||
CreateCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
|
||||
}
|
||||
DialogType.GET_CREDENTIALS -> {
|
||||
val viewModel: GetCredentialViewModel = viewModel{
|
||||
GetCredentialViewModel(credManRepo)
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
viewModel.observeDialogResult().collect{ dialogResult ->
|
||||
onCancel(dialogResult)
|
||||
}
|
||||
}
|
||||
providerActivityResult.value?.let {
|
||||
viewModel.onProviderActivityResult(it)
|
||||
providerActivityResult.value = null
|
||||
}
|
||||
GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher)
|
||||
}
|
||||
else -> {
|
||||
Log.w("AccountSelector", "Unknown type, not rendering any UI")
|
||||
this.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCancel(dialogResut: DialogResult) {
|
||||
if (dialogResut.resultState == ResultState
|
||||
.COMPLETE || dialogResut.resultState == ResultState.NORMAL_CANCELED) {
|
||||
this@CredentialSelectorActivity.finish()
|
||||
} else if (dialogResut.resultState == ResultState.LAUNCH_SETTING_CANCELED) {
|
||||
this@CredentialSelectorActivity.startActivity(Intent(Settings.ACTION_SYNC_SETTINGS))
|
||||
this@CredentialSelectorActivity.finish()
|
||||
private fun handleDialogState(dialogState: DialogState) {
|
||||
if (dialogState == DialogState.COMPLETE) {
|
||||
Log.i("AccountSelector", "Received signal to finish the activity.")
|
||||
this@CredentialSelectorActivity.finish()
|
||||
} else if (dialogState == DialogState.CANCELED_FOR_SETTINGS) {
|
||||
Log.i("AccountSelector", "Received signal to finish the activity and launch settings.")
|
||||
this@CredentialSelectorActivity.startActivity(Intent(Settings.ACTION_SYNC_SETTINGS))
|
||||
this@CredentialSelectorActivity.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
|
||||
package com.android.credentialmanager.common
|
||||
|
||||
enum class DialogState {
|
||||
ACTIVE,
|
||||
COMPLETE,
|
||||
CANCELED_FOR_SETTINGS,
|
||||
}
|
||||
|
||||
enum class ResultState {
|
||||
COMPLETE,
|
||||
NORMAL_CANCELED,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -28,240 +28,231 @@ import androidx.lifecycle.ViewModel
|
||||
import com.android.credentialmanager.CreateFlowUtils
|
||||
import com.android.credentialmanager.CredentialManagerRepo
|
||||
import com.android.credentialmanager.UserConfigRepo
|
||||
import com.android.credentialmanager.common.DialogResult
|
||||
import com.android.credentialmanager.common.DialogState
|
||||
import com.android.credentialmanager.common.ProviderActivityResult
|
||||
import com.android.credentialmanager.common.ResultState
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import com.android.credentialmanager.common.ProviderActivityState
|
||||
|
||||
data class CreateCredentialUiState(
|
||||
val enabledProviders: List<EnabledProviderInfo>,
|
||||
val disabledProviders: List<DisabledProviderInfo>? = null,
|
||||
val currentScreenState: CreateScreenState,
|
||||
val requestDisplayInfo: RequestDisplayInfo,
|
||||
val sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
|
||||
// Should not change with the real time update of default provider, only determine whether we're
|
||||
// showing provider selection page at the beginning
|
||||
val hasDefaultProvider: Boolean,
|
||||
val activeEntry: ActiveEntry? = null,
|
||||
val selectedEntry: EntryInfo? = null,
|
||||
val hidden: Boolean = false,
|
||||
val providerActivityPending: Boolean = false,
|
||||
val isFromProviderSelection: Boolean? = null,
|
||||
val enabledProviders: List<EnabledProviderInfo>,
|
||||
val disabledProviders: List<DisabledProviderInfo>? = null,
|
||||
val currentScreenState: CreateScreenState,
|
||||
val requestDisplayInfo: RequestDisplayInfo,
|
||||
val sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
|
||||
// Should not change with the real time update of default provider, only determine whether
|
||||
// we're showing provider selection page at the beginning
|
||||
val hasDefaultProvider: Boolean,
|
||||
val activeEntry: ActiveEntry? = null,
|
||||
val selectedEntry: EntryInfo? = null,
|
||||
val providerActivityState: ProviderActivityState =
|
||||
ProviderActivityState.NOT_APPLICABLE,
|
||||
val isFromProviderSelection: Boolean? = null,
|
||||
val dialogState: DialogState = DialogState.ACTIVE,
|
||||
)
|
||||
|
||||
class CreateCredentialViewModel(
|
||||
private val credManRepo: CredentialManagerRepo,
|
||||
userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance(),
|
||||
private val credManRepo: CredentialManagerRepo,
|
||||
userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance(),
|
||||
) : ViewModel() {
|
||||
var providerEnableListUiState = credManRepo.getCreateProviderEnableListInitialUiState()
|
||||
val providerEnableListUiState = credManRepo.getCreateProviderEnableListInitialUiState()
|
||||
|
||||
var providerDisableListUiState = credManRepo.getCreateProviderDisableListInitialUiState()
|
||||
val providerDisableListUiState = credManRepo.getCreateProviderDisableListInitialUiState()
|
||||
|
||||
var requestDisplayInfoUiState = credManRepo.getCreateRequestDisplayInfoInitialUiState()
|
||||
val requestDisplayInfoUiState = credManRepo.getCreateRequestDisplayInfoInitialUiState()
|
||||
|
||||
var defaultProviderId = userConfigRepo.getDefaultProviderId()
|
||||
val defaultProviderId = userConfigRepo.getDefaultProviderId()
|
||||
|
||||
var isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse()
|
||||
val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse()
|
||||
|
||||
var uiState by mutableStateOf(
|
||||
CreateFlowUtils.toCreateCredentialUiState(
|
||||
providerEnableListUiState,
|
||||
providerDisableListUiState,
|
||||
defaultProviderId,
|
||||
requestDisplayInfoUiState,
|
||||
false,
|
||||
isPasskeyFirstUse))
|
||||
private set
|
||||
var uiState by mutableStateOf(
|
||||
CreateFlowUtils.toCreateCredentialUiState(
|
||||
providerEnableListUiState,
|
||||
providerDisableListUiState,
|
||||
defaultProviderId,
|
||||
requestDisplayInfoUiState,
|
||||
false,
|
||||
isPasskeyFirstUse))
|
||||
private set
|
||||
|
||||
val dialogResult: MutableSharedFlow<DialogResult> =
|
||||
MutableSharedFlow(replay = 0, extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
fun observeDialogResult(): SharedFlow<DialogResult> {
|
||||
return dialogResult
|
||||
}
|
||||
|
||||
fun onConfirmIntro() {
|
||||
uiState = CreateFlowUtils.toCreateCredentialUiState(
|
||||
providerEnableListUiState, providerDisableListUiState, defaultProviderId,
|
||||
requestDisplayInfoUiState, true, isPasskeyFirstUse)
|
||||
UserConfigRepo.getInstance().setIsPasskeyFirstUse(false)
|
||||
}
|
||||
|
||||
fun getProviderInfoByName(providerId: String): EnabledProviderInfo {
|
||||
return uiState.enabledProviders.single {
|
||||
it.id == providerId
|
||||
fun onConfirmIntro() {
|
||||
uiState = CreateFlowUtils.toCreateCredentialUiState(
|
||||
providerEnableListUiState, providerDisableListUiState, defaultProviderId,
|
||||
requestDisplayInfoUiState, true, isPasskeyFirstUse)
|
||||
UserConfigRepo.getInstance().setIsPasskeyFirstUse(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMoreOptionsSelectedOnProviderSelection() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
|
||||
isFromProviderSelection = true
|
||||
)
|
||||
}
|
||||
fun getProviderInfoByName(providerId: String): EnabledProviderInfo {
|
||||
return uiState.enabledProviders.single {
|
||||
it.id == providerId
|
||||
}
|
||||
}
|
||||
|
||||
fun onMoreOptionsSelectedOnCreationSelection() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
|
||||
isFromProviderSelection = false
|
||||
)
|
||||
}
|
||||
fun onMoreOptionsSelectedOnProviderSelection() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
|
||||
isFromProviderSelection = true
|
||||
)
|
||||
}
|
||||
|
||||
fun onBackProviderSelectionButtonSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.PROVIDER_SELECTION,
|
||||
)
|
||||
}
|
||||
fun onMoreOptionsSelectedOnCreationSelection() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
|
||||
isFromProviderSelection = false
|
||||
)
|
||||
}
|
||||
|
||||
fun onBackCreationSelectionButtonSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
|
||||
)
|
||||
}
|
||||
fun onBackProviderSelectionButtonSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.PROVIDER_SELECTION,
|
||||
)
|
||||
}
|
||||
|
||||
fun onBackPasskeyIntroButtonSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.PASSKEY_INTRO,
|
||||
)
|
||||
}
|
||||
fun onBackCreationSelectionButtonSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
|
||||
)
|
||||
}
|
||||
|
||||
fun onEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = if (
|
||||
activeEntry.activeProvider.id == UserConfigRepo.getInstance().getDefaultProviderId()
|
||||
) CreateScreenState.CREATION_OPTION_SELECTION else CreateScreenState.MORE_OPTIONS_ROW_INTRO,
|
||||
activeEntry = activeEntry
|
||||
)
|
||||
}
|
||||
fun onBackPasskeyIntroButtonSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.PASSKEY_INTRO,
|
||||
)
|
||||
}
|
||||
|
||||
fun onEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
|
||||
activeEntry = activeEntry
|
||||
)
|
||||
val providerId = uiState.activeEntry?.activeProvider?.id
|
||||
onDefaultChanged(providerId)
|
||||
}
|
||||
fun onEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState =
|
||||
if (activeEntry.activeProvider.id ==
|
||||
UserConfigRepo.getInstance().getDefaultProviderId())
|
||||
CreateScreenState.CREATION_OPTION_SELECTION
|
||||
else CreateScreenState.MORE_OPTIONS_ROW_INTRO,
|
||||
activeEntry = activeEntry
|
||||
)
|
||||
}
|
||||
|
||||
fun onDisabledProvidersSelected() {
|
||||
credManRepo.onSettingLaunchCancel()
|
||||
dialogResult.tryEmit(DialogResult(ResultState.LAUNCH_SETTING_CANCELED))
|
||||
}
|
||||
fun onEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
|
||||
activeEntry = activeEntry
|
||||
)
|
||||
val providerId = uiState.activeEntry?.activeProvider?.id
|
||||
onDefaultChanged(providerId)
|
||||
}
|
||||
|
||||
fun onCancel() {
|
||||
credManRepo.onUserCancel()
|
||||
dialogResult.tryEmit(DialogResult(ResultState.NORMAL_CANCELED))
|
||||
}
|
||||
fun onDisabledProvidersSelected() {
|
||||
credManRepo.onSettingLaunchCancel()
|
||||
uiState = uiState.copy(dialogState = DialogState.CANCELED_FOR_SETTINGS)
|
||||
}
|
||||
|
||||
fun onLearnMore() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO,
|
||||
)
|
||||
}
|
||||
fun onCancel() {
|
||||
credManRepo.onUserCancel()
|
||||
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
|
||||
}
|
||||
|
||||
fun onChangeDefaultSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
|
||||
)
|
||||
val providerId = uiState.activeEntry?.activeProvider?.id
|
||||
onDefaultChanged(providerId)
|
||||
}
|
||||
fun onLearnMore() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO,
|
||||
)
|
||||
}
|
||||
|
||||
fun onUseOnceSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
|
||||
)
|
||||
}
|
||||
fun onChangeDefaultSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
|
||||
)
|
||||
val providerId = uiState.activeEntry?.activeProvider?.id
|
||||
onDefaultChanged(providerId)
|
||||
}
|
||||
|
||||
fun onDefaultChanged(providerId: String?) {
|
||||
if (providerId != null) {
|
||||
Log.d(
|
||||
"Account Selector", "Default provider changed to: " +
|
||||
fun onUseOnceSelected() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
|
||||
)
|
||||
}
|
||||
|
||||
fun onDefaultChanged(providerId: String?) {
|
||||
if (providerId != null) {
|
||||
Log.d(
|
||||
"Account Selector", "Default provider changed to: " +
|
||||
" {provider=$providerId")
|
||||
UserConfigRepo.getInstance().setDefaultProvider(providerId)
|
||||
} else {
|
||||
Log.w("Account Selector", "Null provider is being changed")
|
||||
UserConfigRepo.getInstance().setDefaultProvider(providerId)
|
||||
} else {
|
||||
Log.w("Account Selector", "Null provider is being changed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onEntrySelected(selectedEntry: EntryInfo) {
|
||||
val providerId = selectedEntry.providerId
|
||||
val entryKey = selectedEntry.entryKey
|
||||
val entrySubkey = selectedEntry.entrySubkey
|
||||
Log.d(
|
||||
"Account Selector", "Option selected for entry: " +
|
||||
" {provider=$providerId, key=$entryKey, subkey=$entrySubkey")
|
||||
if (selectedEntry.pendingIntent != null) {
|
||||
uiState = uiState.copy(
|
||||
selectedEntry = selectedEntry,
|
||||
hidden = true,
|
||||
)
|
||||
} else {
|
||||
credManRepo.onOptionSelected(
|
||||
providerId,
|
||||
entryKey,
|
||||
entrySubkey
|
||||
)
|
||||
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
|
||||
fun onEntrySelected(selectedEntry: EntryInfo) {
|
||||
val providerId = selectedEntry.providerId
|
||||
val entryKey = selectedEntry.entryKey
|
||||
val entrySubkey = selectedEntry.entrySubkey
|
||||
Log.d(
|
||||
"Account Selector", "Option selected for entry: " +
|
||||
" {provider=$providerId, key=$entryKey, subkey=$entrySubkey")
|
||||
if (selectedEntry.pendingIntent != null) {
|
||||
uiState = uiState.copy(
|
||||
selectedEntry = selectedEntry,
|
||||
providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
|
||||
)
|
||||
} else {
|
||||
credManRepo.onOptionSelected(
|
||||
providerId,
|
||||
entryKey,
|
||||
entrySubkey
|
||||
)
|
||||
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launchProviderUi(
|
||||
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
|
||||
) {
|
||||
val entry = uiState.selectedEntry
|
||||
if (entry != null && entry.pendingIntent != null) {
|
||||
uiState = uiState.copy(
|
||||
providerActivityPending = true,
|
||||
)
|
||||
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
|
||||
.setFillInIntent(entry.fillInIntent).build()
|
||||
launcher.launch(intentSenderRequest)
|
||||
} else {
|
||||
Log.w("Account Selector", "No provider UI to launch")
|
||||
fun launchProviderUi(
|
||||
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
|
||||
) {
|
||||
val entry = uiState.selectedEntry
|
||||
if (entry != null && entry.pendingIntent != null) {
|
||||
uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING)
|
||||
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
|
||||
.setFillInIntent(entry.fillInIntent).build()
|
||||
launcher.launch(intentSenderRequest)
|
||||
} else {
|
||||
Log.w("Account Selector", "No provider UI to launch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfirmEntrySelected() {
|
||||
val selectedEntry = uiState.activeEntry?.activeEntryInfo
|
||||
if (selectedEntry != null) {
|
||||
onEntrySelected(selectedEntry)
|
||||
} else {
|
||||
Log.w("Account Selector",
|
||||
"Illegal state: confirm is pressed but activeEntry isn't set.")
|
||||
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
|
||||
fun onConfirmEntrySelected() {
|
||||
val selectedEntry = uiState.activeEntry?.activeEntryInfo
|
||||
if (selectedEntry != null) {
|
||||
onEntrySelected(selectedEntry)
|
||||
} else {
|
||||
Log.w("Account Selector",
|
||||
"Illegal state: confirm is pressed but activeEntry isn't set.")
|
||||
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
|
||||
val entry = uiState.selectedEntry
|
||||
val resultCode = providerActivityResult.resultCode
|
||||
val resultData = providerActivityResult.data
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
// Re-display the CredMan UI if the user canceled from the provider UI.
|
||||
uiState = uiState.copy(
|
||||
selectedEntry = null,
|
||||
hidden = false,
|
||||
providerActivityPending = false,
|
||||
)
|
||||
} else {
|
||||
if (entry != null) {
|
||||
val providerId = entry.providerId
|
||||
Log.d("Account Selector", "Got provider activity result: {provider=" +
|
||||
"$providerId, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
|
||||
"resultCode=$resultCode, resultData=$resultData}"
|
||||
)
|
||||
credManRepo.onOptionSelected(
|
||||
providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData,
|
||||
)
|
||||
} else {
|
||||
Log.w("Account Selector",
|
||||
"Illegal state: received a provider result but found no matching entry.")
|
||||
}
|
||||
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
|
||||
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
|
||||
val entry = uiState.selectedEntry
|
||||
val resultCode = providerActivityResult.resultCode
|
||||
val resultData = providerActivityResult.data
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
// Re-display the CredMan UI if the user canceled from the provider UI.
|
||||
Log.d("Account Selector", "The provider activity was cancelled," +
|
||||
" re-displaying our UI.")
|
||||
uiState = uiState.copy(
|
||||
selectedEntry = null,
|
||||
providerActivityState = ProviderActivityState.NOT_APPLICABLE,
|
||||
)
|
||||
} else {
|
||||
if (entry != null) {
|
||||
val providerId = entry.providerId
|
||||
Log.d("Account Selector", "Got provider activity result: {provider=" +
|
||||
"$providerId, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
|
||||
"resultCode=$resultCode, resultData=$resultData}"
|
||||
)
|
||||
credManRepo.onOptionSelected(
|
||||
providerId, entry.entryKey, entry.entrySubkey, resultCode, resultData,
|
||||
)
|
||||
} else {
|
||||
Log.w("Account Selector",
|
||||
"Illegal state: received a provider result but found no matching entry.")
|
||||
}
|
||||
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -26,251 +26,242 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.android.credentialmanager.CredentialManagerRepo
|
||||
import com.android.credentialmanager.common.DialogResult
|
||||
import com.android.credentialmanager.common.DialogState
|
||||
import com.android.credentialmanager.common.ProviderActivityResult
|
||||
import com.android.credentialmanager.common.ResultState
|
||||
import com.android.credentialmanager.common.ProviderActivityState
|
||||
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
|
||||
import com.android.internal.util.Preconditions
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
data class GetCredentialUiState(
|
||||
val providerInfoList: List<ProviderInfo>,
|
||||
val requestDisplayInfo: RequestDisplayInfo,
|
||||
val currentScreenState: GetScreenState = toGetScreenState(providerInfoList),
|
||||
val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
|
||||
val selectedEntry: EntryInfo? = null,
|
||||
val activeEntry: EntryInfo? = toActiveEntry(providerDisplayInfo),
|
||||
val hidden: Boolean = false,
|
||||
val providerActivityPending: Boolean = false,
|
||||
val isNoAccount: Boolean = false,
|
||||
val providerInfoList: List<ProviderInfo>,
|
||||
val requestDisplayInfo: RequestDisplayInfo,
|
||||
val currentScreenState: GetScreenState = toGetScreenState(providerInfoList),
|
||||
val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
|
||||
val selectedEntry: EntryInfo? = null,
|
||||
val activeEntry: EntryInfo? = toActiveEntry(providerDisplayInfo),
|
||||
val providerActivityState: ProviderActivityState =
|
||||
ProviderActivityState.NOT_APPLICABLE,
|
||||
val isNoAccount: Boolean = false,
|
||||
val dialogState: DialogState = DialogState.ACTIVE,
|
||||
)
|
||||
|
||||
class GetCredentialViewModel(private val credManRepo: CredentialManagerRepo) : ViewModel() {
|
||||
|
||||
var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState())
|
||||
private set
|
||||
var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState())
|
||||
private set
|
||||
|
||||
val dialogResult: MutableSharedFlow<DialogResult> =
|
||||
MutableSharedFlow(replay = 0, extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
fun observeDialogResult(): SharedFlow<DialogResult> {
|
||||
return dialogResult
|
||||
}
|
||||
|
||||
fun onEntrySelected(entry: EntryInfo) {
|
||||
Log.d("Account Selector", "credential selected:" +
|
||||
" {provider=${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
|
||||
if (entry.pendingIntent != null) {
|
||||
uiState = uiState.copy(
|
||||
selectedEntry = entry,
|
||||
hidden = true,
|
||||
)
|
||||
} else {
|
||||
credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey)
|
||||
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
|
||||
fun onEntrySelected(entry: EntryInfo) {
|
||||
Log.d("Account Selector", "credential selected: {provider=${entry.providerId}" +
|
||||
", key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
|
||||
if (entry.pendingIntent != null) {
|
||||
uiState = uiState.copy(
|
||||
selectedEntry = entry,
|
||||
providerActivityState = ProviderActivityState.READY_TO_LAUNCH,
|
||||
)
|
||||
} else {
|
||||
credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey)
|
||||
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfirmEntrySelected() {
|
||||
val activeEntry = uiState.activeEntry
|
||||
if (activeEntry != null) {
|
||||
onEntrySelected(activeEntry)
|
||||
} else {
|
||||
Log.w("Account Selector",
|
||||
"Illegal state: confirm is pressed but activeEntry isn't set.")
|
||||
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
|
||||
fun onConfirmEntrySelected() {
|
||||
val activeEntry = uiState.activeEntry
|
||||
if (activeEntry != null) {
|
||||
onEntrySelected(activeEntry)
|
||||
} else {
|
||||
Log.w("Account Selector",
|
||||
"Illegal state: confirm is pressed but activeEntry isn't set.")
|
||||
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launchProviderUi(
|
||||
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
|
||||
) {
|
||||
val entry = uiState.selectedEntry
|
||||
if (entry != null && entry.pendingIntent != null) {
|
||||
uiState = uiState.copy(
|
||||
providerActivityPending = true,
|
||||
)
|
||||
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
|
||||
.setFillInIntent(entry.fillInIntent).build()
|
||||
launcher.launch(intentSenderRequest)
|
||||
} else {
|
||||
Log.w("Account Selector", "No provider UI to launch")
|
||||
fun launchProviderUi(
|
||||
launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
|
||||
) {
|
||||
val entry = uiState.selectedEntry
|
||||
if (entry != null && entry.pendingIntent != null) {
|
||||
Log.d("credentials", "Launching provider activity")
|
||||
uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING)
|
||||
val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent)
|
||||
.setFillInIntent(entry.fillInIntent).build()
|
||||
launcher.launch(intentSenderRequest)
|
||||
} else {
|
||||
Log.w("Account Selector", "No provider UI to launch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
|
||||
val entry = uiState.selectedEntry
|
||||
val resultCode = providerActivityResult.resultCode
|
||||
val resultData = providerActivityResult.data
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
// Re-display the CredMan UI if the user canceled from the provider UI.
|
||||
uiState = uiState.copy(
|
||||
selectedEntry = null,
|
||||
hidden = false,
|
||||
providerActivityPending = false,
|
||||
)
|
||||
} else {
|
||||
if (entry != null) {
|
||||
Log.d("Account Selector", "Got provider activity result: {provider=" +
|
||||
"${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}, " +
|
||||
"resultCode=$resultCode, resultData=$resultData}"
|
||||
fun onProviderActivityResult(providerActivityResult: ProviderActivityResult) {
|
||||
val entry = uiState.selectedEntry
|
||||
val resultCode = providerActivityResult.resultCode
|
||||
val resultData = providerActivityResult.data
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
// Re-display the CredMan UI if the user canceled from the provider UI.
|
||||
Log.d("Account Selector", "The provider activity was cancelled," +
|
||||
" re-displaying our UI.")
|
||||
uiState = uiState.copy(
|
||||
selectedEntry = null,
|
||||
providerActivityState = ProviderActivityState.NOT_APPLICABLE,
|
||||
)
|
||||
} else {
|
||||
if (entry != null) {
|
||||
Log.d("Account Selector", "Got provider activity result: {provider=" +
|
||||
"${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}" +
|
||||
", resultCode=$resultCode, resultData=$resultData}"
|
||||
)
|
||||
credManRepo.onOptionSelected(
|
||||
entry.providerId, entry.entryKey, entry.entrySubkey,
|
||||
resultCode, resultData,
|
||||
)
|
||||
} else {
|
||||
Log.w("Account Selector",
|
||||
"Illegal state: received a provider result but found no matching entry.")
|
||||
}
|
||||
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMoreOptionSelected() {
|
||||
Log.d("Account Selector", "More Option selected")
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS
|
||||
)
|
||||
credManRepo.onOptionSelected(
|
||||
entry.providerId, entry.entryKey, entry.entrySubkey,
|
||||
resultCode, resultData,
|
||||
)
|
||||
} else {
|
||||
Log.w("Account Selector",
|
||||
"Illegal state: received a provider result but found no matching entry.")
|
||||
}
|
||||
dialogResult.tryEmit(DialogResult(ResultState.COMPLETE))
|
||||
}
|
||||
}
|
||||
|
||||
fun onMoreOptionSelected() {
|
||||
Log.d("Account Selector", "More Option selected")
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS
|
||||
)
|
||||
}
|
||||
fun onMoreOptionOnSnackBarSelected(isNoAccount: Boolean) {
|
||||
Log.d("Account Selector", "More Option on snackBar selected")
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS,
|
||||
isNoAccount = isNoAccount,
|
||||
)
|
||||
}
|
||||
|
||||
fun onMoreOptionOnSnackBarSelected(isNoAccount: Boolean) {
|
||||
Log.d("Account Selector", "More Option on snackBar selected")
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS,
|
||||
isNoAccount = isNoAccount,
|
||||
)
|
||||
}
|
||||
fun onBackToPrimarySelectionScreen() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = GetScreenState.PRIMARY_SELECTION
|
||||
)
|
||||
}
|
||||
|
||||
fun onBackToPrimarySelectionScreen() {
|
||||
uiState = uiState.copy(
|
||||
currentScreenState = GetScreenState.PRIMARY_SELECTION
|
||||
)
|
||||
}
|
||||
|
||||
fun onCancel() {
|
||||
credManRepo.onUserCancel()
|
||||
dialogResult.tryEmit(DialogResult(ResultState.NORMAL_CANCELED))
|
||||
}
|
||||
fun onCancel() {
|
||||
credManRepo.onUserCancel()
|
||||
uiState = uiState.copy(dialogState = DialogState.COMPLETE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toProviderDisplayInfo(
|
||||
providerInfoList: List<ProviderInfo>
|
||||
providerInfoList: List<ProviderInfo>
|
||||
): ProviderDisplayInfo {
|
||||
|
||||
val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
|
||||
val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
|
||||
val remoteEntryList = mutableListOf<RemoteEntryInfo>()
|
||||
providerInfoList.forEach { providerInfo ->
|
||||
if (providerInfo.authenticationEntry != null) {
|
||||
authenticationEntryList.add(providerInfo.authenticationEntry)
|
||||
}
|
||||
if (providerInfo.remoteEntry != null) {
|
||||
remoteEntryList.add(providerInfo.remoteEntry)
|
||||
}
|
||||
|
||||
providerInfo.credentialEntryList.forEach {
|
||||
userNameToCredentialEntryMap.compute(
|
||||
it.userName
|
||||
) {
|
||||
_, v ->
|
||||
if (v == null) {
|
||||
mutableListOf(it)
|
||||
} else {
|
||||
v.add(it)
|
||||
v
|
||||
val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
|
||||
val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
|
||||
val remoteEntryList = mutableListOf<RemoteEntryInfo>()
|
||||
providerInfoList.forEach { providerInfo ->
|
||||
if (providerInfo.authenticationEntry != null) {
|
||||
authenticationEntryList.add(providerInfo.authenticationEntry)
|
||||
}
|
||||
if (providerInfo.remoteEntry != null) {
|
||||
remoteEntryList.add(providerInfo.remoteEntry)
|
||||
}
|
||||
|
||||
providerInfo.credentialEntryList.forEach {
|
||||
userNameToCredentialEntryMap.compute(
|
||||
it.userName
|
||||
) { _, v ->
|
||||
if (v == null) {
|
||||
mutableListOf(it)
|
||||
} else {
|
||||
v.add(it)
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// There can only be at most one remote entry
|
||||
// TODO: fail elegantly
|
||||
Preconditions.checkState(remoteEntryList.size <= 1)
|
||||
// There can only be at most one remote entry
|
||||
// TODO: fail elegantly
|
||||
Preconditions.checkState(remoteEntryList.size <= 1)
|
||||
|
||||
// Compose sortedUserNameToCredentialEntryList
|
||||
val comparator = CredentialEntryInfoComparatorByTypeThenTimestamp()
|
||||
// Sort per username
|
||||
userNameToCredentialEntryMap.values.forEach {
|
||||
it.sortWith(comparator)
|
||||
}
|
||||
// Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
|
||||
val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
|
||||
PerUserNameCredentialEntryList(it.key, it.value)
|
||||
}.sortedWith(
|
||||
compareByDescending{ it.sortedCredentialEntryList.first().lastUsedTimeMillis }
|
||||
)
|
||||
// Compose sortedUserNameToCredentialEntryList
|
||||
val comparator = CredentialEntryInfoComparatorByTypeThenTimestamp()
|
||||
// Sort per username
|
||||
userNameToCredentialEntryMap.values.forEach {
|
||||
it.sortWith(comparator)
|
||||
}
|
||||
// Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
|
||||
val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
|
||||
PerUserNameCredentialEntryList(it.key, it.value)
|
||||
}.sortedWith(
|
||||
compareByDescending { it.sortedCredentialEntryList.first().lastUsedTimeMillis }
|
||||
)
|
||||
|
||||
return ProviderDisplayInfo(
|
||||
sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
|
||||
authenticationEntryList = authenticationEntryList,
|
||||
remoteEntry = remoteEntryList.getOrNull(0),
|
||||
)
|
||||
return ProviderDisplayInfo(
|
||||
sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
|
||||
authenticationEntryList = authenticationEntryList,
|
||||
remoteEntry = remoteEntryList.getOrNull(0),
|
||||
)
|
||||
}
|
||||
|
||||
private fun toActiveEntry(
|
||||
providerDisplayInfo: ProviderDisplayInfo,
|
||||
providerDisplayInfo: ProviderDisplayInfo,
|
||||
): EntryInfo? {
|
||||
val sortedUserNameToCredentialEntryList =
|
||||
providerDisplayInfo.sortedUserNameToCredentialEntryList
|
||||
val authenticationEntryList = providerDisplayInfo.authenticationEntryList
|
||||
var activeEntry: EntryInfo? = null
|
||||
if (sortedUserNameToCredentialEntryList
|
||||
.size == 1 && authenticationEntryList.isEmpty()
|
||||
) {
|
||||
activeEntry = sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList.first()
|
||||
} else if (
|
||||
sortedUserNameToCredentialEntryList
|
||||
.isEmpty() && authenticationEntryList.size == 1
|
||||
) {
|
||||
activeEntry = authenticationEntryList.first()
|
||||
}
|
||||
return activeEntry
|
||||
val sortedUserNameToCredentialEntryList =
|
||||
providerDisplayInfo.sortedUserNameToCredentialEntryList
|
||||
val authenticationEntryList = providerDisplayInfo.authenticationEntryList
|
||||
var activeEntry: EntryInfo? = null
|
||||
if (sortedUserNameToCredentialEntryList
|
||||
.size == 1 && authenticationEntryList.isEmpty()
|
||||
) {
|
||||
activeEntry = sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList.first()
|
||||
} else if (
|
||||
sortedUserNameToCredentialEntryList
|
||||
.isEmpty() && authenticationEntryList.size == 1
|
||||
) {
|
||||
activeEntry = authenticationEntryList.first()
|
||||
}
|
||||
return activeEntry
|
||||
}
|
||||
|
||||
private fun toGetScreenState(
|
||||
providerInfoList: List<ProviderInfo>
|
||||
providerInfoList: List<ProviderInfo>
|
||||
): GetScreenState {
|
||||
var noLocalAccount = true
|
||||
var remoteInfo: RemoteEntryInfo? = null
|
||||
providerInfoList.forEach{providerInfo -> if (
|
||||
providerInfo.credentialEntryList.isNotEmpty() || providerInfo.authenticationEntry != null
|
||||
) { noLocalAccount = false }
|
||||
// TODO: handle the error situation that if multiple remoteInfos exists
|
||||
if (providerInfo.remoteEntry != null) {
|
||||
remoteInfo = providerInfo.remoteEntry
|
||||
var noLocalAccount = true
|
||||
var remoteInfo: RemoteEntryInfo? = null
|
||||
providerInfoList.forEach { providerInfo ->
|
||||
if (providerInfo.credentialEntryList.isNotEmpty() ||
|
||||
providerInfo.authenticationEntry != null) {
|
||||
noLocalAccount = false
|
||||
}
|
||||
// TODO: handle the error situation that if multiple remoteInfos exists
|
||||
if (providerInfo.remoteEntry != null) {
|
||||
remoteInfo = providerInfo.remoteEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (noLocalAccount && remoteInfo != null)
|
||||
GetScreenState.REMOTE_ONLY else GetScreenState.PRIMARY_SELECTION
|
||||
return if (noLocalAccount && remoteInfo != null)
|
||||
GetScreenState.REMOTE_ONLY else GetScreenState.PRIMARY_SELECTION
|
||||
}
|
||||
|
||||
internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator<CredentialEntryInfo> {
|
||||
override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
|
||||
// First prefer passkey type for its security benefits
|
||||
if (p0.credentialType != p1.credentialType) {
|
||||
if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p0.credentialType) {
|
||||
return -1
|
||||
} else if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p1.credentialType) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
|
||||
// First prefer passkey type for its security benefits
|
||||
if (p0.credentialType != p1.credentialType) {
|
||||
if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p0.credentialType) {
|
||||
return -1
|
||||
} else if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p1.credentialType) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Then order by last used timestamp
|
||||
if (p0.lastUsedTimeMillis != null && p1.lastUsedTimeMillis != null) {
|
||||
if (p0.lastUsedTimeMillis < p1.lastUsedTimeMillis) {
|
||||
return 1
|
||||
} else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
|
||||
return -1
|
||||
}
|
||||
} else if (p0.lastUsedTimeMillis != null && p0.lastUsedTimeMillis > 0) {
|
||||
return -1
|
||||
} else if (p1.lastUsedTimeMillis != null && p1.lastUsedTimeMillis > 0) {
|
||||
return 1
|
||||
// Then order by last used timestamp
|
||||
if (p0.lastUsedTimeMillis != null && p1.lastUsedTimeMillis != null) {
|
||||
if (p0.lastUsedTimeMillis < p1.lastUsedTimeMillis) {
|
||||
return 1
|
||||
} else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
|
||||
return -1
|
||||
}
|
||||
} else if (p0.lastUsedTimeMillis != null && p0.lastUsedTimeMillis > 0) {
|
||||
return -1
|
||||
} else if (p1.lastUsedTimeMillis != null && p1.lastUsedTimeMillis > 0) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user