chromium/chrome/android/java/src/org/chromium/chrome/browser/ChromeBaseAppCompatActivity.java

// Copyright 2019 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;

import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;

import static org.chromium.chrome.browser.base.SplitCompatApplication.CHROME_SPLIT_NAME;

import android.app.ActivityManager.TaskDescription;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;

import androidx.annotation.CallSuper;
import androidx.annotation.IntDef;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import com.google.android.material.color.DynamicColors;

import org.chromium.base.BuildInfo;
import org.chromium.base.BundleUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.cached_flags.BooleanCachedFieldTrialParameter;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.base.ServiceTracingProxyProvider;
import org.chromium.chrome.browser.base.SplitChromeApplication;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.language.GlobalAppLocaleController;
import org.chromium.chrome.browser.metrics.UmaSessionStats;
import org.chromium.chrome.browser.night_mode.GlobalNightModeStateProviderHolder;
import org.chromium.chrome.browser.night_mode.NightModeStateProvider;
import org.chromium.chrome.browser.night_mode.NightModeUtils;
import org.chromium.components.browser_ui.util.AutomotiveUtils;
import org.chromium.ui.display.DisplaySwitches;
import org.chromium.ui.display.DisplayUtil;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogManagerHolder;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedHashSet;

/**
 * A subclass of {@link AppCompatActivity} that maintains states and objects applied to all
 * activities in {@link ChromeApplication} (e.g. night mode).
 */
