chromium/chrome/android/java/src/org/chromium/chrome/browser/base/SplitCompatApplication.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.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;

import androidx.annotation.CallSuper;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.BundleUtils;
import org.chromium.base.CommandLineInitUtil;
import org.chromium.base.ContextUtils;
import org.chromium.base.EarlyTraceEvent;
import org.chromium.base.IntentUtils;
import org.chromium.base.JNIUtils;
import org.chromium.base.LocaleUtils;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.memory.MemoryPressureMonitor;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.version_info.VersionConstants;
import org.chromium.build.BuildConfig;
import org.chromium.build.NativeLibraries;
import org.chromium.chrome.browser.ProductConfig;
import org.chromium.chrome.browser.crash.ApplicationStatusTracker;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.language.AppLocaleUtils;
import org.chromium.chrome.browser.language.GlobalAppLocaleController;
import org.chromium.chrome.browser.metrics.UmaUtils;
import org.chromium.components.crash.CustomAssertionHandler;
import org.chromium.components.crash.PureJavaExceptionHandler;
import org.chromium.components.crash.PureJavaExceptionHandler.JavaExceptionReporter;
import org.chromium.components.crash.PureJavaExceptionHandler.JavaExceptionReporterFactory;
import org.chromium.components.embedder_support.application.FontPreloadingWorkaround;
import org.chromium.components.module_installer.util.ModuleUtil;
import org.chromium.ui.base.ResourceBundle;

/**
 * Application base class which will call through to the given {@link Impl}. Application classes
 * which extend this class should also extend {@link Impl}, and call {@link #setImpl(Impl)} before
 * calling {@link attachBaseContext(Context)}.
 *
 * This is the base class of all Chrome applications. Logic specific to isolated splits should go in
 * {@link SplitChromeApplication}.
 */
public class SplitCompatApplication extends Application {
    public static final String CHROME_SPLIT_NAME = "chrome";
    private static final String TAG = "SplitCompatApp";
    private static final String COMMAND_LINE_FILE = "chrome-command-line";
    private static final String ATTACH_BASE_CONTEXT_EVENT = "ChromeApplication.attachBaseContext";
    // Public to allow use in ChromeBackupAgent
    public static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "chrome";

    @VisibleForTesting
    public static final String LAUNCH_FAILED_ACTIVITY_CLASS_NAME =
            "org.chromium.chrome.browser.init.LaunchFailedActivity";

    private Supplier<Impl> mImplSupplier;
    private Impl mImpl;
    private ServiceTracingProxyProvider mServiceTracingProxyProvider;

    /**
     * Holds the implementation of application logic. Will be called by {@link
     * SplitCompatApplication}.
     */
    public static class Impl {
        private SplitCompatApplication mApplication;

        private final void setApplication(SplitCompatApplication application) {
            mApplication = application;
        }

        protected final SplitCompatApplication getApplication() {
            return mApplication;
        }

        @CallSuper
        public void startActivity(Intent intent, Bundle options) {
            mApplication.superStartActivity(intent, options);
        }

        public void onCreate() {}

        public void onTrimMemory(int level) {}

        public void onConfigurationChanged(Configuration newConfig) {}
    }

    public final void setImplSupplier(Supplier<Impl> implSupplier) {
        assert mImpl == null;
        assert mImplSupplier == null;
        mImplSupplier = implSupplier;
    }

    private Impl getImpl() {
        if (mImpl == null) {
            mImpl = mImplSupplier.get();
            mImpl.setApplication(this);
        }
        return mImpl;
    }

    /**
     * This exposes the super method so it can be called inside the Impl class code instead of just
     * at the start.
     */
    private void superStartActivity(Intent intent, Bundle options) {
        super.startActivity(intent, options);
    }

