chromium/components/cronet/android/api/src/org/chromium/net/CronetProvider.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.net;

import android.content.Context;
import android.util.Log;

import androidx.annotation.Nullable;

import org.chromium.net.impl.CronetLogger;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * Provides a factory method to create {@link CronetEngine.Builder} instances. A {@code
 * CronetEngine.Builder} instance can be used to create a specific {@link CronetEngine}
 * implementation. To get the list of available {@link CronetProvider}s call {@link
 * #getAllProviderInfos(Context)}.
 *
 * <p><b>NOTE:</b> This class is for advanced users that want to select a particular Cronet
 * implementation. Most users should simply use {@code new} {@link
 * CronetEngine.Builder#CronetEngine.Builder(android.content.Context)}.
 *
 * <p>{@hide}
 */
public abstract class CronetProvider {
    /**
     * String returned by {@link CronetProvider#getName} for {@link CronetProvider} that provides
     * native Cronet implementation packaged inside an application. This implementation offers
     * significantly higher performance relative to the fallback Cronet implementations (see {@link
     * #PROVIDER_NAME_FALLBACK}).
     */
    public static final String PROVIDER_NAME_APP_PACKAGED = "App-Packaged-Cronet-Provider";

    /**
     * String returned by {@link CronetProvider#getName} for {@link CronetProvider} that provides
     * Cronet implementation based on the system's {@link java.net.HttpURLConnection}
     * implementation. This implementation offers significantly degraded performance relative to
     * native Cronet implementations (see {@link #PROVIDER_NAME_APP_PACKAGED}).
     */
    public static final String PROVIDER_NAME_FALLBACK = "Fallback-Cronet-Provider";

    /**
     * The name of an optional key in the app string resource file that contains the class name of
     * an alternative {@code CronetProvider} implementation.
     */
    private static final String RES_KEY_CRONET_IMPL_CLASS = "CronetProviderClassName";

    private static final String TAG = CronetProvider.class.getSimpleName();

    protected final Context mContext;

    protected CronetProvider(Context context) {
        if (context == null) {
            throw new IllegalArgumentException("Context must not be null");
        }
        mContext = context;
    }

    /**
     * Creates and returns an instance of {@link CronetEngine.Builder}.
     * <p/>
     * <b>NOTE:</b> This class is for advanced users that want to select a particular
     * Cronet implementation. Most users should simply use {@code new} {@link
     * CronetEngine.Builder#CronetEngine.Builder(android.content.Context)}.
     *
     * @return {@code CronetEngine.Builder}.
     * @throws IllegalStateException if the provider is not enabled (see {@link #isEnabled}.
     */
    public abstract CronetEngine.Builder createBuilder();

    /**
     * Returns the provider name. The well-know provider names include:
     * <ul>
     *     <li>{@link #PROVIDER_NAME_APP_PACKAGED}</li>
     *     <li>{@link #PROVIDER_NAME_FALLBACK}</li>
     * </ul>
     *
     * @return provider name.
     */
    public abstract String getName();

    /**
     * Returns the provider version. The version can be used to select the newest available provider
     * if multiple providers are available.
     *
     * @return provider version.
     */
    public abstract String getVersion();

    /**
     * Returns whether the provider is enabled and can be used to instantiate the Cronet engine. A
     * provider being out-of-date (older than the API) and needing updating is one potential reason
     * it could be disabled. Please read the provider documentation for enablement procedure.
     *
     * @return {@code true} if the provider is enabled.
     */
    public abstract boolean isEnabled();

    @Override
    public String toString() {
        return "["
                + "class="
                + getClass().getName()
                + ", "
                + "name="
                + getName()
                + ", "
                + "version="
                + getVersion()
                + ", "
                + "enabled="
                + isEnabled()
                + "]";
    }

    /** Name of the Java {@link CronetProvider} class. */
    private static final String JAVA_CRONET_PROVIDER_CLASS =
            "org.chromium.net.impl.JavaCronetProvider";

    /** Name of the native {@link CronetProvider} class. */
    private static final String NATIVE_CRONET_PROVIDER_CLASS =
            "org.chromium.net.impl.NativeCronetProvider";

    /** {@link CronetProvider} class that is packaged with Google Play Services. */
    private static final String PLAY_SERVICES_CRONET_PROVIDER_CLASS =
            "com.google.android.gms.net.PlayServicesCronetProvider";

    /**
     * {@link CronetProvider} a deprecated class that may be packaged with some old versions of
     * Google Play Services.
     */
    private static final String GMS_CORE_CRONET_PROVIDER_CLASS =
            "com.google.android.gms.net.GmsCoreCronetProvider";

    static final class ProviderInfo {
        public CronetProvider provider;
        public CronetLogger.CronetSource logSource;

        // Delegate ProviderInfo comparisons to `provider`. This actually matters in some cases such
        // as PLAY_SERVICES vs GMS_CORE, see b/329440572.

        @Override
        public int hashCode() {
            return provider.hashCode();
        }

        @Override
        public boolean equals(@Nullable Object other) {
            return other instanceof ProviderInfo
                    && provider.equals(((ProviderInfo) other).provider);
        }
    }

    /**
     * Returns an unmodifiable list of all available {@link CronetProvider}s. The providers are
     * returned in no particular order. Some of the returned providers may be in a disabled state
     * and should be enabled by the invoker. See {@link CronetProvider#isEnabled()}.
     *
     * @return the list of available providers.
     */
    public static List<CronetProvider> getAllProviders(Context context) {
        var providers = new ArrayList<CronetProvider>();
        for (var providerInfo : getAllProviderInfos(context)) {
            providers.add(providerInfo.provider);
        }
        return Collections.unmodifiableList(providers);
    }

    /**
     * Same as {@link #getAllProviders}, but returning the providerInfos directly.
     *
     * @return the list of available providerInfos.
     */
    static List<ProviderInfo> getAllProviderInfos(Context context) {
        // Use LinkedHashSet to preserve the order and eliminate duplicate providers.
        Set<ProviderInfo> providers = new LinkedHashSet<>();
        addCronetProviderFromResourceFile(
                context, CronetLogger.CronetSource.CRONET_SOURCE_UNSPECIFIED, providers);
        addCronetProviderImplByClassName(
                context,
                PLAY_SERVICES_CRONET_PROVIDER_CLASS,
                CronetLogger.CronetSource.CRONET_SOURCE_PLAY_SERVICES,
                providers,
                false);
        addCronetProviderImplByClassName(
                context,
                GMS_CORE_CRONET_PROVIDER_CLASS,
                CronetLogger.CronetSource.CRONET_SOURCE_PLAY_SERVICES,
                providers,
                false);
        addCronetProviderImplByClassName(
                context,
                NATIVE_CRONET_PROVIDER_CLASS,
                CronetLogger.CronetSource.CRONET_SOURCE_STATICALLY_LINKED,
                providers,
                false);
        addCronetProviderImplByClassName(
                context,
                JAVA_CRONET_PROVIDER_CLASS,
                CronetLogger.CronetSource.CRONET_SOURCE_FALLBACK,
                providers,
                false);
        return Collections.unmodifiableList(new ArrayList<>(providers));
    }

    /**
     * Attempts to add a new provider referenced by the class name to a set.
     *
     * @param className the class name of the provider that should be instantiated.
     * @param providers the set of providers to add the new provider to.
     * @return {@code true} if the provider was added to the set; {@code false} if the provider
     *     couldn't be instantiated.
     */
    private static boolean addCronetProviderImplByClassName(
            Context context,
            String className,
            CronetLogger.CronetSource logSource,
            Set<ProviderInfo> providers,
            boolean logError) {
        ClassLoader loader = context.getClassLoader();
        try {
            Class<? extends CronetProvider> providerClass =
                    loader.loadClass(className).asSubclass(CronetProvider.class);
            Constructor<? extends CronetProvider> ctor =
                    providerClass.getConstructor(Context.class);
            var providerInfo = new ProviderInfo();
            providerInfo.provider = ctor.newInstance(context);
            providerInfo.logSource = logSource;
            providers.add(providerInfo);
            return true;
        } catch (InstantiationException e) {
            logReflectiveOperationException(className, logError, e);
        } catch (InvocationTargetException e) {
            logReflectiveOperationException(className, logError, e);
        } catch (NoSuchMethodException e) {
            logReflectiveOperationException(className, logError, e);
        } catch (IllegalAccessException e) {
            logReflectiveOperationException(className, logError, e);
        } catch (ClassNotFoundException e) {
            logReflectiveOperationException(className, logError, e);
        }
        return false;
    }

    /**
     * De-duplicates exception handling logic in {@link #addCronetProviderImplByClassName}. It
     * should be removed when support of API Levels lower than 19 is deprecated.
     */
    private static void logReflectiveOperationException(
            String className, boolean logError, Exception e) {
        if (logError) {
            Log.e(TAG, "Unable to load provider class: " + className, e);
        } else {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(
                        TAG,
                        "Tried to load "
                                + className
                                + " provider class but it wasn't"
                                + " included in the app classpath");
            }
        }
    }

    /**
     * Attempts to add a provider specified in the app resource file to a set.
     *
     * @param providers the set of providers to add the new provider to.
     * @return {@code true} if the provider was added to the set; {@code false} if the app resources
     *     do not include the string with {@link #RES_KEY_CRONET_IMPL_CLASS} key.
     * @throws RuntimeException if the provider cannot be found or instantiated.
     */
    // looking up resources from other apps requires the use of getIdentifier()
    @SuppressWarnings("DiscouragedApi")
    private static boolean addCronetProviderFromResourceFile(
            Context context, CronetLogger.CronetSource logSource, Set<ProviderInfo> providers) {
        int resId =
                context.getResources()
                        .getIdentifier(
                                RES_KEY_CRONET_IMPL_CLASS, "string", context.getPackageName());
        // Resource not found
        if (resId == 0) {
            // The resource wasn't included in the app; therefore, there is nothing to add.
            return false;
        }
        String className = context.getResources().getString(resId);

        // If the resource specifies a well known provider, don't load it because
        // there will be an attempt to load it anyways.
        if (className == null
                || className.equals(PLAY_SERVICES_CRONET_PROVIDER_CLASS)
                || className.equals(GMS_CORE_CRONET_PROVIDER_CLASS)
                || className.equals(JAVA_CRONET_PROVIDER_CLASS)
                || className.equals(NATIVE_CRONET_PROVIDER_CLASS)) {
            return false;
        }

        if (!addCronetProviderImplByClassName(context, className, logSource, providers, true)) {
            Log.e(
                    TAG,
                    "Unable to instantiate Cronet implementation class "
                            + className
                            + " that is listed as in the app string resource file under "
                            + RES_KEY_CRONET_IMPL_CLASS
                            + " key");
        }
        return true;
    }
}