chromium/components/cronet/android/java/src/org/chromium/net/impl/CronetLibraryLoader.java

// Copyright 2015 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.impl;

import android.content.Context;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.util.Pair;

import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.net.httpflags.BaseFeature;
import org.chromium.net.httpflags.Flags;
import org.chromium.net.httpflags.HttpFlagsLoader;
import org.chromium.net.httpflags.ResolvedFlags;
import org.chromium.net.telemetry.Hash;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.annotation.concurrent.GuardedBy;

/** CronetLibraryLoader loads and initializes native library on init thread. */
@JNINamespace("cronet")
@VisibleForTesting
public class CronetLibraryLoader {
    // Synchronize initialization.
    private static final Object sLoadLock = new Object();

    @GuardedBy("sLoadLock")
    private static boolean sInitialized;

    private static final String LIBRARY_NAME = "cronet." + ImplVersion.getCronetVersion();
    @VisibleForTesting public static final String TAG = CronetLibraryLoader.class.getSimpleName();
    // Thread used for initialization work and processing callbacks for
    // long-lived global singletons. This thread lives forever as things like
    // the global singleton NetworkChangeNotifier live on it and are never killed.
    private static final HandlerThread sInitThread = new HandlerThread("CronetInit");
    // Block calling native methods until this ConditionVariable opens to indicate loadLibrary()
    // is completed and native methods have been registered.
    private static final ConditionVariable sWaitForLibLoad = new ConditionVariable();

    private static final ConditionVariable sHttpFlagsLoaded = new ConditionVariable();
    private static ResolvedFlags sHttpFlags;

    /**
     * A subset of {@code CronetLogger.CronetInitializedInfo} that this class is responsible for
     * populating.
     */
    public static final class CronetInitializedInfo {
        public int httpFlagsLatencyMillis = -1;
        public Boolean httpFlagsSuccessful;
        public List<Long> httpFlagsNames;
        public List<Long> httpFlagsValues;
    }

    private static CronetInitializedInfo sInitializedInfo;

    @VisibleForTesting public static final String LOG_FLAG_NAME = "Cronet_log_me";

    /**
     * Ensure that native library is loaded and initialized. Can be called from any thread, the load
     * and initialization is performed on init thread.
     *
     * @return True if the library was initialized as part of this call, false if it was already
     *     initialized.
     */
    public static boolean ensureInitialized(
            Context applicationContext, final CronetEngineBuilderImpl builder) {
        return ensureInitialized(applicationContext, builder, /* libAlreadyLoaded= */ false);
    }

    public static boolean ensureInitialized(
            Context applicationContext,
            final CronetEngineBuilderImpl builder,
            boolean libAlreadyLoaded) {
        synchronized (sLoadLock) {
            if (sInitialized) return false;
            ContextUtils.initApplicationContext(applicationContext);
            // The init thread may already be running if a previous initialization attempt failed.
            // In this case there is no need to spin it up again.
            //
            // Note: if we never succeed in loading the library, the init thread will end up
            // blocking on `sWaitForLibLoad` forever. Obviously this is suboptimal, but given this
            // is not supposed to fail, it's arguably benign.
            if (!sInitThread.isAlive()) {
                sInitThread.start();
                postToInitThread(
                        () -> {
                            initializeOnInitThread();
                        });
            }
            if (!libAlreadyLoaded) {
                if (builder.libraryLoader() != null) {
                    builder.libraryLoader().loadLibrary(LIBRARY_NAME);
                } else {
                    System.loadLibrary(LIBRARY_NAME);
                }
            }
            CronetLibraryLoaderJni.get().nativeInit();
            String implVersion = ImplVersion.getCronetVersion();
            if (!implVersion.equals(CronetLibraryLoaderJni.get().getCronetVersion())) {
                throw new RuntimeException(
                        String.format(
                                "Expected Cronet version number %s, " + "actual version number %s.",
                                implVersion, CronetLibraryLoaderJni.get().getCronetVersion()));
            }
            Log.i(TAG, "Cronet version: %s, arch: %s", implVersion, System.getProperty("os.arch"));
            setNativeLoggingLevel();
            sWaitForLibLoad.open();
            sInitialized = true;
            return true;
        }
    }

