From ccb6f37c5de4e43c7e5be4580bc56bb2c2358e16 Mon Sep 17 00:00:00 2001 From: Helen Qin Date: Tue, 7 Feb 2023 07:34:28 +0000 Subject: [PATCH] Support onNewIntent(). This allows UI updates upon new request / entries. Bug: 265037946 Bug: 267669563 Test: manual Change-Id: I55ab941fe00e7b1e827a27d5b54eeafbe986c312 --- .../CredentialManagerRepo.kt | 66 +++- .../CredentialSelectorActivity.kt | 128 ++++--- .../CredentialSelectorViewModel.kt | 340 ++++++++++++++++++ .../credentialmanager/DataConverter.kt | 43 ++- .../credentialmanager/UserConfigRepo.kt | 12 - .../credentialmanager/common/BaseEntry.kt | 28 ++ .../createflow/CreateCredentialComponents.kt | 100 +++--- .../createflow/CreateCredentialViewModel.kt | 280 --------------- .../createflow/CreateModel.kt | 21 +- .../getflow/GetCredentialComponents.kt | 62 ++-- .../getflow/GetCredentialViewModel.kt | 284 --------------- .../credentialmanager/getflow/GetModel.kt | 145 +++++++- 12 files changed, 726 insertions(+), 783 deletions(-) create mode 100644 packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt create mode 100644 packages/CredentialManager/src/com/android/credentialmanager/common/BaseEntry.kt delete mode 100644 packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt delete mode 100644 packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 7b8175973faf6..8c502717a19bc 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -35,6 +35,7 @@ import android.credentials.ui.BaseDialogResult import android.credentials.ui.ProviderPendingIntentResponse import android.credentials.ui.UserSelectionDialogResult import android.net.Uri +import android.os.IBinder import android.os.Binder import android.os.Bundle import android.os.ResultReceiver @@ -52,14 +53,16 @@ import java.time.Instant class CredentialManagerRepo( private val context: Context, intent: Intent, + userConfigRepo: UserConfigRepo, ) { val requestInfo: RequestInfo private val providerEnabledList: List private val providerDisabledList: List? - // TODO: require non-null. val resultReceiver: ResultReceiver? + var initialUiState: UiState + init { requestInfo = intent.extras?.getParcelable( RequestInfo.EXTRA_REQUEST_INFO, @@ -93,6 +96,35 @@ class CredentialManagerRepo( Constants.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java ) + + initialUiState = when (requestInfo.type) { + RequestInfo.TYPE_CREATE -> { + val defaultProviderId = userConfigRepo.getDefaultProviderId() + val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse() + val providerEnableListUiState = getCreateProviderEnableListInitialUiState() + val providerDisableListUiState = getCreateProviderDisableListInitialUiState() + val requestDisplayInfoUiState = getCreateRequestDisplayInfoInitialUiState()!! + UiState( + createCredentialUiState = CreateFlowUtils.toCreateCredentialUiState( + providerEnableListUiState, + providerDisableListUiState, + defaultProviderId, + requestDisplayInfoUiState, + /** isOnPasskeyIntroStateAlready = */ false, + isPasskeyFirstUse)!!, + getCredentialUiState = null, + ) + } + RequestInfo.TYPE_GET -> UiState( + createCredentialUiState = null, + getCredentialUiState = getCredentialInitialUiState()!!, + ) + else -> throw IllegalStateException("Unrecognized request type: ${requestInfo.type}") + } + } + + fun initState(): UiState { + return initialUiState } // The dialog is canceled by the user. @@ -110,9 +142,7 @@ class CredentialManagerRepo( } fun onCancel(cancelCode: Int) { - val resultData = Bundle() - BaseDialogResult.addToBundle(BaseDialogResult(requestInfo.token), resultData) - resultReceiver?.send(cancelCode, resultData) + sendCancellationCode(cancelCode, requestInfo.token, resultReceiver) } fun onOptionSelected( @@ -129,15 +159,15 @@ class CredentialManagerRepo( entrySubkey, if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null ) - val resultData = Bundle() - UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultData) + val resultDataBundle = Bundle() + UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle) resultReceiver?.send( BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, - resultData + resultDataBundle ) } - fun getCredentialInitialUiState(): GetCredentialUiState? { + private fun getCredentialInitialUiState(): GetCredentialUiState? { val providerEnabledList = GetFlowUtils.toProviderList( // TODO: handle runtime cast error providerEnabledList as List, context @@ -149,7 +179,7 @@ class CredentialManagerRepo( ) } - fun getCreateProviderEnableListInitialUiState(): List { + private fun getCreateProviderEnableListInitialUiState(): List { val providerEnabledList = CreateFlowUtils.toEnabledProviderList( // Handle runtime cast error providerEnabledList as List, context @@ -157,17 +187,31 @@ class CredentialManagerRepo( return providerEnabledList } - fun getCreateProviderDisableListInitialUiState(): List { + private fun getCreateProviderDisableListInitialUiState(): List { return CreateFlowUtils.toDisabledProviderList( // Handle runtime cast error providerDisabledList, context ) } - fun getCreateRequestDisplayInfoInitialUiState(): RequestDisplayInfo? { + private fun getCreateRequestDisplayInfoInitialUiState(): RequestDisplayInfo? { return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context) } + companion object { + fun sendCancellationCode( + cancelCode: Int, + requestToken: IBinder?, + resultReceiver: ResultReceiver? + ) { + if (requestToken != null && resultReceiver != null) { + val resultData = Bundle() + BaseDialogResult.addToBundle(BaseDialogResult(requestToken), resultData) + resultReceiver.send(cancelCode, resultData) + } + } + } + // TODO: below are prototype functionalities. To be removed for productionization. private fun testCreateCredentialEnabledProviderList(): List { return listOf( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 3b9c02adde841..0802afe9a2d28 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -17,8 +17,10 @@ package com.android.credentialmanager import android.content.Intent +import android.credentials.ui.BaseDialogResult import android.credentials.ui.RequestInfo import android.os.Bundle +import android.os.ResultReceiver import android.provider.Settings import android.util.Log import androidx.activity.ComponentActivity @@ -28,101 +30,74 @@ 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.viewmodel.compose.viewModel import com.android.credentialmanager.common.Constants import com.android.credentialmanager.common.DialogState import com.android.credentialmanager.common.ProviderActivityResult 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 @ExperimentalMaterialApi class CredentialSelectorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val credManRepo = CredentialManagerRepo(this, intent) - UserConfigRepo.setup(this) + Log.d(Constants.LOG_TAG, "Creating new CredentialSelectorActivity") + init(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + Log.d(Constants.LOG_TAG, "Existing activity received new intent") + init(intent) + } + + fun init(intent: Intent) { try { + val userConfigRepo = UserConfigRepo(this) + val credManRepo = CredentialManagerRepo(this, intent, userConfigRepo) setContent { CredentialSelectorTheme { - CredentialManagerBottomSheet(credManRepo.requestInfo.type, credManRepo) + CredentialManagerBottomSheet( + credManRepo, + userConfigRepo + ) } } } catch (e: Exception) { - Log.e(Constants.LOG_TAG, "Failed to show the credential selector", e) - reportInstantiationErrorAndFinishActivity(credManRepo) + onInitializationError(e, intent) } } @ExperimentalMaterialApi @Composable - fun CredentialManagerBottomSheet(requestType: String, credManRepo: CredentialManagerRepo) { - val providerActivityResult = remember { mutableStateOf(null) } + fun CredentialManagerBottomSheet( + credManRepo: CredentialManagerRepo, + userConfigRepo: UserConfigRepo + ) { + val viewModel: CredentialSelectorViewModel = viewModel { + CredentialSelectorViewModel(credManRepo, userConfigRepo) + } val launcher = rememberLauncherForActivityResult( ActivityResultContracts.StartIntentSenderForResult() ) { - providerActivityResult.value = ProviderActivityResult(it.resultCode, it.data) + viewModel.onProviderActivityResult(ProviderActivityResult(it.resultCode, it.data)) } - when (requestType) { - RequestInfo.TYPE_CREATE -> { - val viewModel: CreateCredentialViewModel = viewModel { - val vm = CreateCredentialViewModel.newInstance( - credManRepo = credManRepo, - providerEnableListUiState = - credManRepo.getCreateProviderEnableListInitialUiState(), - providerDisableListUiState = - credManRepo.getCreateProviderDisableListInitialUiState(), - requestDisplayInfoUiState = - credManRepo.getCreateRequestDisplayInfoInitialUiState() - ) - if (vm == null) { - // Input parsing failed. Close the activity. - reportInstantiationErrorAndFinishActivity(credManRepo) - throw IllegalStateException() - } else { - vm - } - } - 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 { - val initialUiState = credManRepo.getCredentialInitialUiState() - if (initialUiState == null) { - // Input parsing failed. Close the activity. - reportInstantiationErrorAndFinishActivity(credManRepo) - throw IllegalStateException() - } else { - GetCredentialViewModel(credManRepo, initialUiState) - } - } - LaunchedEffect(viewModel.uiState.dialogState) { - handleDialogState(viewModel.uiState.dialogState) - } - providerActivityResult.value?.let { - viewModel.onProviderActivityResult(it) - providerActivityResult.value = null - } - GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher) - } - else -> { - Log.d(Constants.LOG_TAG, "Unknown type, not rendering any UI") - reportInstantiationErrorAndFinishActivity(credManRepo) - } + LaunchedEffect(viewModel.uiState.dialogState) { + handleDialogState(viewModel.uiState.dialogState) + } + + if (viewModel.uiState.createCredentialUiState != null) { + CreateCredentialScreen( + viewModel = viewModel, + providerActivityLauncher = launcher + ) + } else if (viewModel.uiState.getCredentialUiState != null) { + GetCredentialScreen(viewModel = viewModel, providerActivityLauncher = launcher) + } else { + Log.d(Constants.LOG_TAG, "UI wasn't able to render neither get nor create flow") + reportInstantiationErrorAndFinishActivity(credManRepo) } } @@ -142,4 +117,21 @@ class CredentialSelectorActivity : ComponentActivity() { this@CredentialSelectorActivity.finish() } } + + private fun onInitializationError(e: Exception, intent: Intent) { + Log.e(Constants.LOG_TAG, "Failed to show the credential selector", e) + val resultReceiver = intent.getParcelableExtra( + android.credentials.ui.Constants.EXTRA_RESULT_RECEIVER, + ResultReceiver::class.java + ) + val requestInfo = intent.extras?.getParcelable( + RequestInfo.EXTRA_REQUEST_INFO, + RequestInfo::class.java + ) + CredentialManagerRepo.sendCancellationCode( + BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE, + requestInfo?.token, resultReceiver + ) + this.finish() + } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt new file mode 100644 index 0000000000000..47ea53b00a39c --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -0,0 +1,340 @@ +/* + * 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 + +import android.app.Activity +import android.util.Log +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.result.ActivityResult +import androidx.activity.result.IntentSenderRequest +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import com.android.credentialmanager.common.BaseEntry +import com.android.credentialmanager.common.Constants +import com.android.credentialmanager.common.DialogState +import com.android.credentialmanager.common.ProviderActivityResult +import com.android.credentialmanager.common.ProviderActivityState +import com.android.credentialmanager.createflow.ActiveEntry +import com.android.credentialmanager.createflow.CreateCredentialUiState +import com.android.credentialmanager.createflow.CreateScreenState +import com.android.credentialmanager.getflow.GetCredentialUiState +import com.android.credentialmanager.getflow.GetScreenState + +/** One and only one of create or get state can be active at any given time. */ +data class UiState( + val createCredentialUiState: CreateCredentialUiState?, + val getCredentialUiState: GetCredentialUiState?, + val selectedEntry: BaseEntry? = null, + val providerActivityState: ProviderActivityState = ProviderActivityState.NOT_APPLICABLE, + val dialogState: DialogState = DialogState.ACTIVE, +) + +class CredentialSelectorViewModel( + private var credManRepo: CredentialManagerRepo, + private val userConfigRepo: UserConfigRepo, +) : ViewModel() { + var uiState by mutableStateOf(credManRepo.initState()) + private set + + /**************************************************************************/ + /***** Shared Callbacks *****/ + /**************************************************************************/ + fun onCancel() { + credManRepo.onUserCancel() + uiState = uiState.copy(dialogState = DialogState.COMPLETE) + } + + fun onNewCredentialManagerRepo(credManRepo: CredentialManagerRepo) { + this.credManRepo = credManRepo + uiState = credManRepo.initState() + } + + fun launchProviderUi( + launcher: ManagedActivityResultLauncher + ) { + val entry = uiState.selectedEntry + if (entry != null && entry.pendingIntent != null) { + Log.d(Constants.LOG_TAG, "Launching provider activity") + uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING) + val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent) + .setFillInIntent(entry.fillInIntent).build() + launcher.launch(intentSenderRequest) + } else { + Log.d(Constants.LOG_TAG, "No provider UI to launch") + onInternalError() + } + } + + 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(Constants.LOG_TAG, "The provider activity was cancelled," + + " re-displaying our UI.") + uiState = uiState.copy( + selectedEntry = null, + providerActivityState = ProviderActivityState.NOT_APPLICABLE, + ) + } else { + if (entry != null) { + Log.d( + Constants.LOG_TAG, "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, + ) + uiState = uiState.copy(dialogState = DialogState.COMPLETE) + } else { + Log.w(Constants.LOG_TAG, + "Illegal state: received a provider result but found no matching entry.") + onInternalError() + } + } + } + + private fun onInternalError() { + Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state") + credManRepo.onParsingFailureCancel() + uiState = uiState.copy(dialogState = DialogState.COMPLETE) + } + + /**************************************************************************/ + /***** Get Flow Callbacks *****/ + /**************************************************************************/ + fun getFlowOnEntrySelected(entry: BaseEntry) { + Log.d(Constants.LOG_TAG, "credential selected: {provider=${entry.providerId}" + + ", key=${entry.entryKey}, subkey=${entry.entrySubkey}}") + uiState = if (entry.pendingIntent != null) { + uiState.copy( + selectedEntry = entry, + providerActivityState = ProviderActivityState.READY_TO_LAUNCH, + ) + } else { + credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey) + uiState.copy(dialogState = DialogState.COMPLETE) + } + } + + fun getFlowOnConfirmEntrySelected() { + val activeEntry = uiState.getCredentialUiState?.activeEntry + if (activeEntry != null) { + getFlowOnEntrySelected(activeEntry) + } else { + Log.d(Constants.LOG_TAG, + "Illegal state: confirm is pressed but activeEntry isn't set.") + onInternalError() + } + } + + fun getFlowOnMoreOptionSelected() { + Log.d(Constants.LOG_TAG, "More Option selected") + uiState = uiState.copy( + getCredentialUiState = uiState.getCredentialUiState?.copy( + currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS + ) + ) + } + + fun getFlowOnMoreOptionOnSnackBarSelected(isNoAccount: Boolean) { + Log.d(Constants.LOG_TAG, "More Option on snackBar selected") + uiState = uiState.copy( + getCredentialUiState = uiState.getCredentialUiState?.copy( + currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS, + isNoAccount = isNoAccount, + ) + ) + } + + fun getFlowOnBackToPrimarySelectionScreen() { + uiState = uiState.copy( + getCredentialUiState = uiState.getCredentialUiState?.copy( + currentScreenState = GetScreenState.PRIMARY_SELECTION + ) + ) + } + + /**************************************************************************/ + /***** Create Flow Callbacks *****/ + /**************************************************************************/ + fun createFlowOnConfirmIntro() { + val prevUiState = uiState.createCredentialUiState + if (prevUiState == null) { + Log.d(Constants.LOG_TAG, "Encountered unexpected null create ui state") + onInternalError() + return + } + val newUiState = CreateFlowUtils.toCreateCredentialUiState( + prevUiState.enabledProviders, prevUiState.disabledProviders, + userConfigRepo.getDefaultProviderId(), prevUiState.requestDisplayInfo, true, + userConfigRepo.getIsPasskeyFirstUse()) + if (newUiState == null) { + Log.d(Constants.LOG_TAG, "Unable to update create ui state") + onInternalError() + return + } + uiState = uiState.copy(createCredentialUiState = newUiState) + userConfigRepo.setIsPasskeyFirstUse(false) + } + + fun createFlowOnMoreOptionsSelectedOnProviderSelection() { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, + isFromProviderSelection = true + ) + ) + } + + fun createFlowOnMoreOptionsSelectedOnCreationSelection() { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, + isFromProviderSelection = false + ) + ) + } + + fun createFlowOnBackProviderSelectionButtonSelected() { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.PROVIDER_SELECTION, + ) + ) + } + + fun createFlowOnBackCreationSelectionButtonSelected() { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + ) + ) + } + + fun createFlowOnBackPasskeyIntroButtonSelected() { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.PASSKEY_INTRO, + ) + ) + } + + fun createFlowOnEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = + if (activeEntry.activeProvider.id == + userConfigRepo.getDefaultProviderId()) + CreateScreenState.CREATION_OPTION_SELECTION + else CreateScreenState.MORE_OPTIONS_ROW_INTRO, + activeEntry = activeEntry + ) + ) + } + + fun createFlowOnEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + activeEntry = activeEntry + ) + ) + val providerId = uiState.createCredentialUiState?.activeEntry?.activeProvider?.id + createFlowOnDefaultChanged(providerId) + } + + fun createFlowOnDisabledProvidersSelected() { + credManRepo.onSettingLaunchCancel() + uiState = uiState.copy(dialogState = DialogState.CANCELED_FOR_SETTINGS) + } + + fun createFlowOnLearnMore() { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO, + ) + ) + } + + fun createFlowOnChangeDefaultSelected() { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + ) + ) + val providerId = uiState.createCredentialUiState?.activeEntry?.activeProvider?.id + createFlowOnDefaultChanged(providerId) + } + + fun createFlowOnUseOnceSelected() { + uiState = uiState.copy( + createCredentialUiState = uiState.createCredentialUiState?.copy( + currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, + ) + ) + } + + fun createFlowOnDefaultChanged(providerId: String?) { + if (providerId != null) { + Log.d( + Constants.LOG_TAG, "Default provider changed to: " + + " {provider=$providerId") + userConfigRepo.setDefaultProvider(providerId) + } else { + Log.w(Constants.LOG_TAG, "Null provider is being changed") + } + } + + fun createFlowOnEntrySelected(selectedEntry: BaseEntry) { + val providerId = selectedEntry.providerId + val entryKey = selectedEntry.entryKey + val entrySubkey = selectedEntry.entrySubkey + Log.d( + Constants.LOG_TAG, "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 createFlowOnConfirmEntrySelected() { + val selectedEntry = uiState.createCredentialUiState?.activeEntry?.activeEntryInfo + if (selectedEntry != null) { + createFlowOnEntrySelected(selectedEntry) + } else { + Log.d(Constants.LOG_TAG, + "Unexpected: confirm is pressed but no active entry exists.") + onInternalError() + } + } +} \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index df5bd046d5ac8..167b956e92920 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -148,10 +148,10 @@ class GetFlowUtils { it.providerFlattenedComponentName, it.credentialEntries, context ), authenticationEntryList = getAuthenticationEntryList( - it.providerFlattenedComponentName, - providerLabel, - providerIcon, - it.authenticationEntries), + it.providerFlattenedComponentName, + providerLabel, + providerIcon, + it.authenticationEntries), remoteEntry = getRemoteEntry( it.providerFlattenedComponentName, it.remoteEntry @@ -261,21 +261,21 @@ class GetFlowUtils { providerIcon: Drawable, authEntryList: List, ): List { - if (authEntryList.isEmpty()) { - return listOf() - } - val authEntry = authEntryList[0] - val structuredAuthEntry = - AuthenticationAction.fromSlice(authEntry.slice) ?: return listOf() - return listOf(AuthenticationEntryInfo( - providerId = providerId, - entryKey = authEntry.key, - entrySubkey = authEntry.subkey, - pendingIntent = structuredAuthEntry.pendingIntent, - fillInIntent = authEntry.frameworkExtrasIntent, - title = providerDisplayName, - icon = providerIcon, - )) + val result: MutableList = mutableListOf() + authEntryList.forEach { + val structuredAuthEntry = + AuthenticationAction.fromSlice(it.slice) ?: return@forEach + result.add(AuthenticationEntryInfo( + providerId = providerId, + entryKey = it.key, + entrySubkey = it.subkey, + pendingIntent = structuredAuthEntry.pendingIntent, + fillInIntent = it.frameworkExtrasIntent, + title = providerDisplayName, + icon = providerIcon, + )) + } + return result } private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? { @@ -459,10 +459,7 @@ class CreateFlowUtils { /*requestDisplayInfo=*/requestDisplayInfo, /*defaultProvider=*/defaultProvider, /*remoteEntry=*/remoteEntry, /*isPasskeyFirstUse=*/isPasskeyFirstUse - ) - if (initialScreenState == null) { - return null - } + ) ?: return null return CreateCredentialUiState( enabledProviders = enabledProviders, disabledProviders = disabledProviders, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt index 021dcab2a1d5c..a17f2c88abcd4 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt @@ -50,21 +50,9 @@ class UserConfigRepo(context: Context) { } companion object { - lateinit var repo: UserConfigRepo - const val DEFAULT_PROVIDER = "default_provider" // This first use value only applies to passkeys, not related with if generally // credential manager is first use or not const val IS_PASSKEY_FIRST_USE = "is_passkey_first_use" - - fun setup( - context: Context, - ) { - repo = UserConfigRepo(context) - } - - fun getInstance(): UserConfigRepo { - return repo - } } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BaseEntry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BaseEntry.kt new file mode 100644 index 0000000000000..4b8bc97d96595 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BaseEntry.kt @@ -0,0 +1,28 @@ +/* + * 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 + +import android.app.PendingIntent +import android.content.Intent + +open class BaseEntry ( + val providerId: String, + val entryKey: String, + val entrySubkey: String, + val pendingIntent: PendingIntent?, + val fillInIntent: Intent?, +) \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 216428c3c10c3..adb54676223b3 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -42,7 +42,9 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap +import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.R +import com.android.credentialmanager.common.BaseEntry import com.android.credentialmanager.common.CredentialType import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.common.ui.ActionButton @@ -59,83 +61,89 @@ import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun CreateCredentialScreen( - viewModel: CreateCredentialViewModel, + viewModel: CredentialSelectorViewModel, providerActivityLauncher: ManagedActivityResultLauncher ) { + val createCredentialUiState = viewModel.uiState.createCredentialUiState ?: return ModalBottomSheet( sheetContent = { - val uiState = viewModel.uiState // 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) { + when (viewModel.uiState.providerActivityState) { ProviderActivityState.NOT_APPLICABLE -> { - when (uiState.currentScreenState) { + when (createCredentialUiState.currentScreenState) { CreateScreenState.PASSKEY_INTRO -> ConfirmationCard( - onConfirm = viewModel::onConfirmIntro, - onLearnMore = viewModel::onLearnMore, + onConfirm = viewModel::createFlowOnConfirmIntro, + onLearnMore = viewModel::createFlowOnLearnMore, ) CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard( - requestDisplayInfo = uiState.requestDisplayInfo, - enabledProviderList = uiState.enabledProviders, - disabledProviderList = uiState.disabledProviders, - sortedCreateOptionsPairs = uiState.sortedCreateOptionsPairs, - onOptionSelected = viewModel::onEntrySelectedFromFirstUseScreen, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + enabledProviderList = createCredentialUiState.enabledProviders, + disabledProviderList = createCredentialUiState.disabledProviders, + sortedCreateOptionsPairs = + createCredentialUiState.sortedCreateOptionsPairs, + onOptionSelected = + viewModel::createFlowOnEntrySelectedFromFirstUseScreen, onDisabledProvidersSelected = - viewModel::onDisabledProvidersSelected, + viewModel::createFlowOnDisabledProvidersSelected, onMoreOptionsSelected = - viewModel::onMoreOptionsSelectedOnProviderSelection, + viewModel::createFlowOnMoreOptionsSelectedOnProviderSelection, ) CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard( - requestDisplayInfo = uiState.requestDisplayInfo, - enabledProviderList = uiState.enabledProviders, - providerInfo = uiState.activeEntry?.activeProvider!!, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + enabledProviderList = createCredentialUiState.enabledProviders, + providerInfo = createCredentialUiState.activeEntry?.activeProvider!!, createOptionInfo = - uiState.activeEntry.activeEntryInfo as CreateOptionInfo, - onOptionSelected = viewModel::onEntrySelected, - onConfirm = viewModel::onConfirmEntrySelected, + createCredentialUiState.activeEntry.activeEntryInfo + as CreateOptionInfo, + onOptionSelected = viewModel::createFlowOnEntrySelected, + onConfirm = viewModel::createFlowOnConfirmEntrySelected, onMoreOptionsSelected = - viewModel::onMoreOptionsSelectedOnCreationSelection, + viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection, ) CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( - requestDisplayInfo = uiState.requestDisplayInfo, - enabledProviderList = uiState.enabledProviders, - disabledProviderList = uiState.disabledProviders, - sortedCreateOptionsPairs = uiState.sortedCreateOptionsPairs, - hasDefaultProvider = uiState.hasDefaultProvider, - isFromProviderSelection = uiState.isFromProviderSelection!!, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + enabledProviderList = createCredentialUiState.enabledProviders, + disabledProviderList = createCredentialUiState.disabledProviders, + sortedCreateOptionsPairs = + createCredentialUiState.sortedCreateOptionsPairs, + hasDefaultProvider = createCredentialUiState.hasDefaultProvider, + isFromProviderSelection = + createCredentialUiState.isFromProviderSelection!!, onBackProviderSelectionButtonSelected = - viewModel::onBackProviderSelectionButtonSelected, + viewModel::createFlowOnBackProviderSelectionButtonSelected, onBackCreationSelectionButtonSelected = - viewModel::onBackCreationSelectionButtonSelected, + viewModel::createFlowOnBackCreationSelectionButtonSelected, onOptionSelected = - viewModel::onEntrySelectedFromMoreOptionScreen, + viewModel::createFlowOnEntrySelectedFromMoreOptionScreen, onDisabledProvidersSelected = - viewModel::onDisabledProvidersSelected, - onRemoteEntrySelected = viewModel::onEntrySelected, + viewModel::createFlowOnDisabledProvidersSelected, + onRemoteEntrySelected = viewModel::createFlowOnEntrySelected, ) CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard( - providerInfo = uiState.activeEntry?.activeProvider!!, - onChangeDefaultSelected = viewModel::onChangeDefaultSelected, - onUseOnceSelected = viewModel::onUseOnceSelected, + providerInfo = createCredentialUiState.activeEntry?.activeProvider!!, + onChangeDefaultSelected = viewModel::createFlowOnChangeDefaultSelected, + onUseOnceSelected = viewModel::createFlowOnUseOnceSelected, ) CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard( - requestDisplayInfo = uiState.requestDisplayInfo, - activeRemoteEntry = uiState.activeEntry?.activeEntryInfo!!, - onOptionSelected = viewModel::onEntrySelected, - onConfirm = viewModel::onConfirmEntrySelected, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + activeRemoteEntry = + createCredentialUiState.activeEntry?.activeEntryInfo!!, + onOptionSelected = viewModel::createFlowOnEntrySelected, + onConfirm = viewModel::createFlowOnConfirmEntrySelected, ) CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO -> MoreAboutPasskeysIntroCard( onBackPasskeyIntroButtonSelected = - viewModel::onBackPasskeyIntroButtonSelected, + viewModel::createFlowOnBackPasskeyIntroButtonSelected, ) } } ProviderActivityState.READY_TO_LAUNCH -> { // Launch only once per providerActivityState change so that the provider // UI will not be accidentally launched twice. - LaunchedEffect(uiState.providerActivityState) { + LaunchedEffect(viewModel.uiState.providerActivityState) { viewModel.launchProviderUi(providerActivityLauncher) } } @@ -379,7 +387,7 @@ fun MoreOptionsSelectionCard( onBackCreationSelectionButtonSelected: () -> Unit, onOptionSelected: (ActiveEntry) -> Unit, onDisabledProvidersSelected: () -> Unit, - onRemoteEntrySelected: (EntryInfo) -> Unit, + onRemoteEntrySelected: (BaseEntry) -> Unit, ) { ContainerCard() { Column() { @@ -538,7 +546,7 @@ fun CreationSelectionCard( enabledProviderList: List, providerInfo: EnabledProviderInfo, createOptionInfo: CreateOptionInfo, - onOptionSelected: (EntryInfo) -> Unit, + onOptionSelected: (BaseEntry) -> Unit, onConfirm: () -> Unit, onMoreOptionsSelected: () -> Unit, ) { @@ -647,8 +655,8 @@ fun CreationSelectionCard( @Composable fun ExternalOnlySelectionCard( requestDisplayInfo: RequestDisplayInfo, - activeRemoteEntry: EntryInfo, - onOptionSelected: (EntryInfo) -> Unit, + activeRemoteEntry: BaseEntry, + onOptionSelected: (BaseEntry) -> Unit, onConfirm: () -> Unit, ) { ContainerCard() { @@ -795,8 +803,8 @@ fun MoreAboutPasskeysIntroCard( @Composable fun PrimaryCreateOptionRow( requestDisplayInfo: RequestDisplayInfo, - entryInfo: EntryInfo, - onOptionSelected: (EntryInfo) -> Unit + entryInfo: BaseEntry, + onOptionSelected: (BaseEntry) -> Unit ) { Entry( onClick = { onOptionSelected(entryInfo) }, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt deleted file mode 100644 index 01318b1edcccc..0000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (C) 2022 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.createflow - -import android.app.Activity -import android.util.Log -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.result.ActivityResult -import androidx.activity.result.IntentSenderRequest -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import com.android.credentialmanager.common.Constants -import com.android.credentialmanager.CreateFlowUtils -import com.android.credentialmanager.CredentialManagerRepo -import com.android.credentialmanager.UserConfigRepo -import com.android.credentialmanager.common.DialogState -import com.android.credentialmanager.common.ProviderActivityResult -import com.android.credentialmanager.common.ProviderActivityState - - -class CreateCredentialViewModel( - private val credManRepo: CredentialManagerRepo, - private val providerEnableListUiState: List, - private val providerDisableListUiState: List, - private val requestDisplayInfoUiState: RequestDisplayInfo, - userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance(), -) : ViewModel() { - - val defaultProviderId = userConfigRepo.getDefaultProviderId() - val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse() - - var uiState by mutableStateOf( - CreateFlowUtils.toCreateCredentialUiState( - providerEnableListUiState, - providerDisableListUiState, - defaultProviderId, - requestDisplayInfoUiState, - false, - isPasskeyFirstUse)!!) - private set - - fun onConfirmIntro() { - val newUiState = CreateFlowUtils.toCreateCredentialUiState( - providerEnableListUiState, providerDisableListUiState, defaultProviderId, - requestDisplayInfoUiState, true, isPasskeyFirstUse) - if (newUiState == null) { - onInternalError() - return - } - uiState = newUiState - UserConfigRepo.getInstance().setIsPasskeyFirstUse(false) - } - - fun getProviderInfoByName(providerId: String): EnabledProviderInfo { - return uiState.enabledProviders.single { - it.id == providerId - } - } - - fun onMoreOptionsSelectedOnProviderSelection() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, - isFromProviderSelection = true - ) - } - - fun onMoreOptionsSelectedOnCreationSelection() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION, - isFromProviderSelection = false - ) - } - - fun onBackProviderSelectionButtonSelected() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.PROVIDER_SELECTION, - ) - } - - fun onBackCreationSelectionButtonSelected() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - ) - } - - fun onBackPasskeyIntroButtonSelected() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.PASSKEY_INTRO, - ) - } - - 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 onEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) { - uiState = uiState.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - activeEntry = activeEntry - ) - val providerId = uiState.activeEntry?.activeProvider?.id - onDefaultChanged(providerId) - } - - fun onDisabledProvidersSelected() { - credManRepo.onSettingLaunchCancel() - uiState = uiState.copy(dialogState = DialogState.CANCELED_FOR_SETTINGS) - } - - // When the view model runs into unexpected illegal state, reports the error back and close - // the activity gracefully. - private fun onInternalError() { - Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state") - credManRepo.onParsingFailureCancel() - uiState = uiState.copy(dialogState = DialogState.COMPLETE) - } - - fun onCancel() { - credManRepo.onUserCancel() - uiState = uiState.copy(dialogState = DialogState.COMPLETE) - } - - fun onLearnMore() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.MORE_ABOUT_PASSKEYS_INTRO, - ) - } - - fun onChangeDefaultSelected() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - ) - val providerId = uiState.activeEntry?.activeProvider?.id - onDefaultChanged(providerId) - } - - fun onUseOnceSelected() { - uiState = uiState.copy( - currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - ) - } - - fun onDefaultChanged(providerId: String?) { - if (providerId != null) { - Log.d( - Constants.LOG_TAG, "Default provider changed to: " + - " {provider=$providerId") - UserConfigRepo.getInstance().setDefaultProvider(providerId) - } else { - Log.w(Constants.LOG_TAG, "Null provider is being changed") - } - } - - fun onEntrySelected(selectedEntry: EntryInfo) { - val providerId = selectedEntry.providerId - val entryKey = selectedEntry.entryKey - val entrySubkey = selectedEntry.entrySubkey - Log.d( - Constants.LOG_TAG, "Option selected for entry: " + - " {provider=$providerId, key=$entryKey, subkey=$entrySubkey") - if (selectedEntry.pendingIntent != null) { - uiState = uiState.copy( - selectedEntry = selectedEntry, - providerActivityState = ProviderActivityState.READY_TO_LAUNCH, - ) - } else { - credManRepo.onOptionSelected( - providerId, - entryKey, - entrySubkey - ) - uiState = uiState.copy(dialogState = DialogState.COMPLETE) - } - } - - fun launchProviderUi( - launcher: ManagedActivityResultLauncher - ) { - val entry = uiState.selectedEntry - if (entry != null && entry.pendingIntent != null) { - uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING) - val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent) - .setFillInIntent(entry.fillInIntent).build() - launcher.launch(intentSenderRequest) - } else { - Log.d(Constants.LOG_TAG, "Unexpected: no provider UI to launch") - onInternalError() - } - } - - fun onConfirmEntrySelected() { - val selectedEntry = uiState.activeEntry?.activeEntryInfo - if (selectedEntry != null) { - onEntrySelected(selectedEntry) - } else { - Log.d(Constants.LOG_TAG, - "Unexpected: confirm is pressed but no active entry exists.") - onInternalError() - } - } - - 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(Constants.LOG_TAG, "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(Constants.LOG_TAG, "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, - ) - uiState = uiState.copy(dialogState = DialogState.COMPLETE) - } else { - Log.d(Constants.LOG_TAG, - "Illegal state: received a provider result but found no matching entry.") - onInternalError() - } - } - } - - companion object Factory { - // Validates the input and returns null if the input is invalid. - fun newInstance( - credManRepo: CredentialManagerRepo, - providerEnableListUiState: List, - providerDisableListUiState: List, - requestDisplayInfoUiState: RequestDisplayInfo?, - ): CreateCredentialViewModel? { - if (providerEnableListUiState.isEmpty() || requestDisplayInfoUiState == null) { - return null - } - return try { - val result = CreateCredentialViewModel( - credManRepo = credManRepo, - providerEnableListUiState = providerEnableListUiState, - providerDisableListUiState = providerDisableListUiState, - requestDisplayInfoUiState = requestDisplayInfoUiState - ) - result - } catch (e: Exception) { - null - } - } - } -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 05be0a65ce13f..919411ef10720 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -19,8 +19,7 @@ package com.android.credentialmanager.createflow import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Drawable -import com.android.credentialmanager.common.DialogState -import com.android.credentialmanager.common.ProviderActivityState +import com.android.credentialmanager.common.BaseEntry import com.android.credentialmanager.common.CredentialType import java.time.Instant @@ -34,11 +33,7 @@ data class CreateCredentialUiState( // 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, ) open class ProviderInfo( @@ -61,14 +56,6 @@ class DisabledProviderInfo( displayName: String, ) : ProviderInfo(icon, id, displayName) -open class EntryInfo ( - val providerId: String, - val entryKey: String, - val entrySubkey: String, - val pendingIntent: PendingIntent?, - val fillInIntent: Intent?, -) - class CreateOptionInfo( providerId: String, entryKey: String, @@ -82,7 +69,7 @@ class CreateOptionInfo( val totalCredentialCount: Int?, val lastUsedTime: Instant?, val footerDescription: String?, -) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) +) : BaseEntry(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) class RemoteInfo( providerId: String, @@ -90,7 +77,7 @@ class RemoteInfo( entrySubkey: String, pendingIntent: PendingIntent?, fillInIntent: Intent?, -) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) +) : BaseEntry(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) data class RequestDisplayInfo( val title: String, @@ -106,7 +93,7 @@ data class RequestDisplayInfo( */ data class ActiveEntry ( val activeProvider: EnabledProviderInfo, - val activeEntryInfo: EntryInfo, + val activeEntryInfo: BaseEntry, ) /** The name of the current screen. */ diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 59d2f4dd89369..8b311fe23914d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -56,7 +56,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap +import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.R +import com.android.credentialmanager.common.BaseEntry import com.android.credentialmanager.common.CredentialType import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.common.ui.ActionButton @@ -73,43 +75,45 @@ import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme @Composable fun GetCredentialScreen( - viewModel: GetCredentialViewModel, + viewModel: CredentialSelectorViewModel, providerActivityLauncher: ManagedActivityResultLauncher ) { - val uiState = viewModel.uiState - if (uiState.currentScreenState != GetScreenState.REMOTE_ONLY) { + val getCredentialUiState = viewModel.uiState.getCredentialUiState ?: return + if (getCredentialUiState.currentScreenState != GetScreenState.REMOTE_ONLY) { ModalBottomSheet( sheetContent = { // 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) { + when (viewModel.uiState.providerActivityState) { ProviderActivityState.NOT_APPLICABLE -> { - if (uiState.currentScreenState == GetScreenState.PRIMARY_SELECTION) { + if (getCredentialUiState.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, + requestDisplayInfo = getCredentialUiState.requestDisplayInfo, + providerDisplayInfo = getCredentialUiState.providerDisplayInfo, + providerInfoList = getCredentialUiState.providerInfoList, + activeEntry = getCredentialUiState.activeEntry, + onEntrySelected = viewModel::getFlowOnEntrySelected, + onConfirm = viewModel::getFlowOnConfirmEntrySelected, + onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected, ) } else { AllSignInOptionCard( - providerInfoList = uiState.providerInfoList, - providerDisplayInfo = uiState.providerDisplayInfo, - onEntrySelected = viewModel::onEntrySelected, - onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen, + providerInfoList = getCredentialUiState.providerInfoList, + providerDisplayInfo = getCredentialUiState.providerDisplayInfo, + onEntrySelected = viewModel::getFlowOnEntrySelected, + onBackButtonClicked = + viewModel::getFlowOnBackToPrimarySelectionScreen, onCancel = viewModel::onCancel, - isNoAccount = uiState.isNoAccount, + isNoAccount = getCredentialUiState.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) { + LaunchedEffect(viewModel.uiState.providerActivityState) { viewModel.launchProviderUi(providerActivityLauncher) } } @@ -122,7 +126,7 @@ fun GetCredentialScreen( ) } else { SnackBarScreen( - onClick = viewModel::onMoreOptionOnSnackBarSelected, + onClick = viewModel::getFlowOnMoreOptionOnSnackBarSelected, onCancel = viewModel::onCancel, ) } @@ -134,8 +138,8 @@ fun PrimarySelectionCard( requestDisplayInfo: RequestDisplayInfo, providerDisplayInfo: ProviderDisplayInfo, providerInfoList: List, - activeEntry: EntryInfo?, - onEntrySelected: (EntryInfo) -> Unit, + activeEntry: BaseEntry?, + onEntrySelected: (BaseEntry) -> Unit, onConfirm: () -> Unit, onMoreOptionSelected: () -> Unit, ) { @@ -263,7 +267,7 @@ fun PrimarySelectionCard( fun AllSignInOptionCard( providerInfoList: List, providerDisplayInfo: ProviderDisplayInfo, - onEntrySelected: (EntryInfo) -> Unit, + onEntrySelected: (BaseEntry) -> Unit, onBackButtonClicked: () -> Unit, onCancel: () -> Unit, isNoAccount: Boolean, @@ -362,7 +366,7 @@ fun AllSignInOptionCard( @Composable fun ActionChips( providerInfoList: List, - onEntrySelected: (EntryInfo) -> Unit, + onEntrySelected: (BaseEntry) -> Unit, ) { val actionChips = providerInfoList.flatMap { it.actionEntryList } if (actionChips.isEmpty()) { @@ -390,7 +394,7 @@ fun ActionChips( @Composable fun RemoteEntryCard( remoteEntry: RemoteEntryInfo, - onEntrySelected: (EntryInfo) -> Unit, + onEntrySelected: (BaseEntry) -> Unit, ) { TextSecondary( text = stringResource(R.string.get_dialog_heading_from_another_device), @@ -432,7 +436,7 @@ fun RemoteEntryCard( @Composable fun LockedCredentials( authenticationEntryList: List, - onEntrySelected: (EntryInfo) -> Unit, + onEntrySelected: (BaseEntry) -> Unit, ) { TextSecondary( text = stringResource(R.string.get_dialog_heading_locked_password_managers), @@ -457,7 +461,7 @@ fun LockedCredentials( @Composable fun PerUserNameCredentials( perUserNameCredentialEntryList: PerUserNameCredentialEntryList, - onEntrySelected: (EntryInfo) -> Unit, + onEntrySelected: (BaseEntry) -> Unit, ) { TextSecondary( text = stringResource( @@ -485,7 +489,7 @@ fun PerUserNameCredentials( @Composable fun CredentialEntryRow( credentialEntryInfo: CredentialEntryInfo, - onEntrySelected: (EntryInfo) -> Unit, + onEntrySelected: (BaseEntry) -> Unit, ) { Entry( onClick = { onEntrySelected(credentialEntryInfo) }, @@ -541,7 +545,7 @@ fun CredentialEntryRow( @Composable fun AuthenticationEntryRow( authenticationEntryInfo: AuthenticationEntryInfo, - onEntrySelected: (EntryInfo) -> Unit, + onEntrySelected: (BaseEntry) -> Unit, ) { Entry( onClick = { onEntrySelected(authenticationEntryInfo) }, @@ -585,7 +589,7 @@ fun AuthenticationEntryRow( @Composable fun ActionEntryRow( actionEntryInfo: ActionEntryInfo, - onEntrySelected: (EntryInfo) -> Unit, + onEntrySelected: (BaseEntry) -> Unit, ) { TransparentBackgroundEntry( icon = { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt deleted file mode 100644 index 8148082e58e62..0000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (C) 2022 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.getflow - -import android.app.Activity -import android.util.Log -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.result.ActivityResult -import androidx.activity.result.IntentSenderRequest -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import com.android.credentialmanager.CredentialManagerRepo -import com.android.credentialmanager.common.CredentialType -import com.android.credentialmanager.common.Constants -import com.android.credentialmanager.common.DialogState -import com.android.credentialmanager.common.ProviderActivityResult -import com.android.credentialmanager.common.ProviderActivityState -import com.android.internal.util.Preconditions - -data class GetCredentialUiState( - val providerInfoList: List, - val requestDisplayInfo: RequestDisplayInfo, - val currentScreenState: GetScreenState = toGetScreenState(providerInfoList), - val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList), - val selectedEntry: EntryInfo? = null, - val activeEntry: EntryInfo? = toActiveEntry(providerDisplayInfo), - val providerActivityState: ProviderActivityState = - ProviderActivityState.NOT_APPLICABLE, - val isNoAccount: Boolean = false, - val dialogState: DialogState = DialogState.ACTIVE, -) - -class GetCredentialViewModel( - private val credManRepo: CredentialManagerRepo, - initialUiState: GetCredentialUiState, -) : ViewModel() { - - var uiState by mutableStateOf(initialUiState) - private set - - fun onEntrySelected(entry: EntryInfo) { - Log.d(Constants.LOG_TAG, "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.d(Constants.LOG_TAG, - "Illegal state: confirm is pressed but activeEntry isn't set.") - onInternalError() - } - } - - fun launchProviderUi( - launcher: ManagedActivityResultLauncher - ) { - val entry = uiState.selectedEntry - if (entry != null && entry.pendingIntent != null) { - Log.d(Constants.LOG_TAG, "Launching provider activity") - uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING) - val intentSenderRequest = IntentSenderRequest.Builder(entry.pendingIntent) - .setFillInIntent(entry.fillInIntent).build() - launcher.launch(intentSenderRequest) - } else { - Log.d(Constants.LOG_TAG, "No provider UI to launch") - onInternalError() - } - } - - // When the view model runs into unexpected illegal state, reports the error back and close - // the activity gracefully. - private fun onInternalError() { - Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state") - credManRepo.onParsingFailureCancel() - 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. - Log.d(Constants.LOG_TAG, "The provider activity was cancelled," + - " re-displaying our UI.") - uiState = uiState.copy( - selectedEntry = null, - providerActivityState = ProviderActivityState.NOT_APPLICABLE, - ) - } else { - if (entry != null) { - Log.d( - Constants.LOG_TAG, "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, - ) - uiState = uiState.copy(dialogState = DialogState.COMPLETE) - } else { - Log.w(Constants.LOG_TAG, - "Illegal state: received a provider result but found no matching entry.") - onInternalError() - } - } - } - - fun onMoreOptionSelected() { - Log.d(Constants.LOG_TAG, "More Option selected") - uiState = uiState.copy( - currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS - ) - } - - fun onMoreOptionOnSnackBarSelected(isNoAccount: Boolean) { - Log.d(Constants.LOG_TAG, "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 onCancel() { - credManRepo.onUserCancel() - uiState = uiState.copy(dialogState = DialogState.COMPLETE) - } -} - -private fun toProviderDisplayInfo( - providerInfoList: List -): ProviderDisplayInfo { - - val userNameToCredentialEntryMap = mutableMapOf>() - val authenticationEntryList = mutableListOf() - val remoteEntryList = mutableListOf() - providerInfoList.forEach { providerInfo -> - if (providerInfo.authenticationEntryList != null && - !providerInfo.authenticationEntryList.isEmpty()) { - authenticationEntryList.add(providerInfo.authenticationEntryList[0]) - } - 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) - - // 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), - ) -} - -private fun toActiveEntry( - 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 -} - -private fun toGetScreenState( - providerInfoList: List -): GetScreenState { - var noLocalAccount = true - var remoteInfo: RemoteEntryInfo? = null - providerInfoList.forEach { providerInfo -> - if (providerInfo.credentialEntryList.isNotEmpty() || - (providerInfo.authenticationEntryList != null && - !providerInfo.authenticationEntryList.isEmpty())) { - 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 -} - -internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator { - override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int { - // First prefer passkey type for its security benefits - if (p0.credentialType != p1.credentialType) { - if (CredentialType.PASSKEY == p0.credentialType) { - return -1 - } else if (CredentialType.PASSKEY == 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) { - return -1 - } else if (p1.lastUsedTimeMillis != null) { - return 1 - } - return 0 - } -} \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index b3183493d6504..5ab933a0f6515 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -19,10 +19,21 @@ package com.android.credentialmanager.getflow import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Drawable +import com.android.credentialmanager.common.BaseEntry import com.android.credentialmanager.common.CredentialType +import com.android.internal.util.Preconditions import java.time.Instant +data class GetCredentialUiState( + val providerInfoList: List, + val requestDisplayInfo: RequestDisplayInfo, + val currentScreenState: GetScreenState = toGetScreenState(providerInfoList), + val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList), + val activeEntry: BaseEntry? = toActiveEntry(providerDisplayInfo), + val isNoAccount: Boolean = false, +) + data class ProviderInfo( /** * Unique id (component name) of this provider. @@ -49,15 +60,6 @@ data class ProviderDisplayInfo( val remoteEntry: RemoteEntryInfo? ) -abstract class EntryInfo ( - /** Unique id combination of this entry. Not for display purpose. */ - val providerId: String, - val entryKey: String, - val entrySubkey: String, - val pendingIntent: PendingIntent?, - val fillInIntent: Intent?, -) - class CredentialEntryInfo( providerId: String, entryKey: String, @@ -72,7 +74,7 @@ class CredentialEntryInfo( val displayName: String?, val icon: Drawable?, val lastUsedTimeMillis: Instant?, -) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) +) : BaseEntry(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) class AuthenticationEntryInfo( providerId: String, @@ -82,7 +84,7 @@ class AuthenticationEntryInfo( fillInIntent: Intent?, val title: String, val icon: Drawable, -) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) +) : BaseEntry(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) class RemoteEntryInfo( providerId: String, @@ -90,7 +92,7 @@ class RemoteEntryInfo( entrySubkey: String, pendingIntent: PendingIntent?, fillInIntent: Intent?, -) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) +) : BaseEntry(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) class ActionEntryInfo( providerId: String, @@ -101,7 +103,7 @@ class ActionEntryInfo( val title: String, val icon: Drawable, val subTitle: String?, -) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) +) : BaseEntry(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent) data class RequestDisplayInfo( val appName: String, @@ -126,3 +128,120 @@ enum class GetScreenState { /** The snackbar only page when there's no account but only a remoteEntry. */ REMOTE_ONLY, } + +// IMPORTANT: new invocation should be mindful that this method will throw if more than 1 remote +// entry exists +private fun toProviderDisplayInfo( + providerInfoList: List +): ProviderDisplayInfo { + + val userNameToCredentialEntryMap = mutableMapOf>() + val authenticationEntryList = mutableListOf() + val remoteEntryList = mutableListOf() + providerInfoList.forEach { providerInfo -> + authenticationEntryList.addAll(providerInfo.authenticationEntryList) + if (providerInfo.remoteEntry != null) { + remoteEntryList.add(providerInfo.remoteEntry) + } + // There can only be at most one remote entry + Preconditions.checkState(remoteEntryList.size <= 1) + + providerInfo.credentialEntryList.forEach { + userNameToCredentialEntryMap.compute( + it.userName + ) { _, v -> + if (v == null) { + mutableListOf(it) + } else { + v.add(it) + v + } + } + } + } + + // 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), + ) +} + +private fun toActiveEntry( + providerDisplayInfo: ProviderDisplayInfo, +): BaseEntry? { + val sortedUserNameToCredentialEntryList = + providerDisplayInfo.sortedUserNameToCredentialEntryList + val authenticationEntryList = providerDisplayInfo.authenticationEntryList + var activeEntry: BaseEntry? = 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 +): GetScreenState { + var noLocalAccount = true + var remoteInfo: RemoteEntryInfo? = null + providerInfoList.forEach { providerInfo -> + if (providerInfo.credentialEntryList.isNotEmpty() || + providerInfo.authenticationEntryList.isNotEmpty()) { + noLocalAccount = false + } + if (providerInfo.remoteEntry != null) { + remoteInfo = providerInfo.remoteEntry + } + } + + return if (noLocalAccount && remoteInfo != null) + GetScreenState.REMOTE_ONLY else GetScreenState.PRIMARY_SELECTION +} + +internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator { + override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int { + // First prefer passkey type for its security benefits + if (p0.credentialType != p1.credentialType) { + if (CredentialType.PASSKEY == p0.credentialType) { + return -1 + } else if (CredentialType.PASSKEY == 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) { + return -1 + } else if (p1.lastUsedTimeMillis != null) { + return 1 + } + return 0 + } +} \ No newline at end of file