| // 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 org.chromium.base.ApplicationStatus; |
| import org.chromium.base.CommandLine; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.StrictModeContext; |
| import org.chromium.base.library_loader.LibraryProcessType; |
| import org.chromium.base.metrics.CachedMetrics; |
| 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.PaymentHandlerActivity; |
| import org.chromium.chrome.browser.customtabs.SeparateTaskCustomTabActivity; |
| import org.chromium.chrome.browser.firstrun.FirstRunFlowSequencer; |
| import org.chromium.chrome.browser.incognito.IncognitoDisclosureActivity; |
| 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.profiles.Profile; |
| 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.VrModuleProvider; |
| import org.chromium.chrome.browser.webapps.ActivityAssigner; |
| import org.chromium.chrome.browser.webapps.WebappLauncherActivity; |
| import org.chromium.content_public.browser.BrowserStartupController; |
| import org.chromium.ui.widget.Toast; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.ref.WeakReference; |
| 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 = VrModuleProvider.getIntentDelegate().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(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) { |
| 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()); |
| |
| // Ensure the new intent is routed into the instance of CustomTabActivity in this task, if |
| // it exists. If the existing CustomTabActivity can't handle the intent, it will re-launch |
| // the intent without these flags. |
| // If you change this flow, please make sure it works correctly with |
| // - "Don't keep activities", |
| // - Multiple clients hosting CCTs, |
| // - Multiwindow mode. |
| newIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); |
| |
| // Use a custom tab with a unique theme for payment handlers. |
| if (intent.getIntExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE, |
| CustomTabIntentDataProvider.CustomTabsUiType.DEFAULT) |
| == CustomTabIntentDataProvider.CustomTabsUiType.PAYMENT_REQUEST) { |
| newIntent.setClassName(context, PaymentHandlerActivity.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); |
| } |
| |
| // Handle activity started in a new task. |
| // See https://developer.android.com/guide/components/activities/tasks-and-back-stack |
| if ((newIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0 |
| || (newIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0) { |
| // 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. |
| newIntent.setFlags(newIntent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); |
| |
| // Android will try to find and reuse an existing CCT activity in the background. Use |
| // this flag to always start a new one instead. |
| newIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); |
| |
| // Provide the general feeling of supporting multi tasks in Android version that did not |
| // fully support them. Reuse the least recently used SeparateTaskCustomTabActivity |
| // instance. |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { |
| String uuid = UUID.randomUUID().toString(); |
| int activityIndex = ActivityAssigner |
| .instance(ActivityAssigner.ActivityAssignerNamespace |
| .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()); |
| } else { |
| // Force a new document to ensure the proper task/stack creation. |
| newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); |
| } |
| } |
| |
| // If the previous caller was not Chrome, but added EXTRA_IS_OPENED_BY_CHROME or |
| // EXTRA_IS_OPENED_BY_WEBAPK for malicious purpose, remove it. The new intent will be sent |
| // by Chrome, but was not sent by Chrome initially. |
| if (!IntentHandler.wasIntentSenderChrome(intent)) { |
| IntentUtils.safeRemoveExtra( |
| newIntent, CustomTabIntentDataProvider.EXTRA_IS_OPENED_BY_CHROME); |
| IntentUtils.safeRemoveExtra( |
| newIntent, CustomTabIntentDataProvider.EXTRA_IS_OPENED_BY_WEBAPK); |
| } |
| |
| return newIntent; |
| } |
| |
| /** |
| * Handles launching a {@link CustomTabActivity}, which will sit on top of a client's activity |
| * in the same task. |
| */ |
| private void launchCustomTabActivity() { |
| maybePrefetchDnsInBackground(); |
| CustomTabsConnection.getInstance().onHandledIntent( |
| CustomTabsSessionToken.getSessionTokenFromIntent(mIntent), mIntent); |
| |
| // Create and fire a launch intent. |
| Intent launchIntent = createCustomTabActivityIntent(mActivity, mIntent); |
| |
| boolean hasOffTheRecordProfile = |
| BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER) |
| .isStartupSuccessfullyCompleted() |
| && Profile.getLastUsedProfile().hasOffTheRecordProfile(); |
| |
| boolean shouldShowIncognitoDisclosure = |
| CustomTabIntentDataProvider.isValidExternalIncognitoIntent(launchIntent) |
| && hasOffTheRecordProfile; |
| |
| if (shouldShowIncognitoDisclosure) { |
| IncognitoDisclosureActivity.launch(mActivity, launchIntent); |
| return; |
| } |
| |
| // 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); |
| } |
| } |
| |
| /** |
| * Handles launching a {@link ChromeTabbedActivity}. |
| */ |
| @SuppressLint("InlinedApi") |
| private @Action int dispatchToTabbedActivity() { |
| if (mIsVrIntent) { |
| for (WeakReference<Activity> weakActivity : ApplicationStatus.getRunningActivities()) { |
| final Activity activity = weakActivity.get(); |
| if (activity == null) continue; |
| if (activity instanceof ChromeTabbedActivity) { |
| if (VrModuleProvider.getDelegate().willChangeDensityInVr( |
| (ChromeActivity) activity)) { |
| // In the rare case that entering VR will trigger a density change (and |
| // hence an Activity recreation), just return to Daydream home and kill the |
| // process, as there's no good way to recreate without showing 2D UI |
| // in-headset. |
| mActivity.finish(); |
| System.exit(0); |
| } |
| } |
| } |
| } |
| |
| maybePrefetchDnsInBackground(); |
| |
| Intent newIntent = new Intent(mIntent); |
| Class<?> tabbedActivityClass = null; |
| if (CommandLine.getInstance().hasSwitch(ChromeSwitches.NO_TOUCH_MODE)) { |
| // When in No Touch Mode we don't support tabs, and replace the TabbedActivity with the |
| // NoTouchActivity. |
| tabbedActivityClass = NoTouchActivity.class; |
| } else { |
| 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 |
| ? VrModuleProvider.getIntentDelegate().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); |
| } |
| } |