CredentialProviderPlayServicesImpl.kt
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.credentials.playservices
import android.content.Context
import android.os.CancellationSignal
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.credentials.ClearCredentialStateRequest
import androidx.credentials.CreateCredentialRequest
import androidx.credentials.CreateCredentialResponse
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CredentialManagerCallback
import androidx.credentials.CredentialProvider
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.ClearCredentialUnknownException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.CredentialProviderCreatePublicKeyCredentialController
import androidx.credentials.playservices.controllers.GetSignInIntent.CredentialProviderGetSignInIntentController
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import java.util.concurrent.Executor
/**
* Entry point of all credential manager requests to the play-services-auth
* module.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Suppress("deprecation")
class CredentialProviderPlayServicesImpl(private val context: Context) : CredentialProvider {
@VisibleForTesting
var googleApiAvailability = GoogleApiAvailability.getInstance()
override fun onGetCredential(
context: Context,
request: GetCredentialRequest,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
) {
if (cancellationReviewer(cancellationSignal)) { return }
if (isGetSignInIntentRequest(request)) {
CredentialProviderGetSignInIntentController(context).invokePlayServices(
request, callback, executor, cancellationSignal
)
} else {
CredentialProviderBeginSignInController(context).invokePlayServices(
request, callback, executor, cancellationSignal
)
}
}
@SuppressWarnings("deprecated")
override fun onCreateCredential(
context: Context,
request: CreateCredentialRequest,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>
) {
if (cancellationReviewer(cancellationSignal)) { return }
when (request) {
is CreatePasswordRequest -> {
CredentialProviderCreatePasswordController.getInstance(
context).invokePlayServices(
request,
callback,
executor,
cancellationSignal)
}
is CreatePublicKeyCredentialRequest -> {
CredentialProviderCreatePublicKeyCredentialController.getInstance(
context).invokePlayServices(
request,
callback,
executor,
cancellationSignal)
}
else -> {
throw UnsupportedOperationException(
"Create Credential request is unsupported, not password or " +
"publickeycredential")
}
}
}
override fun isAvailableOnDevice(): Boolean {
val resultCode = isGooglePlayServicesAvailable(context)
val isSuccessful = resultCode == ConnectionResult.SUCCESS
if (!isSuccessful) {
val connectionResult = ConnectionResult(resultCode)
Log.w(TAG, "Connection with Google Play Services was not " +
"successful. Connection result is: " + connectionResult.toString())
}
return isSuccessful
}
// https://developers.google.com/android/reference/com/google/android/gms/common/ConnectionResult
// There is one error code that supports retry API_DISABLED_FOR_CONNECTION but it would not
// be useful to retry that one because our connection to GMSCore is a static variable
// (see GoogleApiAvailability.getInstance()) so we cannot recreate the connection to retry.
private fun isGooglePlayServicesAvailable(context: Context): Int {
return googleApiAvailability.isGooglePlayServicesAvailable(
context, /*minApkVersion=*/ MIN_GMS_APK_VERSION)
}
override fun onClearCredential(
request: ClearCredentialStateRequest,
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<Void?, ClearCredentialException>
) {
if (cancellationReviewer(cancellationSignal)) { return }
Identity.getSignInClient(context)
.signOut()
.addOnSuccessListener {
cancellationReviewerWithCallback(cancellationSignal, {
Log.i(TAG, "During clear credential, signed out successfully!")
executor.execute { callback.onResult(null) }
})
}
.addOnFailureListener { e ->
run {
cancellationReviewerWithCallback(cancellationSignal, {
Log.w(TAG, "During clear credential sign out failed with $e")
executor.execute {
callback.onError(ClearCredentialUnknownException(e.message))
}
})
}
}
}
companion object {
private const val TAG = "PlayServicesImpl"
// This points to the min APK version of GMS that contains required changes
// to make passkeys work well
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
const val MIN_GMS_APK_VERSION = 230815045
internal fun cancellationReviewerWithCallback(
cancellationSignal: CancellationSignal?,
callback: () -> Unit,
) {
if (!cancellationReviewer(cancellationSignal)) {
callback()
}
}
internal fun cancellationReviewer(
cancellationSignal: CancellationSignal?
): Boolean {
if (cancellationSignal != null) {
if (cancellationSignal.isCanceled) {
Log.i(TAG, "the flow has been canceled")
return true
}
} else {
Log.i(TAG, "No cancellationSignal found")
}
return false
}
internal fun isGetSignInIntentRequest(request: GetCredentialRequest): Boolean {
for (option in request.credentialOptions) {
if (option is GetSignInWithGoogleOption) {
return true
}
}
return false
}
}
}