1 /*
<lambda>null2  * 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 
17 package androidx.credentials.playservices
18 
19 import android.content.Context
20 import android.os.Build
21 import android.os.CancellationSignal
22 import android.util.Log
23 import androidx.annotation.RestrictTo
24 import androidx.annotation.VisibleForTesting
25 import androidx.credentials.ClearCredentialStateRequest
26 import androidx.credentials.ClearCredentialStateRequest.Companion.TYPE_CLEAR_RESTORE_CREDENTIAL
27 import androidx.credentials.CreateCredentialRequest
28 import androidx.credentials.CreateCredentialResponse
29 import androidx.credentials.CreatePasswordRequest
30 import androidx.credentials.CreatePublicKeyCredentialRequest
31 import androidx.credentials.CreateRestoreCredentialRequest
32 import androidx.credentials.CredentialManagerCallback
33 import androidx.credentials.CredentialProvider
34 import androidx.credentials.ExperimentalDigitalCredentialApi
35 import androidx.credentials.GetCredentialRequest
36 import androidx.credentials.GetCredentialResponse
37 import androidx.credentials.GetDigitalCredentialOption
38 import androidx.credentials.GetRestoreCredentialOption
39 import androidx.credentials.exceptions.ClearCredentialException
40 import androidx.credentials.exceptions.ClearCredentialProviderConfigurationException
41 import androidx.credentials.exceptions.ClearCredentialUnknownException
42 import androidx.credentials.exceptions.CreateCredentialException
43 import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
44 import androidx.credentials.exceptions.GetCredentialException
45 import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
46 import androidx.credentials.playservices.controllers.blockstore.createrestorecredential.CredentialProviderCreateRestoreCredentialController
47 import androidx.credentials.playservices.controllers.blockstore.getrestorecredential.CredentialProviderGetRestoreCredentialController
48 import androidx.credentials.playservices.controllers.identityauth.beginsignin.CredentialProviderBeginSignInController
49 import androidx.credentials.playservices.controllers.identityauth.createpassword.CredentialProviderCreatePasswordController
50 import androidx.credentials.playservices.controllers.identityauth.createpublickeycredential.CredentialProviderCreatePublicKeyCredentialController
51 import androidx.credentials.playservices.controllers.identityauth.getsigninintent.CredentialProviderGetSignInIntentController
52 import androidx.credentials.playservices.controllers.identitycredentials.createpublickeycredential.CreatePublicKeyCredentialController
53 import androidx.credentials.playservices.controllers.identitycredentials.getdigitalcredential.CredentialProviderGetDigitalCredentialController
54 import com.google.android.gms.auth.api.identity.Identity
55 import com.google.android.gms.auth.blockstore.restorecredential.RestoreCredential
56 import com.google.android.gms.auth.blockstore.restorecredential.RestoreCredentialStatusCodes
57 import com.google.android.gms.common.ConnectionResult
58 import com.google.android.gms.common.GoogleApiAvailability
59 import com.google.android.gms.common.api.ApiException
60 import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
61 import java.util.concurrent.Executor
62 
63 /** Entry point of all credential manager requests to the play-services-auth module. */
64 @RestrictTo(RestrictTo.Scope.LIBRARY)
65 @Suppress("deprecation")
66 @OptIn(ExperimentalDigitalCredentialApi::class)
67 class CredentialProviderPlayServicesImpl(private val context: Context) : CredentialProvider {
68 
69     @VisibleForTesting var googleApiAvailability = GoogleApiAvailability.getInstance()
70 
71     override fun onGetCredential(
72         context: Context,
73         request: GetCredentialRequest,
74         cancellationSignal: CancellationSignal?,
75         executor: Executor,
76         callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
77     ) {
78         if (cancellationReviewer(cancellationSignal)) {
79             return
80         }
81         if (isDigitalCredentialRequest(request)) {
82             if (!isAvailableOnDevice(MIN_GMS_APK_VERSION_DIGITAL_CRED)) {
83                 cancellationReviewerWithCallback(cancellationSignal) {
84                     executor.execute {
85                         callback.onError(
86                             GetCredentialProviderConfigurationException(
87                                 "this device requires a Google Play Services update for the" +
88                                     " given feature to be supported"
89                             )
90                         )
91                     }
92                 }
93                 return
94             }
95             if (Build.VERSION.SDK_INT >= 23) {
96                 CredentialProviderGetDigitalCredentialController(context)
97                     .invokePlayServices(request, callback, executor, cancellationSignal)
98             } else {
99                 cancellationReviewerWithCallback(cancellationSignal) {
100                     executor.execute {
101                         callback.onError(
102                             GetCredentialProviderConfigurationException(
103                                 "this feature requires the minimum API level to be 23"
104                             )
105                         )
106                     }
107                 }
108                 return
109             }
110         } else if (isGetRestoreCredentialRequest(request)) {
111             if (!isAvailableOnDevice(MIN_GMS_APK_VERSION_RESTORE_CRED)) {
112                 cancellationReviewerWithCallback(cancellationSignal) {
113                     executor.execute {
114                         callback.onError(
115                             GetCredentialProviderConfigurationException(
116                                 "getCredentialAsync no provider dependencies found - please ensure " +
117                                     "the desired provider dependencies are added"
118                             )
119                         )
120                     }
121                 }
122                 return
123             }
124             CredentialProviderGetRestoreCredentialController(context)
125                 .invokePlayServices(request, callback, executor, cancellationSignal)
126         } else if (isGetSignInIntentRequest(request)) {
127             CredentialProviderGetSignInIntentController(context)
128                 .invokePlayServices(request, callback, executor, cancellationSignal)
129         } else {
130             CredentialProviderBeginSignInController(context)
131                 .invokePlayServices(request, callback, executor, cancellationSignal)
132         }
133     }
134 
135     @SuppressWarnings("deprecated")
136     override fun onCreateCredential(
137         context: Context,
138         request: CreateCredentialRequest,
139         cancellationSignal: CancellationSignal?,
140         executor: Executor,
141         callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>
142     ) {
143         if (cancellationReviewer(cancellationSignal)) {
144             return
145         }
146         when (request) {
147             is CreatePasswordRequest -> {
148                 CredentialProviderCreatePasswordController.getInstance(context)
149                     .invokePlayServices(request, callback, executor, cancellationSignal)
150             }
151             is CreatePublicKeyCredentialRequest -> {
152                 if (request.isConditionalCreateRequest) {
153                     CreatePublicKeyCredentialController.getInstance(context)
154                         .invokePlayServices(request, callback, executor, cancellationSignal)
155                 } else {
156                     CredentialProviderCreatePublicKeyCredentialController.getInstance(context)
157                         .invokePlayServices(request, callback, executor, cancellationSignal)
158                 }
159             }
160             is CreateRestoreCredentialRequest -> {
161                 if (!isAvailableOnDevice(MIN_GMS_APK_VERSION_RESTORE_CRED)) {
162                     cancellationReviewerWithCallback(cancellationSignal) {
163                         executor.execute {
164                             callback.onError(
165                                 CreateCredentialProviderConfigurationException(
166                                     "createCredentialAsync no provider dependencies found - please ensure the " +
167                                         "desired provider dependencies are added"
168                                 )
169                             )
170                         }
171                     }
172                     return
173                 }
174                 CredentialProviderCreateRestoreCredentialController(context)
175                     .invokePlayServices(request, callback, executor, cancellationSignal)
176             }
177             else -> {
178                 throw UnsupportedOperationException(
179                     "Create Credential request is unsupported, not password or " +
180                         "publickeycredential"
181                 )
182             }
183         }
184     }
185 
186     override fun isAvailableOnDevice(): Boolean {
187         return isAvailableOnDevice(MIN_GMS_APK_VERSION)
188     }
189 
190     fun isAvailableOnDevice(minApkVersion: Int): Boolean {
191         val resultCode = isGooglePlayServicesAvailable(context, minApkVersion)
192         val isSuccessful = resultCode == ConnectionResult.SUCCESS
193         if (!isSuccessful) {
194             val connectionResult = ConnectionResult(resultCode)
195             Log.w(
196                 TAG,
197                 "Connection with Google Play Services was not " +
198                     "successful. Connection result is: " +
199                     connectionResult.toString()
200             )
201         }
202         return isSuccessful
203     }
204 
205     // https://developers.google.com/android/reference/com/google/android/gms/common/ConnectionResult
206     // There is one error code that supports retry API_DISABLED_FOR_CONNECTION but it would not
207     // be useful to retry that one because our connection to GMSCore is a static variable
208     // (see GoogleApiAvailability.getInstance()) so we cannot recreate the connection to retry.
209     private fun isGooglePlayServicesAvailable(context: Context, minApkVersion: Int): Int {
210         return googleApiAvailability.isGooglePlayServicesAvailable(
211             context,
212             /*minApkVersion=*/ minApkVersion
213         )
214     }
215 
216     override fun onClearCredential(
217         request: ClearCredentialStateRequest,
218         cancellationSignal: CancellationSignal?,
219         executor: Executor,
220         callback: CredentialManagerCallback<Void?, ClearCredentialException>
221     ) {
222         if (cancellationReviewer(cancellationSignal)) {
223             return
224         }
225         if (request.requestType == TYPE_CLEAR_RESTORE_CREDENTIAL) {
226             if (!isAvailableOnDevice(MIN_GMS_APK_VERSION_RESTORE_CRED)) {
227                 cancellationReviewerWithCallback(cancellationSignal) {
228                     executor.execute {
229                         callback.onError(
230                             ClearCredentialProviderConfigurationException(
231                                 "clearCredentialStateAsync no provider dependencies found - please ensure the " +
232                                     "desired provider dependencies are added"
233                             )
234                         )
235                     }
236                 }
237                 return
238             }
239             RestoreCredential.getRestoreCredentialClient(context)
240                 .clearRestoreCredential(
241                     com.google.android.gms.auth.blockstore.restorecredential
242                         .ClearRestoreCredentialRequest(request.requestBundle)
243                 )
244                 .addOnSuccessListener {
245                     cancellationReviewerWithCallback(cancellationSignal) {
246                         Log.i(TAG, "Cleared restore credential successfully!")
247                         executor.execute { callback.onResult(null) }
248                     }
249                 }
250                 .addOnFailureListener { e ->
251                     Log.w(TAG, "Clearing restore credential failed", e)
252                     var clearException: ClearCredentialException =
253                         ClearCredentialUnknownException(
254                             "Clear restore credential failed for unknown reason."
255                         )
256                     if (e is ApiException) {
257                         when (e.statusCode) {
258                             RestoreCredentialStatusCodes.RESTORE_CREDENTIAL_INTERNAL_FAILURE -> {
259                                 clearException =
260                                     ClearCredentialUnknownException(
261                                         "The restore credential internal service had a failure."
262                                     )
263                             }
264                         }
265                     }
266                     cancellationReviewerWithCallback(cancellationSignal) {
267                         executor.execute { callback.onError(clearException) }
268                     }
269                 }
270         } else {
271             Identity.getSignInClient(context)
272                 .signOut()
273                 .addOnSuccessListener {
274                     cancellationReviewerWithCallback(
275                         cancellationSignal,
276                         {
277                             Log.i(TAG, "During clear credential, signed out successfully!")
278                             executor.execute { callback.onResult(null) }
279                         }
280                     )
281                 }
282                 .addOnFailureListener { e ->
283                     run {
284                         cancellationReviewerWithCallback(
285                             cancellationSignal,
286                             {
287                                 Log.w(TAG, "During clear credential sign out failed with $e")
288                                 executor.execute {
289                                     callback.onError(ClearCredentialUnknownException(e.message))
290                                 }
291                             }
292                         )
293                     }
294                 }
295         }
296     }
297 
298     companion object {
299         private const val TAG = "PlayServicesImpl"
300 
301         // This points to the min APK version of GMS that contains required changes
302         // to make passkeys work well
303         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) const val MIN_GMS_APK_VERSION = 230815045
304         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
305         const val MIN_GMS_APK_VERSION_RESTORE_CRED = 242200000
306         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
307         const val MIN_GMS_APK_VERSION_DIGITAL_CRED = 243100000
308 
309         internal fun cancellationReviewerWithCallback(
310             cancellationSignal: CancellationSignal?,
311             callback: () -> Unit,
312         ) {
313             if (!cancellationReviewer(cancellationSignal)) {
314                 callback()
315             }
316         }
317 
318         internal fun cancellationReviewer(cancellationSignal: CancellationSignal?): Boolean {
319             if (cancellationSignal != null) {
320                 if (cancellationSignal.isCanceled) {
321                     Log.i(TAG, "the flow has been canceled")
322                     return true
323                 }
324             } else {
325                 Log.i(TAG, "No cancellationSignal found")
326             }
327             return false
328         }
329 
330         internal fun isGetSignInIntentRequest(request: GetCredentialRequest): Boolean {
331             for (option in request.credentialOptions) {
332                 if (option is GetSignInWithGoogleOption) {
333                     return true
334                 }
335             }
336             return false
337         }
338 
339         internal fun isGetRestoreCredentialRequest(request: GetCredentialRequest): Boolean {
340             for (option in request.credentialOptions) {
341                 if (option is GetRestoreCredentialOption) {
342                     return true
343                 }
344             }
345             return false
346         }
347 
348         internal fun isDigitalCredentialRequest(request: GetCredentialRequest): Boolean {
349             for (option in request.credentialOptions) {
350                 if (option is GetDigitalCredentialOption) {
351                     return true
352                 }
353             }
354             return false
355         }
356     }
357 }
358