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