chromium/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunActivity.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.firstrun;

import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;

import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.widget.ViewPager2;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.Promise;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.back_press.SecondaryActivityBackPressUma.SecondaryActivity;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.fonts.FontPreloader;
import org.chromium.chrome.browser.metrics.UmaUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.signin.SigninCheckerProvider;
import org.chromium.chrome.browser.signin.SigninFirstRunFragment;
import org.chromium.chrome.browser.ui.signin.DialogWhenLargeContentLayout;
import org.chromium.chrome.browser.ui.signin.SigninUtils;
import org.chromium.chrome.browser.ui.signin.history_sync.HistorySyncHelper;
import org.chromium.chrome.browser.ui.system.StatusBarColorController;
import org.chromium.components.browser_ui.modaldialog.AppModalPresenter;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.metrics.LowEntropySource;
import org.chromium.components.signin.metrics.SigninAccessPoint;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.function.BooleanSupplier;

/**
 * Handles the First Run Experience sequences shown to the user launching Chrome for the first time.
 * It supports only a simple format of FRE:
 *   [Welcome]
 *   [Intro pages...]
 *   [Sign-in page]
 * The activity might be run more than once, e.g. 1) for ToS and sign-in, and 2) for intro.
 */
public class FirstRunActivity extends FirstRunActivityBase implements FirstRunPageDelegate {
    /**
     * Alerted about various events when FirstRunActivity performs them.
     * TODO(crbug.com/40710744): Rework and use a better testing setup.
     * Rework and use a better testing setup.
     */
    public interface FirstRunActivityObserver {
        /** See {@link #createPostNativeAndPoliciesPageSequence}. */
        void onCreatePostNativeAndPoliciesPageSequence(FirstRunActivity caller);

        /** See {@link #acceptTermsOfService}. */
        void onAcceptTermsOfService(FirstRunActivity caller);

        /** See {@link #setCurrentItemForPager}. */
        void onJumpToPage(FirstRunActivity caller, int position);

        /** Called when First Run is completed. */
        void onUpdateCachedEngineName(FirstRunActivity caller);

        /** See {@link #abortFirstRunExperience}. */
        void onAbortFirstRunExperience(FirstRunActivity caller);

        /** See {@link #exitFirstRun()}. */
        void onExitFirstRun(FirstRunActivity caller);
    }

    private final BitSet mFreProgressStepsRecorded = new BitSet(MobileFreProgress.MAX);

    @Nullable private static FirstRunActivityObserver sObserver;

    private boolean mPostNativeAndPolicyPagesCreated;

    /** Use {@link Promise#isFulfilled()} to verify whether the native has been initialized. */
    private final Promise<Void> mNativeInitializationPromise = new Promise<>();

    private FirstRunFlowSequencer mFirstRunFlowSequencer;

    private Bundle mFreProperties;

    /**
     * Whether the first run activity was launched as a result of the user launching Chrome from the
     * Android app list.
     */
    private boolean mLaunchedFromChromeIcon;

    private boolean mLaunchedFromCCT;

    /**
     * {@link SystemClock} timestamp from when the FRE intent was initially created. This marks when
     * we first knew an FRE was needed, and is used as a reference point for various timing metrics.
     */
    private long mIntentCreationElapsedRealtimeMs;

    private final List<FirstRunPage> mPages = new ArrayList<>();
    private final List<Integer> mFreProgressStates = new ArrayList<>();

    private ViewPager2 mPager;

    /** The pager adapter, which provides the pages to the view pager widget. */
    private FirstRunPagerAdapter mPagerAdapter;

    private boolean isFlowKnown() {
        return mFreProperties != null;
    }

