// 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); } }