// 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.document;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Notification;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.provider.Browser;
import android.support.customtabs.CustomTabsIntent;
import android.text.TextUtils;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.CommandLine;
import org.chromium.base.CommandLineInitUtil;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.TraceEvent;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.IntentHandler.ExternalAppId;
import org.chromium.chrome.browser.IntentHandler.TabOpenType;
import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.SeparateTaskCustomTabActivity;
import org.chromium.chrome.browser.firstrun.FirstRunActivity;
import org.chromium.chrome.browser.firstrun.FirstRunFlowSequencer;
import org.chromium.chrome.browser.firstrun.LightweightFirstRunActivity;
import org.chromium.chrome.browser.instantapps.InstantAppsHandler;
import org.chromium.chrome.browser.metrics.LaunchMetrics;
import org.chromium.chrome.browser.metrics.MediaNotificationUma;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.notifications.NotificationPlatformBridge;
import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.DocumentModeAssassin;
import org.chromium.chrome.browser.upgrade.UpgradeActivity;
import org.chromium.chrome.browser.util.FeatureUtilities;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.chrome.browser.webapps.ActivityAssigner;
import org.chromium.chrome.browser.webapps.WebappLauncherActivity;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.List;
import java.util.UUID;
/**
* Dispatches incoming intents to the appropriate activity based on the current configuration and
* Intent fired.
*/
public class ChromeLauncherActivity extends Activity
implements IntentHandler.IntentHandlerDelegate {
/**
* Extra indicating launch mode used.
*/
public static final String EXTRA_LAUNCH_MODE =
"com.google.android.apps.chrome.EXTRA_LAUNCH_MODE";
/**
* Whether or not the toolbar should indicate that a tab was spawned by another Activity.
*/
public static final String EXTRA_IS_ALLOWED_TO_RETURN_TO_PARENT =
"org.chromium.chrome.browser.document.IS_ALLOWED_TO_RETURN_TO_PARENT";
private static final String TAG = "document_CLActivity";
/**
* Timeout in ms for reading PartnerBrowserCustomizations provider. We do not trust third party
* provider by default.
*/
private static final int PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS = 10000;
private static final LaunchMetrics.SparseHistogramSample sIntentFlagsHistogram =
new LaunchMetrics.SparseHistogramSample("Launch.IntentFlags");
private IntentHandler mIntentHandler;
private boolean mIsInLegacyMultiInstanceMode;
private boolean mIsCustomTabIntent;
private boolean mIsHerbIntent;
/** When started with an intent, maybe pre-resolve the domain. */
private void maybePrefetchDnsInBackground() {
if (getIntent() != null && Intent.ACTION_VIEW.equals(getIntent().getAction())) {
String maybeUrl = IntentHandler.getUrlFromIntent(getIntent());
if (maybeUrl != null) {
WarmupManager.getInstance().maybePrefetchDnsForUrlInBackground(this, maybeUrl);
}
}
}
/**
* Figure out how to route the Intent. Because this is on the critical path to startup, please
* avoid making the pathway any more complicated than it already is. Make sure that anything
* you add _absolutely has_ to be here.
*/
@Override
@SuppressLint("MissingSuperCall") // Called in doOnCreate.
public void onCreate(Bundle savedInstanceState) {
// Third-party code adds disk access to Activity.onCreate. http://crbug.com/619824
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
TraceEvent.begin("ChromeLauncherActivity");
TraceEvent.begin("ChromeLauncherActivity.onCreate");
try {
doOnCreate(savedInstanceState);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
TraceEvent.end("ChromeLauncherActivity.onCreate");
}
}
private final void doOnCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// This Activity is only transient. It launches another activity and
// terminates itself. However, some of the work is performed outside of
// {@link Activity#onCreate()}. To capture this, the TraceEvent starts
// in onCreate(), and ends in onPause().
// Needs to be called as early as possible, to accurately capture the
// time at which the intent was received.
IntentHandler.addTimestampToIntent(getIntent());
// Initialize the command line in case we've disabled document mode from there.
CommandLineInitUtil.initCommandLine(this, ChromeApplication.COMMAND_LINE_FILE);
// Read partner browser customizations information asynchronously.
// We want to initialize early because when there is no tabs to restore, we should possibly
// show homepage, which might require reading PartnerBrowserCustomizations provider.
PartnerBrowserCustomizations.initializeAsync(getApplicationContext(),
PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS);
recordIntentMetrics();
mIsInLegacyMultiInstanceMode =
MultiWindowUtils.getInstance().shouldRunInLegacyMultiInstanceMode(this);
mIntentHandler = new IntentHandler(this, getPackageName());
mIsCustomTabIntent = isCustomTabIntent(getIntent());
if (!mIsCustomTabIntent) {
mIsHerbIntent = isHerbIntent();
mIsCustomTabIntent = mIsHerbIntent;
}
Intent intent = getIntent();
int tabId = IntentUtils.safeGetIntExtra(intent,
TabOpenType.BRING_TAB_TO_FRONT.name(), Tab.INVALID_TAB_ID);
boolean incognito = intent.getBooleanExtra(
IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, false);
// Check if this intent was fired at the end of the first run experience and return if
// first run was not successfully completed.
boolean firstRunActivityResult = IntentUtils.safeGetBooleanExtra(intent,
FirstRunActivity.EXTRA_FIRST_RUN_ACTIVITY_RESULT, false);
boolean firstRunComplete = IntentUtils.safeGetBooleanExtra(intent,
FirstRunActivity.EXTRA_FIRST_RUN_COMPLETE, false);
if (firstRunActivityResult && !firstRunComplete) {
finish();
return;
}
// Check if a web search Intent is being handled.
String url = IntentHandler.getUrlFromIntent(intent);
if (url == null && tabId == Tab.INVALID_TAB_ID
&& !incognito && mIntentHandler.handleWebSearchIntent(intent)) {
finish();
return;
}
// Check if a LIVE WebappActivity has to be brought back to the foreground. We can't
// check for a dead WebappActivity because we don't have that information without a global
// TabManager. If that ever lands, code to bring back any Tab could be consolidated
// here instead of being spread between ChromeTabbedActivity and ChromeLauncherActivity.
// https://crbug.com/443772, https://crbug.com/522918
if (WebappLauncherActivity.bringWebappToFront(tabId)) {
ApiCompatibilityUtils.finishAndRemoveTask(this);
return;
}
// The notification settings cog on the flipped side of Notifications and in the Android
// Settings "App Notifications" view will open us with a specific category.
if (intent.hasCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)) {
NotificationPlatformBridge.launchNotificationPreferences(this, getIntent());
finish();
return;
}
// Check if we should launch an Instant App to handle the intent.
if (InstantAppsHandler.getInstance().handleIncomingIntent(
this, intent, mIsCustomTabIntent && !mIsHerbIntent)) {
finish();
return;
}
// Check if we should launch the ChromeTabbedActivity.
if (!mIsCustomTabIntent && !FeatureUtilities.isDocumentMode(this)) {
boolean checkedFre = false;
if (CommandLine.getInstance().hasSwitch(
ChromeSwitches.ENABLE_LIGHTWEIGHT_FIRST_RUN_EXPERIENCE)) {
// Launch the First Run Experience for VIEW Intents with URLs before launching
// ChromeTabbedActivity if necessary.
if (getIntent() != null && getIntent().getAction() == Intent.ACTION_VIEW
&& IntentHandler.getUrlFromIntent(getIntent()) != null) {
if (launchFirstRunExperience(true)) {
finish();
return;
}
checkedFre = true;
}
}
launchTabbedMode(checkedFre);
finish();
return;
}
// Check if we should launch the FirstRunActivity. This occurs after the check to launch
// ChromeTabbedActivity because ChromeTabbedActivity handles FRE in its own way.
if (launchFirstRunExperience(false)) {
finish();
return;
}
// Check if we should launch a Custom Tab.
if (mIsCustomTabIntent) {
launchCustomTabActivity();
finish();
return;
}
// Force a user to migrate to document mode, if necessary.
if (DocumentModeAssassin.getInstance().isMigrationNecessary()) {
Log.d(TAG, "Diverting to UpgradeActivity via ChromeLauncherActivity.");
UpgradeActivity.launchInstance(this, intent);
ApiCompatibilityUtils.finishAndRemoveTask(this);
return;
}
// All possible bounces to other activities should have already been enumerated above.
Log.e(TAG, "User wasn't sent to another Activity.");
assert false;
ApiCompatibilityUtils.finishAndRemoveTask(this);
}
@Override
public void onDestroy() {
super.onDestroy();
TraceEvent.end("ChromeLauncherActivity");
}
@Override
public void processWebSearchIntent(String query) {
Intent searchIntent = new Intent(Intent.ACTION_WEB_SEARCH);
searchIntent.putExtra(SearchManager.QUERY, query);
startActivity(searchIntent);
}
@Override
public void processUrlViewIntent(String url, String referer, String headers,
IntentHandler.TabOpenType tabOpenType, String externalAppId,
int tabIdToBringToFront, boolean hasUserGesture, Intent intent) {
assert false;
}
/**
* @return Whether or not an Herb prototype may hijack an Intent.
*/
public static boolean canBeHijackedByHerb(Intent intent) {
String url = IntentHandler.getUrlFromIntent(intent);
// Only VIEW Intents with URLs are rerouted to Custom Tabs.
if (intent == null || !TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())
|| TextUtils.isEmpty(url)) {
return false;
}
// Don't open explicitly opted out intents in custom tabs.
if (CustomTabsIntent.shouldAlwaysUseBrowserUI(intent)) {
return false;
}
// Don't reroute Chrome Intents.
Context context = ContextUtils.getApplicationContext();
if (TextUtils.equals(context.getPackageName(),
IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID))
|| IntentHandler.wasIntentSenderChrome(intent, context)) {
return false;
}
// Don't reroute internal chrome URLs.
try {
URI uri = URI.create(url);
if (UrlUtilities.isInternalScheme(uri)) return false;
} catch (IllegalArgumentException e) {
return false;
}
// Don't reroute Home screen shortcuts.
if (IntentUtils.safeHasExtra(intent, ShortcutHelper.EXTRA_SOURCE)) {
return false;
}
return true;
}
/**
* @return Whether or not a Custom Tab will be forcefully used for the incoming Intent.
*/
private boolean isHerbIntent() {
if (!canBeHijackedByHerb(getIntent())) return false;
// Different Herb flavors handle incoming intents differently.
String flavor = FeatureUtilities.getHerbFlavor();
if (TextUtils.isEmpty(flavor)
|| TextUtils.equals(ChromeSwitches.HERB_FLAVOR_DISABLED, flavor)) {
return false;
} else if (TextUtils.equals(flavor, ChromeSwitches.HERB_FLAVOR_ELDERBERRY)) {
return IntentUtils.safeGetBooleanExtra(getIntent(),
ChromeLauncherActivity.EXTRA_IS_ALLOWED_TO_RETURN_TO_PARENT, true);
} else {
// Legacy Herb Flavors might hit this path before the caching logic corrects it, so
// treat this as disabled.
return false;
}
}
/**
* Adds extras to the Intent that are needed by Herb.
*/
public static void updateHerbIntent(Context context, Intent newIntent) {
// For Elderberry flavored Herbs that are to be launched in a separate task, add a random
// UUID to try and prevent Android from refocusing/clobbering items that share the same
// base intent. If we do support refocusing of existing Herbs, we need to do it on the
// current URL and not the URL that it was triggered with.
if (TextUtils.equals(
FeatureUtilities.getHerbFlavor(), ChromeSwitches.HERB_FLAVOR_ELDERBERRY)
&& ((newIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0
|| (newIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0)) {
String uuid = UUID.randomUUID().toString();
newIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
newIntent.setFlags(
newIntent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Force a new document L+ to ensure the proper task/stack creation.
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
| Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS);
newIntent.setClassName(context, SeparateTaskCustomTabActivity.class.getName());
} else {
int activityIndex =
ActivityAssigner.instance(ActivityAssigner.SEPARATE_TASK_CCT_NAMESPACE)
.assign(uuid);
String className = SeparateTaskCustomTabActivity.class.getName() + activityIndex;
newIntent.setClassName(context, className);
}
String url = IntentHandler.getUrlFromIntent(newIntent);
assert url != null;
newIntent.setData(new Uri.Builder().scheme(UrlConstants.CUSTOM_TAB_SCHEME)
.authority(uuid).query(url).build());
}
newIntent.putExtra(CustomTabsIntent.EXTRA_DEFAULT_SHARE_MENU_ITEM, true);
}
/**
* @return Whether the intent is for launching a Custom Tab.
*/
public static boolean isCustomTabIntent(Intent intent) {
if (intent == null
|| CustomTabsIntent.shouldAlwaysUseBrowserUI(intent)
|| !intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)) {
return false;
}
return IntentHandler.getUrlFromIntent(intent) != null;
}
/**
* Creates an Intent that can be used to launch a {@link CustomTabActivity}.
*/
public static Intent createCustomTabActivityIntent(
Context context, Intent intent, boolean addHerbExtras) {
// Use the copy constructor to carry over the myriad of extras.
Uri uri = Uri.parse(IntentHandler.getUrlFromIntent(intent));
Intent newIntent = new Intent(intent);
newIntent.setAction(Intent.ACTION_VIEW);
newIntent.setClassName(context, CustomTabActivity.class.getName());
newIntent.setData(uri);
// If a CCT intent triggers First Run, then NEW_TASK will be automatically applied. As
// part of that, it will inherit the EXCLUDE_FROM_RECENTS bit from ChromeLauncherActivity,
// so explicitly remove it to ensure the CCT does not get lost in recents.
if ((newIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0
|| (newIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0) {
newIntent.setFlags(
newIntent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
}
if (addHerbExtras) {
// TODO(tedchoc|mariakhomenko): Specifically not marking the intent is from Chrome via
// IntentHandler.addTrustedIntentExtras as it breaks the
// redirect logic for triggering instant apps. See if
// this is better addressed in TabRedirectHandler long
// term.
newIntent.putExtra(CustomTabIntentDataProvider.EXTRA_IS_OPENED_BY_CHROME, true);
} else {
IntentUtils.safeRemoveExtra(
intent, CustomTabIntentDataProvider.EXTRA_IS_OPENED_BY_CHROME);
}
if (addHerbExtras) updateHerbIntent(context, newIntent);
return newIntent;
}
/**
* Handles launching a {@link CustomTabActivity}, which will sit on top of a client's activity
* in the same task.
*/
private void launchCustomTabActivity() {
boolean handled = CustomTabActivity.handleInActiveContentIfNeeded(getIntent());
if (handled) return;
// Create and fire a launch intent.
startActivity(createCustomTabActivityIntent(
this, getIntent(), !isCustomTabIntent(getIntent()) && mIsHerbIntent));
if (mIsHerbIntent) overridePendingTransition(R.anim.activity_open_enter, R.anim.no_anim);
}
/**
* Handles launching a {@link ChromeTabbedActivity}.
* @param skipFre Whether skip the First Run Experience in ChromeTabbedActivity.
*/
@SuppressLint("InlinedApi")
private void launchTabbedMode(boolean skipFre) {
maybePrefetchDnsInBackground();
Intent newIntent = new Intent(getIntent());
String className = MultiWindowUtils.getInstance().getTabbedActivityForIntent(
newIntent, this).getName();
newIntent.setClassName(getApplicationContext().getPackageName(), className);
newIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
newIntent.addFlags(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS);
}
Uri uri = newIntent.getData();
if (uri != null && "content".equals(uri.getScheme())) {
newIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
if (mIsInLegacyMultiInstanceMode) {
MultiWindowUtils.getInstance().makeLegacyMultiInstanceIntent(this, newIntent);
}
if (skipFre) {
newIntent.putExtra(ChromeTabbedActivity.SKIP_FIRST_RUN_EXPERIENCE, true);
}
// This system call is often modified by OEMs and not actionable. http://crbug.com/619646.
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
StrictMode.allowThreadDiskWrites();
try {
startActivity(newIntent);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
/**
* @return Whether there is already an browser instance of Chrome already running.
*/
public boolean isChromeBrowserActivityRunning() {
for (WeakReference<Activity> reference : ApplicationStatus.getRunningActivities()) {
Activity activity = reference.get();
if (activity == null) continue;
String className = activity.getClass().getName();
if (TextUtils.equals(className, ChromeTabbedActivity.class.getName())) {
return true;
}
}
return false;
}
/**
* Tries to launch the First Run Experience. If ChromeLauncherActivity is running with the
* wrong Intent flags, we instead relaunch ChromeLauncherActivity to make sure it runs in its
* own task, which then triggers First Run.
* @return Whether or not the First Run Experience needed to be shown.
* @param forTabbedMode Whether the First Run Experience is launched for tabbed mode.
*/
private boolean launchFirstRunExperience(boolean forTabbedMode) {
// Tries to launch the Generic First Run Experience for intent from GSA.
boolean showLightweightFre =
IntentHandler.determineExternalIntentSource(this.getPackageName(), getIntent())
!= ExternalAppId.GSA;
Intent freIntent = FirstRunFlowSequencer.checkIfFirstRunIsNecessary(
this, getIntent(), showLightweightFre);
if (freIntent == null) return false;
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
if (CommandLine.getInstance().hasSwitch(
ChromeSwitches.ENABLE_LIGHTWEIGHT_FIRST_RUN_EXPERIENCE)) {
boolean isTabbedModeActive = false;
boolean isLightweightFreActive = false;
boolean isGenericFreActive = false;
List<WeakReference<Activity>> activities = ApplicationStatus.getRunningActivities();
for (WeakReference<Activity> weakActivity : activities) {
Activity activity = weakActivity.get();
if (activity == null) {
continue;
}
if (activity instanceof ChromeTabbedActivity) {
isTabbedModeActive = true;
continue;
}
if (activity instanceof LightweightFirstRunActivity) {
isLightweightFreActive = true;
// A Generic or a new Lightweight First Run Experience will be launched
// below, so finish the old Lightweight First Run Experience.
activity.setResult(Activity.RESULT_CANCELED);
activity.finish();
continue;
}
if (activity instanceof FirstRunActivity) {
isGenericFreActive = true;
continue;
}
}
if (forTabbedMode) {
if (isTabbedModeActive || isLightweightFreActive || !showLightweightFre) {
// Lets ChromeTabbedActivity checks and launches the Generic First Run
// Experience.
launchTabbedMode(false);
finish();
return true;
}
} else if (isGenericFreActive) {
// Launch the Generic First Run Experience if it is active previously.
freIntent = FirstRunFlowSequencer.createGenericFirstRunIntent(
this, TextUtils.equals(getIntent().getAction(), Intent.ACTION_MAIN));
}
}
// Add a PendingIntent so that the intent used to launch Chrome will be resent when
// first run is completed or canceled.
FirstRunFlowSequencer.addPendingIntent(this, freIntent, getIntent());
startActivity(freIntent);
} else {
Intent newIntent = new Intent(getIntent());
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(newIntent);
}
finish();
return true;
}
/**
* Records metrics gleaned from the Intent.
*/
private void recordIntentMetrics() {
Intent intent = getIntent();
IntentHandler.ExternalAppId source =
IntentHandler.determineExternalIntentSource(getPackageName(), intent);
if (intent.getPackage() == null && source != IntentHandler.ExternalAppId.CHROME) {
int flagsOfInterest = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
int maskedFlags = intent.getFlags() & flagsOfInterest;
sIntentFlagsHistogram.record(maskedFlags);
}
MediaNotificationUma.recordClickSource(intent);
}
}