    /** Creates first page and sets up adapter. Should result UI being shown on the screen. */
    private void createFirstPage() {
        BooleanSupplier showWelcomePage = () -> !FirstRunStatus.shouldSkipWelcomePage();
        mPages.add(new FirstRunPage<>(SigninFirstRunFragment.class, showWelcomePage));
        mFreProgressStates.add(MobileFreProgress.WELCOME_SHOWN);
        mPagerAdapter = new FirstRunPagerAdapter(FirstRunActivity.this, mPages);
        mPager.setAdapter(mPagerAdapter);
        // Other pages will be created by createPostNativeAndPoliciesPageSequence() after
        // native and policy service have been initialized.
    }

    /**
     * Create the page sequence which requires native initialized, and policies loaded if any
     * on-device policies may exists.
     *
     * @see #areNativeAndPoliciesInitialized()
     */
    private void createPostNativeAndPoliciesPageSequence() {
        assert !mPostNativeAndPolicyPagesCreated;
        assert areNativeAndPoliciesInitialized();

        // Initialize SigninChecker, to kick off sign-in for child accounts as early as possible.
        //
        // TODO(b/245912657): explicitly sign in supervised users in {@link
        // FullscreenSigninMediator#handleContinueWithNative} rather than relying on SigninChecker.
        SigninCheckerProvider.get(getProfileProviderSupplier().get().getOriginalProfile());

        mFirstRunFlowSequencer.updateFirstRunProperties(mFreProperties);

        BooleanSupplier showSearchEnginePromo =
                () -> mFreProperties.getBoolean(SHOW_SEARCH_ENGINE_PAGE);

        // An optional page to select a default search engine.
        if (showSearchEnginePromo.getAsBoolean()) {
            mPages.add(
                    new FirstRunPage<>(
                            DefaultSearchEngineFirstRunFragment.class, showSearchEnginePromo));
            mFreProgressStates.add(MobileFreProgress.DEFAULT_SEARCH_ENGINE_SHOWN);
        }

        // An optional history sync opt-in page, the visibility of this page will be decided on the
        // fly according to the situation.
        if (ChromeFeatureList.isEnabled(
                ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
            BooleanSupplier showHistorySync =
                    () -> mFreProperties.getBoolean(SHOW_HISTORY_SYNC_PAGE);
            if (!showHistorySync.getAsBoolean()) {
                HistorySyncHelper historySyncHelper =
                        HistorySyncHelper.getForProfile(
                                getProfileProviderSupplier().get().getOriginalProfile());
                historySyncHelper.recordHistorySyncNotShown(SigninAccessPoint.START_PAGE);
            }
            mPages.add(new FirstRunPage<>(HistorySyncFirstRunFragment.class, showHistorySync));
            mFreProgressStates.add(MobileFreProgress.HISTORY_SYNC_OPT_IN_SHOWN);
        } else {
            BooleanSupplier showSyncConsent =
                    () -> mFreProperties.getBoolean(SHOW_SYNC_CONSENT_PAGE);
            mPages.add(new FirstRunPage<>(SyncConsentFirstRunFragment.class, showSyncConsent));
            mFreProgressStates.add(MobileFreProgress.SYNC_CONSENT_SHOWN);
        }

        if (mPagerAdapter != null) {
            mPagerAdapter.notifyDataSetChanged();
        }
        mPostNativeAndPolicyPagesCreated = true;

        if (sObserver != null) {
            sObserver.onCreatePostNativeAndPoliciesPageSequence(FirstRunActivity.this);
        }
    }

    @Override
    protected void onPreCreate() {
        // On tablets, where FRE activity is a dialog, transitions from fullscreen activities
        // (the ones that use Theme.Chromium.TabbedMode, e.g. ChromeTabbedActivity) look ugly,
        // because when FRE is started from CTA.onCreate(), currently running animation for CTA
        // window is aborted. This is perceived as a flash of white and doesn't look good.
        //
        // To solve this, we apply Theme.Chromium.TabbedMode on Tablet and Automotive here, to use
        // the same window background as other tabbed mode activities using the same theme.
        if (SigninUtils.isTabletOrAuto(this)) {
            setTheme(R.style.Theme_Chromium_TabbedMode);
        } else if (DialogWhenLargeContentLayout.shouldShowAsDialog(this)) {
            // For consistency with tablets, the status bar should be black on phones with large
            // screen, where the FRE is shown as dialog.
            StatusBarColorController.setStatusBarColor(getWindow(), Color.BLACK);
        }
        super.onPreCreate();
    }

    @Override
    protected Bundle transformSavedInstanceStateForOnCreate(Bundle savedInstanceState) {
        // We pass null to Activity.onCreate() so that it doesn't automatically restore
        // the FragmentManager state - as that may cause fragments to be loaded that have
        // dependencies on native before native has been loaded (and then crash). Instead,
        // these fragments will be recreated manually by us and their progression restored
        // from |mFreProperties| which we still get from getSavedInstanceState() below.
        return null;
    }

    @Override
    protected ModalDialogManager createModalDialogManager() {
        return new ModalDialogManager(new AppModalPresenter(this), ModalDialogType.APP);
    }

    /**
     * Creates the content view for this activity. The only thing subclasses can do is wrapping the
     * view returned by super implementation in some extra layout.
     */
    @CallSuper
    protected View createContentView() {
        mPager = new ViewPager2(this);

        // Disable swipe gesture.
        mPager.setUserInputEnabled(false);

        mPager.setId(R.id.fre_pager);
        mPager.setOffscreenPageLimit(3);
        return SigninUtils.wrapInDialogWhenLargeLayout(mPager);
    }

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

        initializeStateFromLaunchData();
        RecordHistogram.recordTimesHistogram(
                "MobileFre.FromLaunch.TriggerLayoutInflation",
                SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);

        setFinishOnTouchOutside(true);

        setContentView(createContentView());

        // SigninFirstRunFragment doesn't use getProperties() and can be shown right away, without
        // waiting for FirstRunFlowSequencer.
        createFirstPage();

        mFirstRunFlowSequencer =
                new FirstRunFlowSequencer(
                        getProfileProviderSupplier(), getChildAccountStatusSupplier()) {
                    @Override
                    public void onFlowIsKnown(Bundle freProperties) {
                        assert freProperties != null;
                        mFreProperties = freProperties;
                        RecordHistogram.recordTimesHistogram(
                                "MobileFre.FromLaunch.ChildStatusAvailable",
                                SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);

                        onInternalStateChanged();

                        recordFreProgressHistogram(mFreProgressStates.get(0));
                        long inflationCompletion = SystemClock.elapsedRealtime();
                        RecordHistogram.recordTimesHistogram(
                                "MobileFre.FromLaunch.FirstFragmentInflatedV2",
                                inflationCompletion - mIntentCreationElapsedRealtimeMs);
                        getFirstRunAppRestrictionInfo()
                                .getCompletionElapsedRealtimeMs(
                                        restrictionsCompletion -> {
                                            if (restrictionsCompletion > inflationCompletion) {
                                                RecordHistogram.recordTimesHistogram(
                                                        "MobileFre.FragmentInflationSpeed.FasterThanAppRestriction",
                                                        restrictionsCompletion
                                                                - inflationCompletion);
                                            } else {
                                                RecordHistogram.recordTimesHistogram(
                                                        "MobileFre.FragmentInflationSpeed.SlowerThanAppRestriction",
                                                        inflationCompletion
                                                                - restrictionsCompletion);
                                            }
                                        });
                    }
                };
        mFirstRunFlowSequencer.start();
        FirstRunStatus.setFirstRunTriggered(true);
        recordFreProgressHistogram(MobileFreProgress.STARTED);
        onInitialLayoutInflationComplete();

        RecordHistogram.recordTimesHistogram(
                "MobileFre.FromLaunch.ActivityInflated",
                SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);
    }

