chromium/chrome/android/java/src/org/chromium/chrome/browser/base/SplitPreloader.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.content.Context;
import android.content.res.Configuration;
import android.os.SystemClock;

import androidx.collection.SimpleArrayMap;

import org.chromium.base.BundleUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.language.GlobalAppLocaleController;

/**
 * Handles preloading split Contexts on a background thread. Loading a new isolated split
 * Context can be expensive since the ClassLoader may need to be created. See crbug.com/1150600 for
 * more info.
 */
public class SplitPreloader {
    private final SimpleArrayMap<String, PreloadTask> mPreloadTasks = new SimpleArrayMap<>();
    private final Context mContext;

    /** Interface to run code after preload completion. */
    public interface OnComplete {
        /**
         * Runs immediately on the background thread as soon as the split context is available.
         * Note that normally runInUiThread() should be used instead because the context parameter
         * here may have an incorrect ClassLoader due to b/172602571. This method should only be
         * used for optimizations which need to run as soon as possible, and are safe throw away if
         * a different ClassLoader ends up being used.
         */
        default void runImmediatelyInBackgroundThread(Context unsafeClassLoaderContext) {}

        /**
         * Guaranteed to run in the UI thread before {@link SplitPreloader#wait(String)} returns.
         */
        default void runInUiThread(Context context) {}
    }

    private class PreloadTask extends AsyncTask<Void> {
        private final String mName;
        private OnComplete mOnComplete;

        public PreloadTask(String name, OnComplete onComplete) {
            mName = name;
            mOnComplete = onComplete;
        }

        @Override
        protected Void doInBackground() {
            Context context = createSplitContext();
            if (mOnComplete != null) {
                mOnComplete.runImmediatelyInBackgroundThread(context);
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            finish();
        }

        /**
         * Waits for the preload to finish and calls the onComplete function if needed. onComplete
         * is expected to be called before {@link SplitPreloader#wait(String)} returns, so this
         * method is called there since onPostExecute does not run before get() returns.
         */
        public void finish() {
            try {
                get();
            } catch (Exception e) {
                // Ignore exception, not a problem if preload fails.
            }
            if (mOnComplete != null) {
                // Recreate the context here to make sure we have the latest version, in case there
                // was a race to update the class loader cache, see b/172602571.
                mOnComplete.runInUiThread(createSplitContext());
                mOnComplete = null;
            }
        }

        private Context createSplitContext() {
            if (BundleUtils.isIsolatedSplitInstalled(mName)) {
                Context context = BundleUtils.createIsolatedSplitContext(mContext, mName);
                if (GlobalAppLocaleController.getInstance().isOverridden()) {
                    Configuration config =
                            GlobalAppLocaleController.getInstance().getOverrideConfig(context);
                    context = context.createConfigurationContext(config);
                }
                return context;
            }
            return mContext;
        }
    }

    public SplitPreloader(Context context) {
        mContext = context;
    }

    /** Starts preloading a split context on a background thread. */
    public void preload(String name, OnComplete onComplete) {
        if (!BundleUtils.isIsolatedSplitInstalled(name) && onComplete == null) {
            return;
        }

        PreloadTask task = new PreloadTask(name, onComplete);
        task.executeWithTaskTraits(TaskTraits.USER_BLOCKING_MAY_BLOCK);
        synchronized (mPreloadTasks) {
            assert !mPreloadTasks.containsKey(name);
            mPreloadTasks.put(name, task);
        }
    }

    /** Waits for the specified split to be finished loading. */
    public void wait(String name) {
        try (TraceEvent te = TraceEvent.scoped("SplitPreloader.wait")) {
            PreloadTask task;
            synchronized (mPreloadTasks) {
                task = mPreloadTasks.remove(name);
            }
            if (task != null) {
                long startTime = SystemClock.uptimeMillis();
                // Make sure the task is finished and onComplete has run.
                task.finish();
                RecordHistogram.recordTimesHistogram(
                        "Android.IsolatedSplits.PreloadWaitTime." + name,
                        SystemClock.uptimeMillis() - startTime);
            }
        }
    }
}