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