public class ChromeBaseAppCompatActivity extends AppCompatActivity
        implements NightModeStateProvider.Observer, ModalDialogManagerHolder {
    /**
     * Chrome in automotive needs a persistent back button toolbar above all activities because
     * AAOS/cars do not have a built in back button. This is implemented differently in each
     * activity.
     *
     * Activities that use the <merge> tag or delay layout inflation cannot use WITH_TOOLBAR_VIEW.
     * Activities that use their own action bar cannot use WITH_ACTION_BAR.
     * Activities that appear as Dialogs using themes do not have an automotive toolbar yet (NONE).
     *
     * Full screen alert dialogs display the automotive toolbar using FullscreenAlertDialog.
     * Full screen dialogs display the automotive toolbar using ChromeDialog.
     */
    @IntDef({
        AutomotiveToolbarImplementation.WITH_TOOLBAR_VIEW,
        AutomotiveToolbarImplementation.WITH_ACTION_BAR,
        AutomotiveToolbarImplementation.NONE,
    })
    @Retention(RetentionPolicy.SOURCE)
    protected @interface AutomotiveToolbarImplementation {
        /**
         * Automotive toolbar is added by including the original layout into a bigger LinearLayout
         * that has a Toolbar View, see
         * R.layout.automotive_layout_with_horizontal_back_button_toolbar and
         * R.layout.automotive_layout_with_vertical_back_button_toolbar.
         */
        int WITH_TOOLBAR_VIEW = 0;

        /**
         * Automotive toolbar is added using AppCompatActivity's ActionBar, provided with a
         * ThemeOverlay, see R.style.ThemeOverlay_BrowserUI_Automotive_PersistentBackButtonToolbar.
         *
         * <p>This will be deprecated because it does not support a vertical toolbar.
         */
        @Deprecated int WITH_ACTION_BAR = 1;

        /** Automotive toolbar is not added. */
        int NONE = -1;
    }

    public static final String DEFAULT_FONT_FAMILY_TESTING_PARAM = "dev_testing";
    public static final BooleanCachedFieldTrialParameter DEFAULT_FONT_FAMILY_TESTING =
            ChromeFeatureList.newBooleanCachedFieldTrialParameter(
                    ChromeFeatureList.ANDROID_GOOGLE_SANS_TEXT,
                    DEFAULT_FONT_FAMILY_TESTING_PARAM,
                    false);

    private final ObservableSupplierImpl<ModalDialogManager> mModalDialogManagerSupplier =
            new ObservableSupplierImpl<>();
    private NightModeStateProvider mNightModeStateProvider;
    private LinkedHashSet<Integer> mThemeResIds = new LinkedHashSet<>();
    private ServiceTracingProxyProvider mServiceTracingProxyProvider;

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);

        // Make sure the "chrome" split is loaded before checking if ClassLoaders are equal.
        SplitChromeApplication.finishPreload(CHROME_SPLIT_NAME);
        ClassLoader chromeModuleClassLoader = ChromeBaseAppCompatActivity.class.getClassLoader();
        Context appContext = ContextUtils.getApplicationContext();
        if (!chromeModuleClassLoader.equals(appContext.getClassLoader())) {
            // This should only happen on Android O. See crbug.com/1146745 for more info.
            throw new IllegalStateException(
                    "ClassLoader mismatch detected.\nA: "
                            + chromeModuleClassLoader
                            + "\nB: "
                            + appContext.getClassLoader()
                            + "\nC: "
                            + chromeModuleClassLoader.getParent()
                            + "\nD: "
                            + appContext.getClassLoader().getParent()
                            + "\nE: "
                            + appContext);
        }
        // If ClassLoader was corrected by SplitCompatAppComponentFactory, also need to correct
        // the reference in the associated Context.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            BundleUtils.checkContextClassLoader(newBase, this);
        }

        mServiceTracingProxyProvider = ServiceTracingProxyProvider.create(newBase);

        mNightModeStateProvider = createNightModeStateProvider();

        Configuration config = new Configuration();
        // Pre-Android O, fontScale gets initialized to 1 in the constructor. Set it to 0 so
        // that applyOverrideConfiguration() does not interpret it as an overridden value.
        // https://crbug.com/834191
        config.fontScale = 0;
        // NightMode and other applyOverrides must be done before onCreate in attachBaseContext.
        // https://crbug.com/1139760
        if (applyOverrides(newBase, config)) applyOverrideConfiguration(config);
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        BundleUtils.restoreLoadedSplits(savedInstanceState);
        mModalDialogManagerSupplier.set(createModalDialogManager());

        initializeNightModeStateProvider();
        mNightModeStateProvider.addObserver(this);

        // onCreate may initialize some views, need to apply themes before that can happen.
        applyThemeOverlays();
        super.onCreate(savedInstanceState);

        // Activity level locale overrides must be done in onCreate.
        GlobalAppLocaleController.getInstance().maybeOverrideContextConfig(this);

        setDefaultTaskDescription();
    }

    @Override
    protected void onDestroy() {
        mNightModeStateProvider.removeObserver(this);
        if (mModalDialogManagerSupplier.get() != null) {
            mModalDialogManagerSupplier.get().destroy();
            mModalDialogManagerSupplier.set(null);
        }
        super.onDestroy();
    }

    @Override
    public ClassLoader getClassLoader() {
        // Replace the default ClassLoader with a custom SplitAware one so that
        // LayoutInflaters that use this ClassLoader can find view classes that
        // live inside splits. Very useful when FragmentManger tries to inflate
        // the UI automatically on restore.
        return BundleUtils.getSplitCompatClassLoader();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        BundleUtils.saveLoadedSplits(outState);
    }

    @Override
    protected void onRestoreInstanceState(@Nullable Bundle state) {
        if (state != null) {
            // Ensure that classes from previously loaded splits can be read from the bundle.
            // https://crbug.com/1382227
            ClassLoader splitClassLoader = BundleUtils.getSplitCompatClassLoader();
            state.setClassLoader(splitClassLoader);
            // See: https://cs.android.com/search?q=Activity.java%20symbol:onRestoreInstanceState
            Bundle windowState = state.getBundle("android:viewHierarchyState");
            if (windowState != null) {
                windowState.setClassLoader(splitClassLoader);
            }
        }
        super.onRestoreInstanceState(state);
    }

    @Override
    public void setTheme(@StyleRes int resid) {
        super.setTheme(resid);
        mThemeResIds.add(resid);
    }

    @Override
    @RequiresApi(Build.VERSION_CODES.O)
    public void onMultiWindowModeChanged(boolean inMultiWindowMode, Configuration configuration) {
        super.onMultiWindowModeChanged(inMultiWindowMode, configuration);
        onMultiWindowModeChanged(inMultiWindowMode);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        NightModeUtils.updateConfigurationForNightMode(
                this, mNightModeStateProvider.isInNightMode(), newConfig, mThemeResIds);
        // newConfig will have the default system locale so reapply the app locale override if
        // needed: https://crbug.com/1248944
        GlobalAppLocaleController.getInstance().maybeOverrideContextConfig(this);
    }

    // Implementation of ModalDialogManagerHolder
    /**
     * @return The {@link ModalDialogManager} that manages the display of modal dialogs (e.g.
     *         JavaScript dialogs).
     */
    @Override
    public ModalDialogManager getModalDialogManager() {
        // TODO(jinsukkim): Remove this method in favor of getModalDialogManagerSupplier().
        return getModalDialogManagerSupplier().get();
    }

    /**
     * Returns the supplier of {@link ModalDialogManager} that manages the display of modal dialogs.
     */
    public ObservableSupplier<ModalDialogManager> getModalDialogManagerSupplier() {
        return mModalDialogManagerSupplier;
    }

    /**
     * Creates a {@link ModalDialogManager} for this class. Subclasses that need one should override
     * this method.
     */
    protected @Nullable ModalDialogManager createModalDialogManager() {
        return null;
    }

    /**
     * Called during {@link #attachBaseContext(Context)} to allow configuration overrides to be
     * applied. If this methods return true, the overrides will be applied using
     * {@link #applyOverrideConfiguration(Configuration)}.
     * @param baseContext The base {@link Context} attached to this class.
     * @param overrideConfig The {@link Configuration} that will be passed to
     *                       @link #applyOverrideConfiguration(Configuration)} if necessary.
     * @return True if any configuration overrides were applied, and false otherwise.
     */
    @CallSuper
    protected boolean applyOverrides(Context baseContext, Configuration overrideConfig) {
        applyOverridesForAutomotive(baseContext, overrideConfig);
        return NightModeUtils.applyOverridesForNightMode(
                getNightModeStateProvider(), overrideConfig);
    }

    @VisibleForTesting
    static void applyOverridesForAutomotive(Context baseContext, Configuration overrideConfig) {
        if (BuildInfo.getInstance().isAutomotive) {
            DisplayUtil.scaleUpConfigurationForAutomotive(baseContext, overrideConfig);

            // Enable web ui scaling for automotive devices.
            CommandLine.getInstance()
                    .appendSwitch(DisplaySwitches.AUTOMOTIVE_WEB_UI_SCALE_UP_ENABLED);
        }
    }

    /**
     * @return The {@link NightModeStateProvider} that provides the state of night mode.
     */
    protected final NightModeStateProvider getNightModeStateProvider() {
        return mNightModeStateProvider;
    }

    /**
     * @return The {@link NightModeStateProvider} that provides the state of night mode in the scope
     *         of this class.
     */
    protected NightModeStateProvider createNightModeStateProvider() {
        return GlobalNightModeStateProviderHolder.getInstance();
    }

    /**
     * Initializes the initial night mode state. This will be called at the beginning of
     * {@link #onCreate(Bundle)} so that the correct theme can be applied for the initial night mode
     * state.
     */
    protected void initializeNightModeStateProvider() {}

    /** Apply theme overlay to this activity class. */
    @CallSuper
    protected void applyThemeOverlays() {
        // Note that if you're adding new overlays here, it's quite likely they're needed
        // in org.chromium.chrome.browser.WarmupManager#applyContextOverrides for Custom Tabs
        // UI that's pre-inflated using a themed application context as part of CCT warmup.
        DynamicColors.applyToActivityIfAvailable(this);

        DeferredStartupHandler.getInstance()
                .addDeferredTask(
                        () -> {
                            // #registerSyntheticFieldTrial requires native.
                            boolean isDynamicColorAvailable =
                                    DynamicColors.isDynamicColorAvailable();
                            RecordHistogram.recordBooleanHistogram(
                                    "Android.DynamicColors.IsAvailable", isDynamicColorAvailable);
                            UmaSessionStats.registerSyntheticFieldTrial(
                                    "IsDynamicColorAvailable",
                                    isDynamicColorAvailable ? "Enabled" : "Disabled");
                        });

        if (BuildInfo.getInstance().isAutomotive
                && getAutomotiveToolbarImplementation()
                        == AutomotiveToolbarImplementation.WITH_ACTION_BAR) {
            int automotiveOverlay =
                    R.style.ThemeOverlay_BrowserUI_Automotive_PersistentBackButtonToolbar;
            getTheme().applyStyle(automotiveOverlay, /* force= */ true);
            mThemeResIds.add(automotiveOverlay);
        }

        if (ChromeFeatureList.sAndroidElegantTextHeight.isEnabled()) {
            int elegantTextHeightOverlay = R.style.ThemeOverlay_BrowserUI_ElegantTextHeight;
            getTheme().applyStyle(elegantTextHeightOverlay, true);
            mThemeResIds.add(elegantTextHeightOverlay);
        }

        if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU
                && ChromeFeatureList.sAndroidGoogleSansText.isEnabled()) {
            int defaultFontFamilyOverlay =
                    DEFAULT_FONT_FAMILY_TESTING.getValue()
                            ? R.style.ThemeOverlay_BrowserUI_DevTestingDefaultFontFamilyThemeOverlay
                            : R.style.ThemeOverlay_BrowserUI_DefaultFontFamilyThemeOverlay;
            getTheme().applyStyle(defaultFontFamilyOverlay, true);
            mThemeResIds.add(defaultFontFamilyOverlay);
        }
    }

    /** Sets the default task description that will appear in the recents UI. */
    protected void setDefaultTaskDescription() {
        final TaskDescription taskDescription =
                new TaskDescription(null, null, getColor(R.color.default_task_description_color));
        setTaskDescription(taskDescription);
    }

    // NightModeStateProvider.Observer implementation.
    @Override
    public void onNightModeStateChanged() {
        if (!isFinishing()) recreate();
    }

    /** Required to make preference fragments use InMemorySharedPreferences in tests. */
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        return ContextUtils.getApplicationContext().getSharedPreferences(name, mode);
    }

    // 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;
    }

    /**
     * Set the back button in the automotive toolbar to perform an Android system level back.
     *
     * This toolbar will be used to do things like exit fullscreen YouTube videos because AAOS/cars
     * don't have a built in back button
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            getOnBackPressedDispatcher().onBackPressed();
            return true;
        }
        return false;
    }

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        if (BuildInfo.getInstance().isAutomotive
                && getAutomotiveToolbarImplementation()
                        == AutomotiveToolbarImplementation.WITH_TOOLBAR_VIEW) {
            super.setContentView(AutomotiveUtils.getAutomotiveLayoutWithBackButtonToolbar(this));
            setAutomotiveToolbarBackButtonAction();
            ViewStub stub = findViewById(R.id.original_layout);
            stub.setLayoutResource(layoutResID);
            stub.inflate();
        } else {
            super.setContentView(layoutResID);
        }
    }

    @Override
    public void setContentView(View view) {
        if (BuildInfo.getInstance().isAutomotive
                && getAutomotiveToolbarImplementation()
                        == AutomotiveToolbarImplementation.WITH_TOOLBAR_VIEW) {
            super.setContentView(AutomotiveUtils.getAutomotiveLayoutWithBackButtonToolbar(this));
            setAutomotiveToolbarBackButtonAction();
            LinearLayout linearLayout = findViewById(R.id.automotive_base_linear_layout);
            linearLayout.addView(view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        } else {
            super.setContentView(view);
        }
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (BuildInfo.getInstance().isAutomotive
                && getAutomotiveToolbarImplementation()
                        == AutomotiveToolbarImplementation.WITH_TOOLBAR_VIEW) {
            super.setContentView(AutomotiveUtils.getAutomotiveLayoutWithBackButtonToolbar(this));
            setAutomotiveToolbarBackButtonAction();
            LinearLayout linearLayout = findViewById(R.id.automotive_base_linear_layout);
            linearLayout.setLayoutParams(params);
            linearLayout.addView(view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        } else {
            super.setContentView(view, params);
        }
    }

    @Override
    public void addContentView(View view, ViewGroup.LayoutParams params) {
        if (BuildInfo.getInstance().isAutomotive
                && params.width == MATCH_PARENT
                && params.height == MATCH_PARENT) {
            ViewGroup automotiveLayout =
                    (ViewGroup)
                            getLayoutInflater()
                                    .inflate(
                                            AutomotiveUtils
                                                    .getAutomotiveLayoutWithBackButtonToolbar(this),
                                            null);
            super.addContentView(
                    automotiveLayout, new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            setAutomotiveToolbarBackButtonAction();
            automotiveLayout.addView(view, params);
        } else {
            super.addContentView(view, params);
        }
    }

    @Override
    protected void onResume() {
        if (BuildInfo.getInstance().isAutomotive
                && getAutomotiveToolbarImplementation()
                        == AutomotiveToolbarImplementation.WITH_ACTION_BAR
                && getSupportActionBar() != null) {
            getSupportActionBar().setHomeActionContentDescription(R.string.back);
        }
        super.onResume();
    }

    protected int getAutomotiveToolbarImplementation() {
        int activityStyle = -1;
        try {
            activityStyle =
                    getPackageManager().getActivityInfo(getComponentName(), 0).getThemeResource();
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (activityStyle == R.style.Theme_Chromium_DialogWhenLarge) {
            return AutomotiveToolbarImplementation.NONE;
        } else {
            return AutomotiveToolbarImplementation.WITH_TOOLBAR_VIEW;
        }
    }

    private void setAutomotiveToolbarBackButtonAction() {
        Toolbar backButtonToolbarForAutomotive = findViewById(R.id.back_button_toolbar);
        if (backButtonToolbarForAutomotive != null) {
            backButtonToolbarForAutomotive.setNavigationOnClickListener(
                    backButtonClick -> {
                        getOnBackPressedDispatcher().onBackPressed();
                    });
        }
    }
}