    // Called by the framework for ALL processes. Runs before ContentProviders are created.
    // Quirk: context.getApplicationContext() returns null during this method.
    @Override
    protected void attachBaseContext(Context context) {
        boolean isIsolatedProcess = ContextUtils.isIsolatedProcess();
        boolean isBrowserProcess = isBrowserProcess();
        Log.i(
                TAG,
                "version=%s (%s) minSdkVersion=%s isBundle=%s processName=%s isIsolatedProcess=%s",
                VersionConstants.PRODUCT_VERSION,
                BuildConfig.VERSION_CODE,
                BuildConfig.MIN_SDK_VERSION,
                ProductConfig.IS_BUNDLE,
                ContextUtils.getProcessName(),
                isIsolatedProcess);

        if (isBrowserProcess) {
            UmaUtils.recordMainEntryPointTime();

            // Register Service tracing early as some services are used below in this function.
            mServiceTracingProxyProvider = ServiceTracingProxyProvider.create(context);
            // *** The Application Context should not be used before the locale override is set ***
            if (GlobalAppLocaleController.getInstance().init(context)) {
                // If the app locale override preference is set, create a new override
                // context to use as the base context for the application.
                // Must be initialized early to override Application level localizations.
                Configuration config =
                        GlobalAppLocaleController.getInstance().getOverrideConfig(context);
                LocaleUtils.setDefaultLocalesFromConfiguration(config);
                context = context.createConfigurationContext(config);
            }
        }

        super.attachBaseContext(context);
        // Perform initialization of globals common to all processes.
        ContextUtils.initApplicationContext(this);

        if (isBrowserProcess) {
            // This must come as early as possible to avoid early loading of the native library from
            // failing unnoticed.
            LibraryLoader.sLoadFailedCallback =
                    unsatisfiedLinkError -> {
                        Intent newIntent = new Intent();
                        newIntent.setComponent(
                                new ComponentName(
                                        ContextUtils.getApplicationContext(),
                                        LAUNCH_FAILED_ACTIVITY_CLASS_NAME));
                        newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        IntentUtils.safeStartActivity(
                                ContextUtils.getApplicationContext(), newIntent);
                        if (cannotLoadIn64Bit()) {
                            throw new RuntimeException(
                                    "Starting in 64-bit mode requires the 64-bit native library. If"
                                            + " the device is 64-bit only, see alternatives here: "
                                            + "https://crbug.com/1303857#c7.",
                                    unsatisfiedLinkError);
                        } else if (cannotLoadIn32Bit()) {
                            throw new RuntimeException(
                                    "Starting in 32-bit mode requires the 32-bit native library.",
                                    unsatisfiedLinkError);
                        }
                        throw unsatisfiedLinkError;
                    };
        }

        maybeInitProcessType();
        BundleUtils.setIsBundle(ProductConfig.IS_BUNDLE);

        if (isBrowserProcess) {
            performBrowserProcessPreloading(context);
        }

        // Write installed modules to crash keys. This needs to be done as early as possible so
        // that these values are set before any crashes are reported.
        ModuleUtil.updateCrashKeys();

        AsyncTask.takeOverAndroidThreadPool();
        JNIUtils.setClassLoader(getClassLoader());
        ResourceBundle.setAvailablePakLocales(ProductConfig.LOCALES);
        LibraryLoader.getInstance().setLinkerImplementation(ProductConfig.USE_CHROMIUM_LINKER);

        if (!isBrowserProcess) {
            EarlyTraceEvent.earlyEnableInChildWithoutCommandLine();
            TraceEvent.begin(ATTACH_BASE_CONTEXT_EVENT);
        } else {
            checkAppBeingReplaced();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                // Fixes are never required before O (where "cmd package compile" does not exist).
                DexFixer.scheduleDexFix();
            }

            PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX);
            // Renderer and GPU processes have command line passed to them via IPC
            // (see ChildProcessService.java).
            CommandLineInitUtil.initCommandLine(
                    COMMAND_LINE_FILE, SplitCompatApplication::shouldUseDebugFlags);

            TraceEvent.maybeEnableEarlyTracing(/* readCommandLine= */ true);
            TraceEvent.begin(ATTACH_BASE_CONTEXT_EVENT);

            // Register for activity lifecycle callbacks. Must be done before any activities are
            // created and is needed only by processes that use the ApplicationStatus api (which
            // for Chrome is just the browser process).
            ApplicationStatus.initialize(this);
            ColdStartTracker.initialize();

            // Register and initialize application status listener for crashes, this needs to be
            // done as early as possible so that this value is set before any crashes are
            // reported.
            ApplicationStatusTracker tracker = new ApplicationStatusTracker();
            tracker.onApplicationStateChange(ApplicationStatus.getStateForApplication());
            ApplicationStatus.registerApplicationStateListener(tracker);

            // Disable MemoryPressureMonitor polling when Chrome goes to the background.
            ApplicationStatus.registerApplicationStateListener(
                    SplitCompatApplication::updateMemoryPressurePolling);

