blob: 2b512911c3761a467c0fb181e1ffbfa94c19532a [file] [log] [blame]
// Copyright 2017 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;
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.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.support.annotation.IntDef;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsSessionToken;
import android.support.customtabs.TrustedWebUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.metrics.CachedMetrics;
import org.chromium.chrome.browser.browserservices.BrowserSessionContentUtils;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.SeparateTaskCustomTabActivity;
import org.chromium.chrome.browser.firstrun.FirstRunFlowSequencer;
import org.chromium.chrome.browser.instantapps.InstantAppsHandler;
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.searchwidget.SearchActivity;
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.vr.VrIntentUtils;
import org.chromium.chrome.browser.webapps.ActivityAssigner;
import org.chromium.chrome.browser.webapps.WebappActivity;
import org.chromium.chrome.browser.webapps.WebappInfo;
import org.chromium.chrome.browser.webapps.WebappLauncherActivity;
import org.chromium.ui.widget.Toast;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.UUID;
/**
* Dispatches incoming intents to the appropriate activity based on the current configuration and
* Intent fired.
*/
public class LaunchIntentDispatcher 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 = "ActivitiyDispatcher";
/**
* 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 CachedMetrics.SparseHistogramSample sIntentFlagsHistogram =
new CachedMetrics.SparseHistogramSample("Launch.IntentFlags");
private final Activity mActivity;
private final Intent mIntent;
private final boolean mIsCustomTabIntent;
private final boolean mIsVrIntent;
@IntDef({Action.CONTINUE, Action.FINISH_ACTIVITY, Action.FINISH_ACTIVITY_REMOVE_TASK})
@Retention(RetentionPolicy.SOURCE)
public @interface Action {
int CONTINUE = 0;
int FINISH_ACTIVITY = 1;
int FINISH_ACTIVITY_REMOVE_TASK = 2;
}
/**
* Dispatches the intent in the context of the activity.
* In most cases calling this method will result in starting a new activity, in which case
* the current activity will need to be finished as per the action returned.
*
* @param currentActivity activity that received the intent
* @param intent intent to dispatch
* @return action to take
*/
public static @Action int dispatch(Activity currentActivity, Intent intent) {
return new LaunchIntentDispatcher(currentActivity, intent).dispatch();
}
/**
* Dispatches the intent to proper tabbed activity.
* This method is similar to {@link #dispatch()}, but only handles intents that result in
* starting a tabbed activity (i.e. one of *TabbedActivity classes).
*
* @param currentActivity activity that received the intent
* @param intent intent to dispatch
* @return action to take
*/
public static @Action int dispatchToTabbedActivity(Activity currentActivity, Intent intent) {
return new LaunchIntentDispatcher(currentActivity, intent).dispatchToTabbedActivity();
}
/**
* Dispatches the intent to proper tabbed activity.
* This method is similar to {@link #dispatch()}, but only handles intents that result in
* starting a custom tab activity.
*/
public static @Action int dispatchToCustomTabActivity(Activity currentActivity, Intent intent) {
LaunchIntentDispatcher dispatcher = new LaunchIntentDispatcher(currentActivity, intent);
if (!dispatcher.mIsCustomTabIntent) {
return Action.CONTINUE;
}
dispatcher.launchCustomTabActivity();
return Action.FINISH_ACTIVITY;
}
private LaunchIntentDispatcher(Activity activity, Intent intent) {
mActivity = activity;
mIntent = IntentUtils.sanitizeIntent(intent);
// Needs to be called as early as possible, to accurately capture the
// time at which the intent was received.
if (mIntent != null && IntentHandler.getTimestampFromIntent(mIntent) == -1) {
IntentHandler.addTimestampToIntent(mIntent);
}
recordIntentMetrics();
mIsVrIntent = VrIntentUtils.isVrIntent(mIntent);
mIsCustomTabIntent = isCustomTabIntent(mIntent);
}
/**
* 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.
*/
private @Action int dispatch() {
// Read partner browser customizations information asynchronously.
// We want to initialize early because when there are no tabs to restore, we should possibly
// show homepage, which might require reading PartnerBrowserCustomizations provider.
PartnerBrowserCustomizations.initializeAsync(
mActivity.getApplicationContext(), PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS);
int tabId = IntentUtils.safeGetIntExtra(
mIntent, IntentHandler.TabOpenType.BRING_TAB_TO_FRONT_STRING, Tab.INVALID_TAB_ID);
boolean incognito =
mIntent.getBooleanExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, false);
// Check if a web search Intent is being handled.
IntentHandler intentHandler = new IntentHandler(this, mActivity.getPackageName());
String url = IntentHandler.getUrlFromIntent(mIntent);
if (url == null && tabId == Tab.INVALID_TAB_ID && !incognito
&& intentHandler.handleWebSearchIntent(mIntent)) {
return Action.FINISH_ACTIVITY;
}
// 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)) {
return Action.FINISH_ACTIVITY_REMOVE_TASK;
}
// 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 (mIntent.hasCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)) {
NotificationPlatformBridge.launchNotificationPreferences(mActivity, mIntent);
return Action.FINISH_ACTIVITY;
}
// Check if we should launch an Instant App to handle the intent.
if (InstantAppsHandler.getInstance().handleIncomingIntent(
mActivity, mIntent, mIsCustomTabIntent, false)) {
return Action.FINISH_ACTIVITY;
}
// Check if we should push the user through First Run.
if (FirstRunFlowSequencer.launch(mActivity, mIntent, false /* requiresBroadcast */,
false /* preferLightweightFre */)) {
return Action.FINISH_ACTIVITY;
}
// Check if we should launch the ChromeTabbedActivity.
if (!mIsCustomTabIntent && !FeatureUtilities.isDocumentMode(mActivity)) {
return dispatchToTabbedActivity();
}
// Check if we should launch a Custom Tab.
if (mIsCustomTabIntent) {
if (!mIntent.getBooleanExtra(
TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false)
|| !launchTrustedWebActivity()) {
launchCustomTabActivity();
}
return Action.FINISH_ACTIVITY;
}
// Force a user to migrate to document mode, if necessary.
if (DocumentModeAssassin.getInstance().isMigrationNecessary()) {
Log.d(TAG, "Diverting to UpgradeActivity via " + mActivity.getClass().getName());
UpgradeActivity.launchInstance(mActivity, mIntent);
return Action.FINISH_ACTIVITY_REMOVE_TASK;
}
return Action.CONTINUE;
}
@Override
public void processWebSearchIntent(String query) {
Intent searchIntent = new Intent(Intent.ACTION_WEB_SEARCH);
searchIntent.putExtra(SearchManager.QUERY, query);
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
int resolvers =
ContextUtils.getApplicationContext()
.getPackageManager()
.queryIntentActivities(searchIntent, PackageManager.GET_RESOLVED_FILTER)
.size();
if (resolvers == 0) {
// Phone doesn't have a WEB_SEARCH action handler, open Search Activity with
// the given query.
Intent searchActivityIntent = new Intent(Intent.ACTION_MAIN);
searchActivityIntent.setClass(
ContextUtils.getApplicationContext(), SearchActivity.class);
searchActivityIntent.putExtra(SearchManager.QUERY, query);
mActivity.startActivity(searchActivityIntent);
} else {
mActivity.startActivity(searchIntent);
}
}
}
@Override
public void processUrlViewIntent(String url, String referer, String headers,
@IntentHandler.TabOpenType int tabOpenType, String externalAppId,
int tabIdToBringToFront, boolean hasUserGesture, Intent intent) {
assert false;
}
/** When started with an intent, maybe pre-resolve the domain. */
private void maybePrefetchDnsInBackground() {
if (mIntent != null && Intent.ACTION_VIEW.equals(mIntent.getAction())) {
String maybeUrl = IntentHandler.getUrlFromIntent(mIntent);
if (maybeUrl != null) {
WarmupManager.getInstance().maybePrefetchDnsForUrlInBackground(mActivity, maybeUrl);
}
}
}
/**
* @return Whether the intent is for launching a Custom Tab.
*/
public static boolean isCustomTabIntent(Intent intent) {
if (intent == null) return false;
if (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) {
// 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.setData(uri);
newIntent.setClassName(context, CustomTabActivity.class.getName());
// If |uri| is a content:// URI, we want to propagate the URI permissions. This can't be
// achieved by simply adding the FLAG_GRANT_READ_URI_PERMISSION to the Intent, since the
// data URI on the Intent isn't |uri|, it just has |uri| as a query parameter.
if (uri != null && UrlConstants.CONTENT_SCHEME.equals(uri.getScheme())) {
context.grantUriPermission(
context.getPackageName(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
if (CommandLine.getInstance().hasSwitch(ChromeSwitches.OPEN_CUSTOM_TABS_IN_NEW_TASK)) {
newIntent.setFlags(newIntent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
}
// 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);
String uuid = UUID.randomUUID().toString();
newIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
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);
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());
}
IntentUtils.safeRemoveExtra(intent, CustomTabIntentDataProvider.EXTRA_IS_OPENED_BY_CHROME);
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 = BrowserSessionContentUtils.handleInActiveContentIfNeeded(mIntent);
if (handled) return;
maybePrefetchDnsInBackground();
// Create and fire a launch intent.
Intent launchIntent = createCustomTabActivityIntent(mActivity, mIntent);
// Allow disk writes during startActivity() to avoid strict mode violations on some
// Samsung devices, see https://crbug.com/796548.
try (StrictModeContext smc = StrictModeContext.allowDiskWrites()) {
mActivity.startActivity(launchIntent, null);
}
}
private boolean launchTrustedWebActivity() {
CustomTabsSessionToken session = CustomTabsSessionToken.getSessionTokenFromIntent(mIntent);
if (!CustomTabsConnection.getInstance().canSessionLaunchInTrustedWebActivity(
session, Uri.parse(mIntent.getDataString()))) {
return false;
}
// TODO(yusufo): WebappInfo houses a lot of logic around preparing/easing out the initial
// launch via extras for icons, splashscreens, screen orientation etc. We need a way to
// plumb that information to Trusted Web Activities.
WebappInfo info = WebappInfo.create(mIntent, session);
if (info == null) return false;
WebappActivity.addWebappInfo(info.id(), info);
Intent launchIntent = WebappLauncherActivity.createWebappLaunchIntent(info, false);
launchIntent.putExtras(mIntent.getExtras());
mActivity.startActivity(launchIntent);
return true;
}
/**
* Handles launching a {@link ChromeTabbedActivity}.
*/
@SuppressLint("InlinedApi")
private @Action int dispatchToTabbedActivity() {
maybePrefetchDnsInBackground();
Intent newIntent = new Intent(mIntent);
Class<?> tabbedActivityClass =
MultiWindowUtils.getInstance().getTabbedActivityForIntent(newIntent, mActivity);
newIntent.setClassName(
mActivity.getApplicationContext().getPackageName(), tabbedActivityClass.getName());
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();
boolean isContentScheme = false;
if (uri != null && UrlConstants.CONTENT_SCHEME.equals(uri.getScheme())) {
isContentScheme = true;
newIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
if (MultiWindowUtils.getInstance().shouldRunInLegacyMultiInstanceMode(mActivity, mIntent)) {
MultiWindowUtils.getInstance().makeLegacyMultiInstanceIntent(mActivity, newIntent);
}
if (newIntent.getComponent().getClassName().equals(mActivity.getClass().getName())) {
// We're trying to start activity that is already running - just continue.
return Action.CONTINUE;
}
// This system call is often modified by OEMs and not actionable. http://crbug.com/619646.
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
Bundle options = mIsVrIntent ? VrIntentUtils.getVrIntentOptions(mActivity) : null;
mActivity.startActivity(newIntent, options);
} catch (SecurityException ex) {
if (isContentScheme) {
Toast.makeText(mActivity,
org.chromium.chrome.R.string.external_app_restricted_access_error,
Toast.LENGTH_LONG)
.show();
} else {
throw ex;
}
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
return Action.FINISH_ACTIVITY;
}
/**
* Records metrics gleaned from the Intent.
*/
private void recordIntentMetrics() {
@IntentHandler.ExternalAppId
int source = IntentHandler.determineExternalIntentSource(mIntent);
if (mIntent.getPackage() == null && source != IntentHandler.ExternalAppId.CHROME) {
int flagsOfInterest = Intent.FLAG_ACTIVITY_NEW_TASK;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
flagsOfInterest |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
}
int maskedFlags = mIntent.getFlags() & flagsOfInterest;
sIntentFlagsHistogram.record(maskedFlags);
}
MediaNotificationUma.recordClickSource(mIntent);
}
}