    private static void setNativeLoggingLevel() {
        // The constants used here should be kept in sync with logging::LogMessage::~LogMessage().
        final String nativeLogTag = "chromium";
        int loggingLevel;
        // TODO: this way of enabling VLOG is a hack - it doesn't make a ton of sense because
        // logging::LogMessage() will still log VLOG() at the Android INFO log level, not DEBUG or
        // VERBOSE; also this doesn't make it possible to use advanced filters like --vmodule. See
        // https://crbug.com/1488393 for a proposed alternative.
        if (Log.isLoggable(nativeLogTag, Log.VERBOSE)) {
            loggingLevel = -2; // VLOG(2)
        } else if (Log.isLoggable(nativeLogTag, Log.DEBUG)) {
            loggingLevel = -1; // VLOG(1)
        } else {
            // Use the default log level, which logs everything except VLOG(). Skip the
            // setMinLogLevel() call to avoid paying for an unnecessary JNI transition.
            return;
        }
        CronetLibraryLoaderJni.get().setMinLogLevel(loggingLevel);
    }

    /** Returns {@code true} if running on the initialization thread. */
    private static boolean onInitThread() {
        return sInitThread.getLooper() == Looper.myLooper();
    }

    /**
     * Runs Cronet initialization tasks on the init thread. Ensures that HTTP flags are loaded, the
     * NetworkChangeNotifier is initialzied and the init thread native MessageLoop is initialized.
     */
    static void initializeOnInitThread() {
        assert onInitThread();
        assert sInitializedInfo == null;
        sInitializedInfo = new CronetInitializedInfo();

        var httpFlagsLoadingStartUptimeMillis = SystemClock.uptimeMillis();
        var applicationContext = ContextUtils.getApplicationContext();
        // Load HTTP flags. This is a potentially expensive call, so we do this in parallel with
        // library loading in the hope of minimizing impact on Cronet initialization latency.
        assert sHttpFlags == null;
        Flags flags;
        if (!CronetManifest.shouldReadHttpFlags(applicationContext)) {
            Log.d(TAG, "Not loading HTTP flags because they are disabled in the manifest");
            flags = null;
        } else {
            flags = HttpFlagsLoader.load(applicationContext);
            sInitializedInfo.httpFlagsSuccessful = flags != null;
        }
        sHttpFlags =
                ResolvedFlags.resolve(
                        flags != null ? flags : Flags.newBuilder().build(),
                        applicationContext.getPackageName(),
                        ImplVersion.getCronetVersion());
        sHttpFlagsLoaded.open();
        ResolvedFlags.Value logMe = sHttpFlags.flags().get(LOG_FLAG_NAME);
        if (logMe != null) {
            Log.i(TAG, "HTTP flags log line: %s", logMe.getStringValue());
        }
        populateCronetInitializedHttpFlagNamesValues();
        sInitializedInfo.httpFlagsLatencyMillis =
                (int) (SystemClock.uptimeMillis() - httpFlagsLoadingStartUptimeMillis);

        NetworkChangeNotifier.init();
        // Registers to always receive network notifications. Note
        // that this call is fine for Cronet because Cronet
        // embedders do not have API access to create network change
        // observers. Existing observers in the net stack do not
        // perform expensive work.
        NetworkChangeNotifier.registerToReceiveNotificationsAlways();
        // Wait for loadLibrary() to complete so JNI is registered.
        sWaitForLibLoad.block();

        // registerToReceiveNotificationsAlways() is called before the native
        // NetworkChangeNotifierAndroid is created, so as to avoid receiving
        // the undesired initial network change observer notification, which
        // will cause active requests to fail with ERR_NETWORK_CHANGED.
        CronetLibraryLoaderJni.get().cronetInitOnInitThread();
    }

    private static void populateCronetInitializedHttpFlagNamesValues() {
        // Make sure the order is deterministic - this may potentially make it easier to
        // deduplicate/aggregate the log entries down the line, by preventing two log entries from
        // being treated as different even though they have the same set of flag names and values.
        // Note we need to pair up the names and values before we do this, as we need the order to
        // be consistent between the two.
        var hashedNamesValues = new ArrayList<Pair<Long, Long>>();
        for (var flag : sHttpFlags.flags().entrySet()) {
            hashedNamesValues.add(
                    new Pair<Long, Long>(
                            Hash.hash(flag.getKey()),
                            hashHttpFlagValueForLogging(flag.getValue())));
        }
        Collections.sort(hashedNamesValues, (left, right) -> left.first.compareTo(right.first));

        sInitializedInfo.httpFlagsNames = new ArrayList<Long>();
        sInitializedInfo.httpFlagsValues = new ArrayList<Long>();
        for (var hashedNameValue : hashedNamesValues) {
            sInitializedInfo.httpFlagsNames.add(hashedNameValue.first);
            sInitializedInfo.httpFlagsValues.add(hashedNameValue.second);
        }
    }

