chromium/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumAwInit.java

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

package com.android.webview.chromium;

import android.Manifest;
import android.app.compat.CompatChanges;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
import android.webkit.WebSettings;
import android.webkit.WebStorage;
import android.webkit.WebViewDatabase;

import androidx.annotation.IntDef;

import com.android.webview.chromium.WebViewChromium.ApiCall;

import org.chromium.android_webview.AwBrowserContext;
import org.chromium.android_webview.AwBrowserProcess;
import org.chromium.android_webview.AwClassPreloader;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwContentsStatics;
import org.chromium.android_webview.AwCookieManager;
import org.chromium.android_webview.AwCrashyClassUtils;
import org.chromium.android_webview.AwDarkMode;
import org.chromium.android_webview.AwFeatureMap;
import org.chromium.android_webview.AwLocaleConfig;
import org.chromium.android_webview.AwNetworkChangeNotifierRegistrationPolicy;
import org.chromium.android_webview.AwProxyController;
import org.chromium.android_webview.AwServiceWorkerController;
import org.chromium.android_webview.AwThreadUtils;
import org.chromium.android_webview.AwTracingController;
import org.chromium.android_webview.HttpAuthDatabase;
import org.chromium.android_webview.ProductConfig;
import org.chromium.android_webview.R;
import org.chromium.android_webview.WebViewChromiumRunQueue;
import org.chromium.android_webview.common.AwFeatures;
import org.chromium.android_webview.common.AwResource;
import org.chromium.android_webview.common.AwSwitches;
import org.chromium.android_webview.common.Lifetime;
import org.chromium.android_webview.gfx.AwDrawFnImpl;
import org.chromium.android_webview.variations.FastVariationsSeedSafeModeAction;
import org.chromium.android_webview.variations.VariationsSeedLoader;
import org.chromium.base.BuildInfo;
import org.chromium.base.BundleUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.FieldTrialList;
import org.chromium.base.JNIUtils;
import org.chromium.base.PathService;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.build.BuildConfig;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.ResourceBundle;

/**
 * Class controlling the Chromium initialization for WebView.
 * We hold on to most static objects used by WebView here.
 * This class is shared between the webkit glue layer and the support library glue layer.
 */
@Lifetime.Singleton
public class WebViewChromiumAwInit {
    private static final String TAG = "WebViewChromiumAwInit";

    private static final String HTTP_AUTH_DATABASE_FILE = "http_auth.db";

    // TODO(gsennton): store aw-objects instead of adapters here
    // Initialization guarded by mLock.
    private AwBrowserContext mDefaultBrowserContext;
    private AwTracingController mTracingController;
    private SharedStatics mSharedStatics;
    private GeolocationPermissionsAdapter mDefaultGeolocationPermissions;
    private CookieManagerAdapter mDefaultCookieManager;

    private WebIconDatabaseAdapter mWebIconDatabase;
    private WebStorageAdapter mDefaultWebStorage;
    private WebViewDatabaseAdapter mDefaultWebViewDatabase;
    private AwServiceWorkerController mDefaultServiceWorkerController;
    private AwTracingController mAwTracingController;
    private VariationsSeedLoader mSeedLoader;
    private Thread mSetUpResourcesThread;
    private AwProxyController mAwProxyController;

    // Guards accees to the other members, and is notifyAll() signalled on the UI thread
    // when the chromium process has been started.
    // This member is not private only because the downstream subclass needs to access it,
    // it shouldn't be accessed from anywhere else.
    /* package */ final Object mLock = new Object();

    // mInitState should only transition INIT_NOT_STARTED -> INIT_STARTED -> INIT_FINISHED
    private static final int INIT_NOT_STARTED = 0;
    private static final int INIT_STARTED = 1;
    private static final int INIT_FINISHED = 2;
    // Read/write protected by mLock
    private int mInitState;

    private final WebViewChromiumFactoryProvider mFactory;

