HiddenActivity.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.app.Activity
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentSender
import android.os.Bundle
import android.os.ResultReceiver
import android.util.Log
import androidx.credentials.exceptions.CreateCredentialInterruptedException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialInterruptedException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.playservices.controllers.CredentialProviderBaseController
import com.google.android.gms.auth.api.identity.BeginSignInRequest
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.auth.api.identity.SavePasswordRequest
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.fido.Fido
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions

/**
 * An activity used to ensure all required API versions work as intended.
 * @hide
 */
@Suppress("Deprecation", "ForbiddenSuperClass")
open class HiddenActivity : Activity() {

    private var resultReceiver: ResultReceiver? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        overridePendingTransition(0, 0)
        val type: String? = intent.getStringExtra(CredentialProviderBaseController.TYPE_TAG)
        resultReceiver = intent.getParcelableExtra(
            CredentialProviderBaseController.RESULT_RECEIVER_TAG)

        if (resultReceiver == null) {
            finish()
        }

        when (type) {
            CredentialProviderBaseController.BEGIN_SIGN_IN_TAG -> {
                handleBeginSignIn()
            }
            CredentialProviderBaseController.CREATE_PASSWORD_TAG -> {
                handleCreatePassword()
            }
            CredentialProviderBaseController.CREATE_PUBLIC_KEY_CREDENTIAL_TAG -> {
                handleCreatePublicKeyCredential()
            } else -> {
                Log.w(TAG, "Activity handed an unsupported type")
                finish()
            }
        }
    }

    private fun handleCreatePublicKeyCredential() {
        val fidoRegistrationRequest: PublicKeyCredentialCreationOptions? = intent
            .getParcelableExtra(CredentialProviderBaseController.REQUEST_TAG)
        val requestCode: Int = intent.getIntExtra(
            CredentialProviderBaseController.ACTIVITY_REQUEST_CODE_TAG,
                DEFAULT_VALUE)
        fidoRegistrationRequest?.let {
            Fido.getFido2ApiClient(this)
                .getRegisterPendingIntent(fidoRegistrationRequest)
                .addOnSuccessListener { result: PendingIntent ->
                    try {
                        startIntentSenderForResult(
                            result.intentSender,
                            requestCode,
                            null, /* fillInIntent= */
                            0, /* flagsMask= */
                            0, /* flagsValue= */
                            0, /* extraFlags= */
                            null /* options= */
                        )
                    } catch (e: IntentSender.SendIntentException) {
                        setupFailure(resultReceiver!!,
                            CreateCredentialUnknownException::class.java.name,
                            "During public key credential, found IntentSender " +
                                "failure on public key creation: ${e.message}")
                    }
                }
                .addOnFailureListener { e: Exception ->
                    var errName: String = CreateCredentialUnknownException::class.java.name
                    if (e is ApiException && e.statusCode in
                        CredentialProviderBaseController.retryables) {
                        errName = CreateCredentialInterruptedException::class.java.name
                    }
                    setupFailure(resultReceiver!!, errName,
                        "During create public key credential, fido registration " +
                            "failure: ${e.message}")
                }
        } ?: run {
            Log.w(TAG, "During create public key credential, request is null, so nothing to " +
                "launch for public key credentials")
            finish()
        }
    }

    private fun setupFailure(resultReceiver: ResultReceiver, errName: String, errMsg: String) {
        val bundle = Bundle()
        bundle.putBoolean(CredentialProviderBaseController.FAILURE_RESPONSE_TAG, true)
        bundle.putString(CredentialProviderBaseController.EXCEPTION_TYPE_TAG, errName)
        bundle.putString(CredentialProviderBaseController.EXCEPTION_MESSAGE_TAG, errMsg)
        resultReceiver.send(Integer.MAX_VALUE, bundle)
        finish()
    }

    private fun handleBeginSignIn() {
        val params: BeginSignInRequest? = intent.getParcelableExtra(
            CredentialProviderBaseController.REQUEST_TAG)
        val requestCode: Int = intent.getIntExtra(
            CredentialProviderBaseController.ACTIVITY_REQUEST_CODE_TAG,
            DEFAULT_VALUE)
        params?.let {
            Identity.getSignInClient(this).beginSignIn(params).addOnSuccessListener {
                try {
                    startIntentSenderForResult(
                        it.pendingIntent.intentSender,
                        requestCode,
                        null,
                        0,
                        0,
                        0,
                        null
                    )
                } catch (e: IntentSender.SendIntentException) {
                    setupFailure(resultReceiver!!,
                        GetCredentialUnknownException::class.java.name,
                            "During begin sign in, one tap ui intent sender " +
                                "failure: ${e.message}")
                }
            }.addOnFailureListener { e: Exception ->
                var errName: String = NoCredentialException::class.java.name
                if (e is ApiException && e.statusCode in
                    CredentialProviderBaseController.retryables) {
                    errName = GetCredentialInterruptedException::class.java.name
                }
                setupFailure(resultReceiver!!, errName,
                    "During begin sign in, failure response from one tap: ${e.message}")
            }
        } ?: run {
            Log.i(TAG, "During begin sign in, params is null, nothing to launch for " +
                "begin sign in")
            finish()
        }
    }

    private fun handleCreatePassword() {
        val params: SavePasswordRequest? = intent.getParcelableExtra(
            CredentialProviderBaseController.REQUEST_TAG)
        val requestCode: Int = intent.getIntExtra(
            CredentialProviderBaseController.ACTIVITY_REQUEST_CODE_TAG,
            DEFAULT_VALUE)
        params?.let {
            Identity.getCredentialSavingClient(this).savePassword(params)
                .addOnSuccessListener {
                    try {
                        startIntentSenderForResult(
                            it.pendingIntent.intentSender,
                            requestCode,
                            null,
                            0,
                            0,
                            0,
                            null
                        )
                    } catch (e: IntentSender.SendIntentException) {
                        setupFailure(resultReceiver!!,
                            GetCredentialUnknownException::class.java.name,
                                "During save password, found UI intent sender " +
                                    "failure: ${e.message}")
                    }
            }.addOnFailureListener { e: Exception ->
                    var errName: String = CreateCredentialUnknownException::class.java.name
                    if (e is ApiException && e.statusCode in
                        CredentialProviderBaseController.retryables) {
                        errName = CreateCredentialInterruptedException::class.java.name
                    }
                    setupFailure(resultReceiver!!, errName, "During save password, found " +
                        "password failure response from one tap ${e.message}")
            }
        } ?: run {
            Log.i(TAG, "During save password, params is null, nothing to launch for create" +
                " password")
            finish()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        val bundle = Bundle()
        bundle.putBoolean(CredentialProviderBaseController.FAILURE_RESPONSE_TAG, false)
        bundle.putInt(CredentialProviderBaseController.ACTIVITY_REQUEST_CODE_TAG, requestCode)
        bundle.putParcelable(CredentialProviderBaseController.RESULT_DATA_TAG, data)
        resultReceiver?.send(resultCode, bundle)
        finish()
    }

    companion object {
        private const val DEFAULT_VALUE: Int = 1
        private val TAG: String = HiddenActivity::class.java.name
    }
}