| // 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; |
| |
| import static org.chromium.webapk.lib.common.WebApkConstants.WEBAPK_PACKAGE_PREFIX; |
| |
| import android.app.Activity; |
| import android.app.KeyguardManager; |
| import android.app.PendingIntent; |
| import android.app.SearchManager; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| import android.provider.Browser; |
| import android.provider.MediaStore; |
| import android.speech.RecognizerResultsIntent; |
| import android.support.annotation.IntDef; |
| import android.support.annotation.Nullable; |
| import android.text.TextUtils; |
| import android.util.Pair; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.FileUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.library_loader.LibraryProcessType; |
| import org.chromium.base.metrics.RecordHistogram; |
| import org.chromium.base.metrics.RecordUserAction; |
| import org.chromium.blink_public.web.WebReferrerPolicy; |
| import org.chromium.chrome.browser.browserservices.BrowserSessionContentUtils; |
| import org.chromium.chrome.browser.document.ChromeLauncherActivity; |
| import org.chromium.chrome.browser.externalauth.ExternalAuthUtils; |
| import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl; |
| import org.chromium.chrome.browser.externalnav.IntentWithGesturesHandler; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageUtils; |
| import org.chromium.chrome.browser.omnibox.AutocompleteController; |
| import org.chromium.chrome.browser.rappor.RapporServiceBridge; |
| import org.chromium.chrome.browser.search_engines.TemplateUrlService; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType; |
| import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; |
| import org.chromium.chrome.browser.util.IntentUtils; |
| import org.chromium.chrome.browser.util.UrlUtilities; |
| import org.chromium.content_public.browser.BrowserStartupController; |
| import org.chromium.content_public.browser.LoadUrlParams; |
| import org.chromium.content_public.common.ContentUrlConstants; |
| import org.chromium.content_public.common.Referrer; |
| import org.chromium.ui.base.PageTransition; |
| import org.chromium.webapk.lib.common.WebApkConstants; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| |
| /** |
| * Handles all browser-related Intents. |
| */ |
| public class IntentHandler { |
| private static final String TAG = "IntentHandler"; |
| |
| /** |
| * Document mode: If true, Chrome is launched into the same Task. |
| * Note: used by first-party applications, do not rename. |
| */ |
| public static final String EXTRA_APPEND_TASK = "com.android.chrome.append_task"; |
| |
| /** |
| * Document mode: If true, keep tasks in Recents when a user hits back at the root URL. |
| * Note: used by first-party applications, do not rename. |
| */ |
| public static final String EXTRA_PRESERVE_TASK = "com.android.chrome.preserve_task"; |
| |
| /** |
| * Document mode: If true, opens the document in background. |
| * Note: used by first-party applications, do not rename. |
| */ |
| public static final String EXTRA_OPEN_IN_BG = "com.android.chrome.open_with_affiliation"; |
| |
| /** |
| * Document mode: Records what caused a document to be created. |
| */ |
| public static final String EXTRA_STARTED_BY = "com.android.chrome.started_by"; |
| |
| /** |
| * Tab ID to use when creating a new Tab. |
| */ |
| public static final String EXTRA_TAB_ID = "com.android.chrome.tab_id"; |
| |
| /** |
| * Parcelable FullscreenOptions to use when creating a new Tab. |
| */ |
| public static final String EXTRA_FULLSCREEN_OPTIONS = "com.android.chrome.fullscreen_options"; |
| |
| /** |
| * The tab id of the parent tab, if any. |
| */ |
| public static final String EXTRA_PARENT_TAB_ID = "com.android.chrome.parent_tab_id"; |
| |
| /** |
| * Intent to bring the parent Activity back, if the parent Tab lives in a different Activity. |
| */ |
| public static final String EXTRA_PARENT_INTENT = "com.android.chrome.parent_intent"; |
| |
| /** |
| * ComponentName of the parent Activity. Can be used by an Activity launched on top of another |
| * Activity (e.g. BookmarkActivity) to intent back into the Activity it sits on top of. |
| */ |
| public static final String EXTRA_PARENT_COMPONENT = |
| "org.chromium.chrome.browser.parent_component"; |
| |
| /** |
| * Transition type is only set internally by a first-party app and has to be signed. |
| */ |
| public static final String EXTRA_PAGE_TRANSITION_TYPE = "com.google.chrome.transition_type"; |
| |
| /** |
| * The original intent of the given intent before it was modified. |
| */ |
| public static final String EXTRA_ORIGINAL_INTENT = "com.android.chrome.original_intent"; |
| |
| /** |
| * An extra to indicate that a particular intent was triggered from the first run experience |
| * flow. |
| */ |
| public static final String EXTRA_INVOKED_FROM_FRE = "com.android.chrome.invoked_from_fre"; |
| |
| /** |
| * An extra to indicate that the intent was triggered from a launcher shortcut. |
| */ |
| public static final String EXTRA_INVOKED_FROM_SHORTCUT = |
| "com.android.chrome.invoked_from_shortcut"; |
| |
| /** |
| * Intent extra used to identify the sending application. |
| */ |
| private static final String TRUSTED_APPLICATION_CODE_EXTRA = "trusted_application_code_extra"; |
| |
| /** |
| * The scheme for referrer coming from an application. |
| */ |
| public static final String ANDROID_APP_REFERRER_SCHEME = "android-app"; |
| |
| /** |
| * A referrer id used for Chrome to Chrome referrer passing. |
| */ |
| public static final String EXTRA_REFERRER_ID = "org.chromium.chrome.browser.referrer_id"; |
| |
| /** |
| * An extra for identifying the referrer policy to be used. |
| * TODO(yusufo): Move this to support library. |
| */ |
| public static final String EXTRA_REFERRER_POLICY = |
| "android.support.browser.extra.referrer_policy"; |
| |
| /** |
| * Key to associate a timestamp with an intent. |
| */ |
| private static final String EXTRA_TIMESTAMP_MS = "org.chromium.chrome.browser.timestamp"; |
| |
| /** |
| * For multi-window, passes the id of the window. |
| */ |
| public static final String EXTRA_WINDOW_ID = "org.chromium.chrome.browser.window_id"; |
| |
| /** |
| * Records package names of other applications in the system that could have handled |
| * this intent. |
| */ |
| public static final String EXTRA_EXTERNAL_NAV_PACKAGES = "org.chromium.chrome.browser.eenp"; |
| |
| /** |
| * Extra to indicate the launch type of the tab to be created. |
| */ |
| private static final String EXTRA_TAB_LAUNCH_TYPE = |
| "org.chromium.chrome.browser.tab_launch_type"; |
| |
| /** |
| * A hash code for the URL to verify intent data hasn't been modified. |
| */ |
| public static final String EXTRA_DATA_HASH_CODE = "org.chromium.chrome.browser.data_hash"; |
| |
| /** |
| * A boolean to indicate whether incognito mode is currently selected. |
| */ |
| public static final String EXTRA_INCOGNITO_MODE = "org.chromium.chrome.browser.incognito_mode"; |
| |
| /** |
| * Fake ComponentName used in constructing TRUSTED_APPLICATION_CODE_EXTRA. |
| */ |
| private static ComponentName sFakeComponentName; |
| |
| private static final Object LOCK = new Object(); |
| |
| private static Pair<Integer, String> sPendingReferrer; |
| private static int sReferrerId; |
| private static String sPendingIncognitoUrl; |
| |
| private static final String PACKAGE_GSA = "com.google.android.googlequicksearchbox"; |
| private static final String PACKAGE_GMAIL = "com.google.android.gm"; |
| private static final String PACKAGE_PLUS = "com.google.android.apps.plus"; |
| private static final String PACKAGE_HANGOUTS = "com.google.android.talk"; |
| private static final String PACKAGE_MESSENGER = "com.google.android.apps.messaging"; |
| private static final String PACKAGE_LINE = "jp.naver.line.android"; |
| private static final String PACKAGE_WHATSAPP = "com.whatsapp"; |
| private static final String PACKAGE_YAHOO_MAIL = "com.yahoo.mobile.client.android.mail"; |
| private static final String PACKAGE_VIBER = "com.viber.voip"; |
| private static final String FACEBOOK_REFERRER_URL = "android-app://m.facebook.com"; |
| private static final String FACEBOOK_INTERNAL_BROWSER_REFERRER = "http://m.facebook.com"; |
| private static final String TWITTER_LINK_PREFIX = "http://t.co/"; |
| private static final String NEWS_LINK_PREFIX = "http://news.google.com/news/url?"; |
| |
| /** |
| * Represents popular external applications that can load a page in Chrome via intent. |
| * DO NOT reorder items in this interface, because it's mirrored to UMA (as ClientAppId). |
| * Values should be enumerated from 0 and can't have gaps. When removing items, |
| * comment them out and keep existing numeric values stable. |
| */ |
| @IntDef({ExternalAppId.OTHER, ExternalAppId.GMAIL, ExternalAppId.FACEBOOK, ExternalAppId.PLUS, |
| ExternalAppId.TWITTER, ExternalAppId.CHROME, ExternalAppId.HANGOUTS, |
| ExternalAppId.MESSENGER, ExternalAppId.NEWS, ExternalAppId.LINE, ExternalAppId.WHATSAPP, |
| ExternalAppId.GSA, ExternalAppId.WEBAPK, ExternalAppId.YAHOO_MAIL, ExternalAppId.VIBER}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface ExternalAppId { |
| int OTHER = 0; |
| int GMAIL = 1; |
| int FACEBOOK = 2; |
| int PLUS = 3; |
| int TWITTER = 4; |
| int CHROME = 5; |
| int HANGOUTS = 6; |
| int MESSENGER = 7; |
| int NEWS = 8; |
| int LINE = 9; |
| int WHATSAPP = 10; |
| int GSA = 11; |
| int WEBAPK = 12; |
| int YAHOO_MAIL = 13; |
| int VIBER = 14; |
| // Update ClientAppId in enums.xml when adding new items. |
| int NUM_ENTRIES = 15; |
| } |
| |
| private static ComponentName getFakeComponentName(String packageName) { |
| synchronized (LOCK) { |
| if (sFakeComponentName == null) { |
| sFakeComponentName = new ComponentName(packageName, "FakeClass"); |
| } |
| } |
| |
| return sFakeComponentName; |
| } |
| |
| /** Intent extra to open an incognito tab. */ |
| public static final String EXTRA_OPEN_NEW_INCOGNITO_TAB = |
| "com.google.android.apps.chrome.EXTRA_OPEN_NEW_INCOGNITO_TAB"; |
| |
| /** Schemes used by web pages to start up Chrome without an explicit Intent. */ |
| public static final String GOOGLECHROME_SCHEME = "googlechrome"; |
| public static final String GOOGLECHROME_NAVIGATE_PREFIX = |
| GOOGLECHROME_SCHEME + "://navigate?url="; |
| |
| private static boolean sTestIntentsEnabled; |
| |
| private final IntentHandlerDelegate mDelegate; |
| private final String mPackageName; |
| |
| /** |
| * Receiver for screen unlock broadcast. |
| */ |
| private DelayedScreenLockIntentHandler mDelayedScreenIntentHandler; |
| |
| @IntDef({TabOpenType.OPEN_NEW_TAB, TabOpenType.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, |
| TabOpenType.REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB, TabOpenType.CLOBBER_CURRENT_TAB, |
| TabOpenType.BRING_TAB_TO_FRONT, TabOpenType.OPEN_NEW_INCOGNITO_TAB}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface TabOpenType { |
| int OPEN_NEW_TAB = 0; |
| // Tab is reused only if the URLs perfectly match. |
| int REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB = 1; |
| // Tab is reused only if there's an existing tab opened by the same app ID. |
| int REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB = 2; |
| int CLOBBER_CURRENT_TAB = 3; |
| int BRING_TAB_TO_FRONT = 4; |
| // Opens a new incognito tab. |
| int OPEN_NEW_INCOGNITO_TAB = 5; |
| |
| String BRING_TAB_TO_FRONT_STRING = "BRING_TAB_TO_FRONT"; |
| } |
| |
| /** |
| * A delegate interface for users of IntentHandler. |
| */ |
| public static interface IntentHandlerDelegate { |
| /** |
| * Processes a URL VIEW Intent. |
| */ |
| void processUrlViewIntent(String url, String referer, String headers, |
| @TabOpenType int tabOpenType, String externalAppId, int tabIdToBringToFront, |
| boolean hasUserGesture, Intent intent); |
| |
| void processWebSearchIntent(String query); |
| } |
| |
| /** Sets whether or not test intents are enabled. */ |
| @VisibleForTesting |
| public static void setTestIntentsEnabled(boolean enabled) { |
| sTestIntentsEnabled = enabled; |
| } |
| |
| public IntentHandler(IntentHandlerDelegate delegate, String packageName) { |
| mDelegate = delegate; |
| mPackageName = packageName; |
| } |
| |
| /** |
| * Determines what App was used to fire this Intent. |
| * @param intent Intent that was used to launch Chrome. |
| * @return ExternalAppId representing the app. |
| */ |
| public static @ExternalAppId int determineExternalIntentSource(Intent intent) { |
| String appId = IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID); |
| @ExternalAppId |
| int externalId = ExternalAppId.OTHER; |
| if (appId == null) { |
| String url = getUrlFromIntent(intent); |
| String referrer = getReferrerUrl(intent); |
| if (url != null && url.startsWith(TWITTER_LINK_PREFIX)) { |
| externalId = ExternalAppId.TWITTER; |
| } else if (FACEBOOK_REFERRER_URL.equals(referrer)) { |
| // This happens when "Links Open Externally" is checked in the Facebook app. |
| externalId = ExternalAppId.FACEBOOK; |
| } else if (url != null && url.startsWith(NEWS_LINK_PREFIX)) { |
| externalId = ExternalAppId.NEWS; |
| } else { |
| Bundle headers = IntentUtils.safeGetBundleExtra(intent, Browser.EXTRA_HEADERS); |
| if (headers != null |
| && FACEBOOK_INTERNAL_BROWSER_REFERRER.equals(headers.get("Referer"))) { |
| // This happens when "Links Open Externally" is unchecked in the Facebook app, |
| // and we use "Open With..." from the internal browser. |
| externalId = ExternalAppId.FACEBOOK; |
| } |
| } |
| } else { |
| externalId = mapPackageToExternalAppId(appId); |
| } |
| return externalId; |
| } |
| |
| /** |
| * Returns the appropriate entry of the ExteranAppId enum based on the supplied package name. |
| * @param packageName String The application package name to map. |
| * @return ExternalAppId representing the app. |
| */ |
| public static @ExternalAppId int mapPackageToExternalAppId(String packageName) { |
| if (packageName.equals(PACKAGE_PLUS)) { |
| return ExternalAppId.PLUS; |
| } else if (packageName.equals(PACKAGE_GMAIL)) { |
| return ExternalAppId.GMAIL; |
| } else if (packageName.equals(PACKAGE_HANGOUTS)) { |
| return ExternalAppId.HANGOUTS; |
| } else if (packageName.equals(PACKAGE_MESSENGER)) { |
| return ExternalAppId.MESSENGER; |
| } else if (packageName.equals(PACKAGE_LINE)) { |
| return ExternalAppId.LINE; |
| } else if (packageName.equals(PACKAGE_WHATSAPP)) { |
| return ExternalAppId.WHATSAPP; |
| } else if (packageName.equals(PACKAGE_GSA)) { |
| return ExternalAppId.GSA; |
| } else if (packageName.equals(ContextUtils.getApplicationContext().getPackageName())) { |
| return ExternalAppId.CHROME; |
| } else if (packageName.startsWith(WEBAPK_PACKAGE_PREFIX)) { |
| return ExternalAppId.WEBAPK; |
| } else if (packageName.equals(PACKAGE_YAHOO_MAIL)) { |
| return ExternalAppId.YAHOO_MAIL; |
| } else if (packageName.equals(PACKAGE_VIBER)) { |
| return ExternalAppId.VIBER; |
| } |
| return ExternalAppId.OTHER; |
| } |
| |
| private void recordExternalIntentSourceUMA(Intent intent) { |
| @ExternalAppId |
| int externalId = determineExternalIntentSource(intent); |
| RecordHistogram.recordEnumeratedHistogram( |
| "MobileIntent.PageLoadDueToExternalApp", externalId, ExternalAppId.NUM_ENTRIES); |
| if (externalId == ExternalAppId.OTHER) { |
| String appId = IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID); |
| if (!TextUtils.isEmpty(appId)) { |
| RapporServiceBridge.sampleString("Android.PageLoadDueToExternalApp", appId); |
| } |
| } |
| } |
| |
| /** |
| * Records an action when a user chose to handle a URL in Chrome that could have been handled |
| * by an application installed on the phone. Also records the name of that application. |
| * This doesn't include generic URL handlers, such as browsers. |
| */ |
| private void recordAppHandlersForIntent(Intent intent) { |
| List<String> packages = IntentUtils.safeGetStringArrayListExtra(intent, |
| IntentHandler.EXTRA_EXTERNAL_NAV_PACKAGES); |
| if (packages != null && packages.size() > 0) { |
| RecordUserAction.record("MobileExternalNavigationReceived"); |
| for (String name : packages) { |
| RapporServiceBridge.sampleString("Android.ExternalNavigationNotChosen", name); |
| } |
| } |
| } |
| |
| private void updateDeferredIntent(Intent intent) { |
| if (mDelayedScreenIntentHandler == null && intent != null) { |
| mDelayedScreenIntentHandler = new DelayedScreenLockIntentHandler(); |
| } |
| |
| if (mDelayedScreenIntentHandler != null) { |
| mDelayedScreenIntentHandler.updateDeferredIntent(intent); |
| } |
| } |
| |
| /** |
| * Handles an Intent after the ChromeTabbedActivity decides that it shouldn't ignore the |
| * Intent. |
| * @param intent Target intent. |
| * @return Whether the Intent was successfully handled. |
| */ |
| boolean onNewIntent(Intent intent) { |
| updateDeferredIntent(null); |
| |
| assert intentHasValidUrl(intent); |
| String url = getUrlFromIntent(intent); |
| boolean hasUserGesture = |
| IntentWithGesturesHandler.getInstance().getUserGestureAndClear(intent); |
| @TabOpenType |
| int tabOpenType = getTabOpenType(intent); |
| int tabIdToBringToFront = IntentUtils.safeGetIntExtra( |
| intent, TabOpenType.BRING_TAB_TO_FRONT_STRING, Tab.INVALID_TAB_ID); |
| if (url == null && tabIdToBringToFront == Tab.INVALID_TAB_ID |
| && tabOpenType != TabOpenType.OPEN_NEW_INCOGNITO_TAB) { |
| return handleWebSearchIntent(intent); |
| } |
| |
| String referrerUrl = getReferrerUrlIncludingExtraHeaders(intent); |
| String extraHeaders = getExtraHeadersFromIntent(intent); |
| |
| if (isIntentForMhtmlFileOrContent(intent) && tabOpenType == TabOpenType.OPEN_NEW_TAB |
| && referrerUrl == null && extraHeaders == null) { |
| handleMhtmlFileOrContentIntent(url, intent); |
| return true; |
| } |
| |
| processUrlViewIntent(url, referrerUrl, extraHeaders, tabOpenType, |
| IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID), |
| tabIdToBringToFront, hasUserGesture, intent); |
| return true; |
| } |
| |
| private void processUrlViewIntent(String url, String referrerUrl, String extraHeaders, |
| @TabOpenType int tabOpenType, String externalAppId, int tabIdToBringToFront, |
| boolean hasUserGesture, Intent intent) { |
| extraHeaders = maybeAddAdditionalExtraHeaders(intent, url, extraHeaders); |
| |
| // TODO(joth): Presumably this should check the action too. |
| mDelegate.processUrlViewIntent(url, referrerUrl, extraHeaders, tabOpenType, |
| IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID), |
| tabIdToBringToFront, hasUserGesture, intent); |
| recordExternalIntentSourceUMA(intent); |
| recordAppHandlersForIntent(intent); |
| } |
| |
| /** |
| * Extracts referrer Uri from intent, if supplied. |
| * @param intent The intent to use. |
| * @return The referrer Uri. |
| */ |
| private static Uri getReferrer(Intent intent) { |
| Uri referrer = IntentUtils.safeGetParcelableExtra(intent, Intent.EXTRA_REFERRER); |
| if (referrer != null) { |
| return referrer; |
| } |
| String referrerName = IntentUtils.safeGetStringExtra(intent, Intent.EXTRA_REFERRER_NAME); |
| if (referrerName != null) { |
| return Uri.parse(referrerName); |
| } |
| return null; |
| } |
| |
| /** |
| * Extracts referrer URL string. The extra is used if we received it from a first party app or |
| * if the referrer_extra is specified as android-app://package style URL. |
| * @param intent The intent from which to extract the URL. |
| * @return The URL string or null if none should be used. |
| */ |
| private static String getReferrerUrl(Intent intent) { |
| Uri referrerExtra = getReferrer(intent); |
| if (referrerExtra == null) return null; |
| String referrerUrl = IntentHandler.getPendingReferrerUrl( |
| IntentUtils.safeGetIntExtra(intent, EXTRA_REFERRER_ID, 0)); |
| if (!TextUtils.isEmpty(referrerUrl)) { |
| return referrerUrl; |
| } else if (isValidReferrerHeader(referrerExtra)) { |
| return referrerExtra.toString(); |
| } else if (IntentHandler.isIntentChromeOrFirstParty(intent) |
| || BrowserSessionContentUtils.canActiveContentHandlerUseReferrer( |
| intent, referrerExtra)) { |
| return referrerExtra.toString(); |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the referrer, looking in the Intent extra and in the extra headers extra. |
| * |
| * The referrer extra takes priority over the "extra headers" one. |
| * |
| * @param intent The Intent containing the extras. |
| * @return The referrer, or null. |
| */ |
| public static String getReferrerUrlIncludingExtraHeaders(Intent intent) { |
| String referrerUrl = getReferrerUrl(intent); |
| if (referrerUrl != null) return referrerUrl; |
| |
| Bundle bundleExtraHeaders = IntentUtils.safeGetBundleExtra(intent, Browser.EXTRA_HEADERS); |
| if (bundleExtraHeaders == null) return null; |
| for (String key : bundleExtraHeaders.keySet()) { |
| String value = bundleExtraHeaders.getString(key); |
| if (value != null && "referer".equals(key.toLowerCase(Locale.US))) { |
| Uri referrer = Uri.parse(value).normalizeScheme(); |
| if (isValidReferrerHeader(referrer)) return referrer.toString(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Add referrer and extra headers to a {@link LoadUrlParams}, if we managed to parse them from |
| * the intent. |
| * @param params The {@link LoadUrlParams} to add referrer and headers. |
| * @param intent The intent we use to parse the extras. |
| */ |
| public static void addReferrerAndHeaders(LoadUrlParams params, Intent intent) { |
| String referrer = getReferrerUrlIncludingExtraHeaders(intent); |
| if (referrer != null) { |
| params.setReferrer(new Referrer(referrer, getReferrerPolicyFromIntent(intent))); |
| } |
| String headers = getExtraHeadersFromIntent(intent); |
| if (headers != null) params.setVerbatimHeaders(headers); |
| } |
| |
| public static @WebReferrerPolicy int getReferrerPolicyFromIntent(Intent intent) { |
| int policy = IntentUtils.safeGetIntExtra( |
| intent, EXTRA_REFERRER_POLICY, WebReferrerPolicy.DEFAULT); |
| if (policy < 0 || policy >= WebReferrerPolicy.LAST) { |
| policy = WebReferrerPolicy.DEFAULT; |
| } |
| return policy; |
| } |
| |
| /** |
| * @return Whether that the given referrer is of the format that Chrome allows external |
| * apps to specify. |
| */ |
| private static boolean isValidReferrerHeader(Uri referrer) { |
| if (referrer == null) return false; |
| Uri normalized = referrer.normalizeScheme(); |
| return TextUtils.equals(normalized.getScheme(), ANDROID_APP_REFERRER_SCHEME) |
| && !TextUtils.isEmpty(normalized.getHost()); |
| } |
| |
| /** |
| * Constructs a valid referrer using the given authority. |
| * @param authority The authority to use. |
| * @return Referrer with default policy that uses the valid android app scheme, or null. |
| */ |
| public static Referrer constructValidReferrerForAuthority(String authority) { |
| if (TextUtils.isEmpty(authority)) return null; |
| return new Referrer(new Uri.Builder() |
| .scheme(ANDROID_APP_REFERRER_SCHEME) |
| .authority(authority) |
| .build() |
| .toString(), |
| WebReferrerPolicy.DEFAULT); |
| } |
| |
| /** |
| * Extracts the URL from voice search result intent. |
| * @return URL if it was found, null otherwise. |
| */ |
| static String getUrlFromVoiceSearchResult(Intent intent) { |
| if (!RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS.equals(intent.getAction())) { |
| return null; |
| } |
| ArrayList<String> results = IntentUtils.safeGetStringArrayListExtra( |
| intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS); |
| |
| // Allow specifying a single voice result via the command line during testing (as the |
| // 'am' command does not allow specifying an array of strings). |
| if (results == null && sTestIntentsEnabled) { |
| String testResult = IntentUtils.safeGetStringExtra( |
| intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS); |
| if (testResult != null) { |
| results = new ArrayList<String>(); |
| results.add(testResult); |
| } |
| } |
| if (results == null || results.size() == 0 |
| || !BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER) |
| .isStartupSuccessfullyCompleted()) { |
| return null; |
| } |
| String query = results.get(0); |
| String url = AutocompleteController.nativeQualifyPartialURLQuery(query); |
| if (url == null) { |
| List<String> urls = IntentUtils.safeGetStringArrayListExtra( |
| intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS); |
| if (urls != null && urls.size() > 0) { |
| url = urls.get(0); |
| } else { |
| url = TemplateUrlService.getInstance().getUrlForVoiceSearchQuery(query); |
| } |
| } |
| return url; |
| } |
| |
| public boolean handleWebSearchIntent(Intent intent) { |
| if (intent == null) return false; |
| |
| String query = null; |
| final String action = intent.getAction(); |
| if (Intent.ACTION_SEARCH.equals(action) |
| || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)) { |
| query = IntentUtils.safeGetStringExtra(intent, SearchManager.QUERY); |
| } |
| |
| if (query == null || TextUtils.isEmpty(query)) return false; |
| |
| mDelegate.processWebSearchIntent(query); |
| return true; |
| } |
| |
| private void handleMhtmlFileOrContentIntent(final String url, final Intent intent) { |
| OfflinePageUtils.getLoadUrlParamsForOpeningMhtmlFileOrContent(url, (loadUrlParams) -> { |
| processUrlViewIntent(loadUrlParams.getUrl(), null, loadUrlParams.getVerbatimHeaders(), |
| TabOpenType.OPEN_NEW_TAB, null, 0, false, intent); |
| }); |
| } |
| |
| private static PendingIntent getAuthenticationToken() { |
| Intent fakeIntent = new Intent(); |
| Context appContext = ContextUtils.getApplicationContext(); |
| fakeIntent.setComponent(getFakeComponentName(appContext.getPackageName())); |
| return PendingIntent.getActivity(appContext, 0, fakeIntent, 0); |
| } |
| |
| /** |
| * Start activity for the given trusted Intent. |
| * |
| * To make sure the intent is not dropped by Chrome, we send along an authentication token to |
| * identify ourselves as a trusted sender. The method {@link #shouldIgnoreIntent} validates the |
| * token. |
| */ |
| public static void startActivityForTrustedIntent(Intent intent) { |
| startActivityForTrustedIntentInternal(intent, null); |
| } |
| |
| /** |
| * Start the activity that handles launching tabs in Chrome given the trusted intent. |
| * |
| * This allows specifying URLs that chrome:// handles internally, but does not expose in |
| * intent-filters for global use. |
| * |
| * To make sure the intent is not dropped by Chrome, we send along an authentication token to |
| * identify ourselves as a trusted sender. The method {@link #shouldIgnoreIntent} validates the |
| * token. |
| */ |
| public static void startChromeLauncherActivityForTrustedIntent(Intent intent) { |
| // Specify the exact component that will handle creating a new tab. This allows specifying |
| // URLs that are not exposed in the intent filters (i.e. chrome://). |
| startActivityForTrustedIntentInternal(intent, ChromeLauncherActivity.class.getName()); |
| } |
| |
| private static void startActivityForTrustedIntentInternal( |
| Intent intent, String componentClassName) { |
| Context appContext = ContextUtils.getApplicationContext(); |
| // The caller might want to re-use the Intent, so we'll use a copy. |
| Intent copiedIntent = new Intent(intent); |
| |
| if (componentClassName != null) { |
| assert copiedIntent.getComponent() == null; |
| // Specify the exact component that will handle creating a new tab. This allows |
| // specifying URLs that are not exposed in the intent filters (i.e. chrome://). |
| copiedIntent.setComponent( |
| new ComponentName(appContext.getPackageName(), componentClassName)); |
| } |
| |
| // Because we are starting this activity from the application context, we need |
| // FLAG_ACTIVITY_NEW_TASK on pre-N versions of Android. On N+ we can get away with |
| // specifying a task ID or not specifying an options bundle. |
| assert (copiedIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0; |
| addTrustedIntentExtras(copiedIntent); |
| appContext.startActivity(copiedIntent); |
| } |
| |
| /** |
| * Sets TRUSTED_APPLICATION_CODE_EXTRA on the provided intent to identify it as coming from |
| * a trusted source. |
| */ |
| public static void addTrustedIntentExtras(Intent intent) { |
| if (ExternalNavigationDelegateImpl.willChromeHandleIntent(intent, true)) { |
| // It is crucial that we never leak the authentication token to other packages, because |
| // then the other package could be used to impersonate us/do things as us. Therefore, |
| // scope the real Intent to our package. |
| intent.setPackage(ContextUtils.getApplicationContext().getPackageName()); |
| // The PendingIntent functions as an authentication token --- it could only have come |
| // from us. Stash it in the real Intent as an extra. shouldIgnoreIntent will retrieve it |
| // and check it with isIntentChromeInternal. |
| intent.putExtra(TRUSTED_APPLICATION_CODE_EXTRA, getAuthenticationToken()); |
| } |
| } |
| |
| /** |
| * Returns a String (or null) containing the extra headers sent by the intent, if any. |
| * |
| * This methods skips the referrer header. |
| * |
| * @param intent The intent containing the bundle extra with the HTTP headers. |
| */ |
| public static String getExtraHeadersFromIntent(Intent intent) { |
| Bundle bundleExtraHeaders = IntentUtils.safeGetBundleExtra(intent, Browser.EXTRA_HEADERS); |
| if (bundleExtraHeaders == null) return null; |
| StringBuilder extraHeaders = new StringBuilder(); |
| Iterator<String> keys = bundleExtraHeaders.keySet().iterator(); |
| while (keys.hasNext()) { |
| String key = keys.next(); |
| String value = bundleExtraHeaders.getString(key); |
| if ("referer".equals(key.toLowerCase(Locale.US))) continue; |
| // Strip the custom header that can only be added by ourselves. |
| if ("x-chrome-intent-type".equals(key.toLowerCase(Locale.US))) continue; |
| if (extraHeaders.length() != 0) extraHeaders.append("\n"); |
| extraHeaders.append(key); |
| extraHeaders.append(": "); |
| extraHeaders.append(value); |
| } |
| return extraHeaders.length() == 0 ? null : extraHeaders.toString(); |
| } |
| |
| /** |
| * Adds a timestamp to an intent, as returned by {@link SystemClock#elapsedRealtime()}. |
| * |
| * To track page load time, this needs to be called as close as possible to |
| * the entry point (in {@link Activity#onCreate()} for instance). |
| */ |
| public static void addTimestampToIntent(Intent intent) { |
| addTimestampToIntent(intent, SystemClock.elapsedRealtime()); |
| } |
| |
| /** |
| * Adds provided timestamp to an intent. |
| * |
| * To track page load time, the value passed in should be as close as possible to |
| * the entry point (in {@link Activity#onCreate()} for instance). |
| */ |
| public static void addTimestampToIntent(Intent intent, long timeStamp) { |
| intent.putExtra(EXTRA_TIMESTAMP_MS, timeStamp); |
| } |
| |
| /** |
| * @return the timestamp associated with an intent, or -1. |
| */ |
| public static long getTimestampFromIntent(Intent intent) { |
| return intent.getLongExtra(EXTRA_TIMESTAMP_MS, -1); |
| } |
| |
| /** |
| * Adds provided WebAPK's shell launch timestamp to an intent. |
| */ |
| public static void addShellLaunchTimestampToIntent(Intent intent, long timestamp) { |
| intent.putExtra(WebApkConstants.EXTRA_WEBAPK_LAUNCH_TIME, timestamp); |
| } |
| |
| /** |
| * @return the WebAPK's shell launch timestamp associated with an intent, or -1. |
| */ |
| public static long getWebApkShellLaunchTimestampFromIntent(Intent intent) { |
| return intent.getLongExtra(WebApkConstants.EXTRA_WEBAPK_LAUNCH_TIME, -1); |
| } |
| |
| /** |
| * Returns true if the app should ignore a given intent. |
| * |
| * @param intent Intent to check. |
| * @return true if the intent should be ignored. |
| */ |
| public boolean shouldIgnoreIntent(Intent intent) { |
| // Although not documented to, many/most methods that retrieve values from an Intent may |
| // throw. Because we can't control what packages might send to us, we should catch any |
| // Throwable and then fail closed (safe). This is ugly, but resolves top crashers in the |
| // wild. |
| try { |
| // Ignore all invalid URLs, regardless of what the intent was. |
| if (!intentHasValidUrl(intent)) { |
| return true; |
| } |
| |
| // Determine if this intent came from a trustworthy source (either Chrome or Google |
| // first party applications). |
| boolean isInternal = isIntentChromeOrFirstParty(intent); |
| boolean isFromChrome = wasIntentSenderChrome(intent); |
| |
| // "Open new incognito tab" is currently limited to Chrome. |
| // |
| // The pending incognito URL check is to handle the case where the user is shown an |
| // Android intent picker while in incognito and they select the current Chrome instance |
| // from the list. In this case, we do not apply our Chrome token as the user has the |
| // option to select apps outside of our control, so we rely on this in memory check |
| // instead. |
| if (!isFromChrome |
| && IntentUtils.safeGetBooleanExtra( |
| intent, EXTRA_OPEN_NEW_INCOGNITO_TAB, false) |
| && (getPendingIncognitoUrl() == null |
| || !getPendingIncognitoUrl().equals(intent.getDataString()))) { |
| return true; |
| } |
| |
| // Now if we have an empty URL and the intent was ACTION_MAIN, |
| // we are pretty sure it is the launcher calling us to show up. |
| // We can safely ignore the screen state. |
| String url = getUrlFromIntent(intent); |
| if (url == null && Intent.ACTION_MAIN.equals(intent.getAction())) { |
| return false; |
| } |
| |
| // Ignore all intents that specify a Chrome internal scheme if they did not come from |
| // a trustworthy source. |
| String scheme = getSanitizedUrlScheme(url); |
| if (!isInternal && scheme != null |
| && (intent.hasCategory(Intent.CATEGORY_BROWSABLE) |
| || intent.hasCategory(Intent.CATEGORY_DEFAULT) |
| || intent.getCategories() == null)) { |
| String lowerCaseScheme = scheme.toLowerCase(Locale.US); |
| if (UrlConstants.CHROME_SCHEME.equals(lowerCaseScheme) |
| || UrlConstants.CHROME_NATIVE_SCHEME.equals(lowerCaseScheme) |
| || ContentUrlConstants.ABOUT_SCHEME.equals(lowerCaseScheme)) { |
| // Allow certain "safe" internal URLs to be launched by external |
| // applications. |
| String lowerCaseUrl = url.toLowerCase(Locale.US); |
| if (ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL.equals(lowerCaseUrl) |
| || ContentUrlConstants.ABOUT_BLANK_URL.equals(lowerCaseUrl)) { |
| return false; |
| } |
| |
| Log.w(TAG, "Ignoring internal Chrome URL from untrustworthy source."); |
| return true; |
| } |
| } |
| |
| // We must check for screen state at this point. |
| // These might be slow. |
| boolean internalOrVisible = isInternal || isIntentUserVisible(); |
| if (!internalOrVisible) { |
| updateDeferredIntent(intent); |
| return true; |
| } |
| return false; |
| } catch (Throwable t) { |
| return true; |
| } |
| } |
| |
| @VisibleForTesting |
| boolean intentHasValidUrl(Intent intent) { |
| String url = extractUrlFromIntent(intent); |
| |
| // Check if this is a valid googlechrome:// URL. |
| if (isGoogleChromeScheme(url)) { |
| url = getUrlFromGoogleChromeSchemeUrl(url); |
| if (url == null) return false; |
| } |
| |
| // Always drop insecure urls. |
| if (url != null && isJavascriptSchemeOrInvalidUrl(url)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Fetch the authentication token (a PendingIntent) created by startActivityForTrustedIntent, |
| * if any. If anything goes wrong trying to retrieve the token (examples include |
| * BadParcelableException or ClassNotFoundException), fail closed. |
| */ |
| private static PendingIntent fetchAuthenticationTokenFromIntent(Intent intent) { |
| return (PendingIntent) IntentUtils.safeGetParcelableExtra( |
| intent, TRUSTED_APPLICATION_CODE_EXTRA); |
| } |
| |
| private static boolean isChromeToken(PendingIntent token) { |
| // Fetch what should be a matching token. |
| PendingIntent pending = getAuthenticationToken(); |
| return pending.equals(token); |
| } |
| |
| /** |
| * @param intent An Intent to be checked. |
| * @return Whether an intent originates from Chrome. |
| */ |
| public static boolean wasIntentSenderChrome(Intent intent) { |
| if (intent == null) return false; |
| |
| PendingIntent token = fetchAuthenticationTokenFromIntent(intent); |
| if (token == null) return false; |
| |
| // Do not ignore a valid URL Intent if the sender is Chrome. (If the PendingIntents are |
| // equal, we know that the sender was us.) |
| return isChromeToken(token); |
| } |
| |
| /** |
| * @param intent An Intent to be checked. |
| * @return Whether an intent originates from Chrome or a first-party app. |
| */ |
| public static boolean isIntentChromeOrFirstParty(Intent intent) { |
| if (intent == null) return false; |
| |
| PendingIntent token = fetchAuthenticationTokenFromIntent(intent); |
| if (token == null) return false; |
| |
| // Do not ignore a valid URL Intent if the sender is Chrome. (If the PendingIntents are |
| // equal, we know that the sender was us.) |
| if (isChromeToken(token)) { |
| return true; |
| } |
| if (ExternalAuthUtils.getInstance().isGoogleSigned( |
| ApiCompatibilityUtils.getCreatorPackage(token))) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * @param intent An Intent to be checked. |
| * @param packageName The app where the intent is expected to originate from |
| * @return Whether the intent originates from the first-party app with the given package name. |
| */ |
| public static boolean isIntentFromTrustedApp(Intent intent, String packageName) { |
| if (intent == null) return false; |
| |
| PendingIntent token = fetchAuthenticationTokenFromIntent(intent); |
| if (token == null) return false; |
| |
| return isIntentChromeOrFirstParty(intent) |
| && ApiCompatibilityUtils.getCreatorPackage(token).equals(packageName); |
| } |
| |
| @VisibleForTesting |
| boolean isIntentUserVisible() { |
| // Only process Intents if the screen is on and the device is unlocked; |
| // i.e. the user will see what is going on. |
| Context appContext = ContextUtils.getApplicationContext(); |
| if (!ApiCompatibilityUtils.isInteractive(appContext)) return false; |
| if (!ApiCompatibilityUtils.isDeviceProvisioned(appContext)) return true; |
| return !((KeyguardManager) appContext.getSystemService(Context.KEYGUARD_SERVICE)) |
| .inKeyguardRestrictedInputMode(); |
| } |
| |
| /* |
| * The default behavior here is to open in a new tab. If this is changed, ensure |
| * intents with action NDEF_DISCOVERED (links beamed over NFC) are handled properly. |
| */ |
| private @TabOpenType int getTabOpenType(Intent intent) { |
| if (IntentUtils.safeGetBooleanExtra( |
| intent, ShortcutHelper.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, false)) { |
| return TabOpenType.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB; |
| } |
| if (IntentUtils.safeGetBooleanExtra(intent, EXTRA_OPEN_NEW_INCOGNITO_TAB, false)) { |
| return TabOpenType.OPEN_NEW_INCOGNITO_TAB; |
| } |
| if (IntentUtils.safeGetIntExtra( |
| intent, TabOpenType.BRING_TAB_TO_FRONT_STRING, Tab.INVALID_TAB_ID) |
| != Tab.INVALID_TAB_ID) { |
| return TabOpenType.BRING_TAB_TO_FRONT; |
| } |
| |
| String appId = IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID); |
| // Due to users complaints, we are NOT reusing tabs for apps that do not specify an appId. |
| if (appId == null |
| || IntentUtils.safeGetBooleanExtra(intent, Browser.EXTRA_CREATE_NEW_TAB, false)) { |
| return TabOpenType.OPEN_NEW_TAB; |
| } |
| |
| // Intents from chrome open in the same tab by default, all others only clobber |
| // tabs created by the same app. |
| return mPackageName.equals(appId) ? TabOpenType.CLOBBER_CURRENT_TAB |
| : TabOpenType.REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB; |
| } |
| |
| private boolean isInvalidScheme(String scheme) { |
| return scheme != null |
| && (scheme.toLowerCase(Locale.US).equals(UrlConstants.JAVASCRIPT_SCHEME) |
| || scheme.toLowerCase(Locale.US).equals(UrlConstants.JAR_SCHEME)); |
| } |
| |
| /** |
| * Parses the scheme out of the URL if possible, trimming and getting rid of unsafe characters. |
| * This is useful for determining if a URL has a sneaky, unsafe scheme, e.g. "java script" or |
| * "j$a$r". See: http://crbug.com/248398 |
| * @return The sanitized URL scheme or null if no scheme is specified. |
| */ |
| private static String getSanitizedUrlScheme(String url) { |
| if (url == null) { |
| return null; |
| } |
| |
| int colonIdx = url.indexOf(":"); |
| if (colonIdx < 0) { |
| // No scheme specified for the url |
| return null; |
| } |
| |
| String scheme = url.substring(0, colonIdx).toLowerCase(Locale.US).trim(); |
| |
| // Check for the presence of and get rid of all non-alphanumeric characters in the scheme, |
| // except dash, plus and period. Those are the only valid scheme chars: |
| // https://tools.ietf.org/html/rfc3986#section-3.1 |
| boolean nonAlphaNum = false; |
| for (char ch : scheme.toCharArray()) { |
| if (!Character.isLetterOrDigit(ch) && ch != '-' && ch != '+' && ch != '.') { |
| nonAlphaNum = true; |
| break; |
| } |
| } |
| |
| if (nonAlphaNum) { |
| scheme = scheme.replaceAll("[^a-z0-9.+-]", ""); |
| } |
| return scheme; |
| } |
| |
| private boolean isJavascriptSchemeOrInvalidUrl(String url) { |
| String urlScheme = getSanitizedUrlScheme(url); |
| return isInvalidScheme(urlScheme); |
| } |
| |
| /** |
| * Retrieve the URL from the Intent, which may be in multiple locations. |
| * If the URL is googlechrome:// scheme, parse the actual navigation URL. |
| * @param intent Intent to examine. |
| * @return URL from the Intent, or null if a valid URL couldn't be found. |
| */ |
| public static String getUrlFromIntent(Intent intent) { |
| String url = extractUrlFromIntent(intent); |
| if (isGoogleChromeScheme(url)) { |
| url = getUrlFromGoogleChromeSchemeUrl(url); |
| } |
| return url; |
| } |
| |
| /** |
| * Helper method to extract the raw URL from the intent, without further processing. |
| * The URL may be in multiple locations. |
| * @param intent Intent to examine. |
| * @return Raw URL from the intent, or null if raw URL could't be found. |
| */ |
| private static String extractUrlFromIntent(Intent intent) { |
| if (intent == null) return null; |
| String url = getUrlFromVoiceSearchResult(intent); |
| if (url == null) url = ActivityDelegate.getInitialUrlForDocument(intent); |
| if (url == null) url = getUrlForCustomTab(intent); |
| if (url == null) url = intent.getDataString(); |
| if (url == null) return null; |
| url = url.trim(); |
| return TextUtils.isEmpty(url) ? null : url; |
| } |
| |
| private static String getUrlForCustomTab(Intent intent) { |
| if (intent == null || intent.getData() == null) return null; |
| Uri data = intent.getData(); |
| return TextUtils.equals(data.getScheme(), UrlConstants.CUSTOM_TAB_SCHEME) |
| ? data.getQuery() : null; |
| } |
| |
| @VisibleForTesting |
| static String maybeAddAdditionalExtraHeaders(Intent intent, String url, String extraHeaders) { |
| // For some apps, ContentResolver.getType(contentUri) returns "application/octet-stream", |
| // instead of the registered MIME type when opening a document from Downloads. To work |
| // around this, we pass the intent type in extra headers such that content request job can |
| // get it. |
| if (intent == null || url == null) return extraHeaders; |
| |
| String scheme = getSanitizedUrlScheme(url); |
| if (!TextUtils.equals(scheme, UrlConstants.CONTENT_SCHEME)) return extraHeaders; |
| |
| String type = intent.getType(); |
| if (type == null || type.isEmpty()) return extraHeaders; |
| |
| // Only override the type for MHTML related types, which some applications get wrong. |
| if (!isMhtmlMimeType(type)) return extraHeaders; |
| |
| String typeHeader = "X-Chrome-intent-type: " + type; |
| return (extraHeaders == null) ? typeHeader : (extraHeaders + "\n" + typeHeader); |
| } |
| |
| /** Return true if the type is one of the Mime types used for MHTML */ |
| static boolean isMhtmlMimeType(String type) { |
| return type.equals("multipart/related") || type.equals("message/rfc822"); |
| } |
| |
| /** |
| * @param intent An Intent to be checked. |
| * @return Whether the intent has an file:// or content:// URL with MHTML MIME type. |
| */ |
| @VisibleForTesting |
| static boolean isIntentForMhtmlFileOrContent(Intent intent) { |
| String url = getUrlFromIntent(intent); |
| if (url == null) return false; |
| String scheme = getSanitizedUrlScheme(url); |
| boolean isContentUriScheme = TextUtils.equals(scheme, UrlConstants.CONTENT_SCHEME); |
| boolean isFileUriScheme = TextUtils.equals(scheme, UrlConstants.FILE_SCHEME); |
| if (!isContentUriScheme && !isFileUriScheme) return false; |
| String type = intent.getType(); |
| if (type != null && isMhtmlMimeType(type)) { |
| return true; |
| } |
| // Note that "application/octet-stream" type may be passed by some apps that do not know |
| // about MHTML file types. |
| if (!isFileUriScheme |
| || (!TextUtils.isEmpty(type) && !type.equals("application/octet-stream"))) { |
| return false; |
| } |
| |
| // Get the file extension. We can't use MimeTypeMap.getFileExtensionFromUrl because it will |
| // reject urls with characters that are valid in filenames (such as "!"). |
| String extension = FileUtils.getExtension(url); |
| |
| return extension.equals("mhtml") || extension.equals("mht"); |
| } |
| |
| /** |
| * Adjusts the URL to account for the googlechrome:// scheme. |
| * Currently, its only use is to handle navigations, only http and https URL is allowed. |
| * @param url URL to be processed |
| * @return The string with the scheme and prefixes chopped off, if a valid prefix was used. |
| * Otherwise returns null. |
| */ |
| public static String getUrlFromGoogleChromeSchemeUrl(String url) { |
| if (url.toLowerCase(Locale.US).startsWith(GOOGLECHROME_NAVIGATE_PREFIX)) { |
| String parsedUrl = url.substring(GOOGLECHROME_NAVIGATE_PREFIX.length()); |
| if (!TextUtils.isEmpty(parsedUrl)) { |
| String scheme = getSanitizedUrlScheme(parsedUrl); |
| if (scheme == null) { |
| // If no scheme, assuming this is an http url. |
| parsedUrl = UrlConstants.HTTP_URL_PREFIX + parsedUrl; |
| } |
| } |
| if (UrlUtilities.isHttpOrHttps(parsedUrl)) return parsedUrl; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * @param url URL to be tested |
| * @return Whether the given URL adheres to the googlechrome:// scheme definition. |
| */ |
| public static boolean isGoogleChromeScheme(String url) { |
| if (url == null) return false; |
| String urlScheme = Uri.parse(url).getScheme(); |
| return urlScheme != null && urlScheme.equals(GOOGLECHROME_SCHEME); |
| } |
| |
| // TODO(mariakhomenko): pending referrer and pending incognito intent could potentially |
| // not work correctly in multi-window. Store per-window information instead. |
| |
| /** |
| * Records a pending referrer URL that we may be sending to ourselves through an intent. |
| * @param intent The intent to which we add a referrer. |
| * @param url The referrer URL. |
| */ |
| public static void setPendingReferrer(Intent intent, String url) { |
| intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(url)); |
| intent.putExtra(IntentHandler.EXTRA_REFERRER_ID, ++sReferrerId); |
| sPendingReferrer = new Pair<Integer, String>(sReferrerId, url); |
| } |
| |
| /** |
| * Clears any pending referrer data. |
| */ |
| public static void clearPendingReferrer() { |
| sPendingReferrer = null; |
| } |
| |
| /** |
| * Retrieves pending referrer URL based on the given id. |
| * @param id The referrer id. |
| * @return The URL for the referrer or null if none found. |
| */ |
| public static String getPendingReferrerUrl(int id) { |
| if (sPendingReferrer != null && (sPendingReferrer.first == id)) { |
| return sPendingReferrer.second; |
| } |
| return null; |
| } |
| |
| /** |
| * Keeps track of pending incognito URL to be loaded and ensures we allow to load it if it |
| * comes back to us. This is a method for dispatching incognito URL intents from Chrome that |
| * may or may not end up in Chrome. |
| * @param intent The intent that will be sent. |
| */ |
| public static void setPendingIncognitoUrl(Intent intent) { |
| if (intent.getData() != null) { |
| intent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, true); |
| sPendingIncognitoUrl = intent.getDataString(); |
| } |
| } |
| |
| /** |
| * Clears the pending incognito URL. |
| */ |
| public static void clearPendingIncognitoUrl() { |
| sPendingIncognitoUrl = null; |
| } |
| |
| /** |
| * @return Pending incognito URL that is allowed to be loaded without system token. |
| */ |
| public static String getPendingIncognitoUrl() { |
| return sPendingIncognitoUrl; |
| } |
| |
| /** |
| * Some applications may request to load the URL with a particular transition type. |
| * @param intent Intent causing the URL load, may be null. |
| * @param defaultTransition The transition to return if none specified in the intent. |
| * @return The transition type to use for loading the URL. |
| */ |
| public static int getTransitionTypeFromIntent(Intent intent, int defaultTransition) { |
| if (intent == null) return defaultTransition; |
| int transitionType = IntentUtils.safeGetIntExtra( |
| intent, IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, PageTransition.LINK); |
| if (transitionType == PageTransition.TYPED) { |
| return transitionType; |
| } else if (transitionType != PageTransition.LINK |
| && isIntentChromeOrFirstParty(intent)) { |
| // 1st party applications may specify any transition type. |
| return transitionType; |
| } |
| return defaultTransition; |
| } |
| |
| /** |
| * Sets the launch type in a tab creation intent. |
| * @param intent The Intent to be set. |
| */ |
| public static void setTabLaunchType(Intent intent, @TabLaunchType int type) { |
| intent.putExtra(EXTRA_TAB_LAUNCH_TYPE, type); |
| } |
| |
| /** |
| * @param intent An Intent to be checked. |
| * @return The launch type of the tab to be created. |
| */ |
| public static @Nullable @TabLaunchType Integer getTabLaunchType(Intent intent) { |
| return IntentUtils.safeGetSerializableExtra(intent, EXTRA_TAB_LAUNCH_TYPE); |
| } |
| } |