            if (AppLocaleUtils.shouldUseSystemManagedLocale()) {
                AppLocaleUtils.maybeMigrateOverrideLanguage();
            }
        }

        // WebView installs its own PureJavaExceptionHandler.
        // Incremental install disables process isolation, so things in this block will
        // actually be run for incremental apks, but not normal apks.
        if (!isIsolatedProcess && !isWebViewProcess()) {
            JavaExceptionReporterFactory factory =
                    new JavaExceptionReporterFactory() {
                        @Override
                        public JavaExceptionReporter createJavaExceptionReporter() {
                            // ChromePureJavaExceptionReporter may be in the chrome module, so load
                            // by reflection from there.
                            return (JavaExceptionReporter)
                                    BundleUtils.newInstance(
                                            createChromeContext(
                                                    ContextUtils.getApplicationContext()),
                                            "org.chromium.chrome.browser.crash.ChromePureJavaExceptionReporter");
                        }
                    };
            PureJavaExceptionHandler.installHandler(factory);
            CustomAssertionHandler.installPreNativeHandler(factory);
        }

        TraceEvent.end(ATTACH_BASE_CONTEXT_EVENT);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // These can't go in attachBaseContext because Context.getApplicationContext() (which
        // they use under-the-hood) does not work until after it returns.
        FontPreloadingWorkaround.maybeInstallWorkaround(this);
        MemoryPressureMonitor.INSTANCE.registerComponentCallbacks();

        getImpl().onCreate();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        getImpl().onTrimMemory(level);
    }

    /** Forward all startActivity() calls to the two argument version. */
    @Override
    public void startActivity(Intent intent) {
        startActivity(intent, null);
    }

    @Override
    public void startActivity(Intent intent, Bundle options) {
        getImpl().startActivity(intent, options);
    }

    // Note that we do not need to (and can't) override getSystemService(Class<T>) as internally
    // that just gets the name of the Service and calls getSystemService(String) for backwards
    // compatibility with overrides like this one.
    @Override
    public Object getSystemService(String name) {
        Object service = super.getSystemService(name);
        if (mServiceTracingProxyProvider != null) {
            mServiceTracingProxyProvider.traceSystemServices();
        }
        return service;
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        getImpl().onConfigurationChanged(newConfig);
    }

    /**
     * Called right after main entry point time is recorded in the browser process. This is called
     * as early as possible to maximize preload time.
     */
    protected void performBrowserProcessPreloading(Context context) {}

    public boolean isWebViewProcess() {
        return false;
    }

    public static boolean isBrowserProcess() {
        return !ContextUtils.getProcessName().contains(":");
    }

    /** Creates a context which can be used to load code and resources in the chrome split. */
    public static Context createChromeContext(Context base) {
        if (!BundleUtils.isIsolatedSplitInstalled(CHROME_SPLIT_NAME)) {
            return base;
        }
        return BundleUtils.createIsolatedSplitContext(base, CHROME_SPLIT_NAME);
    }

    public static boolean cannotLoadIn64Bit() {
        if (LibraryLoader.sOverrideNativeLibraryCannotBeLoadedForTesting) {
            return true;
        }
        return Process.is64Bit() && !NativeLibraries.sSupport64Bit;
    }

    public static boolean cannotLoadIn32Bit() {
        return !Process.is64Bit() && !NativeLibraries.sSupport32Bit;
    }

    private void maybeInitProcessType() {
        if (isBrowserProcess()) {
            LibraryLoader.getInstance().setLibraryProcessType(LibraryProcessType.PROCESS_BROWSER);
            return;
        }
        // WebView initialization sets the correct process type.
        if (isWebViewProcess()) return;

        // Child processes set their own process type when bound.
        String processName = ContextUtils.getProcessName();
        if (processName.contains("privileged_process")
                || processName.contains("sandboxed_process")) {
            return;
        }

        // We must be in an isolated service process.
        LibraryLoader.getInstance().setLibraryProcessType(LibraryProcessType.PROCESS_CHILD);
    }

    private static Boolean shouldUseDebugFlags() {
        return ChromeFeatureList.sCommandLineOnNonRooted.isEnabled();
    }

    private static void updateMemoryPressurePolling(@ApplicationState int newState) {
        if (newState == ApplicationState.HAS_RUNNING_ACTIVITIES) {
            MemoryPressureMonitor.INSTANCE.enablePolling(
                    ChromeFeatureList.sPostGetMyMemoryStateToBackground.isEnabled());
        } else if (newState == ApplicationState.HAS_STOPPED_ACTIVITIES) {
            MemoryPressureMonitor.INSTANCE.disablePolling();
        }
    }

    /** Ensure this application object is not out-of-date. */
    private static void checkAppBeingReplaced() {
        // During app update the old apk can still be triggered by broadcasts and spin up an
        // out-of-date application. Kill old applications in this bad state. See
        // http://crbug.com/658130 for more context and http://b.android.com/56296 for the bug.
        if (ContextUtils.getApplicationContext().getAssets() == null) {
            throw new RuntimeException("App out of date, getResources() null, closing app.");
        }
    }
}