Merge "Support onNewIntent()."

This commit is contained in:
Helen Qin
2023-02-07 19:21:36 +00:00
committed by Android (Google) Code Review
12 changed files with 726 additions and 783 deletions

View File

@@ -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<ProviderData>
private val providerDisabledList: List<DisabledProviderData>?
// 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<GetCredentialProviderData>, context
@@ -149,7 +179,7 @@ class CredentialManagerRepo(
)
}
fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
private fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
val providerEnabledList = CreateFlowUtils.toEnabledProviderList(
// Handle runtime cast error
providerEnabledList as List<CreateCredentialProviderData>, context
@@ -157,17 +187,31 @@ class CredentialManagerRepo(
return providerEnabledList
}
fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo> {
private fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo> {
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<CreateCredentialProviderData> {
return listOf(

View File

@@ -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<ProviderActivityResult?>(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()
}
}

View File

@@ -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<IntentSenderRequest, ActivityResult>
) {
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()
}
}
}

View File

@@ -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<Entry>,
): List<AuthenticationEntryInfo> {
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<AuthenticationEntryInfo> = 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,

View File

@@ -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
}
}
}

View File

@@ -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?,
)

View File

@@ -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<IntentSenderRequest, ActivityResult>
) {
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<EnabledProviderInfo>,
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) },

View File

@@ -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<EnabledProviderInfo>,
private val providerDisableListUiState: List<DisabledProviderInfo>,
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<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.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<EnabledProviderInfo>,
providerDisableListUiState: List<DisabledProviderInfo>,
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
}
}
}
}

View File

@@ -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. */

View File

@@ -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<IntentSenderRequest, ActivityResult>
) {
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<ProviderInfo>,
activeEntry: EntryInfo?,
onEntrySelected: (EntryInfo) -> Unit,
activeEntry: BaseEntry?,
onEntrySelected: (BaseEntry) -> Unit,
onConfirm: () -> Unit,
onMoreOptionSelected: () -> Unit,
) {
@@ -263,7 +267,7 @@ fun PrimarySelectionCard(
fun AllSignInOptionCard(
providerInfoList: List<ProviderInfo>,
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<ProviderInfo>,
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<AuthenticationEntryInfo>,
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 = {

View File

@@ -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<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,
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<IntentSenderRequest, ActivityResult>
) {
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<ProviderInfo>
): ProviderDisplayInfo {
val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
val remoteEntryList = mutableListOf<RemoteEntryInfo>()
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<ProviderInfo>
): 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<CredentialEntryInfo> {
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
}
}

View File

@@ -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<ProviderInfo>,
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<ProviderInfo>
): ProviderDisplayInfo {
val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
val remoteEntryList = mutableListOf<RemoteEntryInfo>()
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<ProviderInfo>
): 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<CredentialEntryInfo> {
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
}
}