Support onNewIntent().
This allows UI updates upon new request / entries. Bug: 265037946 Bug: 267669563 Test: manual Change-Id: I55ab941fe00e7b1e827a27d5b54eeafbe986c312
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
)
|
||||
@@ -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) },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user