chromium/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java

// Copyright 2015 The Chromium Authors
// 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.components.webapk.lib.common.WebApkConstants.WEBAPK_PACKAGE_PREFIX;

import android.app.Activity;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Browser;
import android.speech.RecognizerResultsIntent;
import android.text.TextUtils;
import android.util.Pair;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsSessionToken;

import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.app.tabmodel.AsyncTabParamsManagerSingleton;
import org.chromium.chrome.browser.browserservices.intents.WebappConstants;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.externalnav.IntentWithRequestMetadataHandler;
import org.chromium.chrome.browser.externalnav.IntentWithRequestMetadataHandler.RequestMetadata;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.gsa.GSAUtils;
import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteCoordinator;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.renderer_host.ChromeNavigationUIData;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.webapps.WebappActivity;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.bookmarks.BookmarkType;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.external_intents.ExternalNavigationHandler;
import org.chromium.components.externalauth.ExternalAuthUtils;
import org.chromium.components.omnibox.AutocompleteMatch;
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.content_public.common.ResourceRequestBody;
import org.chromium.net.HttpUtil;
import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.PageTransition;
import org.chromium.url.GURL;
import org.chromium.url.Origin;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/** Handles all browser-related Intents. */
@JNINamespace("chrome::android")
public class IntentHandler {
    private static final String TAG = "IntentHandler";

    /** Tab ID to use when creating a new Tab. */
    private static final String EXTRA_TAB_ID = "com.android.chrome.tab_id";

    /** 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";

    /** Transition bookmark id is only set internally by a first-party app and has to be signed. */
    public static final String EXTRA_PAGE_TRANSITION_BOOKMARK_ID =
            "com.google.chrome.transition_bookmark_id";

    /** 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";

    /** An extra to indicate that the intent was triggered from an app widget. */
    public static final String EXTRA_INVOKED_FROM_APP_WIDGET =
            "com.android.chrome.invoked_from_app_widget";

    /**
     * An extra to indicate that the intent was triggered by the launch new incognito tab feature.
     * See {@link org.chromium.chrome.browser.incognito.IncognitoTabLauncher}.
     */
    public static final String EXTRA_INVOKED_FROM_LAUNCH_NEW_INCOGNITO_TAB =
            "org.chromium.chrome.browser.incognito.invoked_from_launch_new_incognito_tab";

    /** Intent extra used to deliver the original activity referrer. */
    public static final String EXTRA_ACTIVITY_REFERRER =
            "org.chromium.chrome.browser.activity_referrer";

    /** Intent extra used to deliver the package name of original #getCallingActivity if present. */
    public static final String EXTRA_CALLING_ACTIVITY_PACKAGE =
            "org.chromium.chrome.browser.calling_activity_package";

    /** Intent extra used to deliver the package name provided via #getLaunchedFromPackage. */
    public static final String EXTRA_LAUNCHED_FROM_PACKAGE =
            "org.chromium.chrome.browser.launched_from_package";

    /** 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";

    /**
     * Extra specifying additional urls that should each be opened in a new tab. If
     * EXTRA_OPEN_ADDITIONAL_URLS_IN_TAB_GROUP is present and true, these will be opened in a tab
     * group.
     */
    public static final String EXTRA_ADDITIONAL_URLS =
            "org.chromium.chrome.browser.additional_urls";

    /**
     * Extra specifying that additional urls opened should be part of a tab group parented to the
     * root url of the intent. Only valid if EXTRA_ADDITIONAL_URLS is present.
     */
    public static final String EXTRA_OPEN_ADDITIONAL_URLS_IN_TAB_GROUP =
            "org.chromium.chrome.browser.open_additional_urls_in_tab_group";

    /** Extra specifying to show regular overview mode. */
    public static final String EXTRA_OPEN_REGULAR_OVERVIEW_MODE =
            "org.chromium.chrome.browser.open_regular_overview_mode";

    /**
     * For multi-window, passes the id of the window. On Android S, this is synonymous with
     * the id of 'activity instance' among multiple instances that can be chosen on instance
     * switcher UI, ranging from 0 ~ max_instances - 1. -1 for an invalid id.
     */
    public static final String EXTRA_WINDOW_ID = "org.chromium.chrome.browser.window_id";

    /** 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";

    /** Byte array for the POST data when load a url, only Intents sent by Chrome can use this. */
    public static final String EXTRA_POST_DATA = "com.android.chrome.post_data";

    /**
     * The type of the POST data, need to be added to the HTTP request header, only Intents sent by
     * Chrome can use this.
     */
    public static final String EXTRA_POST_DATA_TYPE = "com.android.chrome.post_data_type";

    /**
     * A boolean to indicate whether this Intent originated from the Open In Browser Custom Tab
     * feature.
     */
    public static final String EXTRA_FROM_OPEN_IN_BROWSER =
            "com.android.chrome.from_open_in_browser";

    /**
     * A boolean to indicate that the Intent prefer a fresh new Chrome instance, not with tabs
     * from one of the existing disk files.
     */
    public static final String EXTRA_PREFER_NEW = "com.android.chrome.prefer_new";