    // This enum must be kept in sync with WebViewStartup.CallSite in chrome_track_event.proto and
    // WebViewStartupCallSite in enums.xml.
    @IntDef({
        CallSite.GET_AW_TRACING_CONTROLLER,
        CallSite.GET_AW_PROXY_CONTROLLER,
        CallSite.WEBVIEW_INSTANCE,
        CallSite.GET_STATICS,
        CallSite.GET_DEFAULT_GEOLOCATION_PERMISSIONS,
        CallSite.GET_DEFAULT_SERVICE_WORKER_CONTROLLER,
        CallSite.GET_WEB_ICON_DATABASE,
        CallSite.GET_DEFAULT_WEB_STORAGE,
        CallSite.GET_DEFAULT_WEBVIEW_DATABASE,
        CallSite.GET_TRACING_CONTROLLER,
        CallSite.COUNT,
    })
    public @interface CallSite {
        int GET_AW_TRACING_CONTROLLER = 0;
        int GET_AW_PROXY_CONTROLLER = 1;
        int WEBVIEW_INSTANCE = 2;
        int GET_STATICS = 3;
        int GET_DEFAULT_GEOLOCATION_PERMISSIONS = 4;
        int GET_DEFAULT_SERVICE_WORKER_CONTROLLER = 5;
        int GET_WEB_ICON_DATABASE = 6;
        int GET_DEFAULT_WEB_STORAGE = 7;
        int GET_DEFAULT_WEBVIEW_DATABASE = 8;
        int GET_TRACING_CONTROLLER = 9;
        // Remember to update WebViewStartupCallSite in enums.xml when adding new values here.
        int COUNT = 10;
    };

    WebViewChromiumAwInit(WebViewChromiumFactoryProvider factory) {
        mFactory = factory;
        // Do not make calls into 'factory' in this ctor - this ctor is called from the
        // WebViewChromiumFactoryProvider ctor, so 'factory' is not properly initialized yet.
        TraceEvent.maybeEnableEarlyTracing(/* readCommandLine= */ false);
    }

    public AwTracingController getAwTracingController() {
        synchronized (mLock) {
            if (mAwTracingController == null) {
                ensureChromiumStartedLocked(true, CallSite.GET_AW_TRACING_CONTROLLER);
            }
        }
        return mAwTracingController;
    }

    public AwProxyController getAwProxyController() {
        synchronized (mLock) {
            if (mAwProxyController == null) {
                ensureChromiumStartedLocked(true, CallSite.GET_AW_PROXY_CONTROLLER);
            }
        }
        return mAwProxyController;
    }

    // TODO: DIR_RESOURCE_PAKS_ANDROID needs to live somewhere sensible,
    // inlined here for simplicity setting up the HTMLViewer demo. Unfortunately
    // it can't go into base.PathService, as the native constant it refers to
    // lives in the ui/ layer. See ui/base/ui_base_paths.h
    private static final int DIR_RESOURCE_PAKS_ANDROID = 3003;

