chromium/chrome/android/java/src/org/chromium/chrome/browser/base/SplitChromeApplication.java

// Copyright 2020 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.chrome.browser.base;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;

import org.chromium.base.BundleUtils;
import org.chromium.base.JNIUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.build.annotations.IdentifierNameString;

/**
 * Application class to use for Chrome when //chrome code is in an isolated split. This class will
 * perform any necessary initialization for non-browser processes without loading code from the
 * chrome split. In the browser process, the necessary logic is loaded from the chrome split using
 * reflection.
 *
 * This class will be used when isolated splits are enabled.
 */
public class SplitChromeApplication extends SplitCompatApplication {
    private static @IdentifierNameString String sImplClassName =
            "org.chromium.chrome.browser.ChromeApplicationImpl";

    @SuppressLint("StaticFieldLeak")
    private static SplitPreloader sSplitPreloader;

    private String mChromeApplicationClassName;

    private Resources mResources;

    public SplitChromeApplication() {
        this(sImplClassName);
    }

    public SplitChromeApplication(String chromeApplicationClassName) {
        mChromeApplicationClassName = chromeApplicationClassName;
    }

    @Override
    public void onCreate() {
        finishPreload(CHROME_SPLIT_NAME);
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context context) {
        super.attachBaseContext(context);
        if (isBrowserProcess()) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                DexFixer.setHasIsolatedSplits(true);
            }
            setImplSupplier(
                    () -> {
                        Context chromeContext = createChromeContext(this);
                        return (Impl)
                                BundleUtils.newInstance(chromeContext, mChromeApplicationClassName);
                    });
        } else {
            setImplSupplier(() -> createNonBrowserApplication());
        }
    }

    @Override
    public Context createContextForSplit(String name) throws PackageManager.NameNotFoundException {
        try (TraceEvent te = TraceEvent.scoped("SplitChromeApplication.createContextForSplit")) {
            // Wait for any splits that are preloading so we don't have a race to update the
            // class loader cache (b/172602571).
            finishPreload(name);
            long startTime = SystemClock.uptimeMillis();
            Context context;
            synchronized (BundleUtils.getSplitContextLock()) {
                context = super.createContextForSplit(name);
            }
            RecordHistogram.recordTimesHistogram(
                    "Android.IsolatedSplits.ContextCreateTime." + name,
                    SystemClock.uptimeMillis() - startTime);
            return context;
        }
    }

    @Override
    protected void performBrowserProcessPreloading(Context context) {
        // The chrome split has a large amount of code, which can slow down startup. Loading
        // this in the background allows us to do this in parallel with startup tasks which do
        // not depend on code in the chrome split.
        sSplitPreloader = new SplitPreloader(context);
        // If the chrome module is not enabled or isolated splits are not supported (e.g. in Android
        // N), the onComplete function will run immediately so it must handle the case where the
        // base context of the application has not been set yet.
        sSplitPreloader.preload(
                CHROME_SPLIT_NAME,
                new SplitPreloader.OnComplete() {
                    @Override
                    public void runImmediatelyInBackgroundThread(Context chromeContext) {
                        // A new thread is started here because we do not want to delay returning
                        // the chrome Context, since that slows down startup. This thread must be
                        // a HandlerThread because AsyncInitializationActivity (a base class of
                        // ChromeTabbedActivity) creates a Handler, so needs to have a Looper
                        // prepared.
                        HandlerThread thread = new HandlerThread("ActivityPreload");
                        thread.start();
                        new Handler(thread.getLooper())
                                .post(
                                        () -> {
                                            try {
                                                // Create a throwaway instance of
                                                // ChromeTabbedActivity. This will warm up
                                                // the chrome ClassLoader, and perform loading of
                                                // classes used early in startup in the
                                                // background.
                                                chromeContext
                                                        .getClassLoader()
                                                        .loadClass(
                                                                "org.chromium.chrome.browser.ChromeTabbedActivity$Preload")
                                                        .newInstance();
                                            } catch (ReflectiveOperationException e) {
                                                throw new RuntimeException(e);
                                            }
                                            thread.quit();
                                        });
                    }

                    @Override
                    public void runInUiThread(Context chromeContext) {
                        // If the chrome module is not enabled or isolated splits are not supported,
                        // chromeContext will have the same ClassLoader as the base context, so no
                        // need to replace the ClassLoaders here.
                        if (!context.getClassLoader().equals(chromeContext.getClassLoader())) {
                            // Replace the application Context's ClassLoader with the chrome
                            // ClassLoader, because the application ClassLoader is expected to be
                            // able to access all chrome classes.
                            BundleUtils.replaceClassLoader(
                                    SplitChromeApplication.this, chromeContext.getClassLoader());
                            JNIUtils.setClassLoader(chromeContext.getClassLoader());
                            // Resources holds a reference to a ClassLoader. Make our Application's
                            // getResources() return a reference to the Chrome split's resources
                            // since there are a spots where ContextUtils.getApplicationContext()
                            // is used to retrieve resources (https://crbug.com/1287000).
                            mResources = chromeContext.getResources();
                        }
                    }
                });
    }

    @Override
    public Resources getResources() {
        // If the cached resources from the Chrome split are available use those. Note that
        // retrieving resources will use resources from the base split until the Chrome split is
        // fully loaded. We don't want to ensure the Chrome split is loaded here because resources
        // may be accessed early in startup, and forcing a load here will reduce the benefits of
        // preloading the Chrome split in the background.
        if (mResources != null) {
            return mResources;
        }
        return getBaseContext().getResources();
    }

    /** Waits for the specified split to finish preloading if necessary. */
    public static void finishPreload(String name) {
        if (sSplitPreloader != null) {
            sSplitPreloader.wait(name);
        }
    }

    protected Impl createNonBrowserApplication() {
        return new Impl();
    }
}