    private static long hashHttpFlagValueForLogging(ResolvedFlags.Value value) {
        switch (value.getType()) {
            case BOOL:
                return value.getBoolValue() ? 1 : 0;
            case INT:
                return value.getIntValue();
            case FLOAT:
                // Converting to double first to avoid precision issues (e.g. 42.5 would end up as
                // 42500001792 instead of 42500000000 otherwise)
                return Math.round((double) value.getFloatValue() * 1_000_000_000d);
            case STRING:
                return Hash.hash(value.getStringValue());
            case BYTES:
                return Hash.hash(value.getBytesValue().toByteArray());
            default:
                throw new IllegalArgumentException(
                        "Unexpected flag value type: " + value.getClass().getName());
        }
    }

    /**
     * Retrieves the initialization info for logging. Only safe to call after the init thread has
     * become ready.
     */
    public static CronetInitializedInfo getCronetInitializedInfo() {
        assert sInitializedInfo != null;
        return sInitializedInfo;
    }

    /** Run {@code r} on the initialization thread. */
    public static void postToInitThread(Runnable r) {
        if (onInitThread()) {
            r.run();
        } else {
            new Handler(sInitThread.getLooper()).post(r);
        }
    }

    /**
     * Returns the HTTP flags that apply to this instance of the Cronet library.
     *
     * <p>Never returns null: if HTTP flags were not loaded, will return an empty set of flags.
     *
     * <p>This function will deadlock if {@link #ensureInitialized} is not called.
     */
    public static ResolvedFlags getHttpFlags() {
        sHttpFlagsLoaded.block();
        return sHttpFlags;
    }

    /**
     * Called by native Cronet library early initialization code to obtain the values of native
     * base::Feature overrides that will be applied for the entire lifetime of the Cronet native
     * library.
     *
     * <p>Note that this call sits in the critical path of native library initialization, as
     * practically no Chromium native code can run until base::Feature values have settled.
     *
     * @return The base::Feature overrides as a binary serialized {@link
     *     org.chromium.net.httpflags.BaseFeatureOverrides} proto.
     */
    @CalledByNative
    private static byte[] getBaseFeatureOverrides() {
        return BaseFeature.getOverrides(getHttpFlags()).toByteArray();
    }

    /**
     * Called from native library to get default user agent constructed using application context.
     * May be called on any thread.
     *
     * <p>Expects that ContextUtils.initApplicationContext() was called already either by some
     * testing framework or an embedder constructing a Java CronetEngine via
     * CronetEngine.Builder.build().
     */
    @CalledByNative
    private static String getDefaultUserAgent() {
        return UserAgent.from(ContextUtils.getApplicationContext());
    }

    /**
     * Called from native library to ensure that library is initialized.
     * May be called on any thread, but initialization is performed on
     * this.sInitThread.
     *
     * Expects that ContextUtils.initApplicationContext() was called already
     * either by some testing framework or an embedder constructing a Java
     * CronetEngine via CronetEngine.Builder.build().
     *
     * TODO(mef): In the long term this should be changed to some API with
     * lower overhead like CronetEngine.Builder.loadNativeCronet().
     */
    @CalledByNative
    private static void ensureInitializedFromNative() {
        // The application context must already be initialized
        // using ContextUtils.initApplicationContext().
        Context applicationContext = ContextUtils.getApplicationContext();
        assert applicationContext != null;
        ensureInitialized(applicationContext, null, /* libAlreadyLoaded= */ true);
    }

    @CalledByNative
    private static void setNetworkThreadPriorityOnNetworkThread(int priority) {
        Log.d(TAG, "Setting network thread priority to " + priority);
        Process.setThreadPriority(priority);
    }

    @NativeMethods
    interface Natives {
        // Native methods are implemented in cronet_library_loader.cc.
        void nativeInit();

        void cronetInitOnInitThread();

        String getCronetVersion();

        void setMinLogLevel(int loggingLevel);
    }
}