    protected void startChromiumLocked(@CallSite int callSite, boolean triggeredFromUIThread) {
        long startTime = SystemClock.uptimeMillis();
        try (ScopedSysTraceEvent event =
                ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.startChromiumLocked")) {
            assert Thread.holdsLock(mLock) && ThreadUtils.runningOnUiThread();

            // The post-condition of this method is everything is ready, so notify now to cover all
            // return paths. (Other threads will not wake-up until we release |mLock|, whatever).
            mLock.notifyAll();

            if (mInitState == INIT_FINISHED) {
                return;
            }

            final Context context = ContextUtils.getApplicationContext();

            JNIUtils.setClassLoader(WebViewChromiumAwInit.class.getClassLoader());

            ResourceBundle.setAvailablePakLocales(AwLocaleConfig.getWebViewSupportedPakLocales());

            BundleUtils.setIsBundle(ProductConfig.IS_BUNDLE);

            // We are rewriting Java resources in the background.
            // NOTE: Any reference to Java resources will cause a crash.

            try (ScopedSysTraceEvent e =
                    ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.LibraryLoader")) {
                LibraryLoader.getInstance().ensureInitialized();
            }

            PathService.override(PathService.DIR_MODULE, "/system/lib/");
            PathService.override(DIR_RESOURCE_PAKS_ANDROID, "/system/framework/webview/paks");

            initPlatSupportLibrary();
            AwContentsStatics.setCheckClearTextPermitted(
                    context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O);

            waitUntilSetUpResources();

            // NOTE: Finished writing Java resources. From this point on, it's safe to use them.

            // Try to work around the resources problem.
            //
            // WebViewFactory adds WebView's asset path to the host app before any of the code in
            // the APK starts running, but it adds it using an old mechanism that doesn't persist if
            // the app's resource configuration changes for any other reason.
            //
            // By the time we get here, it's possible it's gone missing due to something on the UI
            // thread having triggered a resource update. This can happen either because WebView
            // initialization was triggered by a background thread (and thus this code is running
            // inside a posted task on the UI thread which may have taken any amount of time to
            // actually run), or because the app used CookieManager first, which triggers the code
            // being loaded and WebViewFactory doing the initial resources add, but does not call
            // startChromiumLocked until the app uses some other API, an arbitrary amount of time
            // later. So, we can try to add them again using the "better" method in WebViewDelegate.
            //
            // However, we only want to try this if the resources are actually missing, because
            // in the past we've seen this cause apps that were working to *start* crashing.
            // The first resource that gets accessed in startup happens during the
            // AwBrowserProcess.start() call when trying to determine if the device is a tablet,
            // and that's the most common place for us to crash. So, try calling that same
            // method and see if it throws - if so then we're unlikely to make the situation
            // any worse by trying to fix the path.
            //
            // This cannot fix the problem in all cases - if the app is using a weird ContextWrapper
            // or doing other unusual things with resources/assets then even adding it with this
            // mechanism might not help.
            try {
                DeviceFormFactor.isTablet();
            } catch (Resources.NotFoundException e) {
                mFactory.addWebViewAssetPath(context);
            }

            AwBrowserProcess.configureChildProcessLauncher();

            // finishVariationsInitLocked() must precede native initialization so the seed is
            // available when AwFeatureListCreator::SetUpFieldTrials() runs.
            if (!FastVariationsSeedSafeModeAction.hasRun()) {
                finishVariationsInitLocked();
            }

            AwBrowserProcess.start();

            // TODO(crbug.com/332706093): See if this can be moved before loading native.
            AwClassPreloader.preloadClasses();

            AwBrowserProcess.handleMinidumpsAndSetMetricsConsent(/* updateMetricsConsent= */ true);
            doNetworkInitializations(context);

            // This has to be done after variations are initialized, so components could be
            // registered or not depending on the variations flags.
            AwBrowserProcess.loadComponents();
            AwBrowserProcess.initializeMetricsLogUploader();

            mSharedStatics = new SharedStatics();
            if (BuildInfo.isDebugAndroidOrApp()) {
                mSharedStatics.setWebContentsDebuggingEnabledUnconditionally(true);
            }

            mInitState = INIT_FINISHED;

            RecordHistogram.recordSparseHistogram(
                    "Android.WebView.TargetSdkVersion",
                    context.getApplicationInfo().targetSdkVersion);

            try (ScopedSysTraceEvent e =
                    ScopedSysTraceEvent.scoped(
                            "WebViewChromiumAwInit.initThreadUnsafeSingletons")) {
                // Initialize thread-unsafe singletons.
                AwBrowserContext defaultBrowserContext = getDefaultBrowserContextOnUiThread();
                mDefaultGeolocationPermissions =
                        new GeolocationPermissionsAdapter(
                                mFactory, defaultBrowserContext.getGeolocationPermissions());
                mDefaultWebStorage =
                        new WebStorageAdapter(
                                mFactory, defaultBrowserContext.getQuotaManagerBridge());
                mAwTracingController = getTracingController();
                mDefaultServiceWorkerController =
                        defaultBrowserContext.getServiceWorkerController();
                mAwProxyController = new AwProxyController();
            }

            if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
                    ? CompatChanges.isChangeEnabled(WebSettings.ENABLE_SIMPLIFIED_DARK_MODE)
                    : BuildInfo.targetsAtLeastT()) {
                AwDarkMode.enableSimplifiedDarkMode();
            }

            if (CommandLine.getInstance().hasSwitch(AwSwitches.WEBVIEW_VERBOSE_LOGGING)) {
                logCommandLineAndActiveTrials();
            }

            PostTask.postTask(
                    TaskTraits.BEST_EFFORT,
                    () ->
                            mFactory.setWebViewContextExperimentValue(
                                    AwFeatureMap.isEnabled(
                                            AwFeatures.WEBVIEW_SEPARATE_RESOURCE_CONTEXT)));

            // This runs all the pending tasks queued for after Chromium init is finished,
            // so should be the last thing that happens in startChromiumLocked.
            mFactory.getRunQueue().drainQueue();

            AwCrashyClassUtils.maybeCrashIfEnabled();
        }

