blob: 10ec995594008c4d1606c5088dc0ec57d9614fe5 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewTreeObserver.OnPreDrawListener;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.widget.ViewPager2;
import org.chromium.base.ActivityState;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.datareduction.DataReductionPromoUtils;
import org.chromium.chrome.browser.datareduction.DataReductionProxyUma;
import org.chromium.chrome.browser.fonts.FontPreloader;
import org.chromium.chrome.browser.lifecycle.NativeInitObserver;
import org.chromium.chrome.browser.metrics.UmaUtils;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.signin.SigninFirstRunFragment;
import org.chromium.chrome.browser.signin.services.FREMobileIdentityConsistencyFieldTrial;
import org.chromium.components.browser_ui.modaldialog.AppModalPresenter;
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.List;
/**
* 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/1114319): Rework and use a better testing setup.
*/
public interface FirstRunActivityObserver {
/** See {@link #onCreatePostNativeAndPoliciesPageSequence}. */
void onCreatePostNativeAndPoliciesPageSequence(
FirstRunActivity caller, Bundle freProperties);
/** See {@link #acceptTermsOfService}. */
void onAcceptTermsOfService(FirstRunActivity caller);
/** See {@link #jumpToPage}. */
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);
}
// TODO(https://crbug.com/1196404): Replace with call into shared code once
// https://crrev.com/c/2815659 lands.
private static class ViewDrawBlocker {
public static void blockViewDrawUntilReady(View view, Supplier<Boolean> viewReadySupplier) {
view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (!viewReadySupplier.get()) return false;
view.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});
}
}
private static final int FRE_PROGRESS_STARTED = 0;
private static final int FRE_PROGRESS_WELCOME_SHOWN = 1;
private static final int FRE_PROGRESS_DATA_SAVER_SHOWN = 2;
private static final int FRE_PROGRESS_SIGNIN_SHOWN = 3;
private static final int FRE_PROGRESS_COMPLETED_SIGNED_IN = 4;
private static final int FRE_PROGRESS_COMPLETED_NOT_SIGNED_IN = 5;
private static final int FRE_PROGRESS_DEFAULT_SEARCH_ENGINE_SHOWN = 6;
private static final int FRE_PROGRESS_MAX = 7;
@Nullable
private static FirstRunActivityObserver sObserver;
private String mResultSignInAccountName;
private boolean mResultShowSignInSettings;
private boolean mFlowIsKnown;
private boolean mPostNativeAndPolicyPagesCreated;
private boolean mNativeSideIsInitialized;
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;
/**
* Defines a sequence of pages to be shown (depending on parameters etc).
*/
private void createPageSequence() {
FREMobileIdentityConsistencyFieldTrial.createFirstRunTrial();
if (FREMobileIdentityConsistencyFieldTrial.isEnabled()) {
mPages.add(SigninFirstRunFragment::new);
} else {
mPages.add(shouldCreateEnterpriseCctTosPage()
? new TosAndUmaFirstRunFragmentWithEnterpriseSupport.Page()
: new ToSAndUMAFirstRunFragment.Page());
}
mFreProgressStates.add(FRE_PROGRESS_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.
}
private boolean shouldCreateEnterpriseCctTosPage() {
// TODO(crbug.com/1111490): Revisit case when #shouldSkipWelcomePage = true.
// If the client has already accepted ToS (FirstRunStatus#shouldSkipWelcomePage), do not
// use the subclass ToSAndUmaCCTFirstRunFragment. Instead, use the base class
// (ToSAndUMAFirstRunFragment) which simply shows a loading spinner while waiting for
// native to be loaded.
return mLaunchedFromCCT && !FirstRunStatus.shouldSkipWelcomePage();
}
/**
* 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();
mFirstRunFlowSequencer.onNativeAndPoliciesInitialized(mFreProperties);
boolean notifyAdapter = false;
// An optional sign-in page.
if (FREMobileIdentityConsistencyFieldTrial.isEnabled()
&& mFreProperties.getBoolean(SHOW_SIGNIN_PAGE)) {
mPages.add(SyncConsentFirstRunFragment::new);
mFreProgressStates.add(FRE_PROGRESS_SIGNIN_SHOWN);
notifyAdapter = true;
}
// An optional Data Saver page.
if (mFreProperties.getBoolean(SHOW_DATA_REDUCTION_PAGE)) {
mPages.add(new DataReductionProxyFirstRunFragment.Page());
mFreProgressStates.add(FRE_PROGRESS_DATA_SAVER_SHOWN);
notifyAdapter = true;
}
// An optional page to select a default search engine.
if (mFreProperties.getBoolean(SHOW_SEARCH_ENGINE_PAGE)) {
mPages.add(new DefaultSearchEngineFirstRunFragment.Page());
mFreProgressStates.add(FRE_PROGRESS_DEFAULT_SEARCH_ENGINE_SHOWN);
notifyAdapter = true;
}
// An optional sign-in page.
if (!FREMobileIdentityConsistencyFieldTrial.isEnabled()
&& mFreProperties.getBoolean(SHOW_SIGNIN_PAGE)) {
mPages.add(SyncConsentFirstRunFragment::new);
mFreProgressStates.add(FRE_PROGRESS_SIGNIN_SHOWN);
notifyAdapter = true;
}
if (notifyAdapter && mPagerAdapter != null) {
mPagerAdapter.notifyDataSetChanged();
}
mPostNativeAndPolicyPagesCreated = true;
if (sObserver != null) {
sObserver.onCreatePostNativeAndPoliciesPageSequence(
FirstRunActivity.this, mFreProperties);
}
}
@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 mPager;
}
@Override
public void triggerLayoutInflation() {
initializeStateFromLaunchData();
RecordHistogram.recordTimesHistogram("MobileFre.FromLaunch.TriggerLayoutInflation",
SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);
setFinishOnTouchOutside(true);
setContentView(createContentView());
ViewDrawBlocker.blockViewDrawUntilReady(
findViewById(android.R.id.content), () -> mPages.size() > 0);
mFirstRunFlowSequencer = new FirstRunFlowSequencer(this) {
@Override
public void onFlowIsKnown(Bundle freProperties) {
mFlowIsKnown = true;
mFreProperties = freProperties;
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(FRE_PROGRESS_STARTED);
onInitialLayoutInflationComplete();
RecordHistogram.recordTimesHistogram("MobileFre.FromLaunch.ActivityInflated",
SystemClock.elapsedRealtime() - mIntentCreationElapsedRealtimeMs);
}
@Override
protected void performPostInflationStartup() {
super.performPostInflationStartup();
FontPreloader.getInstance().onPostInflationStartupFre();
}
@Override
public void finishNativeInitialization() {
super.finishNativeInitialization();
Runnable onNativeFinished = new Runnable() {
@Override
public void run() {
if (isActivityFinishingOrDestroyed()) return;
onNativeDependenciesFullyInitialized();
}
};
TemplateUrlServiceFactory.get().runWhenLoaded(onNativeFinished);
}
private void onNativeDependenciesFullyInitialized() {
mNativeSideIsInitialized = true;
onInternalStateChanged();
}
@Override
protected void onPolicyLoadListenerAvailable(boolean onDevicePolicyFound) {
super.onPolicyLoadListenerAvailable(onDevicePolicyFound);
onInternalStateChanged();
}
private void onInternalStateChanged() {
if (!mFlowIsKnown) {
return;
}
if (mNativeSideIsInitialized && mFreProperties == null) {
completeFirstRunExperience();
return;
}
if (mPagerAdapter == null) {
createPageSequence();
}
if (!mPostNativeAndPolicyPagesCreated && areNativeAndPoliciesInitialized()) {
createPostNativeAndPoliciesPageSequence();
}
if (areNativeAndPoliciesInitialized()) {
skipPagesIfNecessary();
}
}
private boolean areNativeAndPoliciesInitialized() {
return mNativeSideIsInitialized && mFlowIsKnown
&& this.getPolicyLoadListener().get() != null;
}
// Activity:
@Override
public void onAttachFragment(Fragment fragment) {
if (!(fragment instanceof FirstRunFragment)) return;
FirstRunFragment page = (FirstRunFragment) fragment;
// Important that this check delegates to the dispatcher instead of using
// mNativeSideIsInitialized, the two flags are not updated atomically. The dispatcher does
// not call #onFinishNativeInitialization() if it already happened.
if (getLifecycleDispatcher().isNativeInitializationFinished()) {
page.onNativeInitialized();
} else {
getLifecycleDispatcher().register(new NativeInitObserver() {
@Override
public void onFinishNativeInitialization() {
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 {
ApiCompatibilityUtils.finishAndRemoveTask(activity);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
// On L ApiCompatibilityUtils.finishAndRemoveTask() sometimes fails. Try one
// last time, see crbug.com/781396 for origin of this approach.
if (!activity.isFinishing()) {
activity.finish();
}
}
}
}
}
}
@Override
public void onBackPressed() {
// Terminate if we are still waiting for the native or for Android EDU / GAIA Child checks.
if (mPagerAdapter == null) {
abortFirstRunExperience();
return;
}
if (mPager.getCurrentItem() == 0) {
abortFirstRunExperience();
} else {
setCurrentItemForPager(mPager.getCurrentItem() - 1);
}
}
// FirstRunPageDelegate:
@Override
public Bundle getProperties() {
return mFreProperties;
}
@Override
public void advanceToNextPage() {
jumpToPage(mPager.getCurrentItem() + 1);
}
@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);
recordFreProgressHistogram(TextUtils.isEmpty(mResultSignInAccountName)
? FRE_PROGRESS_COMPLETED_NOT_SIGNED_IN
: FRE_PROGRESS_COMPLETED_SIGNED_IN);
FirstRunFlowSequencer.markFlowAsCompleted(
mResultSignInAccountName, mResultShowSignInSettings);
if (DataReductionPromoUtils.getDisplayedFreOrSecondRunPromo()) {
if (DataReductionProxySettings.getInstance().isDataReductionProxyEnabled()) {
DataReductionProxyUma
.dataReductionProxyUIAction(DataReductionProxyUma.ACTION_FRE_ENABLED);
DataReductionPromoUtils.saveFrePromoOptOut(false);
} else {
DataReductionProxyUma
.dataReductionProxyUIAction(DataReductionProxyUma.ACTION_FRE_DISABLED);
DataReductionPromoUtils.saveFrePromoOptOut(true);
}
}
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 void refuseSignIn() {
mResultSignInAccountName = null;
mResultShowSignInSettings = false;
}
@Override
public void acceptSignIn(String accountName, boolean openSettings) {
mResultSignInAccountName = accountName;
mResultShowSignInSettings = openSettings;
}
@Override
public boolean didAcceptTermsOfService() {
return FirstRunUtils.didAcceptTermsOfService();
}
@Override
public void acceptTermsOfService(boolean allowCrashUpload) {
// 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(allowCrashUpload);
FirstRunStatus.setSkipWelcomePage(true);
flushPersistentData();
if (sObserver != null) sObserver.onAcceptTermsOfService(this);
jumpToPage(mPager.getCurrentItem() + 1);
}
/** 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);
}
}
/**
* Transitions to a given page.
* @param position A page index to transition to.
* @return Whether the transition to a given page was allowed.
*/
private boolean jumpToPage(int position) {
if (sObserver != null) sObserver.onJumpToPage(this, position);
if (!didAcceptTermsOfService()) {
return position == 0;
}
if (!setCurrentItemForPager(position)) {
return false;
}
recordFreProgressHistogram(mFreProgressStates.get(position));
return true;
}
private boolean setCurrentItemForPager(int 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() {
boolean shouldSkip = mPages.get(mPager.getCurrentItem()).shouldSkipPageOnCreate();
while (shouldSkip) {
if (!jumpToPage(mPager.getCurrentItem() + 1)) return;
shouldSkip = mPages.get(mPager.getCurrentItem()).shouldSkipPageOnCreate();
}
}
private void recordFreProgressHistogram(int state) {
if (mLaunchedFromChromeIcon) {
RecordHistogram.recordEnumeratedHistogram(
"MobileFre.Progress.MainIntent", state, FRE_PROGRESS_MAX);
} else {
RecordHistogram.recordEnumeratedHistogram(
"MobileFre.Progress.ViewIntent", state, FRE_PROGRESS_MAX);
}
}
@Override
public void showInfoPage(@StringRes int url) {
CustomTabActivity.showInfoPage(
this, LocalizationUtils.substituteLocalePlaceholder(getString(url)));
}
@VisibleForTesting
public boolean isNativeSideIsInitializedForTest() {
return mNativeSideIsInitialized;
}
@VisibleForTesting
public static void setObserverForTest(FirstRunActivityObserver observer) {
assert sObserver == null;
sObserver = observer;
}
}