1 /*
2  * Copyright 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 package androidx.credentials.provider
17 
18 import android.app.PendingIntent
19 import android.content.ComponentName
20 import android.os.Bundle
21 import androidx.annotation.RestrictTo
22 import androidx.credentials.CredentialOption
23 import androidx.credentials.provider.CallingAppInfo.Companion.extractCallingAppInfo
24 import androidx.credentials.provider.CallingAppInfo.Companion.setCallingAppInfo
25 
26 /**
27  * Request received by the provider after the query phase of the get flow is complete i.e. the user
28  * was presented with a list of credentials, and the user has now made a selection from the list of
29  * [CredentialEntry] presented on the selector UI.
30  *
31  * This request will be added to the intent extras of the activity invoked by the [PendingIntent]
32  * set on the [CredentialEntry] that the user selected. The request must be extracted using the
33  * [PendingIntentHandler.retrieveProviderGetCredentialRequest] helper API.
34  *
35  * @property credentialOptions the list of credential retrieval options containing the required
36  *   parameters, expected to contain a single [CredentialOption] when this request is retrieved from
37  *   the [android.app.Activity] invoked by the [android.app.PendingIntent] set on a
38  *   [PasswordCredentialEntry] or a [PublicKeyCredentialEntry], or expected to contain multiple
39  *   [CredentialOption] when this request is retrieved from the [android.app.Activity] invoked by
40  *   the [android.app.PendingIntent] set on a [RemoteEntry]
41  * @property callingAppInfo information pertaining to the calling application
42  * @property biometricPromptResult the result of a Biometric Prompt authentication flow, that is
43  *   propagated to the provider if the provider requested for
44  *   [androidx.credentials.CredentialManager] to handle the authentication flow
45  */
46 class ProviderGetCredentialRequest
47 @RestrictTo(RestrictTo.Scope.LIBRARY)
48 constructor(
49     val credentialOptions: List<CredentialOption>,
50     val callingAppInfo: CallingAppInfo,
51     val biometricPromptResult: BiometricPromptResult?,
52     // The source Bundle used to construct this request, if applicable
53     @get:RestrictTo(RestrictTo.Scope.LIBRARY) val sourceBundle: Bundle?,
54 ) {
55 
56     /**
57      * Constructs an instance of [ProviderGetCredentialRequest]
58      *
59      * @param credentialOptions the list of credential retrieval options containing the required
60      *   parameters, expected to contain a single [CredentialOption] when this request is retrieved
61      *   from the [android.app.Activity] invoked by the [android.app.PendingIntent] set on a
62      *   [PasswordCredentialEntry] or a [PublicKeyCredentialEntry], or expected to contain multiple
63      *   [CredentialOption] when this request is retrieved from the [android.app.Activity] invoked
64      *   by the [android.app.PendingIntent] set on a [RemoteEntry]
65      * @param callingAppInfo information pertaining to the calling application
66      * @param biometricPromptResult the result of a Biometric Prompt authentication flow, that is
67      *   propagated to the provider if the provider requested for
68      *   [androidx.credentials.CredentialManager] to handle the authentication flow
69      */
70     @JvmOverloads
71     constructor(
72         credentialOptions: List<CredentialOption>,
73         callingAppInfo: CallingAppInfo,
74         biometricPromptResult: BiometricPromptResult? = null,
75     ) : this(credentialOptions, callingAppInfo, biometricPromptResult, null)
76 
77     companion object {
78         @JvmStatic
createFromnull79         internal fun createFrom(
80             options: List<CredentialOption>,
81             callingAppInfo: CallingAppInfo,
82             biometricPromptResult: BiometricPromptResult? = null,
83             sourceBundle: Bundle?
84         ): ProviderGetCredentialRequest {
85             return ProviderGetCredentialRequest(
86                 options,
87                 callingAppInfo,
88                 biometricPromptResult,
89                 sourceBundle
90             )
91         }
92 
93         private const val EXTRA_CREDENTIAL_OPTION_SIZE =
94             "androidx.credentials.provider.extra.CREDENTIAL_OPTION_SIZE"
95         private const val EXTRA_CREDENTIAL_OPTION_TYPE_PREFIX =
96             "androidx.credentials.provider.extra.CREDENTIAL_OPTION_TYPE_"
97         private const val EXTRA_CREDENTIAL_OPTION_CREDENTIAL_RETRIEVAL_DATA_PREFIX =
98             "androidx.credentials.provider.extra.CREDENTIAL_OPTION_CREDENTIAL_RETRIEVAL_DATA_"
99         private const val EXTRA_CREDENTIAL_OPTION_CANDIDATE_QUERY_DATA_PREFIX =
100             "androidx.credentials.provider.extra.CREDENTIAL_OPTION_CANDIDATE_QUERY_DATA_"
101         private const val EXTRA_CREDENTIAL_OPTION_IS_SYSTEM_PROVIDER_REQUIRED_PREFIX =
102             "androidx.credentials.provider.extra.CREDENTIAL_OPTION_IS_SYSTEM_PROVIDER_REQUIRED_"
103         private const val EXTRA_CREDENTIAL_OPTION_ALLOWED_PROVIDERS_PREFIX =
104             "androidx.credentials.provider.extra.CREDENTIAL_OPTION_ALLOWED_PROVIDERS_"
105 
106         /**
107          * Helper method to convert the given [request] to a parcelable [Bundle], in case the
108          * instance needs to be sent across a process. Consumers of this method should use
109          * [fromBundle] to reconstruct the class instance back from the bundle returned here.
110          */
111         @JvmStatic
asBundlenull112         fun asBundle(request: ProviderGetCredentialRequest): Bundle {
113             val bundle = Bundle()
114             val optionSize = request.credentialOptions.size
115             bundle.putInt(EXTRA_CREDENTIAL_OPTION_SIZE, optionSize)
116             for (i in 0 until optionSize) {
117                 val option = request.credentialOptions[i]
118                 bundle.putString("$EXTRA_CREDENTIAL_OPTION_TYPE_PREFIX$i", option.type)
119                 bundle.putBundle(
120                     "$EXTRA_CREDENTIAL_OPTION_CANDIDATE_QUERY_DATA_PREFIX$i",
121                     option.candidateQueryData
122                 )
123                 bundle.putBundle(
124                     "$EXTRA_CREDENTIAL_OPTION_CREDENTIAL_RETRIEVAL_DATA_PREFIX$i",
125                     option.requestData
126                 )
127                 bundle.putBoolean(
128                     "$EXTRA_CREDENTIAL_OPTION_IS_SYSTEM_PROVIDER_REQUIRED_PREFIX$i",
129                     option.isSystemProviderRequired
130                 )
131                 bundle.putParcelableArray(
132                     "$EXTRA_CREDENTIAL_OPTION_ALLOWED_PROVIDERS_PREFIX$i",
133                     option.allowedProviders.toTypedArray()
134                 )
135             }
136             bundle.setCallingAppInfo(request.callingAppInfo)
137             return bundle
138         }
139 
140         /**
141          * Helper method to convert a [Bundle] retrieved through [asBundle], back to an instance of
142          * [ProviderGetCredentialRequest].
143          *
144          * Throws [IllegalArgumentException] if the conversion fails. This means that the given
145          * [bundle] does not contain a `ProviderGetCredentialRequest`. The bundle should be
146          * constructed and retrieved from [asBundle] itself and never be created from scratch to
147          * avoid the failure.
148          */
149         @JvmStatic
fromBundlenull150         fun fromBundle(bundle: Bundle): ProviderGetCredentialRequest {
151             val callingAppInfo =
152                 extractCallingAppInfo(bundle)
153                     ?: throw IllegalArgumentException("Bundle was missing CallingAppInfo.")
154             val optionSize = bundle.getInt(EXTRA_CREDENTIAL_OPTION_SIZE, -1)
155             if (optionSize < 0) {
156                 throw IllegalArgumentException("Bundle had invalid option size as $optionSize.")
157             }
158             val options = mutableListOf<CredentialOption>()
159             for (i in 0 until optionSize) {
160                 val type =
161                     bundle.getString("$EXTRA_CREDENTIAL_OPTION_TYPE_PREFIX$i")
162                         ?: throw IllegalArgumentException(
163                             "Bundle was missing option type at index $optionSize."
164                         )
165                 val candidateQueryData =
166                     bundle.getBundle("$EXTRA_CREDENTIAL_OPTION_CANDIDATE_QUERY_DATA_PREFIX$i")
167                         ?: throw IllegalArgumentException(
168                             "Bundle was missing candidate query data at index $optionSize."
169                         )
170                 val requestData =
171                     bundle.getBundle("$EXTRA_CREDENTIAL_OPTION_CREDENTIAL_RETRIEVAL_DATA_PREFIX$i")
172                         ?: throw IllegalArgumentException(
173                             "Bundle was missing request data at index $optionSize."
174                         )
175                 val isSystemProviderRequired =
176                     bundle.getBoolean(
177                         "$EXTRA_CREDENTIAL_OPTION_IS_SYSTEM_PROVIDER_REQUIRED_PREFIX$i",
178                         false
179                     )
180                 val allowedProviders =
181                     try {
182                         @Suppress("DEPRECATION")
183                         bundle
184                             .getParcelableArray(
185                                 "${EXTRA_CREDENTIAL_OPTION_ALLOWED_PROVIDERS_PREFIX}$i"
186                             )
187                             ?.mapNotNull { it as ComponentName? }
188                             ?.toSet() ?: emptySet()
189                     } catch (e: Exception) {
190                         emptySet()
191                     }
192                 options.add(
193                     CredentialOption.createFrom(
194                         type,
195                         requestData,
196                         candidateQueryData,
197                         isSystemProviderRequired,
198                         allowedProviders
199                     )
200                 )
201             }
202 
203             return createFrom(options, callingAppInfo, biometricPromptResult = null, bundle)
204         }
205     }
206 }
207