    @Override
    protected void performPostInflationStartup() {
        super.performPostInflationStartup();

        FontPreloader.getInstance().onPostInflationStartupFre();
    }

    @Override
    protected void onFirstDrawComplete() {
        super.onFirstDrawComplete();

        FontPreloader.getInstance().onFirstDrawFre();
    }

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

        Runnable onNativeFinished =
                () -> {
                    if (isActivityFinishingOrDestroyed()) return;

                    onNativeDependenciesFullyInitialized();
                };
        Profile profile = getProfileProviderSupplier().get().getOriginalProfile();
        TemplateUrlServiceFactory.getForProfile(profile).runWhenLoaded(onNativeFinished);
        // Notify feature engagement that FRE occurred.
        TrackerFactory.getTrackerForProfile(profile)
                .notifyEvent(EventConstants.RESTORE_TABS_ON_FIRST_RUN_SHOW_PROMO);
        RecordHistogram.recordTimesHistogram(
                "MobileFre.NativeInitialized", SystemClock.elapsedRealtime() - getStartTime());
    }

    private void onNativeDependenciesFullyInitialized() {
        mNativeInitializationPromise.fulfill(null);
        if (ChromeFeatureList.isEnabled(
                ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)) {
            mPager.setOffscreenPageLimit(ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT);
        }

        onInternalStateChanged();
    }

    @Override
    protected void onPolicyLoadListenerAvailable(boolean onDevicePolicyFound) {
        super.onPolicyLoadListenerAvailable(onDevicePolicyFound);
        RecordHistogram.recordTimesHistogram(
                "MobileFre.FromLaunch.PoliciesLoaded",
                SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);

        onInternalStateChanged();
    }

    private void onInternalStateChanged() {
        if (!isFlowKnown()) {
            return;
        }

        if (mPagerAdapter == null) {
            createFirstPage();
        }

        if (!mPostNativeAndPolicyPagesCreated && areNativeAndPoliciesInitialized()) {
            createPostNativeAndPoliciesPageSequence();
        }

        if (areNativeAndPoliciesInitialized()) {
            skipPagesIfNecessary();
        }
    }

    private boolean areNativeAndPoliciesInitialized() {
        return mNativeInitializationPromise.isFulfilled()
                && isFlowKnown()
                && this.getPolicyLoadListener().get() != null;
    }

    // Activity:

    @Override
    public void onAttachFragment(Fragment fragment) {
        if (!(fragment instanceof FirstRunFragment)) return;

        FirstRunFragment page = (FirstRunFragment) fragment;
        // Delay notifying the child page until native and the TemplateUrlService are initialized.
        // Tracked by mNativeSideIsInitialized is ready. Otherwise if the next page handles
        // the default search engine, it will be missing dependencies. See https://crbug.com/1275950
        // for when this didn't work.
        if (mNativeInitializationPromise.isFulfilled()) {
            page.onNativeInitialized();
        } else {
            mNativeInitializationPromise.then(
                    (ignored) -> {
                        page.onNativeInitialized();
                    });
        }
    }

    @Override
    public void onRestoreInstanceState(Bundle state) {
        // Don't automatically restore state here. This is a counterpart to the override
        // of transformSavedInstanceStateForOnCreate() as the two need to be consistent.
        // The default implementation of this would restore the state of the views, which
        // would otherwise cause a crash in ViewPager used to manage fragments - as it
        // expects consistency between the states restored by onCreate() and this method.
        // Activity doesn't check for null on the parameter, so pass an empty bundle.
        super.onRestoreInstanceState(new Bundle());
    }

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

        // Multiple active FREs does not really make sense for the user. Once one is complete, the
        // others would become out of date. This approach turns out to be quite tricky to enforce
        // completely with just Android configuration, because of all the different ways the FRE
        // can be launched, especially when it is not launching a new task and another activity's
        // traits are used. So instead just finish any FRE that is not ourselves manually.
        for (Activity activity : ApplicationStatus.getRunningActivities()) {
            if (activity instanceof FirstRunActivity && activity != this) {
                // Simple finish call only works when in the same task.
                if (activity.getTaskId() == this.getTaskId()) {
                    activity.finish();
                } else {
                    activity.finishAndRemoveTask();
                }
            }
        }
    }

    @Override
    public @BackPressResult int handleBackPress() {
        // Terminate if we are still waiting for the native or for Android EDU / GAIA Child checks.
        if (!mPostNativeAndPolicyPagesCreated) {
            abortFirstRunExperience();
            return BackPressResult.SUCCESS;
        }

        mFirstRunFlowSequencer.updateFirstRunProperties(mFreProperties);

        int position = mPager.getCurrentItem() - 1;
        while (position > 0 && !mPages.get(position).shouldShow()) {
            --position;
        }

        if (position < 0) {
            abortFirstRunExperience();
        } else {
            setCurrentItemForPager(position);
        }
        return BackPressResult.SUCCESS;
    }

    @Override
    public int getSecondaryActivity() {
        return SecondaryActivity.FIRST_RUN;
    }

    // FirstRunPageDelegate:
    @Override
    public Bundle getProperties() {
        return mFreProperties;
    }

    @Override
    public boolean advanceToNextPage() {
        mFirstRunFlowSequencer.updateFirstRunProperties(mFreProperties);

        int position = mPager.getCurrentItem() + 1;
        while (position < mPagerAdapter.getItemCount() && !mPages.get(position).shouldShow()) {
            ++position;
        }
        if (!setCurrentItemForPager(position)) return false;

        recordFreProgressHistogram(mFreProgressStates.get(position));
        return true;
    }

    @Override
    public void abortFirstRunExperience() {
        finish();

        notifyCustomTabCallbackFirstRunIfNecessary(getIntent(), false);
        if (sObserver != null) sObserver.onAbortFirstRunExperience(this);
    }

    @Override
    public void completeFirstRunExperience() {
        RecordHistogram.recordMediumTimesHistogram(
                "MobileFre.FromLaunch.FreCompleted",
                SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);

        FirstRunFlowSequencer.markFlowAsCompleted();

        // LowEntropySource can't be used after the FRE has been completed.
        LowEntropySource.markFirstRunComplete();

        if (sObserver != null) sObserver.onUpdateCachedEngineName(this);

        launchPendingIntentAndFinish();
    }

    @Override
    public void exitFirstRun() {
        // This is important because the first run, when completed, will re-launch the original
        // intent. The re-launched intent will still need to know to avoid the FRE.
        FirstRunStatus.setFirstRunSkippedByPolicy(true);

        launchPendingIntentAndFinish();
    }

    private void launchPendingIntentAndFinish() {
        if (!sendFirstRunCompletePendingIntent()) {
            finish();
        } else {
            ApplicationStatus.registerStateListenerForAllActivities(
                    new ActivityStateListener() {
                        @Override
                        public void onActivityStateChange(Activity activity, int newState) {
                            boolean shouldFinish = false;
                            if (activity == FirstRunActivity.this) {
                                shouldFinish =
                                        (newState == ActivityState.STOPPED
                                                || newState == ActivityState.DESTROYED);
                            } else {
                                shouldFinish = newState == ActivityState.RESUMED;
                            }
                            if (shouldFinish) {
                                finish();
                                ApplicationStatus.unregisterActivityStateListener(this);
                            }
                        }
                    });
        }

        if (sObserver != null) sObserver.onExitFirstRun(this);
    }

    @Override
    public boolean didAcceptTermsOfService() {
        return FirstRunUtils.didAcceptTermsOfService();
    }

    @Override
    public boolean isLaunchedFromCct() {
        return mLaunchedFromCCT;
    }

    @Override
    public void acceptTermsOfService(boolean allowMetricsAndCrashUploading) {
        assert mNativeInitializationPromise.isFulfilled();

        // If default is true then it corresponds to opt-out and false corresponds to opt-in.
        UmaUtils.recordMetricsReportingDefaultOptIn(!DEFAULT_METRICS_AND_CRASH_REPORTING);
        RecordHistogram.recordMediumTimesHistogram(
                "MobileFre.FromLaunch.TosAccepted",
                SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);
        FirstRunUtils.acceptTermsOfService(allowMetricsAndCrashUploading);
        FirstRunStatus.setSkipWelcomePage(true);
        flushPersistentData();

        if (sObserver != null) sObserver.onAcceptTermsOfService(this);
    }

    /** Initialize local state from launch intent and from saved instance state. */
    private void initializeStateFromLaunchData() {
        if (getIntent() != null) {
            mLaunchedFromChromeIcon =
                    getIntent().getBooleanExtra(EXTRA_COMING_FROM_CHROME_ICON, false);
            mLaunchedFromCCT =
                    getIntent().getBooleanExtra(EXTRA_CHROME_LAUNCH_INTENT_IS_CCT, false);
            mIntentCreationElapsedRealtimeMs =
                    getIntent().getLongExtra(EXTRA_FRE_INTENT_CREATION_ELAPSED_REALTIME_MS, 0);
        }
    }

    private boolean setCurrentItemForPager(int position) {
        if (sObserver != null) sObserver.onJumpToPage(this, position);

        if (position >= mPagerAdapter.getItemCount()) {
            completeFirstRunExperience();
            return false;
        }

        int oldPosition = mPager.getCurrentItem();
        mPager.setCurrentItem(position, false);

        // Set A11y focus if possible. See https://crbug.com/1094064 for more context.
        // The screen reader can lose focus when switching between pages with ViewPager2.
        FirstRunFragment currentFragment = mPagerAdapter.getFirstRunFragment(position);
        if (currentFragment != null) {
            currentFragment.setInitialA11yFocus();
            if (oldPosition > position) {
                // If the fragment is revisited through back press, reset its state.
                currentFragment.reset();
            }
        }
        return true;
    }

    private void skipPagesIfNecessary() {
        while (!mPages.get(mPager.getCurrentItem()).shouldShow() && advanceToNextPage()) {}
    }

    @Override
    public void recordFreProgressHistogram(@MobileFreProgress int state) {
        assert 0 <= state && state < MobileFreProgress.MAX;

        if (mFreProgressStepsRecorded.get(state)) return;

        mFreProgressStepsRecorded.set(state);
        if (mLaunchedFromChromeIcon) {
            RecordHistogram.recordEnumeratedHistogram(
                    "MobileFre.Progress.MainIntent", state, MobileFreProgress.MAX);
        } else {
            RecordHistogram.recordEnumeratedHistogram(
                    "MobileFre.Progress.ViewIntent", state, MobileFreProgress.MAX);
        }
    }

    @Override
    public void recordNativePolicyAndChildStatusLoadedHistogram() {
        RecordHistogram.recordTimesHistogram(
                "MobileFre.FromLaunch.NativePolicyAndChildStatusLoaded",
                SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);
    }

    @Override
    public void recordNativeInitializedHistogram() {
        RecordHistogram.recordTimesHistogram(
                "MobileFre.FromLaunch.NativeInitialized",
                SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);
    }

    @Override
    public void showInfoPage(@StringRes int url) {
        CustomTabActivity.showInfoPage(
                this, LocalizationUtils.substituteLocalePlaceholder(getString(url)));
    }

    @Override
    public Promise<Void> getNativeInitializationPromise() {
        return mNativeInitializationPromise;
    }

    public FirstRunFragment getCurrentFragmentForTesting() {
        return mPagerAdapter.getFirstRunFragment(mPager.getCurrentItem());
    }

    public static void setObserverForTest(FirstRunActivityObserver observer) {
        assert sObserver == null;
        sObserver = observer;
    }

    @Override
    protected ActivityWindowAndroid createWindowAndroid() {
        return new ActivityWindowAndroid(
                this, /* listenToActivityState= */ true, getIntentRequestTracker());
    }
}