chromium/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunActivityBase.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.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.SystemClock;

import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;

import org.chromium.base.BuildInfo;
import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.chrome.browser.back_press.BackPressHelper;
import org.chromium.chrome.browser.back_press.SecondaryActivityBackPressUma.SecondaryActivity;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.init.ActivityProfileProvider;
import org.chromium.chrome.browser.init.AsyncInitializationActivity;
import org.chromium.chrome.browser.metrics.SimpleStartupForegroundSessionDetector;
import org.chromium.chrome.browser.metrics.UmaUtils;
import org.chromium.chrome.browser.policy.PolicyServiceFactory;
import org.chromium.chrome.browser.profiles.OTRProfileID;
import org.chromium.chrome.browser.profiles.ProfileManagerUtils;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.ui.system.StatusBarColorController;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.policy.PolicyService;
import org.chromium.components.signin.AccountManagerFacade;
import org.chromium.components.signin.AccountManagerFacadeProvider;

/** Base class for First Run Experience. */
// TODO(b/41493788): Consider renaming it now that it is also the base for the upgrade promo.
public abstract class FirstRunActivityBase extends AsyncInitializationActivity
        implements BackPressHandler {
    private static final String TAG = "FirstRunActivity";

    public static final String EXTRA_COMING_FROM_CHROME_ICON = "Extra.ComingFromChromeIcon";
    public static final String EXTRA_CHROME_LAUNCH_INTENT_IS_CCT =
            "Extra.FreChromeLaunchIntentIsCct";
    public static final String EXTRA_FRE_INTENT_CREATION_ELAPSED_REALTIME_MS =
            "Extra.FreIntentCreationElapsedRealtimeMs";

    // The intent to send once the FRE completes.
    public static final String EXTRA_FRE_COMPLETE_LAUNCH_INTENT = "Extra.FreChromeLaunchIntent";

    // The extras on the intent which initiated first run. (e.g. the extras on the intent
    // received by ChromeLauncherActivity.)
    public static final String EXTRA_CHROME_LAUNCH_INTENT_EXTRAS =
            "Extra.FreChromeLaunchIntentExtras";
    static final String SHOW_SEARCH_ENGINE_PAGE = "ShowSearchEnginePage";
    static final String SHOW_SYNC_CONSENT_PAGE = "ShowSyncConsent";
    static final String SHOW_HISTORY_SYNC_PAGE = "ShowHistorySync";

    public static final boolean DEFAULT_METRICS_AND_CRASH_REPORTING = true;

    private static PolicyLoadListenerFactory sPolicyLoadListenerFactoryForTesting;

    private boolean mNativeInitialized;

    private final FirstRunAppRestrictionInfo mFirstRunAppRestrictionInfo;
    private final OneshotSupplierImpl<PolicyService> mPolicyServiceSupplier;
    private final ObservableSupplierImpl<Boolean> mBackPressStateSupplier =
            new ObservableSupplierImpl<>() {
                // Always intercept back press.
                {
                    set(true);
                }
            };
    private final PolicyLoadListener mPolicyLoadListener;

    private final long mStartTime;

    private ChildAccountStatusSupplier mChildAccountStatusSupplier;

    public FirstRunActivityBase() {
        mFirstRunAppRestrictionInfo = FirstRunAppRestrictionInfo.takeMaybeInitialized();
        mPolicyServiceSupplier = new OneshotSupplierImpl<>();
        mPolicyLoadListener =
                sPolicyLoadListenerFactoryForTesting == null
                        ? new PolicyLoadListener(
                                mFirstRunAppRestrictionInfo, mPolicyServiceSupplier)
                        : sPolicyLoadListenerFactoryForTesting.inject(
                                mFirstRunAppRestrictionInfo, mPolicyServiceSupplier);
        mStartTime = SystemClock.elapsedRealtime();
        mPolicyLoadListener.onAvailable(this::onPolicyLoadListenerAvailable);
    }

    protected long getStartTime() {
        return mStartTime;
    }

    @Override
    protected boolean requiresFirstRunToBeCompleted(Intent intent) {
        // The user is already in First Run.
        return false;
    }

    @Override
    public boolean shouldStartGpuProcess() {
        return true;
    }

    @Override
    @CallSuper
    public void triggerLayoutInflation() {
        AccountManagerFacade accountManagerFacade = AccountManagerFacadeProvider.getInstance();
        mChildAccountStatusSupplier =
                new ChildAccountStatusSupplier(accountManagerFacade, mFirstRunAppRestrictionInfo);

        // TODO(crbug.com/40939710): Find the underlying issue causing the status bar not to be set
        //  during FRE, this is just a temporary visual fix.
        if (BuildInfo.getInstance().isAutomotive) {
            StatusBarColorController.setStatusBarColor(getWindow(), Color.BLACK);
        }
    }

    @Override
    protected void onPreCreate() {
        super.onPreCreate();
        BackPressHelper.create(this, getOnBackPressedDispatcher(), this, getSecondaryActivity());
    }

    // Activity:
    @Override
    public void onPause() {
        super.onPause();
        // As with onResume() below, for historical reasons the FRE has been able to report
        // background time before post-native initialization, unlike other activities. See
        // http://crrev.com/436530.
        UmaUtils.recordBackgroundTimeWithNative();
        flushPersistentData();
    }

    @Override
    public void onResume() {
        SimpleStartupForegroundSessionDetector.discardSession();
        super.onResume();
        // Since the FRE may be shown before any tab is shown, mark that this is the point at which
        // Chrome went to foreground. Other activities can only
        // recordForegroundStartTimeWithNative() after the post-native initialization has started.
        // See http://crrev.com/436530.
        UmaUtils.recordForegroundStartTimeWithNative();
    }

    @Override
    protected OneshotSupplier<ProfileProvider> createProfileProvider() {
        return new ActivityProfileProvider(getLifecycleDispatcher()) {
            @Nullable
            @Override
            protected OTRProfileID createOffTheRecordProfileID() {
                throw new IllegalStateException("Attempting to access incognito in the FRE");
            }
        };
    }

    @Override
    public void finishNativeInitialization() {
        super.finishNativeInitialization();
        mNativeInitialized = true;
        mPolicyServiceSupplier.set(PolicyServiceFactory.getGlobalPolicyService());
    }

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

        mPolicyLoadListener.destroy();
        mFirstRunAppRestrictionInfo.destroy();
    }

    @Override
    public ObservableSupplier<Boolean> getHandleBackPressChangedSupplier() {
        return mBackPressStateSupplier;
    }

    /** Called when back press is intercepted. */
    @Override
    public abstract @BackPressResult int handleBackPress();

    public abstract @SecondaryActivity int getSecondaryActivity();

    protected void flushPersistentData() {
        if (mNativeInitialized) {
            ProfileManagerUtils.flushPersistentDataForAllProfiles();
        }
    }

    /**
     * Sends PendingIntent included with the EXTRA_FRE_COMPLETE_LAUNCH_INTENT extra.
     * @return Whether a pending intent was sent.
     */
    protected final boolean sendFirstRunCompletePendingIntent() {
        PendingIntent pendingIntent =
                IntentUtils.safeGetParcelableExtra(getIntent(), EXTRA_FRE_COMPLETE_LAUNCH_INTENT);
        boolean pendingIntentIsCCT =
                IntentUtils.safeGetBooleanExtra(
                        getIntent(), EXTRA_CHROME_LAUNCH_INTENT_IS_CCT, false);
        if (pendingIntent == null) return false;

        try {
            PendingIntent.OnFinished onFinished = null;
            if (pendingIntentIsCCT) {
                // After the PendingIntent has been sent, send a first run callback to custom tabs
                // if necessary.
                onFinished =
                        new PendingIntent.OnFinished() {
                            @Override
                            public void onSendFinished(
                                    PendingIntent pendingIntent,
                                    Intent intent,
                                    int resultCode,
                                    String resultData,
                                    Bundle resultExtras) {
                                // Use {@link FirstRunActivityBase#getIntent()} instead of {@link
                                // intent} parameter in order to use a more similar code path for
                                // completing first run and for aborting first run.
                                notifyCustomTabCallbackFirstRunIfNecessary(getIntent(), true);
                            }
                        };
            }

            // Use the PendingIntent to send the intent that originally launched Chrome. The intent
            // will go back to the ChromeLauncherActivity, which will route it accordingly.
            pendingIntent.send(Activity.RESULT_OK, onFinished, null);
            // Use fade-out animation for the transition from this activity so the original intent.
            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
            return true;
        } catch (CanceledException e) {
            Log.e(TAG, "Unable to send PendingIntent.", e);
        }
        return false;
    }

    protected FirstRunAppRestrictionInfo getFirstRunAppRestrictionInfo() {
        return mFirstRunAppRestrictionInfo;
    }

    /** Observer method for the policy load listener. Overridden by inheriting classes. */
    protected void onPolicyLoadListenerAvailable(boolean onDevicePolicyFound) {}

    /**
     * @return PolicyLoadListener used to indicate if policy initialization is complete.
     * @see PolicyLoadListener for return value expectation.
     */
    public OneshotSupplier<Boolean> getPolicyLoadListener() {
        return mPolicyLoadListener;
    }

    /** Returns the supplier that supplies child account status. */
    public OneshotSupplier<Boolean> getChildAccountStatusSupplier() {
        return mChildAccountStatusSupplier;
    }

    /**
     * If the first run activity was triggered by a custom tab, notify app associated with
     * custom tab whether first run was completed.
     * @param freIntent First run activity intent.
     * @param complete  Whether first run completed successfully.
     */
    public static void notifyCustomTabCallbackFirstRunIfNecessary(
            Intent freIntent, boolean complete) {
        boolean launchedByCCT =
                IntentUtils.safeGetBooleanExtra(
                        freIntent, EXTRA_CHROME_LAUNCH_INTENT_IS_CCT, false);
        if (!launchedByCCT) return;

        Bundle launchIntentExtras =
                IntentUtils.safeGetBundleExtra(freIntent, EXTRA_CHROME_LAUNCH_INTENT_EXTRAS);
        CustomTabsConnection.getInstance()
                .sendFirstRunCallbackIfNecessary(launchIntentExtras, complete);
    }

    /**
     * Allows tests to inject a fake/mock {@link PolicyLoadListener} into {@link
     * FirstRunActivityBase}'s constructor.
     */
    public interface PolicyLoadListenerFactory {
        PolicyLoadListener inject(
                FirstRunAppRestrictionInfo appRestrictionInfo,
                OneshotSupplier<PolicyService> policyServiceSupplier);
    }

    /**
     * Forces the {@link FirstRunActivityBase}'s constructor to use a {@link PolicyLoadListener}
     * defined by a test, instead of creating its own instance.
     */
    public static void setPolicyLoadListenerFactoryForTesting(
            PolicyLoadListenerFactory policyLoadListenerFactory) {
        sPolicyLoadListenerFactoryForTesting = policyLoadListenerFactory;
        ResettersForTesting.register(() -> sPolicyLoadListenerFactoryForTesting = null);
    }
}