    /**
     * Interested entities within Chrome relying on launching Incognito CCT should set this in their
     *{@link CustomTabIntent} in order to identify themselves for metric purposes.
     **/
    public static final String EXTRA_INCOGNITO_CCT_CALLER_ID =
            "org.chromium.chrome.browser.customtabs.EXTRA_INCOGNITO_CCT_CALLER_ID";

    /**
     * A boolean to indicate whether the ChromeTabbedActivity task was started by this Intent. Only
     * used for external View intents.
     */
    public static final String EXTRA_STARTED_TABBED_CHROME_TASK =
            "org.chromium.chrome.browser.started_chrome_task";

    /**
     * An ID of the FedCM invocation associated with this intent. It is used to keep a mapping from
     * IDs to openers, so that a CCT opened as a result of the FedCM API may send notifications to
     * the opener.
     */
    public static final String EXTRA_FEDCM_ID = "org.chromium.chrome.browser.fedcm_id";

    /**
     * A position of the new tab added to the tabs toolbar. Used when a tab is being moved from one
     * instance of the Chrome to another.
     */
    public static final String EXTRA_TAB_INDEX = "com.android.chrome.tab_index";

    /** A boolean to indicate whether an intent was launched via ChromeLauncherActivity. */
    public static final String EXTRA_LAUNCHED_VIA_CHROME_LAUNCHER_ACTIVITY =
            "org.chromium.chrome.browser.launched_via_chrome_launcher_activity";

    /** An enum to indicate whether the intent is created by link or tab. */
    public static final String EXTRA_URL_DRAG_SOURCE =
            "org.chromium.chrome.browser.url_drag_source";

    /** The id of a dragged tab that attempts to launch the intent. */
    public static final String EXTRA_DRAGGED_TAB_ID = "org.chromium.chrome.browser.dragdrop.tab_id";

    /** A boolean to indicate whether the intent should launch the history page in Chrome. */
    public static final String EXTRA_OPEN_HISTORY = "org.chromium.chrome.browser.open_history";

    /** A boolean to indicate whether the intent should launch only app specific history */
    public static final String EXTRA_APP_SPECIFIC_HISTORY =
            "org.chromium.chrome.browser.app_specific_history";

    /** The package name for the Google Search App. */
    public static final String PACKAGE_GSA = GSAUtils.GSA_PACKAGE_NAME;

    private static Pair<Integer, String> sPendingReferrer;
    private static int sReferrerId;
    private static String sPendingIncognitoUrl;

    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_YOUTUBE = "com.google.android.youtube";
    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?";
    private static final String YOUTUBE_LINK_PREFIX_HTTPS = "https://www.youtube.com/redirect?";
    private static final String YOUTUBE_LINK_PREFIX_HTTP = "http://www.youtube.com/redirect?";
    private static final String BRING_TAB_TO_FRONT_EXTRA = "BRING_TAB_TO_FRONT";
    public static final String BRING_TAB_TO_FRONT_SOURCE_EXTRA = "BRING_TAB_TO_FRONT_SOURCE";
    public static final String DAYDREAM_CATEGORY = "com.google.intent.category.DAYDREAM";
    public static final String SHARE_INTENT_HISTOGRAM = "Android.Intent.ShareIntentUrlCount";