        RecordHistogram.recordEnumeratedHistogram(
                "Android.WebView.Startup.CreationTime.InitReason", callSite, CallSite.COUNT);

        RecordHistogram.recordTimesHistogram(
                "Android.WebView.Startup.CreationTime.StartChromiumLocked",
                SystemClock.uptimeMillis() - startTime);
        TraceEvent.webViewStartupStartChromiumLocked(
                startTime,
                SystemClock.uptimeMillis() - startTime,
                /* callSite= */ callSite,
                /* fromUIThread= */ triggeredFromUIThread);
    }

    /**
     * Set up resources on a background thread.
     *
     * @param context The context.
     */
    public void setUpResourcesOnBackgroundThread(int packageId, Context context) {
        try (ScopedSysTraceEvent e =
                ScopedSysTraceEvent.scoped(
                        "WebViewChromiumAwInit.setUpResourcesOnBackgroundThread")) {
            assert mSetUpResourcesThread == null : "This method shouldn't be called twice.";

            // Make sure that ResourceProvider is initialized before starting the browser process.
            mSetUpResourcesThread =
                    new Thread(
                            new Runnable() {
                                @Override
                                public void run() {
                                    // Run this in parallel as it takes some time.
                                    setUpResources(packageId, context);
                                }
                            });
            mSetUpResourcesThread.start();
        }
    }

    private void waitUntilSetUpResources() {
        try (ScopedSysTraceEvent e =
                ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.waitUntilSetUpResources")) {
            mSetUpResourcesThread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void setUpResources(int packageId, Context context) {
        try (ScopedSysTraceEvent e =
                ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.setUpResources")) {
            R.onResourcesLoaded(packageId);

            AwResource.setResources(context.getResources());
            AwResource.setConfigKeySystemUuidMapping(android.R.array.config_keySystemUuidMapping);
        }
    }

    boolean hasStarted() {
        return mInitState == INIT_FINISHED;
    }

    void startYourEngines(boolean fromThreadSafeFunction) {
        synchronized (mLock) {
            ensureChromiumStartedLocked(fromThreadSafeFunction, CallSite.WEBVIEW_INSTANCE);
        }
    }

    // This method is not private only because the downstream subclass needs to access it,
    // it shouldn't be accessed from anywhere else.
    /* package */ void ensureChromiumStartedLocked(
            boolean fromThreadSafeFunction, @CallSite int callSite) {
        assert Thread.holdsLock(mLock);

        if (mInitState == INIT_FINISHED) { // Early-out for the common case.
            return;
        }

        if (mInitState == INIT_NOT_STARTED) {
            // If we're the first thread to enter ensureChromiumStartedLocked, we need to determine
            // which thread will be the UI thread; declare init has started so that no other thread
            // will try to do this.
            mInitState = INIT_STARTED;
            setChromiumUiThreadLocked(fromThreadSafeFunction);
        }

        if (ThreadUtils.runningOnUiThread()) {
            // If we are currently running on the UI thread then we must do init now. If there was
            // already a task posted to the UI thread from another thread to do it, it will just
            // no-op when it runs.
            startChromiumLocked(callSite, /* triggeredFromUIThread= */ true);
            return;
        }

        // If we're not running on the UI thread (because init was triggered by a thread-safe
        // function), post init to the UI thread, since init is *not* thread-safe.
        AwThreadUtils.postToUiThreadLooper(
                new Runnable() {
                    @Override
                    public void run() {
                        synchronized (mLock) {
                            startChromiumLocked(callSite, /* triggeredFromUIThread= */ false);
                        }
                    }
                });

        try (ScopedSysTraceEvent event =
                ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.waitForUIThreadInit")) {
            long startTime = SystemClock.uptimeMillis();
            // Wait for the UI thread to finish init.
            while (mInitState != INIT_FINISHED) {
                try {
                    mLock.wait();
                } catch (InterruptedException e) {
                    // Keep trying; we can't abort init as WebView APIs do not declare that they
                    // throw InterruptedException.
                }
            }
            RecordHistogram.recordTimesHistogram(
                    "Android.WebView.Startup.CreationTime.waitForUIThreadInit",
                    SystemClock.uptimeMillis() - startTime);
        }
    }

    private void setChromiumUiThreadLocked(boolean fromThreadSafeFunction) {
        // If we're being started from a function that's allowed to be called on any thread,
        // then we can't just assume the current thread is the UI thread; instead we assume the
        // process's main looper will be the UI thread, because that's the case for almost all
        // Android apps.
        //
        // If we're being started from a function that must be called from the UI
        // thread, then by definition the current thread is the UI thread whether it's the main
        // looper or not.
        Looper looper = fromThreadSafeFunction ? Looper.getMainLooper() : Looper.myLooper();
        Log.v(
                TAG,
                "Binding Chromium to "
                        + (Looper.getMainLooper().equals(looper) ? "main" : "background")
                        + " looper "
                        + looper);
        ThreadUtils.setUiThread(looper);
    }

    private void initPlatSupportLibrary() {
        try (ScopedSysTraceEvent e =
                ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.initPlatSupportLibrary")) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                AwDrawFnImpl.setDrawFnFunctionTable(DrawFunctor.getDrawFnFunctionTable());
            }
            DrawGLFunctor.setChromiumAwDrawGLFunction(AwContents.getAwDrawGLFunction());
            AwContents.setAwDrawSWFunctionTable(GraphicsUtils.getDrawSWFunctionTable());
            AwContents.setAwDrawGLFunctionTable(GraphicsUtils.getDrawGLFunctionTable());
        }
    }

    private void doNetworkInitializations(Context applicationContext) {
        try (ScopedSysTraceEvent e =
                ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.doNetworkInitializations")) {
            boolean forceUpdateNetworkState =
                    !AwFeatureMap.isEnabled(
                            AwFeatures.WEBVIEW_USE_INITIAL_NETWORK_STATE_AT_STARTUP);
            if (applicationContext.checkPermission(
                            Manifest.permission.ACCESS_NETWORK_STATE,
                            Process.myPid(),
                            Process.myUid())
                    == PackageManager.PERMISSION_GRANTED) {
                NetworkChangeNotifier.init();
                NetworkChangeNotifier.setAutoDetectConnectivityState(
                        new AwNetworkChangeNotifierRegistrationPolicy(), forceUpdateNetworkState);
            }
        }
    }

    public AwTracingController getTracingController() {
        if (mTracingController == null) {
            mTracingController = new AwTracingController();
        }
        return mTracingController;
    }

    // Only on UI thread.
    AwBrowserContext getDefaultBrowserContextOnUiThread() {
        assert mInitState == INIT_FINISHED;

        if (BuildConfig.ENABLE_ASSERTS && !ThreadUtils.runningOnUiThread()) {
            throw new RuntimeException(
                    "getBrowserContextOnUiThread called on " + Thread.currentThread());
        }

        if (mDefaultBrowserContext == null) {
            mDefaultBrowserContext = AwBrowserContext.getDefault();
        }
        return mDefaultBrowserContext;
    }

    /**
     * Returns the lock used for guarding chromium initialization.
     * We make this public to let higher-level classes use this lock to guard variables
     * dependent on this class, to avoid introducing new locks (which can cause deadlocks).
     */
    public Object getLock() {
        return mLock;
    }

    public SharedStatics getStatics() {
        synchronized (mLock) {
            if (mSharedStatics == null) {
                // TODO: Optimization potential: most of the static methods only need the native
                // library loaded and initialized, not the entire browser process started.
                ensureChromiumStartedLocked(true, CallSite.GET_STATICS);
                SharedStatics.setStartupTriggered();
            }
        }
        return mSharedStatics;
    }

    public GeolocationPermissions getDefaultGeolocationPermissions() {
        synchronized (mLock) {
            if (mDefaultGeolocationPermissions == null) {
                ensureChromiumStartedLocked(true, CallSite.GET_DEFAULT_GEOLOCATION_PERMISSIONS);
            }
        }
        return mDefaultGeolocationPermissions;
    }

    public CookieManager getDefaultCookieManager() {
        synchronized (mLock) {
            if (mDefaultCookieManager == null) {
                mDefaultCookieManager =
                        new CookieManagerAdapter(AwCookieManager.getDefaultCookieManager());
            }
        }
        return mDefaultCookieManager;
    }

    public AwServiceWorkerController getDefaultServiceWorkerController() {
        synchronized (mLock) {
            if (mDefaultServiceWorkerController == null) {
                ensureChromiumStartedLocked(true, CallSite.GET_DEFAULT_SERVICE_WORKER_CONTROLLER);
            }
        }
        return mDefaultServiceWorkerController;
    }

    public android.webkit.WebIconDatabase getWebIconDatabase() {
        synchronized (mLock) {
            ensureChromiumStartedLocked(true, CallSite.GET_WEB_ICON_DATABASE);
            WebViewChromium.recordWebViewApiCall(ApiCall.WEB_ICON_DATABASE_GET_INSTANCE);
            if (mWebIconDatabase == null) {
                mWebIconDatabase = new WebIconDatabaseAdapter();
            }
        }
        return mWebIconDatabase;
    }

    public WebStorage getDefaultWebStorage() {
        synchronized (mLock) {
            if (mDefaultWebStorage == null) {
                ensureChromiumStartedLocked(true, CallSite.GET_DEFAULT_WEB_STORAGE);
            }
        }
        return mDefaultWebStorage;
    }

    public WebViewDatabase getDefaultWebViewDatabase(final Context context) {
        synchronized (mLock) {
            ensureChromiumStartedLocked(true, CallSite.GET_DEFAULT_WEBVIEW_DATABASE);
            if (mDefaultWebViewDatabase == null) {
                mDefaultWebViewDatabase =
                        new WebViewDatabaseAdapter(
                                mFactory,
                                HttpAuthDatabase.newInstance(context, HTTP_AUTH_DATABASE_FILE),
                                mDefaultBrowserContext);
            }
        }
        return mDefaultWebViewDatabase;
    }

    // See comments in VariationsSeedLoader.java on when it's safe to call this.
    public void startVariationsInit() {
        synchronized (mLock) {
            if (mSeedLoader == null) {
                mSeedLoader = new VariationsSeedLoader();
                mSeedLoader.startVariationsInit();
            }
        }
    }

    private void finishVariationsInitLocked() {
        try (ScopedSysTraceEvent e =
                ScopedSysTraceEvent.scoped("WebViewChromiumAwInit.finishVariationsInitLocked")) {
            assert Thread.holdsLock(mLock);
            if (mSeedLoader == null) {
                Log.e(TAG, "finishVariationsInitLocked() called before startVariationsInit()");
                startVariationsInit();
            }
            mSeedLoader.finishVariationsInit();
            mSeedLoader = null; // Allow this to be GC'd after its background thread finishes.
        }
    }

    // Log extra information, for debugging purposes. Do the work asynchronously to avoid blocking
    // startup.
    private void logCommandLineAndActiveTrials() {
        PostTask.postTask(
                TaskTraits.BEST_EFFORT,
                () -> {
                    // TODO(ntfschr): CommandLine can change at any time. For simplicity, only log
                    // it once during startup.
                    AwContentsStatics.logCommandLineForDebugging();
                    // Field trials can be activated at any time. We'll continue logging them as
                    // they're activated.
                    FieldTrialList.logActiveTrials();
                    // SafeMode was already determined earlier during the startup sequence, this
                    // just fetches the cached boolean state. If SafeMode was enabled, we already
                    // logged detailed information about the SafeMode config.
                    Log.i(TAG, "SafeMode enabled: " + mFactory.isSafeModeEnabled());
                });
    }

    public WebViewChromiumRunQueue getRunQueue() {
        return mFactory.getRunQueue();
    }
}