CredentialProviderFrameworkImpl.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

import android.annotation.SuppressLint
import android.content.Context
import android.credentials.CredentialManager
import android.os.Build
import android.os.Bundle
import android.os.CancellationSignal
import android.os.OutcomeReceiver
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.ClearCredentialUnknownException
import androidx.credentials.exceptions.ClearCredentialUnsupportedException
import androidx.credentials.exceptions.CreateCredentialCancellationException
import androidx.credentials.exceptions.CreateCredentialCustomException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.CreateCredentialInterruptedException
import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.CreateCredentialUnsupportedException
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialInterruptedException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialUnsupportedException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException
import androidx.credentials.internal.FrameworkImplHelper
import java.util.concurrent.Executor

/**
 * Framework credential provider implementation that allows credential
 * manager requests to be routed to the framework.
 */
@RequiresApi(34)
internal class CredentialProviderFrameworkImpl(context: Context) : CredentialProvider {
    private val credentialManager: CredentialManager? =
        context.getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager?

    override fun onPrepareCredential(
        request: GetCredentialRequest,
        cancellationSignal: CancellationSignal?,
        executor: Executor,
        callback: CredentialManagerCallback<PrepareGetCredentialResponse, GetCredentialException>
    ) {
        if (isCredmanDisabled {
                callback.onError(
                    GetCredentialUnsupportedException(
                        "Your device doesn't support credential manager"
                    )
                )
            }) return
        val outcome = object : OutcomeReceiver<
            android.credentials.PrepareGetCredentialResponse,
            android.credentials.GetCredentialException> {
            override fun onResult(response: android.credentials.PrepareGetCredentialResponse) {
                callback.onResult(convertPrepareGetResponseToJetpackClass(response))
            }

            override fun onError(error: android.credentials.GetCredentialException) {
                callback.onError(convertToJetpackGetException(error))
            }
        }

        credentialManager!!.prepareGetCredential(
            convertGetRequestToFrameworkClass(request),
            cancellationSignal,
            executor,
            outcome
        )
    }

    override fun onGetCredential(
        context: Context,
        pendingGetCredentialHandle: PrepareGetCredentialResponse.PendingGetCredentialHandle,
        cancellationSignal: CancellationSignal?,
        executor: Executor,
        callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
    ) {
        if (isCredmanDisabled {
                callback.onError(
                    GetCredentialUnsupportedException(
                        "Your device doesn't support credential manager"
                    )
                )
            }) return
        val outcome = object : OutcomeReceiver<
            android.credentials.GetCredentialResponse, android.credentials.GetCredentialException> {
            override fun onResult(response: android.credentials.GetCredentialResponse) {
                callback.onResult(convertGetResponseToJetpackClass(response))
            }

            override fun onError(error: android.credentials.GetCredentialException) {
                callback.onError(convertToJetpackGetException(error))
            }
        }

        credentialManager!!.getCredential(
            context,
            pendingGetCredentialHandle.frameworkHandle!!,
            cancellationSignal,
            executor,
            outcome
        )
    }

    override fun onGetCredential(
        context: Context,
        request: GetCredentialRequest,
        cancellationSignal: CancellationSignal?,
        executor: Executor,
        callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
    ) {
        if (isCredmanDisabled {
                callback.onError(
                    GetCredentialUnsupportedException(
                        "Your device doesn't support credential manager"
                    )
                )
            }) return

        val outcome = object : OutcomeReceiver<
            android.credentials.GetCredentialResponse, android.credentials.GetCredentialException> {
            override fun onResult(response: android.credentials.GetCredentialResponse) {
                Log.i(TAG, "GetCredentialResponse returned from framework")
                callback.onResult(convertGetResponseToJetpackClass(response))
            }

            override fun onError(error: android.credentials.GetCredentialException) {
                Log.i(TAG, "GetCredentialResponse error returned from framework")
                callback.onError(convertToJetpackGetException(error))
            }
        }

        credentialManager!!.getCredential(
            context,
            convertGetRequestToFrameworkClass(request),
            cancellationSignal,
            executor,
            outcome
        )
    }

