CredentialProviderFactory.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.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting

/**
 * Factory that returns the credential provider to be used by Credential Manager.
 */
internal class CredentialProviderFactory(val context: Context) {

    @set:VisibleForTesting
    @get:VisibleForTesting
    @set:RestrictTo(RestrictTo.Scope.LIBRARY)
    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
    var testMode = false

    @set:VisibleForTesting
    @get:VisibleForTesting
    @set:RestrictTo(RestrictTo.Scope.LIBRARY)
    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
    var testPostUProvider: CredentialProvider? = null

    @set:VisibleForTesting
    @get:VisibleForTesting
    @set:RestrictTo(RestrictTo.Scope.LIBRARY)
    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
    var testPreUProvider: CredentialProvider? = null

    companion object {
        private const val TAG = "CredProviderFactory"
        private const val MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL = Build.VERSION_CODES.TIRAMISU

        /** The metadata key to be used when specifying the provider class name in the
         * android manifest file. */
        private const val CREDENTIAL_PROVIDER_KEY = "androidx.credentials.CREDENTIAL_PROVIDER_KEY"
    }

    /**
     * Returns the best available provider.
     * Pre-U, the provider is determined by the provider library that the developer includes in
     * the app. Developer must not add more than one provider library.
     * Post-U, providers will be registered with the framework, and enabled by the user.
     */
    fun getBestAvailableProvider(shouldFallbackToPreU: Boolean = true): CredentialProvider? {
        if (Build.VERSION.SDK_INT >= 34) { // Android U
            val postUProvider = tryCreatePostUProvider()
            if (postUProvider == null && shouldFallbackToPreU) {
                return tryCreatePreUOemProvider()
            }
            return postUProvider
        } else if (Build.VERSION.SDK_INT <= MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL) {
            return tryCreatePreUOemProvider()
        } else {
            return null
        }
    }

    private fun tryCreatePreUOemProvider(): CredentialProvider? {
        if (testMode) {
            if (testPreUProvider == null) {
                return null
            }
            val isAvailable = testPreUProvider!!.isAvailableOnDevice()
            if (isAvailable) {
                return testPreUProvider
            }
            return null
        }

        val classNames = getAllowedProvidersFromManifest(context)
        if (classNames.isEmpty()) {
            return null
        } else {
            return instantiatePreUProvider(classNames, context)
        }
    }

    @RequiresApi(34)
    private fun tryCreatePostUProvider(): CredentialProvider? {
        if (testMode) {
            if (testPostUProvider == null) {
                return null
            }
            val isAvailable = testPostUProvider!!.isAvailableOnDevice()
            if (isAvailable) {
                return testPostUProvider
            }
            return null
        }

        val provider = CredentialProviderFrameworkImpl(context)
        if (provider.isAvailableOnDevice()) {
            return provider
        }
        return null
    }

    private fun instantiatePreUProvider(classNames: List<String>, context: Context):
        CredentialProvider? {
        var provider: CredentialProvider? = null
        for (className in classNames) {
            try {
                val klass = Class.forName(className)
                val p = klass.getConstructor(Context::class.java).newInstance(context) as
                    CredentialProvider
                if (p.isAvailableOnDevice()) {
                    if (provider != null) {
                        Log.i(TAG, "Only one active OEM CredentialProvider allowed")
                        return null
                    }
                    provider = p
                }
            } catch (_: Throwable) {
            }
        }
        return provider
    }

    @Suppress("deprecation")
    private fun getAllowedProvidersFromManifest(context: Context): List<String> {
        val packageInfo = context.packageManager
            .getPackageInfo(
                context.packageName, PackageManager.GET_META_DATA or
                    PackageManager.GET_SERVICES
            )

        val classNames = mutableListOf<String>()
        if (packageInfo.services != null) {
            for (serviceInfo in packageInfo.services) {
                if (serviceInfo.metaData != null) {
                    val className = serviceInfo.metaData.getString(CREDENTIAL_PROVIDER_KEY)
                    if (className != null) {
                        classNames.add(className)
                    }
                }
            }
        }
        return classNames.toList()
    }
}