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