    /**
     * 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,
        ExternalAppId.YOUTUBE,
        ExternalAppId.CAMERA,
        ExternalAppId.NUM_ENTRIES
    })
    @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;
        int YOUTUBE = 15;
        int CAMERA = 16;
        // Update ClientAppId in enums.xml when adding new items.
        int NUM_ENTRIES = 17;
    }

    /**
     * Represents apps that launch Incognito CCT. DO NOT reorder items in this interface, because
     * it's mirrored to UMA (as {@link IncognitoCCTCallerId}). Values should be enumerated from 0.
     * When removing items, comment them out and keep existing numeric values stable.
     */
    @IntDef({
        IncognitoCCTCallerId.OTHER_APPS,
        IncognitoCCTCallerId.GOOGLE_APPS,
        IncognitoCCTCallerId.OTHER_CHROME_FEATURES,
        IncognitoCCTCallerId.READER_MODE,
        IncognitoCCTCallerId.READ_LATER,
        IncognitoCCTCallerId.EPHEMERAL_TAB,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface IncognitoCCTCallerId {
        int OTHER_APPS = 0;
        int GOOGLE_APPS = 1;
        // This should not be used, it's a fallback for Chrome features that didn't identify
        // themselves. Please see {@link
        // IncognitoCustomTabIntentDataProvider#addIncognitoExtrasForChromeFeatures}
        int OTHER_CHROME_FEATURES = 2;

        // Chrome Features
        int READER_MODE = 3;
        int READ_LATER = 4;

        // An ephemeral custom tab without incognito branding.
        int EPHEMERAL_TAB = 5;

        // Update {@link IncognitoCCTCallerId} in enums.xml when adding new items.
        int NUM_ENTRIES = 6;
    }

    /** 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";

    /** Intent extra to enable ephemeral browsing within the Custom Tab. */
    public static final String EXTRA_ENABLE_EPHEMERAL_BROWSING =
            "androidx.browser.customtabs.extra.ENABLE_EPHEMERAL_BROWSING";

    /** Scheme used by web pages to start up Chrome without an explicit Intent. */
    public static final String GOOGLECHROME_SCHEME = "googlechrome";

    private static boolean sTestIntentsEnabled;

    @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,
        TabOpenType.REUSE_TAB_MATCHING_ID_ELSE_NEW_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;
        // Tab is reused only if the tab ID exists (tab ID is specified with the integer extra
        // REUSE_TAB_MATCHING_ID_STRING), and if the tab matches either the requested URL, or
        // the URL provided in the REUSE_TAB_ORIGINAL_URL_STRING extra.
        // Otherwise, the URL is opened in a new tab. REUSE_TAB_ORIGINAL_URL_STRING can be used if
        // the intent url is a result of a redirect, so that a tab pointing at the original URL can
        // be reused.
        int REUSE_TAB_MATCHING_ID_ELSE_NEW_TAB = 6;

        String REUSE_TAB_MATCHING_ID_STRING = "REUSE_TAB_MATCHING_ID";
        String REUSE_TAB_ORIGINAL_URL_STRING = "REUSE_TAB_ORIGINAL_URL";
    }

    @IntDef({
        BringToFrontSource.ACTIVATE_TAB,
        BringToFrontSource.NOTIFICATION,
        BringToFrontSource.SEARCH_ACTIVITY
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface BringToFrontSource {
        int INVALID = -1;
        int ACTIVATE_TAB = 0;
        int NOTIFICATION = 1;
        int SEARCH_ACTIVITY = 2;
    }

    /** Sets whether or not test intents are enabled. */
    @VisibleForTesting
    public static void setTestIntentsEnabled(boolean enabled) {
        sTestIntentsEnabled = enabled;
    }

    private IntentHandler() {}

    /**
     * Returns information on the activity referrer. Queries the intent in case that the activity
     * was launched through the Chrome launcher activity, falling back to {@link
     * Activity#getReferrer()}.
     */
    public static String getActivityReferrer(Intent intent, @NonNull Activity activity) {
        String activityReferrer =
                IntentUtils.safeGetStringExtra(intent, IntentHandler.EXTRA_ACTIVITY_REFERRER);
        if (activityReferrer != null) {
            return activityReferrer;
        }
        Uri activityReferrerUri = activity.getReferrer();
        return (activityReferrerUri != null) ? activityReferrerUri.toString() : null;
    }

    /** Determines if Chrome was used to fire this intent. */
    public static boolean isExternalIntentSourceChrome(Intent intent) {
        // Intent should be sufficient for determining whether the intent originated from Chrome.
        return determineExternalIntentSource(intent, /* activity= */ null) == ExternalAppId.CHROME;
    }

    /**
     * Determines what App was used to fire this Intent.
     *
     * @param intent Intent that was used to launch Chrome.
     * @param activity Queried if the app which launched Chrome could not be determined from the
     *     intent.
     * @return ExternalAppId representing the app.
     */
    public static @ExternalAppId int determineExternalIntentSource(
            Intent intent, @Nullable Activity activity) {
        if (wasIntentSenderChrome(intent)) return ExternalAppId.CHROME;

        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 if (url != null
                    && (url.startsWith(YOUTUBE_LINK_PREFIX_HTTPS)
                            || url.startsWith(YOUTUBE_LINK_PREFIX_HTTP))) {
                externalId = ExternalAppId.YOUTUBE;
            } 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);
        }

        if (externalId == ExternalAppId.OTHER && activity != null) {
            String activityReferrer = getActivityReferrer(intent, activity);
            if (activityReferrer != null
                    && activityReferrer.toLowerCase(Locale.getDefault()).endsWith("camera")) {
                return ExternalAppId.CAMERA;
            }
        }

        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_YOUTUBE)) {
            return ExternalAppId.YOUTUBE;
        } 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;
    }

    /**
     * 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) {
            String pendingReferrer =
                    IntentHandler.getPendingReferrerUrl(
                            IntentUtils.safeGetIntExtra(intent, EXTRA_REFERRER_ID, 0));
            return TextUtils.isEmpty(pendingReferrer) ? referrer : Uri.parse(pendingReferrer);
        }
        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);
        CustomTabsSessionToken customTabsSession =
                CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        if (referrerExtra == null && customTabsSession != null) {
            Referrer referrer =
                    CustomTabsConnection.getInstance()
                            .getDefaultReferrerForSession(customTabsSession);
            if (referrer != null) {
                referrerExtra = Uri.parse(referrer.getUrl());
            }
        }

        if (referrerExtra == null) return null;
        if (isValidReferrerHeader(referrerExtra)) {
            return referrerExtra.toString();
        } else if (IntentHandler.notSecureIsIntentChromeOrFirstParty(intent)
                || ChromeApplicationImpl.getComponent()
                        .resolveSessionDataHolder()
                        .canActiveHandlerUseReferrer(customTabsSession, 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 int getReferrerPolicyFromIntent(Intent intent) {
        int policy =
                IntentUtils.safeGetIntExtra(intent, EXTRA_REFERRER_POLICY, ReferrerPolicy.DEFAULT);
        if (policy < ReferrerPolicy.MIN_VALUE || policy >= ReferrerPolicy.MAX_VALUE) {
            policy = ReferrerPolicy.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(), IntentUtils.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(IntentUtils.ANDROID_APP_REFERRER_SCHEME)
                        .authority(authority)
                        .build()
                        .toString(),
                ReferrerPolicy.DEFAULT);
    }

    /**
     * Extracts the URL from voice search result intent.
     *
     * @return URL if it was found, null otherwise.
     */
    // TODO(crbug.com/40549331): Investigate whether this function can return a GURL instead,
    // or split into formatted/unformatted getUrl.
    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);
            }
        }
        // The logic in this method should be moved to ChromeTabbedActivity eventually. We should
        // support async handling of voice search when native finishes initializing.
        if (results == null
                || results.size() == 0
                || !BrowserStartupController.getInstance().isFullBrowserStarted()) {
            return null;
        }
        String query = results.get(0);

        Profile profile = ProfileManager.getLastUsedRegularProfile();
        AutocompleteMatch match = AutocompleteCoordinator.classify(profile, query);

        if (!match.isSearchSuggestion()) return match.getUrl().getSpec();

        List<String> urls =
                IntentUtils.safeGetStringArrayListExtra(
                        intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS);
        if (urls != null && urls.size() > 0) {
            return urls.get(0);
        } else {
            return TemplateUrlServiceFactory.getForProfile(profile)
                    .getUrlForVoiceSearchQuery(query)
                    .getSpec();
        }
    }

    /**
     * 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(null, intent, null);
    }

    /**
     * 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(Context context, Intent intent) {
        startActivityForTrustedIntentInternal(context, 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(null, intent, ChromeLauncherActivity.class.getName());
    }

    private static void startActivityForTrustedIntentInternal(
            Context context, Intent intent, String componentClassName) {
        Context appContext = context == null ? ContextUtils.getApplicationContext() : context;
        // 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;
        IntentUtils.addTrustedIntentExtras(copiedIntent);
        appContext.startActivity(copiedIntent);
    }

    /**
     * Sets the Extra field 'EXTRA_HEADERS' on intent. If |extraHeaders| is empty or null,
     * removes 'EXTRA_HEADERS' from intent.
     *
     * @param extraHeaders   A map containing the set of headers. May be null.
     * @param intent         The intent to modify.
     */
    public static void setIntentExtraHeaders(
            @Nullable Map<String, String> extraHeaders, Intent intent) {
        if (extraHeaders == null || extraHeaders.isEmpty()) {
            intent.removeExtra(Browser.EXTRA_HEADERS);
        } else {
            Bundle bundle = new Bundle();
            for (Map.Entry<String, String> header : extraHeaders.entrySet()) {
                bundle.putString(header.getKey(), header.getValue());
            }
            intent.putExtra(Browser.EXTRA_HEADERS, bundle);
        }
    }

    /**
     * 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();

        boolean fromChrome = IntentHandler.wasIntentSenderChrome(intent);
        boolean shouldAllowNonSafelistedHeaders =
                CustomTabsConnection.getInstance().isFirstPartyOriginForIntent(intent);

        for (String key : bundleExtraHeaders.keySet()) {
            String value = bundleExtraHeaders.getString(key);

            if (!HttpUtil.isAllowedHeader(key, value)) {
                Log.w(TAG, "Ignoring forbidden header " + key + " in EXTRA_HEADERS.");
                continue;
            }

            // Strip the custom header that can only be added by ourselves.
            if ("x-chrome-intent-type".equals(key.toLowerCase(Locale.US))) continue;

            if (!fromChrome) {
                if (key.toLowerCase(Locale.US).startsWith("x-chrome-")) {
                    Log.w(TAG, "Ignoring x-chrome header " + key + " in EXTRA_HEADERS.");
                    continue;
                }

                if (!shouldAllowNonSafelistedHeaders
                        && !IntentHandlerJni.get().isCorsSafelistedHeader(key, value)) {
                    Log.w(TAG, "Ignoring non-CORS-safelisted header " + key + " in EXTRA_HEADERS.");
                    continue;
                }
            }

            if (extraHeaders.length() != 0) extraHeaders.append("\n");
            extraHeaders.append(key);
            extraHeaders.append(": ");
            extraHeaders.append(value);
        }

        return extraHeaders.length() == 0 ? null : extraHeaders.toString();
    }

    /**
     * Returns true if the app should ignore a given intent.
     *
     * @param intent Intent to check.
     * @param context the context to use for running screen-related checks.
     * @return true if the intent should be ignored.
     */
    public static boolean shouldIgnoreIntent(Intent intent, Context context) {
        return shouldIgnoreIntent(intent, context, /* isCustomTab= */ false);
    }

    /**
     * Returns true if the app should ignore a given intent.
     *
     * @param intent Intent to check.
     * @param isCustomTab True if the Intent will end up in a Custom Tab.
     * @return true if the intent should be ignored.
     */
    public static boolean shouldIgnoreIntent(Intent intent, boolean isCustomTab) {
        return shouldIgnoreIntent(intent, /* context= */ null, isCustomTab);
    }

    /**
     * Returns true if the app should ignore a given intent.
     *
     * @param intent Intent to check.
     * @param context the context to use for running screen-related checks.
     * @param isCustomTab True if the Intent will end up in a Custom Tab.
     * @return true if the intent should be ignored.
     */
    public static boolean shouldIgnoreIntent(Intent intent, Context context, boolean isCustomTab) {
        // 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 (Chrome).
            boolean isFromChrome = wasIntentSenderChrome(intent);

            if (IntentUtils.safeGetBooleanExtra(intent, EXTRA_OPEN_NEW_INCOGNITO_TAB, false)
                    && !isAllowedIncognitoIntent(isFromChrome, isCustomTab, intent)) {
                return true;
            }

            // Ignore Daydream intents as these would cause us to re-navigate after the Device ON
            // flow. This can be removed once we migrate to the cardboard library.
            if (intent.hasCategory(DAYDREAM_CATEGORY)) 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;
            }

            if (isFromChrome) return false;

            // Ignore all intents that specify a Chrome internal scheme if they did not come from
            // a trustworthy source.
            String scheme = ExternalNavigationHandler.getSanitizedUrlScheme(url);
            if (intentHasUnsafeInternalScheme(scheme, url, intent)) {
                Log.w(TAG, "Ignoring internal Chrome URL from untrustworthy source.");
                return true;
            }

            // Checking screen on/keyguard last as these calls can be slow.
            // If the screen is off, ignore any intents.
            if (!isScreenOn(context)) return true;
            if (ChromeFeatureList.sBlockIntentsWhileLocked.isEnabled() && isKeyguardLocked()) {
                return true;
            }
            return false;
        } catch (Throwable t) {
            return true;
        }
    }

    private static boolean isAllowedIncognitoIntent(
            boolean isChrome, boolean isCustomTab, Intent intent) {
        // "Open new incognito tab" is currently limited to Chrome for the Chrome app. It can be
        // launched by external apps if it's a Custom Tab, although there are additional checks in
        // IncognitoCustomTabIntentDataProvider#isValidIncognitoIntent.
        if (isChrome || isCustomTab) return true;

        // 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.
        String pendingUrl = getPendingIncognitoUrl();
        return pendingUrl != null && pendingUrl.equals(intent.getDataString());
    }

    private static boolean intentHasUnsafeInternalScheme(String scheme, String url, Intent intent) {
        if (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)
                        || UrlConstants.CHROME_DINO_URL.equals(lowerCaseUrl)) {
                    return false;
                }

                return true;
            }
        }
        return false;
    }

    @VisibleForTesting
    static boolean intentHasValidUrl(Intent intent) {
        String url = extractUrlFromIntent(intent);

        // Check if this is a valid googlechrome:// URL.
        if (isGoogleChromeScheme(url)) {
            url = ExternalNavigationHandler.getUrlFromSelfSchemeUrl(GOOGLECHROME_SCHEME, url);
            if (url == null) return false;
        }

        // Always drop insecure urls.
        if (url != null && isJavascriptSchemeOrInvalidUrl(url)) {
            return false;
        }

        return true;
    }

    /**
     * @param intent An Intent to be checked.
     * @return Whether an intent originates from Chrome.
     */
    public static boolean wasIntentSenderChrome(@Nullable Intent intent) {
        return IntentUtils.isTrustedIntentFromSelf(intent);
    }

    /**
     * Attempts to verify that an Intent was sent from either Chrome or a first-
     * party app by evaluating a PendingIntent token within the passed Intent.
     *
     * This method of verifying first-party apps is not secure, as it is not
     * possible to determine the sender of an Intent. This method only verifies
     * the creator of the PendingIntent token. But a malicious app may be able
     * to obtain a PendingIntent from another application and use it to
     * masquerade as it for the purposes of this check. Do not use this method.
     *
     * @param intent An Intent to be checked.
     * @return Whether an intent originates from Chrome or a first-party app.
     *
     * @deprecated This method is not reliable, see https://crbug.com/832124
     */
    @Deprecated
    public static boolean notSecureIsIntentChromeOrFirstParty(Intent intent) {
        if (intent == null) return false;

        if (IntentUtils.isTrustedIntentFromSelf(intent)) return true;

        // First-party Google apps re-use the secure application code extra for historical reasons.
        PendingIntent token =
                IntentUtils.safeGetParcelableExtra(
                        intent, IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        if (token == null) return false;
        if (ExternalAuthUtils.getInstance().isGoogleSigned(token.getCreatorPackage())) {
            return true;
        }
        return false;
    }

    private static boolean isScreenOn(Context context) {
        if (context == null) {
            context = ContextUtils.getApplicationContext();
        }

        PowerManager powerManager =
                (PowerManager) (context.getSystemService(Context.POWER_SERVICE));

        return powerManager.isInteractive();
    }

    private static boolean isKeyguardLocked() {
        return ((KeyguardManager)
                        ContextUtils.getApplicationContext()
                                .getSystemService(Context.KEYGUARD_SERVICE))
                .isKeyguardLocked();
    }

    /*
     * 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.
     */
    public static @TabOpenType int getTabOpenType(Intent intent) {
        if (IntentUtils.safeGetBooleanExtra(
                intent, WebappConstants.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 (getBringTabToFrontId(intent) != 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;
        }

        int tabId =
                IntentUtils.safeGetIntExtra(
                        intent, TabOpenType.REUSE_TAB_MATCHING_ID_STRING, Tab.INVALID_TAB_ID);
        if (tabId != Tab.INVALID_TAB_ID) {
            return TabOpenType.REUSE_TAB_MATCHING_ID_ELSE_NEW_TAB;
        }

        // Intents from chrome open in the same tab by default, all others only clobber
        // tabs created by the same app.
        return ContextUtils.getApplicationContext().getPackageName().equals(appId)
                ? TabOpenType.CLOBBER_CURRENT_TAB
                : TabOpenType.REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB;
    }

    private static boolean isInvalidScheme(String scheme) {
        return scheme != null
                && (scheme.toLowerCase(Locale.US).equals(UrlConstants.JAVASCRIPT_SCHEME)
                        || scheme.toLowerCase(Locale.US).equals(UrlConstants.JAR_SCHEME));
    }

    private static boolean isJavascriptSchemeOrInvalidUrl(String url) {
        String urlScheme = ExternalNavigationHandler.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 = ExternalNavigationHandler.getUrlFromSelfSchemeUrl(GOOGLECHROME_SCHEME, 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 = getUrlForCustomTab(intent);
        if (url == null) url = getUrlForWebapp(intent);
        if (url == null) url = getUrlFromShareIntent(intent);
        if (url == null) url = intent.getDataString();
        if (url == null) return null;
        url = url.trim();
        return TextUtils.isEmpty(url) ? null : url;
    }

    /**
     * Extracts all Strings terminated by whitespace with the specified prefix.
     *
     * @param text Text to examine.
     * @param prefix The prefix on which to extract Strings.
     * @param results The list to insert results into.
     * @return A possibly empty list of URL Strings.
     */
    private static void extractStringsWithPrefix(String text, String prefix, List<String> results) {
        int i = 0;
        while (i < text.length()) {
            int startIndex = text.indexOf(prefix, i);
            if (startIndex == -1) return;
            for (i = startIndex + prefix.length(); i < text.length(); i++) {
                if (Character.isWhitespace(text.charAt(i))) {
                    results.add(text.substring(startIndex, i));
                    break;
                }
            }
            if (i >= text.length()) results.add(text.substring(startIndex));
        }
    }

    /**
     * Extract a raw URL from the Share intent text.
     *
     * <p>This first try to extract raw http/https URLs. In the case of multiple URLs being present,
     * picks the last one. If no explicit URLs are found, try using autocomplete to resembles the
     * URL. If not, it will construct a search query with the default search engine.
     *
     * @param intent Intent to examine.
     * @return URL from the intent, or null if no URL could be found.
     */
    public static @Nullable String getUrlFromShareIntent(Intent intent) {
        if (!Intent.ACTION_SEND.equals(intent.getAction())
                || !"text/plain".equals(intent.getType())) {
            return null;
        }

        String text = IntentUtils.safeGetStringExtra(intent, Intent.EXTRA_TEXT);
        List<String> urls = new ArrayList<>();
        if (!TextUtils.isEmpty(text)) {
            extractStringsWithPrefix(text, UrlConstants.HTTP_URL_PREFIX, urls);
            extractStringsWithPrefix(text, UrlConstants.HTTPS_URL_PREFIX, urls);
        }

        // Record a small exact linear histogram as we mostly care about 0/1/2, but the presence of
        // larger counts would be interesting.
        RecordHistogram.recordExactLinearHistogram(SHARE_INTENT_HISTOGRAM, urls.size(), 5);

        if (!urls.isEmpty()) {
            // If multiple URLs are present, somewhat arbitrarily pick the last one (preferring
            // https) - share actions seem to usually put the URL at the end.
            return urls.get(urls.size() - 1);
        }

        if (TextUtils.isEmpty(text)
                || !BrowserStartupController.getInstance().isFullBrowserStarted()) {
            return null;
        }

        Profile profile = ProfileManager.getLastUsedRegularProfile();
        AutocompleteMatch match = AutocompleteCoordinator.classify(profile, text);
        if (match != null) return match.getUrl().getSpec();

        return TemplateUrlServiceFactory.getForProfile(profile).getUrlForSearchQuery(text);
    }

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

    private static String getUrlForWebapp(Intent intent) {
        if (intent == null || intent.getData() == null) return null;
        Uri data = intent.getData();
        return TextUtils.equals(data.getScheme(), WebappActivity.WEBAPP_SCHEME)
                ? IntentUtils.safeGetStringExtra(intent, WebappConstants.EXTRA_URL)
                : null;
    }

    public static String maybeAddAdditionalContentHeaders(
            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 = ExternalNavigationHandler.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 = ExternalNavigationHandler.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");
    }

    /**
     * @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, GURL url) {
        intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(url.getSpec()));
        intent.putExtra(IntentHandler.EXTRA_REFERRER_ID, ++sReferrerId);
        sPendingReferrer = new Pair<Integer, String>(sReferrerId, url.getSpec());
    }

    /** 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
                && notSecureIsIntentChromeOrFirstParty(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);
    }

    /**
     * Creates an Intent that will launch a ChromeTabbedActivity on the new tab page. The Intent
     * will be trusted and therefore able to launch Incognito tabs.
     * @param context A {@link Context} to access class and package information.
     * @param incognito Whether the tab should be opened in Incognito.
     * @return The {@link Intent} to launch.
     */
    public static Intent createTrustedOpenNewTabIntent(Context context, boolean incognito) {
        Intent newIntent = new Intent();
        newIntent.setAction(Intent.ACTION_VIEW);
        newIntent.setData(Uri.parse(UrlConstants.NTP_URL));
        newIntent.setClass(context, ChromeLauncherActivity.class);
        newIntent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
        newIntent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
        newIntent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, incognito);
        IntentUtils.addTrustedIntentExtras(newIntent);

        return newIntent;
    }

    /**
     * Creates an Intent that tells Chrome to bring an Activity for a particular Tab back to the
     * foreground.
     * @param tabId The id of the Tab to bring to the foreground.
     * @param bringToFrontSource The source of the bring to front Intent, used for gathering
     *         metrics.
     * @return Created Intent or null if this operation isn't possible.
     */
    public static @Nullable Intent createTrustedBringTabToFrontIntent(
            int tabId, @BringToFrontSource int bringToFrontSource) {
        Context context = ContextUtils.getApplicationContext();
        Intent intent = new Intent(context, ChromeLauncherActivity.class);
        intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
        intent.putExtra(BRING_TAB_TO_FRONT_EXTRA, tabId);
        intent.putExtra(BRING_TAB_TO_FRONT_SOURCE_EXTRA, bringToFrontSource);
        IntentUtils.addTrustedIntentExtras(intent);
        return intent;
    }

    public static int getBringTabToFrontId(Intent intent) {
        if (!wasIntentSenderChrome(intent)) return Tab.INVALID_TAB_ID;
        return IntentUtils.safeGetIntExtra(intent, BRING_TAB_TO_FRONT_EXTRA, Tab.INVALID_TAB_ID);
    }

    /** Sets the Tab Id extra for a given intent. Will only be usable by trusted Chrome intents. */
    public static void setTabId(Intent intent, int tabId) {
        intent.putExtra(IntentHandler.EXTRA_TAB_ID, tabId);
    }

    /**
     * @return the Tab Id extra from an intent, or INVALID_TAB_ID if Tab Id isn't present, or the
     * intent isn't trusted.
     */
    public static int getTabId(@Nullable Intent intent) {
        if (!wasIntentSenderChrome(intent)) return Tab.INVALID_TAB_ID;
        return IntentUtils.safeGetIntExtra(intent, EXTRA_TAB_ID, Tab.INVALID_TAB_ID);
    }

    /**
     * Handles an inconsistency in the Android platform, where if an Activity finishes itself, then
     * is resumed from recents, it's re-launched with the original intent that launched the activity
     * initially.
     *
     * @return the provided intent, if the intent is not from Android Recents. Otherwise, rewrites
     *         the intent to be a consistent MAIN intent from recents.
     */
    public static Intent rewriteFromHistoryIntent(Intent intent) {
        // When a self-finished Activity is created from recents, Android launches it with its
        // original base intent (with FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY added). This can lead
        // to duplicating actions when launched from recents, like re-launching tabs, or firing
        // additional app redirects, etc.
        // Instead of teaching all of Chrome about this, just make intents consistent when Chrome is
        // created from recents.
        if (0 != (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)) {
            Intent newIntent = new Intent(Intent.ACTION_MAIN);
            // Make sure to carry over the FROM_HISTORY flag to avoid confusing metrics.
            newIntent.setFlags(intent.getFlags());
            newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
            newIntent.setComponent(intent.getComponent());
            newIntent.setPackage(intent.getPackage());
            return newIntent;
        }
        return intent;
    }

    /**
     * Bring the browser to foreground and switch to the tab.
     * @param tab Tab to switch to.
     */
    public static void bringTabToFront(Tab tab) {
        Intent newIntent =
                createTrustedBringTabToFrontIntent(tab.getId(), BringToFrontSource.SEARCH_ACTIVITY);
        if (newIntent != null) {
            newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            IntentUtils.safeStartActivity(ContextUtils.getApplicationContext(), newIntent);
        }
    }

    /** Create a LoadUrlParams for handling a VIEW intent. */
    public static LoadUrlParams createLoadUrlParamsForIntent(
            String url, Intent intent, long intentHandlingUptimeMillis) {
        var asyncTabParams =
                AsyncTabParamsManagerSingleton.getInstance()
                        .getAsyncTabParams()
                        .get(getTabId(intent));
        if (asyncTabParams != null && asyncTabParams.getLoadUrlParams() != null) {
            return asyncTabParams.getLoadUrlParams();
        }

        LoadUrlParams loadUrlParams = new LoadUrlParams(url);
        RequestMetadata metadata =
                IntentWithRequestMetadataHandler.getInstance().getRequestMetadataAndClear(intent);

        loadUrlParams.setIntentReceivedTimestamp(intentHandlingUptimeMillis);
        loadUrlParams.setHasUserGesture(metadata == null ? false : metadata.hasUserGesture());
        // Add FROM_API to ensure intent handling isn't used again. Without FROM_API Chrome could
        // get stuck in a loop continually being asked to open a link, and then calling out to the
        // system.
        int transitionType = PageTransition.LINK | PageTransition.FROM_API;
        loadUrlParams.setTransitionType(getTransitionTypeFromIntent(intent, transitionType));
        String referrer = getReferrerUrlIncludingExtraHeaders(intent);
        if (referrer != null) {
            loadUrlParams.setReferrer(
                    new Referrer(referrer, IntentHandler.getReferrerPolicyFromIntent(intent)));
        }

        String headers = getExtraHeadersFromIntent(intent);
        headers = maybeAddAdditionalContentHeaders(intent, url, headers);

        if (IntentHandler.wasIntentSenderChrome(intent)) {
            // Handle post data case.
            String postDataType =
                    IntentUtils.safeGetStringExtra(intent, IntentHandler.EXTRA_POST_DATA_TYPE);
            byte[] postData =
                    IntentUtils.safeGetByteArrayExtra(intent, IntentHandler.EXTRA_POST_DATA);
            if (!TextUtils.isEmpty(postDataType) && postData != null && postData.length != 0) {
                StringBuilder appendToHeader = new StringBuilder();
                appendToHeader.append("Content-Type: ");
                appendToHeader.append(postDataType);
                if (TextUtils.isEmpty(headers)) {
                    headers = appendToHeader.toString();
                } else {
                    headers = headers + "\r\n" + appendToHeader.toString();
                }

                loadUrlParams.setPostData(ResourceRequestBody.createFromBytes(postData));
            }

            // Attach bookmark id to the params if it's present in the intent.
            String bookmarkIdString =
                    IntentUtils.safeGetStringExtra(
                            intent, IntentHandler.EXTRA_PAGE_TRANSITION_BOOKMARK_ID);
            if (!TextUtils.isEmpty(bookmarkIdString)) {
                BookmarkId bookmarkId = BookmarkId.getBookmarkIdFromString(bookmarkIdString);
                ChromeNavigationUIData navData = new ChromeNavigationUIData();
                navData.setBookmarkId(
                        bookmarkId.getType() == BookmarkType.NORMAL ? bookmarkId.getId() : -1);
                loadUrlParams.setNavigationUIDataSupplier(navData::createUnownedNativeCopy);
            }
        } else {
            // Intent is not coming from Chrome, the sender can't be trusted.
            loadUrlParams.setInitiatorOrigin(Origin.createOpaqueOrigin());
        }
        loadUrlParams.setVerbatimHeaders(headers);
        loadUrlParams.setIsRendererInitiated(
                metadata == null ? false : metadata.isRendererInitiated());

        return loadUrlParams;
    }

    /**
     * Whether bundle has any extra that indicates an incognito tab will be launched.
     * @param extras A bundle that carries extras
     * @return True if there is any incognito related extra, otherwise return false.
     */
    public static boolean hasAnyIncognitoExtra(@Nullable Bundle extras) {
        if (extras == null) return false;
        return IntentUtils.safeGetBoolean(extras, EXTRA_INCOGNITO_MODE, false)
                || IntentUtils.safeGetBoolean(extras, EXTRA_OPEN_NEW_INCOGNITO_TAB, false)
                || IntentUtils.safeGetBoolean(
                        extras, EXTRA_INVOKED_FROM_LAUNCH_NEW_INCOGNITO_TAB, false)
                || IntentUtils.safeGetBoolean(extras, EXTRA_ENABLE_EPHEMERAL_BROWSING, false);
    }

    @NativeMethods
    interface Natives {
        boolean isCorsSafelistedHeader(String name, String value);
    }
}