1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.credentialmanager 18 19 import android.content.Context 20 import android.content.Intent 21 import android.credentials.ui.CancelUiRequest 22 import android.credentials.ui.Constants 23 import android.credentials.ui.CreateCredentialProviderData 24 import android.credentials.ui.GetCredentialProviderData 25 import android.credentials.ui.DisabledProviderData 26 import android.credentials.ui.ProviderData 27 import android.credentials.ui.RequestInfo 28 import android.credentials.ui.BaseDialogResult 29 import android.credentials.ui.ProviderPendingIntentResponse 30 import android.credentials.ui.UserSelectionDialogResult 31 import android.os.IBinder 32 import android.os.Bundle 33 import android.os.ResultReceiver 34 import android.util.Log 35 import com.android.credentialmanager.createflow.DisabledProviderInfo 36 import com.android.credentialmanager.createflow.EnabledProviderInfo 37 import com.android.credentialmanager.createflow.RequestDisplayInfo 38 import com.android.credentialmanager.getflow.GetCredentialUiState 39 import com.android.credentialmanager.getflow.findAutoSelectEntry 40 import com.android.credentialmanager.common.ProviderActivityState 41 import com.android.credentialmanager.createflow.isFlowAutoSelectable 42 43 /** 44 * Client for interacting with Credential Manager. Also holds data inputs from it. 45 * 46 * IMPORTANT: instantiation of the object can fail if the data inputs aren't valid. Callers need 47 * to be equipped to handle this gracefully. 48 */ 49 class CredentialManagerRepo( 50 private val context: Context, 51 intent: Intent, 52 userConfigRepo: UserConfigRepo, 53 isNewActivity: Boolean, 54 ) { 55 val requestInfo: RequestInfo? 56 private val providerEnabledList: List<ProviderData> 57 private val providerDisabledList: List<DisabledProviderData>? 58 val resultReceiver: ResultReceiver? 59 60 var initialUiState: UiState 61 62 init { 63 requestInfo = intent.extras?.getParcelable( 64 RequestInfo.EXTRA_REQUEST_INFO, 65 RequestInfo::class.java 66 ) 67 68 val originName: String? = when (requestInfo?.type) { 69 RequestInfo.TYPE_CREATE -> processHttpsOrigin( 70 requestInfo.createCredentialRequest?.origin) 71 RequestInfo.TYPE_GET -> processHttpsOrigin(requestInfo.getCredentialRequest?.origin) 72 else -> null 73 } 74 75 providerEnabledList = when (requestInfo?.type) { 76 RequestInfo.TYPE_CREATE -> 77 intent.extras?.getParcelableArrayList( 78 ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, 79 CreateCredentialProviderData::class.java 80 ) ?: emptyList() 81 RequestInfo.TYPE_GET -> 82 intent.extras?.getParcelableArrayList( 83 ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, 84 GetCredentialProviderData::class.java 85 ) ?: emptyList() 86 else -> { 87 Log.d( 88 com.android.credentialmanager.common.Constants.LOG_TAG, 89 "Unrecognized request type: ${requestInfo?.type}") 90 emptyList() 91 } 92 } 93 94 providerDisabledList = 95 intent.extras?.getParcelableArrayList( 96 ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, 97 DisabledProviderData::class.java 98 ) 99 100 resultReceiver = intent.getParcelableExtra( 101 Constants.EXTRA_RESULT_RECEIVER, 102 ResultReceiver::class.java 103 ) 104 105 val cancellationRequest = getCancelUiRequest(intent) 106 val cancelUiRequestState = cancellationRequest?.let { 107 CancelUiRequestState(getAppLabel(context.getPackageManager(), it.appPackageName)) 108 } 109 110 initialUiState = when (requestInfo?.type) { 111 RequestInfo.TYPE_CREATE -> { 112 val isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse() 113 val providerEnableListUiState = getCreateProviderEnableListInitialUiState() 114 val providerDisableListUiState = getCreateProviderDisableListInitialUiState() 115 val requestDisplayInfoUiState = 116 getCreateRequestDisplayInfoInitialUiState(originName)!! 117 val createCredentialUiState = CreateFlowUtils.toCreateCredentialUiState( 118 enabledProviders = providerEnableListUiState, 119 disabledProviders = providerDisableListUiState, 120 defaultProviderIdPreferredByApp = 121 requestDisplayInfoUiState.appPreferredDefaultProviderId, 122 defaultProviderIdsSetByUser = 123 requestDisplayInfoUiState.userSetDefaultProviderIds, 124 requestDisplayInfo = requestDisplayInfoUiState, 125 isOnPasskeyIntroStateAlready = false, 126 isPasskeyFirstUse = isPasskeyFirstUse, 127 )!! 128 val isFlowAutoSelectable = isFlowAutoSelectable(createCredentialUiState) 129 UiState( 130 createCredentialUiState = createCredentialUiState, 131 getCredentialUiState = null, 132 cancelRequestState = cancelUiRequestState, 133 isInitialRender = isNewActivity, 134 isAutoSelectFlow = isFlowAutoSelectable, 135 providerActivityState = 136 if (isFlowAutoSelectable) ProviderActivityState.READY_TO_LAUNCH 137 else ProviderActivityState.NOT_APPLICABLE, 138 selectedEntry = 139 if (isFlowAutoSelectable) createCredentialUiState.activeEntry?.activeEntryInfo 140 else null, 141 ) 142 } 143 RequestInfo.TYPE_GET -> { 144 val getCredentialInitialUiState = getCredentialInitialUiState(originName)!! 145 val autoSelectEntry = 146 findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo) 147 UiState( 148 createCredentialUiState = null, 149 getCredentialUiState = getCredentialInitialUiState, 150 selectedEntry = autoSelectEntry, 151 providerActivityState = 152 if (autoSelectEntry == null) ProviderActivityState.NOT_APPLICABLE 153 else ProviderActivityState.READY_TO_LAUNCH, 154 isAutoSelectFlow = autoSelectEntry != null, 155 cancelRequestState = cancelUiRequestState, 156 isInitialRender = isNewActivity, 157 ) 158 } 159 else -> { 160 if (cancellationRequest != null) { 161 UiState( 162 createCredentialUiState = null, 163 getCredentialUiState = null, 164 cancelRequestState = cancelUiRequestState, 165 isInitialRender = isNewActivity, 166 ) 167 } else { 168 throw IllegalStateException("Unrecognized request type: ${requestInfo?.type}") 169 } 170 } 171 } 172 } 173 174 fun initState(): UiState { 175 return initialUiState 176 } 177 178 // The dialog is canceled by the user. 179 fun onUserCancel() { 180 onCancel(BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED) 181 } 182 183 // The dialog is canceled because we launched into settings. 184 fun onSettingLaunchCancel() { 185 onCancel(BaseDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS) 186 } 187 188 fun onParsingFailureCancel() { 189 onCancel(BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE) 190 } 191 192 fun onCancel(cancelCode: Int) { 193 sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver) 194 } 195 196 fun onOptionSelected( 197 providerId: String, 198 entryKey: String, 199 entrySubkey: String, 200 resultCode: Int? = null, 201 resultData: Intent? = null, 202 ) { 203 val userSelectionDialogResult = UserSelectionDialogResult( 204 requestInfo?.token, 205 providerId, 206 entryKey, 207 entrySubkey, 208 if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null 209 ) 210 val resultDataBundle = Bundle() 211 UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle) 212 resultReceiver?.send( 213 BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, 214 resultDataBundle 215 ) 216 } 217 218 // IMPORTANT: new invocation should be mindful that this method can throw. 219 private fun getCredentialInitialUiState(originName: String?): GetCredentialUiState? { 220 val providerEnabledList = GetFlowUtils.toProviderList( 221 providerEnabledList as List<GetCredentialProviderData>, context 222 ) 223 val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context, originName) 224 return GetCredentialUiState( 225 providerEnabledList, 226 requestDisplayInfo ?: return null, 227 ) 228 } 229 230 // IMPORTANT: new invocation should be mindful that this method can throw. 231 private fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> { 232 return CreateFlowUtils.toEnabledProviderList( 233 providerEnabledList as List<CreateCredentialProviderData>, context 234 ) 235 } 236 237 private fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo> { 238 return CreateFlowUtils.toDisabledProviderList( 239 // Handle runtime cast error 240 providerDisabledList, context 241 ) 242 } 243 244 private fun getCreateRequestDisplayInfoInitialUiState( 245 originName: String? 246 ): RequestDisplayInfo? { 247 return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context, originName) 248 } 249 250 companion object { 251 private const val HTTPS = "https://" 252 private const val FORWARD_SLASH = "/" 253 254 fun sendCancellationCode( 255 cancelCode: Int, 256 requestToken: IBinder?, 257 resultReceiver: ResultReceiver? 258 ) { 259 if (requestToken != null && resultReceiver != null) { 260 val resultData = Bundle() 261 BaseDialogResult.addToBundle(BaseDialogResult(requestToken), resultData) 262 resultReceiver.send(cancelCode, resultData) 263 } 264 } 265 266 /** Return the cancellation request if present. */ 267 fun getCancelUiRequest(intent: Intent): CancelUiRequest? { 268 return intent.extras?.getParcelable( 269 CancelUiRequest.EXTRA_CANCEL_UI_REQUEST, 270 CancelUiRequest::class.java 271 ) 272 } 273 274 /** Removes "https://", and the trailing slash if present for an https request. */ 275 private fun processHttpsOrigin(origin: String?): String? { 276 var processed = origin 277 if (processed?.startsWith(HTTPS) == true) { // Removes "https://" 278 processed = processed.substring(HTTPS.length) 279 if (processed?.endsWith(FORWARD_SLASH) == true) { // Removes the trailing slash 280 processed = processed.substring(0, processed.length - 1) 281 } 282 } 283 return processed 284 } 285 } 286 } 287