    private fun isCredmanDisabled(handleNullCredMan: () -> Unit): Boolean {
        if (credentialManager == null) {
            handleNullCredMan()
            return true
        }
        return false
    }

    override fun onCreateCredential(
        context: Context,
        request: CreateCredentialRequest,
        cancellationSignal: CancellationSignal?,
        executor: Executor,
        callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>
    ) {
        if (isCredmanDisabled {
                callback.onError(
                    CreateCredentialUnsupportedException(
                        "Your device doesn't support credential manager"
                    )
                )
            }) return
        val outcome = object : OutcomeReceiver<
            android.credentials.CreateCredentialResponse,
            android.credentials.CreateCredentialException> {
            override fun onResult(response: android.credentials.CreateCredentialResponse) {
                Log.i(TAG, "Create Result returned from framework: ")
                callback.onResult(
                    CreateCredentialResponse.createFrom(
                        request.type, response.data
                    )
                )
            }

            override fun onError(error: android.credentials.CreateCredentialException) {
                Log.i(TAG, "CreateCredentialResponse error returned from framework")
                callback.onError(convertToJetpackCreateException(error))
            }
        }

        credentialManager!!.createCredential(
            context,
            convertCreateRequestToFrameworkClass(request, context),
            cancellationSignal,
            executor,
            outcome
        )
    }

    private fun convertCreateRequestToFrameworkClass(
        request: CreateCredentialRequest,
        context: Context
    ): android.credentials.CreateCredentialRequest {
        val createCredentialRequestBuilder: android.credentials.CreateCredentialRequest.Builder =
            android.credentials.CreateCredentialRequest
                .Builder(request.type,
                    FrameworkImplHelper.getFinalCreateCredentialData(request, context),
                    request.candidateQueryData)
                .setIsSystemProviderRequired(request.isSystemProviderRequired)
                .setAlwaysSendAppInfoToProvider(true)
        setOriginForCreateRequest(request, createCredentialRequestBuilder)
        return createCredentialRequestBuilder.build()
    }

    @SuppressLint("MissingPermission")
    private fun setOriginForCreateRequest(
        request: CreateCredentialRequest,
        builder: android.credentials.CreateCredentialRequest.Builder
    ) {
        if (request.origin != null) {
            builder.setOrigin(request.origin)
        }
    }

    private fun convertGetRequestToFrameworkClass(request: GetCredentialRequest):
        android.credentials.GetCredentialRequest {
        val builder = android.credentials.GetCredentialRequest.Builder(
            GetCredentialRequest.toRequestDataBundle(request))
        request.credentialOptions.forEach {
            builder.addCredentialOption(
                android.credentials.CredentialOption.Builder(
                    it.type, it.requestData, it.candidateQueryData
                ).setIsSystemProviderRequired(
                    it.isSystemProviderRequired
                ).setAllowedProviders(it.allowedProviders).build()
            )
        }
        setOriginForGetRequest(request, builder)
        return builder.build()
    }

    @SuppressLint("MissingPermission")
    private fun setOriginForGetRequest(
        request: GetCredentialRequest,
        builder: android.credentials.GetCredentialRequest.Builder
    ) {
        if (request.origin != null) {
            builder.setOrigin(request.origin)
        }
    }

    private fun createFrameworkClearCredentialRequest():
        android.credentials.ClearCredentialStateRequest {
        return android.credentials.ClearCredentialStateRequest(Bundle())
    }

