blob: a78509d897aa0768983e25854e71f7f7fb7816d5 [file] [log] [blame]
/*
* 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
import android.content.Context
import android.content.Intent
import android.credentials.ui.CancelUiRequest
import android.credentials.ui.Constants
import android.credentials.ui.CreateCredentialProviderData
import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.DisabledProviderData
import android.credentials.ui.ProviderData
import android.credentials.ui.RequestInfo
import android.credentials.ui.BaseDialogResult
import android.credentials.ui.ProviderPendingIntentResponse
import android.credentials.ui.UserSelectionDialogResult
import android.os.IBinder
import android.os.Bundle
import android.os.ResultReceiver
import android.util.Log
import com.android.credentialmanager.createflow.DisabledProviderInfo
import com.android.credentialmanager.createflow.EnabledProviderInfo
import com.android.credentialmanager.createflow.RequestDisplayInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.findAutoSelectEntry
import com.android.credentialmanager.common.ProviderActivityState
import com.android.credentialmanager.createflow.isFlowAutoSelectable
/**
* Client for interacting with Credential Manager. Also holds data inputs from it.
*
* IMPORTANT: instantiation of the object can fail if the data inputs aren't valid. Callers need
* to be equipped to handle this gracefully.
*/
class CredentialManagerRepo(
private val context: Context,
intent: Intent,
userConfigRepo: UserConfigRepo,
isNewActivity: Boolean,
) {
val requestInfo: RequestInfo?
private val providerEnabledList: List<ProviderData>
private val providerDisabledList: List<DisabledProviderData>?
val resultReceiver: ResultReceiver?
var initialUiState: UiState
init {
requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
)
val originName: String? = when (requestInfo?.type) {
RequestInfo.TYPE_CREATE -> processHttpsOrigin(
requestInfo.createCredentialRequest?.origin)
RequestInfo.TYPE_GET -> processHttpsOrigin(requestInfo.getCredentialRequest?.origin)
else -> null
}
providerEnabledList = when (requestInfo?.type) {
RequestInfo.TYPE_CREATE ->
intent.extras?.getParcelableArrayList(
ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
CreateCredentialProviderData::class.java
) ?: emptyList()
RequestInfo.TYPE_GET ->
intent.extras?.getParcelableArrayList(
ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
GetCredentialProviderData::class.java
) ?: emptyList()
else -> {
Log.d(
com.android.credentialmanager.common.Constants.LOG_TAG,
"Unrecognized request type: ${requestInfo?.type}")
emptyList()
}
}
providerDisabledList =
intent.extras?.getParcelableArrayList(
ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST,
DisabledProviderData::class.java
)
resultReceiver = intent.getParcelableExtra(
Constants.EXTRA_RESULT_RECEIVER,
ResultReceiver::class.java
)
val cancellationRequest = getCancelUiRequest(intent)
val cancelUiRequestState = cancellationRequest?.let {
CancelUiRequestState(getAppLabel(context.getPackageManager(), it.appPackageName))
}
initialUiState = when (requestInfo?.type) {
RequestInfo.TYPE_CREATE -> {
val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse()
val providerEnableListUiState = getCreateProviderEnableListInitialUiState()
val providerDisableListUiState = getCreateProviderDisableListInitialUiState()
val requestDisplayInfoUiState =
getCreateRequestDisplayInfoInitialUiState(originName)!!
val createCredentialUiState = CreateFlowUtils.toCreateCredentialUiState(
enabledProviders = providerEnableListUiState,
disabledProviders = providerDisableListUiState,
defaultProviderIdPreferredByApp =
requestDisplayInfoUiState.appPreferredDefaultProviderId,
defaultProviderIdsSetByUser =
requestDisplayInfoUiState.userSetDefaultProviderIds,
requestDisplayInfo = requestDisplayInfoUiState,
isOnPasskeyIntroStateAlready = false,
isPasskeyFirstUse = isPasskeyFirstUse,
)!!
val isFlowAutoSelectable = isFlowAutoSelectable(createCredentialUiState)
UiState(
createCredentialUiState = createCredentialUiState,
getCredentialUiState = null,
cancelRequestState = cancelUiRequestState,
isInitialRender = isNewActivity,
isAutoSelectFlow = isFlowAutoSelectable,
providerActivityState =
if (isFlowAutoSelectable) ProviderActivityState.READY_TO_LAUNCH
else ProviderActivityState.NOT_APPLICABLE,
selectedEntry =
if (isFlowAutoSelectable) createCredentialUiState.activeEntry?.activeEntryInfo
else null,
)
}
RequestInfo.TYPE_GET -> {
val getCredentialInitialUiState = getCredentialInitialUiState(originName)!!
val autoSelectEntry =
findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo)
UiState(
createCredentialUiState = null,
getCredentialUiState = getCredentialInitialUiState,
selectedEntry = autoSelectEntry,
providerActivityState =
if (autoSelectEntry == null) ProviderActivityState.NOT_APPLICABLE
else ProviderActivityState.READY_TO_LAUNCH,
isAutoSelectFlow = autoSelectEntry != null,
cancelRequestState = cancelUiRequestState,
isInitialRender = isNewActivity,
)
}
else -> {
if (cancellationRequest != null) {
UiState(
createCredentialUiState = null,
getCredentialUiState = null,
cancelRequestState = cancelUiRequestState,
isInitialRender = isNewActivity,
)
} else {
throw IllegalStateException("Unrecognized request type: ${requestInfo?.type}")
}
}
}
}
fun initState(): UiState {
return initialUiState
}
// The dialog is canceled by the user.
fun onUserCancel() {
onCancel(BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED)
}
// The dialog is canceled because we launched into settings.
fun onSettingLaunchCancel() {
onCancel(BaseDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS)
}
fun onParsingFailureCancel() {
onCancel(BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE)
}
fun onCancel(cancelCode: Int) {
sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver)
}
fun onOptionSelected(
providerId: String,
entryKey: String,
entrySubkey: String,
resultCode: Int? = null,
resultData: Intent? = null,
) {
val userSelectionDialogResult = UserSelectionDialogResult(
requestInfo?.token,
providerId,
entryKey,
entrySubkey,
if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
)
val resultDataBundle = Bundle()
UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle)
resultReceiver?.send(
BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION,
resultDataBundle
)
}
// IMPORTANT: new invocation should be mindful that this method can throw.
private fun getCredentialInitialUiState(originName: String?): GetCredentialUiState? {
val providerEnabledList = GetFlowUtils.toProviderList(
providerEnabledList as List<GetCredentialProviderData>, context
)
val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context, originName)
return GetCredentialUiState(
providerEnabledList,
requestDisplayInfo ?: return null,
)
}
// IMPORTANT: new invocation should be mindful that this method can throw.
private fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
return CreateFlowUtils.toEnabledProviderList(
providerEnabledList as List<CreateCredentialProviderData>, context
)
}
private fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo> {
return CreateFlowUtils.toDisabledProviderList(
// Handle runtime cast error
providerDisabledList, context
)
}
private fun getCreateRequestDisplayInfoInitialUiState(
originName: String?
): RequestDisplayInfo? {
return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context, originName)
}
companion object {
private const val HTTPS = "https://"
private const val FORWARD_SLASH = "/"
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)
}
}
/** Return the cancellation request if present. */
fun getCancelUiRequest(intent: Intent): CancelUiRequest? {
return intent.extras?.getParcelable(
CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
CancelUiRequest::class.java
)
}
/** Removes "https://", and the trailing slash if present for an https request. */
private fun processHttpsOrigin(origin: String?): String? {
var processed = origin
if (processed?.startsWith(HTTPS) == true) { // Removes "https://"
processed = processed.substring(HTTPS.length)
if (processed?.endsWith(FORWARD_SLASH) == true) { // Removes the trailing slash
processed = processed.substring(0, processed.length - 1)
}
}
return processed
}
}
}