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.selection.CancelSelectionRequest 22 import android.credentials.selection.Constants 23 import android.credentials.selection.CreateCredentialProviderData 24 import android.credentials.selection.GetCredentialProviderData 25 import android.credentials.selection.DisabledProviderData 26 import android.credentials.selection.ProviderData 27 import android.credentials.selection.RequestInfo 28 import android.credentials.selection.BaseDialogResult 29 import android.credentials.selection.ProviderPendingIntentResponse 30 import android.credentials.selection.UserSelectionDialogResult 31 import android.os.IBinder 32 import android.os.Bundle 33 import android.os.ResultReceiver 34 import android.util.Log 35 import android.view.autofill.AutofillManager 36 import com.android.credentialmanager.createflow.DisabledProviderInfo 37 import com.android.credentialmanager.createflow.EnabledProviderInfo 38 import com.android.credentialmanager.createflow.RequestDisplayInfo 39 import com.android.credentialmanager.getflow.GetCredentialUiState 40 import com.android.credentialmanager.getflow.findAutoSelectEntry 41 import com.android.credentialmanager.common.ProviderActivityState 42 import com.android.credentialmanager.createflow.isFlowAutoSelectable 43 import com.android.credentialmanager.getflow.findBiometricFlowEntry 44 45 /** 46 * Client for interacting with Credential Manager. Also holds data inputs from it. 47 * 48 * IMPORTANT: instantiation of the object can fail if the data inputs aren't valid. Callers need 49 * to be equipped to handle this gracefully. 50 */ 51 class CredentialManagerRepo( 52 private val context: Context, 53 intent: Intent, 54 isNewActivity: Boolean, 55 ) { 56 val requestInfo: RequestInfo? 57 var isReqForAllOptions: Boolean = false 58 private val providerEnabledList: List<ProviderData> 59 private val providerDisabledList: List<DisabledProviderData>? 60 val resultReceiver: ResultReceiver? 61 62 var initialUiState: UiState 63 64 init { 65 requestInfo = intent.extras?.getParcelable( 66 RequestInfo.EXTRA_REQUEST_INFO, 67 RequestInfo::class.java 68 ) 69 70 val originName: String? = when (requestInfo?.type) { 71 RequestInfo.TYPE_CREATE -> processHttpsOrigin( 72 requestInfo.createCredentialRequest?.origin) 73 RequestInfo.TYPE_GET -> processHttpsOrigin(requestInfo.getCredentialRequest?.origin) 74 else -> null 75 } 76 77 providerEnabledList = when (requestInfo?.type) { 78 RequestInfo.TYPE_CREATE -> 79 intent.extras?.getParcelableArrayList( 80 ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, 81 CreateCredentialProviderData::class.java 82 ) ?: emptyList() 83 RequestInfo.TYPE_GET -> 84 getEnabledProviderDataList( 85 intent 86 ) ?: getEnabledProviderDataListFromAuthExtras( 87 intent 88 ) ?: emptyList() 89 else -> { 90 Log.d( 91 com.android.credentialmanager.common.Constants.LOG_TAG, 92 "Unrecognized request type: ${requestInfo?.type}") 93 emptyList() 94 } 95 } 96 97 providerDisabledList = 98 intent.extras?.getParcelableArrayList( 99 ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, 100 DisabledProviderData::class.java 101 ) 102 103 resultReceiver = intent.getParcelableExtra( 104 Constants.EXTRA_RESULT_RECEIVER, 105 ResultReceiver::class.java 106 ) 107 isReqForAllOptions = requestInfo?.isShowAllOptionsRequested ?: false 108 109 val cancellationRequest = getCancelUiRequest(intent) <lambda>null110 val cancelUiRequestState = cancellationRequest?.let { 111 CancelUiRequestState(getAppLabel(context.getPackageManager(), it.packageName)) 112 } 113 114 initialUiState = when (requestInfo?.type) { 115 RequestInfo.TYPE_CREATE -> { 116 val providerEnableListUiState = getCreateProviderEnableListInitialUiState() 117 val providerDisableListUiState = getCreateProviderDisableListInitialUiState() 118 val requestDisplayInfoUiState = 119 getCreateRequestDisplayInfoInitialUiState(originName)!! 120 val createCredentialUiState = CreateFlowUtils.toCreateCredentialUiState( 121 enabledProviders = providerEnableListUiState, 122 disabledProviders = providerDisableListUiState, 123 defaultProviderIdPreferredByApp = 124 requestDisplayInfoUiState.appPreferredDefaultProviderId, 125 defaultProviderIdsSetByUser = 126 requestDisplayInfoUiState.userSetDefaultProviderIds, 127 requestDisplayInfo = requestDisplayInfoUiState, 128 )!! 129 val isFlowAutoSelectable = isFlowAutoSelectable(createCredentialUiState) 130 UiState( 131 createCredentialUiState = createCredentialUiState, 132 getCredentialUiState = null, 133 cancelRequestState = cancelUiRequestState, 134 isInitialRender = isNewActivity, 135 isAutoSelectFlow = isFlowAutoSelectable, 136 providerActivityState = 137 if (isFlowAutoSelectable) ProviderActivityState.READY_TO_LAUNCH 138 else ProviderActivityState.NOT_APPLICABLE, 139 selectedEntry = 140 if (isFlowAutoSelectable) createCredentialUiState.activeEntry?.activeEntryInfo 141 else null, 142 ) 143 } 144 RequestInfo.TYPE_GET -> { 145 var getCredentialInitialUiState = getCredentialInitialUiState(originName, 146 isReqForAllOptions)!! 147 val autoSelectEntry = 148 findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo) 149 val biometricEntry = findBiometricFlowEntry( 150 getCredentialInitialUiState.providerDisplayInfo, 151 autoSelectEntry != null) 152 if (biometricEntry != null) { 153 getCredentialInitialUiState = getCredentialInitialUiState.copy( 154 activeEntry = biometricEntry) 155 } 156 UiState( 157 createCredentialUiState = null, 158 getCredentialUiState = getCredentialInitialUiState, 159 selectedEntry = autoSelectEntry, 160 providerActivityState = 161 if (autoSelectEntry == null) ProviderActivityState.NOT_APPLICABLE 162 else ProviderActivityState.READY_TO_LAUNCH, 163 isAutoSelectFlow = autoSelectEntry != null, 164 cancelRequestState = cancelUiRequestState, 165 isInitialRender = isNewActivity, 166 ) 167 } 168 else -> { 169 if (cancellationRequest != null) { 170 UiState( 171 createCredentialUiState = null, 172 getCredentialUiState = null, 173 cancelRequestState = cancelUiRequestState, 174 isInitialRender = isNewActivity, 175 ) 176 } else { 177 throw IllegalStateException("Unrecognized request type: ${requestInfo?.type}") 178 } 179 } 180 } 181 } 182 initStatenull183 fun initState(): UiState { 184 return initialUiState 185 } 186 187 // The dialog is canceled by the user. onUserCancelnull188 fun onUserCancel() { 189 onCancel(BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED) 190 } 191 192 // The dialog is canceled because we launched into settings. onSettingLaunchCancelnull193 fun onSettingLaunchCancel() { 194 onCancel(BaseDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS) 195 } 196 onParsingFailureCancelnull197 fun onParsingFailureCancel() { 198 onCancel(BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE) 199 } 200 onCancelnull201 fun onCancel(cancelCode: Int) { 202 sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver) 203 } 204 onOptionSelectednull205 fun onOptionSelected( 206 providerId: String, 207 entryKey: String, 208 entrySubkey: String, 209 resultCode: Int? = null, 210 resultData: Intent? = null, 211 ) { 212 val userSelectionDialogResult = UserSelectionDialogResult( 213 requestInfo?.token, 214 providerId, 215 entryKey, 216 entrySubkey, 217 if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null 218 ) 219 val resultDataBundle = Bundle() 220 UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle) 221 222 resultReceiver?.send( 223 BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, 224 resultDataBundle 225 ) 226 } 227 228 // IMPORTANT: new invocation should be mindful that this method can throw. getCredentialInitialUiStatenull229 private fun getCredentialInitialUiState( 230 originName: String?, 231 isReqForAllOptions: Boolean 232 ): GetCredentialUiState? { 233 val providerEnabledList = GetFlowUtils.toProviderList( 234 providerEnabledList as List<GetCredentialProviderData>, context 235 ) 236 val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context, originName) 237 return GetCredentialUiState( 238 isReqForAllOptions, 239 providerEnabledList, 240 requestDisplayInfo ?: return null 241 ) 242 } 243 getEnabledProviderDataListnull244 private fun getEnabledProviderDataList(intent: Intent): List<GetCredentialProviderData>? { 245 return intent.extras?.getParcelableArrayList( 246 ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, 247 GetCredentialProviderData::class.java 248 ) 249 } 250 getEnabledProviderDataListFromAuthExtrasnull251 private fun getEnabledProviderDataListFromAuthExtras( 252 intent: Intent 253 ): List<GetCredentialProviderData>? { 254 return intent.getBundleExtra( 255 AutofillManager.EXTRA_AUTH_STATE 256 ) ?.getParcelableArrayList( 257 ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, 258 GetCredentialProviderData::class.java 259 ) 260 } 261 262 // IMPORTANT: new invocation should be mindful that this method can throw. getCreateProviderEnableListInitialUiStatenull263 private fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> { 264 return CreateFlowUtils.toEnabledProviderList( 265 providerEnabledList as List<CreateCredentialProviderData>, context 266 ) 267 } 268 getCreateProviderDisableListInitialUiStatenull269 private fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo> { 270 return CreateFlowUtils.toDisabledProviderList( 271 // Handle runtime cast error 272 providerDisabledList, context 273 ) 274 } 275 getCreateRequestDisplayInfoInitialUiStatenull276 private fun getCreateRequestDisplayInfoInitialUiState( 277 originName: String? 278 ): RequestDisplayInfo? { 279 return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context, originName) 280 } 281 282 companion object { 283 private const val HTTPS = "https://" 284 private const val FORWARD_SLASH = "/" 285 sendCancellationCodenull286 fun sendCancellationCode( 287 cancelCode: Int, 288 requestToken: IBinder?, 289 resultReceiver: ResultReceiver? 290 ) { 291 if (requestToken != null && resultReceiver != null) { 292 val resultData = Bundle() 293 294 BaseDialogResult.addToBundle(BaseDialogResult(requestToken), resultData) 295 resultReceiver.send(cancelCode, resultData) 296 } 297 } 298 299 /** Return the cancellation request if present. */ getCancelUiRequestnull300 fun getCancelUiRequest(intent: Intent): CancelSelectionRequest? { 301 return intent.extras?.getParcelable( 302 CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST, 303 CancelSelectionRequest::class.java 304 ) 305 } 306 307 /** Removes "https://", and the trailing slash if present for an https request. */ processHttpsOriginnull308 private fun processHttpsOrigin(origin: String?): String? { 309 var processed = origin 310 if (processed?.startsWith(HTTPS) == true) { // Removes "https://" 311 processed = processed.substring(HTTPS.length) 312 if (processed?.endsWith(FORWARD_SLASH) == true) { // Removes the trailing slash 313 processed = processed.substring(0, processed.length - 1) 314 } 315 } 316 return processed 317 } 318 } 319 } 320