// 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.app.Fragment;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.EmbedContentViewActivity;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.metrics.UmaUtils;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.preferences.datareduction.DataReductionPromoUtils;
import org.chromium.chrome.browser.preferences.datareduction.DataReductionProxyUma;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.util.IntentUtils;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
/**
* 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 AppCompatActivity implements FirstRunPageDelegate {
protected static final String TAG = "FirstRunActivity";
// Incoming parameters:
public static final String EXTRA_COMING_FROM_CHROME_ICON = "Extra.ComingFromChromeIcon";
public static final String EXTRA_USE_FRE_FLOW_SEQUENCER = "Extra.UseFreFlowSequencer";
public static final String EXTRA_START_LIGHTWEIGHT_FRE = "Extra.StartLightweightFRE";
public static final String EXTRA_CHROME_LAUNCH_INTENT = "Extra.FreChromeLaunchIntent";
static final String SHOW_WELCOME_PAGE = "ShowWelcome";
static final String SHOW_SIGNIN_PAGE = "ShowSignIn";
static final String SHOW_DATA_REDUCTION_PAGE = "ShowDataReduction";
// Outgoing results:
public static final String RESULT_CLOSE_APP = "Close App";
public static final String RESULT_SIGNIN_ACCOUNT_NAME = "ResultSignInTo";
public static final String RESULT_SHOW_SIGNIN_SETTINGS = "ResultShowSignInSettings";
public static final String EXTRA_FIRST_RUN_ACTIVITY_RESULT = "Extra.FreActivityResult";
public static final String EXTRA_FIRST_RUN_COMPLETE = "Extra.FreComplete";
public static final boolean DEFAULT_METRICS_AND_CRASH_REPORTING = true;
// UMA constants.
private static final String UMA_SIGNIN_CHOICE = "MobileFre.SignInChoice";
private static final int SIGNIN_SETTINGS_DEFAULT_ACCOUNT = 0;
private static final int SIGNIN_SETTINGS_ANOTHER_ACCOUNT = 1;
private static final int SIGNIN_ACCEPT_DEFAULT_ACCOUNT = 2;
private static final int SIGNIN_ACCEPT_ANOTHER_ACCOUNT = 3;
private static final int SIGNIN_NO_THANKS = 4;
private static final int SIGNIN_OPTION_COUNT = 5;
private static final String FRE_ENTRY_MAIN_INTENT = ".MainIntent";
private static final String FRE_ENTRY_VIEW_INTENT = ".ViewIntent";
private static final String UMA_FRE_PROGRESS = "MobileFre.Progress";
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_TERMINATOR = 6;
@VisibleForTesting
static FirstRunGlue sGlue = new FirstRunGlueImpl();
private boolean mShowWelcomePage = true;
private String mResultSignInAccountName;
private boolean mResultShowSignInSettings;
private boolean mNativeSideIsInitialized;
private ProfileDataCache mProfileDataCache;
private FirstRunViewPager mPager;
protected Bundle mFreProperties;
private List<Callable<FirstRunPage>> mPages;
private List<Integer> mFreProgressStates;
/**
* 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() {
mPages = new ArrayList<Callable<FirstRunPage>>();
mFreProgressStates = new ArrayList<Integer>();
// An optional welcome page.
if (mShowWelcomePage) {
mPages.add(pageOf(ToSAndUMAFirstRunFragment.class));
mFreProgressStates.add(FRE_PROGRESS_WELCOME_SHOWN);
}
// An optional Data Saver page.
if (mFreProperties.getBoolean(SHOW_DATA_REDUCTION_PAGE)) {
mPages.add(pageOf(DataReductionProxyFirstRunFragment.class));
mFreProgressStates.add(FRE_PROGRESS_DATA_SAVER_SHOWN);
}
// An optional sign-in page.
if (mFreProperties.getBoolean(SHOW_SIGNIN_PAGE)) {
mPages.add(pageOf(AccountFirstRunFragment.class));
mFreProgressStates.add(FRE_PROGRESS_SIGNIN_SHOWN);
}
}
// Activity:
@Override
protected void onCreate(Bundle savedInstanceState) {
initializeBrowserProcess();
super.onCreate(savedInstanceState);
setFinishOnTouchOutside(false);
if (savedInstanceState != null) {
mFreProperties = savedInstanceState;
} else if (getIntent() != null) {
mFreProperties = getIntent().getExtras();
} else {
mFreProperties = new Bundle();
}
// Skip creating content view if it is to start a lightweight First Run Experience.
if (mFreProperties.getBoolean(FirstRunActivity.EXTRA_START_LIGHTWEIGHT_FRE)) {
return;
}
mPager = new FirstRunViewPager(this);
mPager.setId(R.id.fre_pager);
setContentView(mPager);
mProfileDataCache = new ProfileDataCache(FirstRunActivity.this, null);
mProfileDataCache.setProfile(Profile.getLastUsedProfile());
new FirstRunFlowSequencer(this, mFreProperties) {
@Override
public void onFlowIsKnown(Bundle freProperties) {
if (freProperties == null) {
completeFirstRunExperience();
return;
}
mFreProperties = freProperties;
mShowWelcomePage = mFreProperties.getBoolean(SHOW_WELCOME_PAGE);
createPageSequence();
if (TextUtils.isEmpty(mResultSignInAccountName)) {
mResultSignInAccountName = mFreProperties.getString(
AccountFirstRunFragment.FORCE_SIGNIN_ACCOUNT_TO);
}
if (mPages.size() == 0) {
completeFirstRunExperience();
return;
}
mPagerAdapter =
new FirstRunPagerAdapter(getFragmentManager(), mPages, mFreProperties);
stopProgressionIfNotAcceptedTermsOfService();
mPager.setAdapter(mPagerAdapter);
recordFreProgressHistogram(mFreProgressStates.get(0));
skipPagesIfNecessary();
}
}.start();
recordFreProgressHistogram(FRE_PROGRESS_STARTED);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putAll(mFreProperties);
}
@Override
protected void onPause() {
super.onPause();
flushPersistentData();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mProfileDataCache != null) mProfileDataCache.destroy();
}
@Override
protected void onStart() {
super.onStart();
stopProgressionIfNotAcceptedTermsOfService();
if (!mFreProperties.getBoolean(EXTRA_USE_FRE_FLOW_SEQUENCER)) {
if (FirstRunStatus.getFirstRunFlowComplete(this)) {
// This is a parallel flow that needs to be refreshed/re-fired.
// Signal the FRE flow completion and re-launch the original intent.
completeFirstRunExperience();
}
}
}
@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;
}
Object currentItem = mPagerAdapter.instantiateItem(mPager, mPager.getCurrentItem());
if (currentItem instanceof FirstRunPage) {
FirstRunPage page = (FirstRunPage) currentItem;
if (page.interceptBackPressed()) return;
}
if (mPager.getCurrentItem() == 0) {
abortFirstRunExperience();
} else {
mPager.setCurrentItem(mPager.getCurrentItem() - 1, false);
}
}
// FirstRunPageDelegate:
@Override
public ProfileDataCache getProfileDataCache() {
return mProfileDataCache;
}
@Override
public void advanceToNextPage() {
jumpToPage(mPager.getCurrentItem() + 1);
}
@Override
public void recreateCurrentPage() {
mPagerAdapter.notifyDataSetChanged();
}
@Override
public void abortFirstRunExperience() {
Intent intent = new Intent();
if (mFreProperties != null) intent.putExtras(mFreProperties);
intent.putExtra(RESULT_CLOSE_APP, true);
finishAllTheActivities(getLocalClassName(), Activity.RESULT_CANCELED, intent);
sendPendingIntentIfNecessary(false);
}
@Override
public void completeFirstRunExperience() {
if (!TextUtils.isEmpty(mResultSignInAccountName)) {
boolean defaultAccountName =
sGlue.isDefaultAccountName(getApplicationContext(), mResultSignInAccountName);
int choice;
if (mResultShowSignInSettings) {
if (defaultAccountName) {
choice = SIGNIN_SETTINGS_DEFAULT_ACCOUNT;
} else {
choice = SIGNIN_SETTINGS_ANOTHER_ACCOUNT;
}
} else {
if (defaultAccountName) {
choice = SIGNIN_ACCEPT_DEFAULT_ACCOUNT;
} else {
choice = SIGNIN_ACCEPT_ANOTHER_ACCOUNT;
}
}
RecordHistogram.recordEnumeratedHistogram(
UMA_SIGNIN_CHOICE, choice, SIGNIN_OPTION_COUNT);
recordFreProgressHistogram(FRE_PROGRESS_COMPLETED_SIGNED_IN);
} else {
recordFreProgressHistogram(FRE_PROGRESS_COMPLETED_NOT_SIGNED_IN);
}
mFreProperties.putString(RESULT_SIGNIN_ACCOUNT_NAME, mResultSignInAccountName);
mFreProperties.putBoolean(RESULT_SHOW_SIGNIN_SETTINGS, mResultShowSignInSettings);
FirstRunFlowSequencer.markFlowAsCompleted(this, mFreProperties);
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);
}
}
Intent resultData = new Intent();
resultData.putExtras(mFreProperties);
finishAllTheActivities(getLocalClassName(), Activity.RESULT_OK, resultData);
sendPendingIntentIfNecessary(true);
}
@Override
public void refuseSignIn() {
RecordHistogram.recordEnumeratedHistogram(
UMA_SIGNIN_CHOICE, SIGNIN_NO_THANKS, SIGNIN_OPTION_COUNT);
mResultSignInAccountName = null;
mResultShowSignInSettings = false;
}
@Override
public void acceptSignIn(String accountName) {
mResultSignInAccountName = accountName;
}
@Override
public void askToOpenSignInSettings() {
mResultShowSignInSettings = true;
}
@Override
public boolean didAcceptTermsOfService() {
return sGlue.didAcceptTermsOfService(getApplicationContext());
}
@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);
sGlue.acceptTermsOfService(allowCrashUpload);
FirstRunStatus.setSkipWelcomePage(FirstRunActivity.this, true);
flushPersistentData();
stopProgressionIfNotAcceptedTermsOfService();
jumpToPage(mPager.getCurrentItem() + 1);
}
@Override
public void openAccountAdder(Fragment fragment) {
sGlue.openAccountAdder(fragment);
}
protected void flushPersistentData() {
if (mNativeSideIsInitialized) ChromeApplication.flushPersistentData();
}
/**
* Finish all the instances of the given Activity.
* @param targetActivity The class name of the target Activity.
* @param result The result code to propagate back to the originating activity.
* @param data The data to propagate back to the originating activity.
*/
protected static void finishAllTheActivities(String targetActivity, int result, Intent data) {
List<WeakReference<Activity>> activities = ApplicationStatus.getRunningActivities();
for (WeakReference<Activity> weakActivity : activities) {
Activity activity = weakActivity.get();
if (activity != null && activity.getLocalClassName().equals(targetActivity)) {
activity.setResult(result, data);
activity.finish();
}
}
}
/**
* Sends the PendingIntent included with the CHROME_LAUNCH_INTENT extra if it exists.
* @param complete Whether first run completed successfully.
*/
protected void sendPendingIntentIfNecessary(final boolean complete) {
PendingIntent pendingIntent = IntentUtils.safeGetParcelableExtra(getIntent(),
EXTRA_CHROME_LAUNCH_INTENT);
if (pendingIntent == null) return;
Intent extraDataIntent = new Intent();
extraDataIntent.putExtra(FirstRunActivity.EXTRA_FIRST_RUN_ACTIVITY_RESULT, true);
extraDataIntent.putExtra(FirstRunActivity.EXTRA_FIRST_RUN_COMPLETE, complete);
try {
// After the PendingIntent has been sent, send a first run callback to custom tabs if
// necessary.
PendingIntent.OnFinished onFinished = new PendingIntent.OnFinished() {
@Override
public void onSendFinished(PendingIntent pendingIntent, Intent intent,
int resultCode, String resultData, Bundle resultExtras) {
if (ChromeLauncherActivity.isCustomTabIntent(intent)) {
CustomTabsConnection.getInstance(
getApplication()).sendFirstRunCallbackIfNecessary(intent, complete);
}
}
};
// 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(this, complete ? Activity.RESULT_OK : Activity.RESULT_CANCELED,
extraDataIntent, onFinished, null);
} catch (CanceledException e) {
Log.e(TAG, "Unable to send PendingIntent.", e);
}
}
/**
* Transitions to a given page.
* @return Whether the transition to a given page was allowed.
* @param position A page index to transition to.
*/
private boolean jumpToPage(int position) {
if (mShowWelcomePage && !didAcceptTermsOfService()) {
return position == 0;
}
if (position >= mPagerAdapter.getCount()) {
completeFirstRunExperience();
return false;
}
mPager.setCurrentItem(position, false);
recordFreProgressHistogram(mFreProgressStates.get(position));
return true;
}
private void stopProgressionIfNotAcceptedTermsOfService() {
if (mPagerAdapter == null) return;
mPagerAdapter.setStopAtTheFirstPage(mShowWelcomePage && !didAcceptTermsOfService());
}
private void skipPagesIfNecessary() {
if (mPagerAdapter == null) return;
int currentPageIndex = mPager.getCurrentItem();
while (currentPageIndex < mPagerAdapter.getCount()) {
FirstRunPage currentPage = (FirstRunPage) mPagerAdapter.getItem(currentPageIndex);
if (!currentPage.shouldSkipPageOnCreate(getApplicationContext())) return;
if (!jumpToPage(currentPageIndex + 1)) return;
currentPageIndex = mPager.getCurrentItem();
}
}
private void initializeBrowserProcess() {
// The Chrome browser process must be started here because this Activity
// may be started explicitly for tests cases, from Android notifications or
// when the application is restoring a FRE fragment after Chrome being killed.
// This should happen before super.onCreate() because it might recreate a fragment,
// and a fragment might depend on the native library.
try {
ChromeBrowserInitializer.getInstance(this).handleSynchronousStartup();
mNativeSideIsInitialized = true;
} catch (ProcessInitException e) {
Log.e(TAG, "Unable to load native library.", e);
abortFirstRunExperience();
return;
}
}
private void recordFreProgressHistogram(int state) {
String entryType = mFreProperties.getBoolean(FirstRunActivity.EXTRA_COMING_FROM_CHROME_ICON)
? FRE_ENTRY_MAIN_INTENT
: FRE_ENTRY_VIEW_INTENT;
RecordHistogram.recordEnumeratedHistogram(
UMA_FRE_PROGRESS + entryType, state, FRE_PROGRESS_TERMINATOR);
}
/**
* Creates a trivial page constructor for a given page type.
* @param clazz The .class of the page type.
* @return The simple constructor for a given page type (no parameters, no tuning).
*/
public static Callable<FirstRunPage> pageOf(final Class<? extends FirstRunPage> clazz) {
return new Callable<FirstRunPage>() {
@Override
public FirstRunPage call() throws Exception {
Constructor<? extends FirstRunPage> constructor = clazz.getDeclaredConstructor();
return constructor.newInstance();
}
};
}
@Override
public void showEmbedContentViewActivity(int title, int url) {
EmbedContentViewActivity.show(this, title, url);
}
}