chromium/chrome/android/java/src/org/chromium/chrome/browser/init/AsyncInitializationActivity.java

// Copyright 2015 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.init;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.view.Display;
import android.view.Menu;
import android.view.View;
import android.view.WindowManager;

import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.SysUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LoaderErrors;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.build.BuildConfig;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeBaseAppCompatActivity;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.LaunchIntentDispatcher;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.firstrun.FirstRunFlowSequencer;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcherProvider;
import org.chromium.chrome.browser.metrics.SimpleStartupForegroundSessionDetector;
import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcherImpl;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.util.BrowserUiUtils;
import org.chromium.components.browser_ui.share.ShareHelper;
import org.chromium.components.browser_ui.util.FirstDrawDetector;
import org.chromium.ui.base.ActivityIntentRequestTrackerDelegate;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.IntentRequestTracker;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.display.DisplayUtil;

/**
 * An activity that talks with application and activity level delegates for async initialization.
 */
public abstract class AsyncInitializationActivity extends ChromeBaseAppCompatActivity
        implements ChromeActivityNativeDelegate, BrowserParts, ActivityLifecycleDispatcherProvider {
    @VisibleForTesting
    public static final String FIRST_DRAW_COMPLETED_TIME_MS_UMA = "FirstDrawCompletedTime";

    public static final String TAG_MULTI_INSTANCE = "MultiInstance";

    protected final Handler mHandler;

    private final NativeInitializationController mNativeInitializationController =
            new NativeInitializationController(this);

    private final ActivityLifecycleDispatcherImpl mLifecycleDispatcher =
            new ActivityLifecycleDispatcherImpl(this);
    private final MultiWindowModeStateDispatcherImpl mMultiWindowModeStateDispatcher =
            new MultiWindowModeStateDispatcherImpl(this);
    private final IntentRequestTracker mIntentRequestTracker;

    /** Time at which onCreate is called. This is realtime, counted in ms since device boot. */
    private long mOnCreateTimestampMs;

    /** Time at which onPause is called. */
    private long mOnPauseTimestampMs;

    /**
     * Time at which onPause is called before the activity is recreated due to unfolding. The
     * timestamp is captured only if recreation starts when the activity is not in stopped state.
     */
    private long mOnPauseBeforeFoldRecreateTimestampMs;

    private ActivityWindowAndroid mWindowAndroid;
    private OneshotSupplier<ProfileProvider> mProfileProviderSupplier;
    private Bundle mSavedInstanceState;
    private int mCurrentOrientation;
    private boolean mDestroyed;
    private long mLastUserInteractionTime;
    private boolean mIsTablet;
    private boolean mHadWarmStart;
    private boolean mIsWarmOnResume;

    // Stores whether the activity was not resumed yet. Always false after the
    // first |onResume| call.
    private boolean mFirstResumePending = true;

    private boolean mStartupDelayed;
    private boolean mFirstDrawComplete;

    private Runnable mOnInflationCompleteCallback;
    private boolean mInitialLayoutInflationComplete;

    // See enableHardwareAcceleration()
    private boolean mSetWindowHWA;

    private static boolean sInterceptMoveTaskToBackForTesting;
    private static boolean sBackInterceptedForTesting;

    public AsyncInitializationActivity() {
        mHandler = new Handler();
        mIntentRequestTracker =
                IntentRequestTracker.createFromDelegate(
                        new ActivityIntentRequestTrackerDelegate(this) {
                            @Override
                            public boolean onCallbackNotFoundError(String error) {
                                return onIntentCallbackNotFoundError(error);
                            }
                        });
    }

    /** Get the tracker of this activity's intents. */
    public IntentRequestTracker getIntentRequestTracker() {
        return mIntentRequestTracker;
    }

    @CallSuper
    @Override
    protected void onDestroy() {
        mDestroyed = true;
        mLifecycleDispatcher.onDestroyStarted();

        if (mWindowAndroid != null) {
            mWindowAndroid.destroy();
            mWindowAndroid = null;
        }

        super.onDestroy();
        mLifecycleDispatcher.dispatchOnDestroy();
    }

    @Override
    @CallSuper
    protected boolean applyOverrides(Context baseContext, Configuration overrideConfig) {
        super.applyOverrides(baseContext, overrideConfig);

        // We override the smallestScreenWidthDp here for two reasons:
        // 1. To prevent multi-window from hiding the tabstrip when on a tablet.
        // 2. To ensure mIsTablet only needs to be set once. Since the override lasts for the life
        //    of the activity, it will never change via onConfigurationUpdated().
        // See crbug.com/588838, crbug.com/662338, crbug.com/780593.
        overrideConfig.smallestScreenWidthDp =
                DisplayUtil.getCurrentSmallestScreenWidth(baseContext);
        return true;
    }

    @Override
    public final void preInflationStartup() {
        performPreInflationStartup();
    }

    /**
     * Perform pre-inflation startup for the activity. Sub-classes providing custom pre-inflation
     * startup logic should override this method.
     */
    @CallSuper
    protected void performPreInflationStartup() {
        mIsTablet = DeviceFormFactor.isNonMultiDisplayContextOnTablet(this);
        mHadWarmStart = LibraryLoader.getInstance().isInitialized();
        // TODO(crbug.com/40621278): Dispatch in #preInflationStartup instead so that
        // subclass's #performPreInflationStartup has executed before observers are notified.
        mLifecycleDispatcher.dispatchPreInflationStartup();
    }

    @Override
    public final void setContentViewAndLoadLibrary(Runnable onInflationCompleteCallback) {
        mOnInflationCompleteCallback = onInflationCompleteCallback;

        // Start loading libraries. It happens before triggerLayoutInflation(). This "hides" library
        // loading behind UI inflation and prevents stalling UI thread.
        // See https://crbug.com/796957 for details. Note that for optimal performance
        // AsyncInitTaskRunner.startBackgroundTasks() needs to start warm up renderer only after
        // library is loaded.

        if (!mStartupDelayed) {
            // Kick off long running IO tasks that can be done in parallel.
            mNativeInitializationController.startBackgroundTasks(shouldAllocateChildConnection());
        }

        triggerLayoutInflation();
    }

    /** Controls the parameter of {@link NativeInitializationController#startBackgroundTasks}. */
    @VisibleForTesting
    public boolean shouldAllocateChildConnection() {
        // If a spare WebContents exists, a child connection has already been allocated that will be
        // used by the next created tab.
        return !WarmupManager.getInstance().hasSpareWebContents();
    }

    @Override
    public final void postInflationStartup() {
        performPostInflationStartup();
        mLifecycleDispatcher.dispatchOnInflationComplete();
        mLifecycleDispatcher.dispatchPostInflationStartup();
    }

    /**
     * Perform post-inflation startup for the activity. Sub-classes providing custom post-inflation
     * startup logic should override this method.
     */
    @CallSuper
    protected void performPostInflationStartup() {
        View firstDrawView = getViewToBeDrawnBeforeInitializingNative();
        assert firstDrawView != null;
        FirstDrawDetector.waitForFirstDraw(
                firstDrawView,
                () -> {
                    mFirstDrawComplete = true;
                    BrowserUiUtils.recordHistogram(
                            FIRST_DRAW_COMPLETED_TIME_MS_UMA,
                            SystemClock.elapsedRealtime() - getOnCreateTimestampMs());
                    if (!mStartupDelayed) {
                        onFirstDrawComplete();
                    }
                });
    }

    /**
     * @return The primary view that must have completed at least one draw before initializing
     *         native.  This must be non-null.
     */
    protected View getViewToBeDrawnBeforeInitializingNative() {
        return findViewById(android.R.id.content);
    }

    @Override
    public void maybePreconnect() {
        try {
            TraceEvent.begin("maybePreconnect");
            Intent intent = getIntent();
            if (intent == null || !Intent.ACTION_VIEW.equals(intent.getAction())) return;
            String url = IntentHandler.getUrlFromIntent(intent);
            if (url == null) return;
            // Blocking pre-connect for all off-the-record profiles.
            if (IntentHandler.hasAnyIncognitoExtra(intent.getExtras())) return;
            assert getProfileProviderSupplier().hasValue();
            getProfileProviderSupplier()
                    .runSyncOrOnAvailable(
                            (profileProvider) -> {
                                WarmupManager.getInstance()
                                        .maybePreconnectUrlAndSubResources(
                                                profileProvider.getOriginalProfile(), url);
                            });
        } finally {
            TraceEvent.end("maybePreconnect");
        }
    }

    @Override
    public void initializeCompositor() {}

    @Override
    public void initializeState() {}

    @CallSuper
    @Override
    public void finishNativeInitialization() {
        // Set up the initial orientation of the device.
        checkOrientation();
        findViewById(android.R.id.content)
                .addOnLayoutChangeListener(
                        new View.OnLayoutChangeListener() {
                            @Override
                            public void onLayoutChange(
                                    View v,
                                    int left,
                                    int top,
                                    int right,
                                    int bottom,
                                    int oldLeft,
                                    int oldTop,
                                    int oldRight,
                                    int oldBottom) {
                                checkOrientation();
                            }
                        });
        mNativeInitializationController.onNativeInitializationComplete();
        mLifecycleDispatcher.dispatchNativeInitializationFinished();
    }

    @CallSuper
    @Override
    public void onStartupFailure(Exception failureCause) {
        throw new ProcessInitException(LoaderErrors.NATIVE_STARTUP_FAILED, failureCause);
    }

    @Override
    public void onTopResumedActivityChangedWithNative(boolean isTopResumedActivity) {}

    /**
     * Extending classes should override {@link AsyncInitializationActivity#preInflationStartup()},
     * {@link AsyncInitializationActivity#triggerLayoutInflation()} and {@link
     * AsyncInitializationActivity#postInflationStartup()} instead of this call which will be called
     * on that order.
     */
    @Override
    @SuppressLint("MissingSuperCall") // Called in onCreateInternal.
    protected final void onCreate(Bundle savedInstanceState) {
        TraceEvent.begin("AsyncInitializationActivity.onCreate()");
        onPreCreate();
        boolean willCreate = onCreateInternal(savedInstanceState);
        if (!willCreate) {
            onAbortCreate();
        } else {
            onPostCreate();
        }
        TraceEvent.end("AsyncInitializationActivity.onCreate()");
    }

    /**
     * Override to perform operations in the first opportunity after the framework calls
     * {@link #onCreate}. Note the activity may still be aborted by {@link #onCreateInternal}.
     */
    protected void onPreCreate() {}

    /**
     * Override to perform operations after the activity's creation is aborted by {@link
     * #onCreateInternal}.
     */
    protected void onAbortCreate() {}

    /**
     * Override to perform operations after the framework successfully calls {@link
     * #onCreateInternal}. This method is used in the ChromeActivity derived class to increment the
     * "Chrome.UMA.OnPostCreateCounter2" counter for the histogram
     * UMA.AndroidPreNative.ChromeActivityCounter2.
     */
    protected void onPostCreate() {}

    /**
     * Called from onCreate() to give derived classes a chance to dispatch the intent using
     * {@link LaunchIntentDispatcher}. If the method returns anything other than Action.CONTINUE,
     * the activity is aborted. Default implementation returns Action.CONTINUE.
     * @param intent intent to dispatch
     * @return {@link LaunchIntentDispatcher.Action} to take
     */
    protected @LaunchIntentDispatcher.Action int maybeDispatchLaunchIntent(
            Intent intent, Bundle savedInstanceState) {
        return LaunchIntentDispatcher.Action.CONTINUE;
    }

    /**
     * @return true if will proceed with Activity creation, false if will abort.
     */
    private boolean onCreateInternal(Bundle savedInstanceState) {
        initializeStartupMetrics();
        setIntent(IntentHandler.rewriteFromHistoryIntent(getIntent()));

        @LaunchIntentDispatcher.Action
        int dispatchAction = maybeDispatchLaunchIntent(getIntent(), savedInstanceState);
        if (dispatchAction != LaunchIntentDispatcher.Action.CONTINUE) {
            abortLaunch(dispatchAction);
            return false;
        }

        Intent intent = getIntent();
        if (!isStartedUpCorrectly(intent)) {
            abortLaunch(LaunchIntentDispatcher.Action.FINISH_ACTIVITY_REMOVE_TASK);
            return false;
        }

        if (requiresFirstRunToBeCompleted(intent)
                && FirstRunFlowSequencer.launch(this, intent, shouldPreferLightweightFre(intent))) {
            abortLaunch(LaunchIntentDispatcher.Action.FINISH_ACTIVITY);
            return false;
        }

        super.onCreate(transformSavedInstanceStateForOnCreate(savedInstanceState));
        mOnCreateTimestampMs = SystemClock.elapsedRealtime();
        mSavedInstanceState = savedInstanceState;

        mWindowAndroid = createWindowAndroid();
        mIntentRequestTracker.restoreInstanceState(getSavedInstanceState());
        mProfileProviderSupplier = createProfileProvider();

        mStartupDelayed = shouldDelayBrowserStartup();

        ChromeBrowserInitializer.getInstance().handlePreNativeStartupAndLoadLibraries(this);
        return true;
    }

    /**
     * A custom error handler for {@link IntentRequestTracker}. When false, the tracker will use
     * the default error handler. Derived classes can override this method to customize the error
     * handling.
     */
    protected boolean onIntentCallbackNotFoundError(String error) {
        return false;
    }

    /**
     * This method is called very early on during Activity.onCreate. Subclassing activities should
     * use this to initialize their tracking metrics including things like Activity start time.
     */
    protected void initializeStartupMetrics() {}

    private void abortLaunch(@LaunchIntentDispatcher.Action int dispatchAction) {
        super.onCreate(null);
        if (dispatchAction == LaunchIntentDispatcher.Action.FINISH_ACTIVITY) {
            finish();
            return;
        } else {
            assert dispatchAction == LaunchIntentDispatcher.Action.FINISH_ACTIVITY_REMOVE_TASK;
            finishAndRemoveTask();
        }
        overridePendingTransition(0, R.anim.no_anim);
    }

    /** Call to begin loading the library, if it was delayed. */
    @CallSuper
    protected void startDelayedNativeInitialization() {
        assert mStartupDelayed;
        mStartupDelayed = false;

        // Kick off long running IO tasks that can be done in parallel.
        mNativeInitializationController.startBackgroundTasks(shouldAllocateChildConnection());

        if (mFirstDrawComplete) onFirstDrawComplete();
    }

    public void startDelayedNativeInitializationForTests() {
        mStartupDelayed = true;
        startDelayedNativeInitialization();
    }

    /**
     * @return Whether the browser startup should be delayed. Note that changing this return value
     *         will have direct impact on startup performance.
     */
    protected boolean shouldDelayBrowserStartup() {
        return false;
    }

    /**
     * Allows subclasses to override the instance state passed to super.onCreate().
     * The original instance state will still be available via getSavedInstanceState().
     */
    protected Bundle transformSavedInstanceStateForOnCreate(Bundle savedInstanceState) {
        return savedInstanceState;
    }

    /**
     * Overriding this function is almost always wrong.
     * @return Whether or not the user needs to go through First Run before using this Activity.
     */
    protected boolean requiresFirstRunToBeCompleted(Intent intent) {
        return true;
    }

    /**
     * Whether to use the Lightweight First Run Experience instead of the
     * non-Lightweight First Run Experience.
     */
    protected boolean shouldPreferLightweightFre(Intent intent) {
        return false;
    }

    /** Whether or not the Activity was started up via a valid Intent. */
    protected boolean isStartedUpCorrectly(Intent intent) {
        return true;
    }

    /**
     * @return The uptime for the activity creation in ms.
     */
    protected long getOnCreateTimestampMs() {
        return mOnCreateTimestampMs;
    }

    /**
     * @return The timestamp for OnPause event before activity restarts due to unfolding in ms.
     */
    protected long getOnPauseBeforeFoldRecreateTimestampMs() {
        try (TraceEvent e =
                TraceEvent.scoped(
                        "AsyncInit.getOnPauseBeforeFoldRecreateTimestampMs",
                        Long.toString(mOnPauseBeforeFoldRecreateTimestampMs))) {
            return mOnPauseBeforeFoldRecreateTimestampMs;
        }
    }

    protected void setOnPauseBeforeFoldRecreateTimestampMs() {
        try (TraceEvent e =
                TraceEvent.scoped(
                        "AsyncInit.setOnPauseBeforeFoldRecreateTimestampMs",
                        Long.toString(mOnPauseTimestampMs))) {
            mOnPauseBeforeFoldRecreateTimestampMs = mOnPauseTimestampMs;
        }
    }

    /**
     * @return The saved bundle for the last recorded state.
     */
    public Bundle getSavedInstanceState() {
        return mSavedInstanceState;
    }

    /** Resets the saved state and makes it unavailable for the rest of the activity lifecycle. */
    protected void resetSavedInstanceState() {
        mSavedInstanceState = null;
    }

    @CallSuper
    @Override
    public void onStart() {
        super.onStart();
        mNativeInitializationController.onStart();

        // Since this activity is being started, the FRE should have been handled somehow already.
        Intent intent = getIntent();
        if (FirstRunFlowSequencer.checkIfFirstRunIsNecessary(
                        shouldPreferLightweightFre(intent), intent)
                && requiresFirstRunToBeCompleted(intent)) {
            throw new IllegalStateException(
                    "The app has not completed the FRE yet "
                            + getClass().getName()
                            + " is trying to start.");
        }
    }

    @CallSuper
    @Override
    public void onResume() {
        super.onResume();

        // Start by setting the launch as cold or warm. It will be used in some resume handlers.
        mIsWarmOnResume = !mFirstResumePending || hadWarmStart();
        mFirstResumePending = false;

        SimpleStartupForegroundSessionDetector.onTransitionToForeground();
        mNativeInitializationController.onResume();
    }

    @CallSuper
    @Override
    public void onPause() {
        mOnPauseTimestampMs = SystemClock.uptimeMillis();
        SimpleStartupForegroundSessionDetector.discardSession();
        mNativeInitializationController.onPause();
        super.onPause();
    }

    @CallSuper
    @Override
    public void onStop() {
        super.onStop();
        mNativeInitializationController.onStop();
    }

    @CallSuper
    @Override
    @SuppressLint("MissingSuperCall") // Empty method in parent Activity class.
    public void onNewIntent(Intent intent) {
        if (intent == null) return;
        if (ShareHelper.isCleanerIntent(intent)) return;
        mNativeInitializationController.onNewIntent(intent);
        setIntent(intent);
    }

    @CallSuper
    @Override
    @SuppressLint("MissingSuperCall") // Empty method in parent Activity class.
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        mNativeInitializationController.onActivityResult(requestCode, resultCode, data);
    }

    @Override
    public final void onCreateWithNative() {
        mLifecycleDispatcher.onCreateWithNative();
        ChromeBrowserInitializer.getInstance().handlePostNativeStartup(true, this);
    }

    @CallSuper
    @Override
    public void onStartWithNative() {
        mLifecycleDispatcher.dispatchOnStartWithNative();
    }

    @CallSuper
    @Override
    public void onResumeWithNative() {
        mLifecycleDispatcher.dispatchOnResumeWithNative();
    }

    @CallSuper
    @Override
    public void onPauseWithNative() {
        mLifecycleDispatcher.dispatchOnPauseWithNative();
    }

    @CallSuper
    @Override
    public void onStopWithNative() {
        mLifecycleDispatcher.dispatchOnStopWithNative();
    }

    @CallSuper
    @Override
    protected void onUserLeaveHint() {
        super.onUserLeaveHint();
        mLifecycleDispatcher.dispatchOnUserLeaveHint();
    }

    @Override
    public boolean isActivityFinishingOrDestroyed() {
        return mDestroyed || isFinishing();
    }

    /**
     * Every child class wanting to perform tasks on configuration changed should override
     * {@link #performOnConfigurationChanged(Configuration)} instead.
     * @param newConfig The new configuration.
     */
    @Override
    public final void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        performOnConfigurationChanged(newConfig);
        mLifecycleDispatcher.dispatchOnConfigurationChanged(newConfig);
    }

    /**
     * Handle an {@link #onConfigurationChanged(Configuration)} event.
     * @param newConfig The new configuration.
     */
    @CallSuper
    public void performOnConfigurationChanged(Configuration newConfig) {}

    @Override
    public void onMultiWindowModeChanged(boolean inMultiWindowMode) {
        super.onMultiWindowModeChanged(inMultiWindowMode);
        mMultiWindowModeStateDispatcher.dispatchMultiWindowModeChanged(inMultiWindowMode);
    }

    @Override
    public abstract boolean shouldStartGpuProcess();

    @CallSuper
    @Override
    public void onContextMenuClosed(Menu menu) {
        if (mWindowAndroid != null) mWindowAndroid.onContextMenuClosed();
    }

    /**
     * Called when the content view gets drawn for the first time. See {@link FirstDrawDetector} for
     * details on the exact signals used to call this.
     */
    @CallSuper
    protected void onFirstDrawComplete() {
        assert mFirstDrawComplete;
        assert !mStartupDelayed;
        TraceEvent.instant("onFirstDrawComplete");
        mNativeInitializationController.firstDrawComplete();
    }

    @Override
    public void onNewIntentWithNative(Intent intent) {}

    @Override
    public Intent getInitialIntent() {
        return getIntent();
    }

    /**
     * Creates an {@link ActivityWindowAndroid} to delegate calls to, if the Activity requires it.
     */
    protected @Nullable ActivityWindowAndroid createWindowAndroid() {
        return null;
    }

    /**
     * @return A {@link ActivityWindowAndroid} instance. May be null if one was not created.
     */
    public @Nullable ActivityWindowAndroid getWindowAndroid() {
        return mWindowAndroid;
    }

    /**
     * Handles creating the {@link ProfileProvider} for the given Activity.
     *
     * <p>Implementers should not assume the native library is loaded when this is triggered.
     */
    protected abstract @NonNull OneshotSupplier<ProfileProvider> createProfileProvider();

    /** Return a supplier for the ProfileProvider. */
    public OneshotSupplier<ProfileProvider> getProfileProviderSupplier() {
        // TODO(crbug.com/40275690): Convert to a thrown exception if no asserts are discovered.
        assert mProfileProviderSupplier != null;
        return mProfileProviderSupplier;
    }

    /**
     * This will handle passing {@link Intent} results back to the {@link WindowAndroid}.  It will
     * return whether or not the {@link WindowAndroid} has consumed the event or not.
     */
    @CallSuper
    @Override
    public boolean onActivityResultWithNative(int requestCode, int resultCode, Intent intent) {
        if (mIntentRequestTracker.onActivityResult(requestCode, resultCode, intent)) {
            return true;
        }
        mLifecycleDispatcher.dispatchOnActivityResultWithNative(requestCode, resultCode, intent);
        super.onActivityResult(requestCode, resultCode, intent);
        return false;
    }

    @CallSuper
    @Override
    public void onRequestPermissionsResult(
            int requestCode, String[] permissions, int[] grantResults) {
        if (mWindowAndroid != null) {
            if (mWindowAndroid.handlePermissionResult(requestCode, permissions, grantResults)) {
                return;
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    @CallSuper
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mIntentRequestTracker.saveInstanceState(outState);

        mLifecycleDispatcher.dispatchOnSaveInstanceState(outState);
    }

    @CallSuper
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        mLifecycleDispatcher.dispatchOnWindowFocusChanged(hasFocus);
    }

    @CallSuper
    @Override
    public void recreate() {
        super.recreate();

        mLifecycleDispatcher.dispatchOnRecreate();

        // TODO(crbug.com/40793204): Remove stack trace logging once root cause of bug is
        // identified & fixed.
        // Piggybacking for multi-instance bug crbug.com/1484026.
        Log.i(TAG_MULTI_INSTANCE, "Tracing recreate().");
        Thread.dumpStack();
    }

    @CallSuper
    @Override
    public void onTopResumedActivityChanged(boolean isTopResumedActivity) {
        super.onTopResumedActivityChanged(isTopResumedActivity);

        mLifecycleDispatcher.dispatchOnTopResumedActivityChanged(isTopResumedActivity);
        mNativeInitializationController.onTopResumedActivityChanged(isTopResumedActivity);
    }

    /**
     * @return Whether the activity is running in tablet mode.
     */
    public boolean isTablet() {
        return mIsTablet;
    }

    /**
     * @return Whether the activity had a warm start because the native library was already fully
     *     loaded and initialized.
     */
    public boolean hadWarmStart() {
        return mHadWarmStart;
    }

    /**
     * This returns true if the activity was started warm (native library loaded and initialized) or
     * if a cold starts have been completed by the time onResume is/will be called.
     * This is useful to distinguish between the case where an already running instance of Chrome is
     * being brought back to the foreground from the case where Chrome is started, in order to avoid
     * contention on browser startup
     * @return Whether the activity is warm in onResume.
     */
    public boolean isWarmOnResume() {
        return mIsWarmOnResume;
    }

    @Override
    public void onUserInteraction() {
        mLastUserInteractionTime = SystemClock.elapsedRealtime();
    }

    /**
     * @return timestamp when the last user interaction was made.
     */
    public long getLastUserInteractionTime() {
        return mLastUserInteractionTime;
    }

    /**
     * Called when the orientation of the device changes.  The orientation is checked/detected on
     * root view layouts.
     * @param orientation One of {@link Configuration#ORIENTATION_PORTRAIT} or
     *                    {@link Configuration#ORIENTATION_LANDSCAPE}.
     */
    protected void onOrientationChange(int orientation) {}

    private void checkOrientation() {
        WindowManager wm = getWindowManager();
        if (wm == null) return;

        Display display = wm.getDefaultDisplay();
        if (display == null) return;

        int oldOrientation = mCurrentOrientation;
        mCurrentOrientation = getResources().getConfiguration().orientation;

        if (oldOrientation != mCurrentOrientation) onOrientationChange(mCurrentOrientation);
    }

    /**
     * Extending classes should implement this, inflate the layout, set the content view and then
     * call {@link #onInitialLayoutInflationComplete}.
     */
    protected abstract void triggerLayoutInflation();

    /**
     * Once inflation is complete, this runs the callback to inform ChromeBrowserInitializer of this
     * and to start the post-inflation pre-native startup.
     */
    @CallSuper
    protected void onInitialLayoutInflationComplete() {
        if (mOnInflationCompleteCallback == null) return;
        mOnInflationCompleteCallback.run();
        mOnInflationCompleteCallback = null;
        mInitialLayoutInflationComplete = true;
    }

    /** Returns whether initial inflation is complete. */
    public boolean isInitialLayoutInflationComplete() {
        return mInitialLayoutInflationComplete;
    }

    /**
     * @return {@link ActivityLifecycleDispatcher} associated with this activity.
     */
    @Override
    public ActivityLifecycleDispatcher getLifecycleDispatcher() {
        return mLifecycleDispatcher;
    }

    /**
     * @return {@link MultiWindowModeStateDispatcher} associated with this activity.
     */
    public MultiWindowModeStateDispatcher getMultiWindowModeStateDispatcher() {
        return mMultiWindowModeStateDispatcher;
    }

    @Override
    public boolean moveTaskToBack(boolean nonRoot) {
        // On Android (at least until N) moving the task to the background flakily stops the
        // Activity from being finished, breaking tests. Trying to bring the task back to the
        // foreground after also happens to be flaky, so just allow tests to prevent actually moving
        // to the background.
        if (sInterceptMoveTaskToBackForTesting) {
            sBackInterceptedForTesting = true;
            return false;
        } else if (BuildConfig.IS_FOR_TEST) {
            assert false
                    : "moveTaskToBack must be intercepted or it will create flaky tests. "
                            + "See #interceptMoveTaskToBackForTesting";
        }
        return super.moveTaskToBack(nonRoot);
    }

    public static void interceptMoveTaskToBackForTesting() {
        sInterceptMoveTaskToBackForTesting = true;
        sBackInterceptedForTesting = false;
        ResettersForTesting.register(() -> sInterceptMoveTaskToBackForTesting = false);
    }

    public static boolean wasMoveTaskToBackInterceptedForTesting() {
        assert sInterceptMoveTaskToBackForTesting;
        return sBackInterceptedForTesting;
    }

    private boolean shouldDisableHardwareAcceleration() {
        // Low end devices should disable hardware acceleration for memory gains.
        return SysUtils.isLowEndDevice();
    }

    protected void enableHardwareAcceleration() {
        // HW acceleration is disabled in the manifest and may be re-enabled here.
        if (!shouldDisableHardwareAcceleration()) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

            // When HW acceleration is enabled manually for an activity, child windows (e.g.
            // dialogs) don't inherit HW acceleration state. However, when HW acceleration is
            // enabled in the manifest, child windows do inherit HW acceleration state. That
            // looks like a bug, so I filed b/23036374
            //
            // In the meanwhile the workaround is to call
            //   window.setWindowManager(..., hardwareAccelerated=true)
            // to let the window know that it's HW accelerated. However, since there is no way
            // to know 'appToken' argument until window's view is attached to the window (!!),
            // we have to do the workaround in onAttachedToWindow()
            mSetWindowHWA = true;
        }
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();

        // See enableHardwareAcceleration()
        if (mSetWindowHWA) {
            mSetWindowHWA = false;
            getWindow()
                    .setWindowManager(
                            getWindow().getWindowManager(),
                            getWindow().getAttributes().token,
                            getComponentName().flattenToString(),
                            /* hardwareAccelerated= */ true);
        }
    }
}