Merge "Fix the duplicate intent launching issue."
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