    internal fun convertToJetpackGetException(error: android.credentials.GetCredentialException):
        GetCredentialException {

        return when (error.type) {
            android.credentials.GetCredentialException.TYPE_NO_CREDENTIAL ->
                NoCredentialException(error.message)

            android.credentials.GetCredentialException.TYPE_USER_CANCELED ->
                GetCredentialCancellationException(error.message)

            android.credentials.GetCredentialException.TYPE_INTERRUPTED ->
                GetCredentialInterruptedException(error.message)

            android.credentials.GetCredentialException.TYPE_UNKNOWN ->
                GetCredentialUnknownException(error.message)

            else -> {
                if (error.type.startsWith(GET_DOM_EXCEPTION_PREFIX)) {
                    GetPublicKeyCredentialException.createFrom(error.type, error.message)
                } else {
                    GetCredentialCustomException(error.type, error.message)
                }
            }
        }
    }

    internal fun convertToJetpackCreateException(
        error: android.credentials.CreateCredentialException
    ): CreateCredentialException {
        return when (error.type) {
            android.credentials.CreateCredentialException.TYPE_NO_CREATE_OPTIONS ->
                CreateCredentialNoCreateOptionException(error.message)

            android.credentials.CreateCredentialException.TYPE_USER_CANCELED ->
                CreateCredentialCancellationException(error.message)

            android.credentials.CreateCredentialException.TYPE_INTERRUPTED ->
                CreateCredentialInterruptedException(error.message)

            android.credentials.CreateCredentialException.TYPE_UNKNOWN ->
                CreateCredentialUnknownException(error.message)

            else -> {
                if (error.type.startsWith(CREATE_DOM_EXCEPTION_PREFIX)) {
                    CreatePublicKeyCredentialException.createFrom(error.type, error.message)
                } else {
                    CreateCredentialCustomException(error.type, error.message)
                }
            }
        }
    }

    internal fun convertGetResponseToJetpackClass(
        response: android.credentials.GetCredentialResponse
    ): GetCredentialResponse {
        val credential = response.credential
        return GetCredentialResponse(
            Credential.createFrom(
                credential.type, credential.data
            )
        )
    }

    internal fun convertPrepareGetResponseToJetpackClass(
        response: android.credentials.PrepareGetCredentialResponse
    ): PrepareGetCredentialResponse {
        val handle = PrepareGetCredentialResponse.PendingGetCredentialHandle(
            response.pendingGetCredentialHandle,
        )

        return PrepareGetCredentialResponse.Builder()
            .setFrameworkResponse(response)
            .setPendingGetCredentialHandle(handle)
            .build()
    }

    override fun isAvailableOnDevice(): Boolean {
        return Build.VERSION.SDK_INT >= 34
    }

    override fun onClearCredential(
        request: ClearCredentialStateRequest,
        cancellationSignal: CancellationSignal?,
        executor: Executor,
        callback: CredentialManagerCallback<Void?, ClearCredentialException>
    ) {
        Log.i(TAG, "In CredentialProviderFrameworkImpl onClearCredential")

        if (isCredmanDisabled { ->
                callback.onError(
                    ClearCredentialUnsupportedException(
                        "Your device doesn't support credential manager"
                    )
                )
            }) return

        val outcome = object : OutcomeReceiver<Void,
            android.credentials.ClearCredentialStateException> {
            override fun onResult(response: Void) {
                Log.i(TAG, "Clear result returned from framework: ")
                callback.onResult(response)
            }

            override fun onError(error: android.credentials.ClearCredentialStateException) {
                Log.i(TAG, "ClearCredentialStateException error returned from framework")
                callback.onError(ClearCredentialUnknownException())
            }
        }

        credentialManager!!.clearCredentialState(
            createFrameworkClearCredentialRequest(),
            cancellationSignal,
            executor,
            outcome
        )
    }

    private companion object {
        private const val TAG = "CredManProvService"

        private const val GET_DOM_EXCEPTION_PREFIX =
            GetPublicKeyCredentialDomException.TYPE_GET_PUBLIC_KEY_CREDENTIAL_DOM_EXCEPTION
        private const val CREATE_DOM_EXCEPTION_PREFIX =
            CreatePublicKeyCredentialDomException.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_DOM_EXCEPTION
    }
}