chromium/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/UrlOverridingTest.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.externalnav;

import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

import android.app.Activity;
import android.app.Instrumentation;
import android.app.Instrumentation.ActivityMonitor;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.PatternMatcher;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Pair;
import android.widget.TextView;

import androidx.annotation.IntDef;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.Espresso;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.lifecycle.Stage;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.ApplicationTestUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.PackageManagerWrapper;
import org.chromium.base.test.util.Restriction;
import org.chromium.blink_public.common.BlinkFeatures;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.LaunchIntentDispatcher;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.CustomTabsIntentTestUtils;
import org.chromium.chrome.browser.customtabs.CustomTabsTestUtils;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.init.AsyncInitializationActivity;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.InterceptNavigationDelegateClientImpl;
import org.chromium.chrome.browser.tab.InterceptNavigationDelegateTabHelper;
import org.chromium.chrome.browser.tab.RedirectHandlerTabHelper;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tabmodel.TabModelImpl;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.components.browser_ui.modaldialog.ModalDialogView;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.external_intents.ExternalIntentsFeatures;
import org.chromium.components.external_intents.ExternalNavigationHandler;
import org.chromium.components.external_intents.ExternalNavigationHandler.OverrideUrlLoadingResult;
import org.chromium.components.external_intents.ExternalNavigationHandler.OverrideUrlLoadingResultType;
import org.chromium.components.external_intents.InterceptNavigationDelegateImpl;
import org.chromium.components.external_intents.RedirectHandler;
import org.chromium.components.external_intents.TestChildFrameNavigationObserver;
import org.chromium.components.messages.MessageBannerProperties;
import org.chromium.components.messages.MessageDispatcher;
import org.chromium.components.messages.MessageDispatcherProvider;
import org.chromium.components.messages.MessageIdentifier;
import org.chromium.components.messages.MessageStateHandler;
import org.chromium.components.messages.MessagesTestHelper;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.FencedFrameUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.net.NetError;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.util.TestWebServer;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.mojom.WindowOpenDisposition;
import org.chromium.url.GURL;
import org.chromium.url.Origin;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/** Test suite for verifying the behavior of various URL overriding actions. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class UrlOverridingTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

    @Rule public CustomTabActivityTestRule mCustomTabActivityRule = new CustomTabActivityTestRule();

    private static final String BASE_PATH = "/chrome/test/data/android/url_overriding/";
    private static final String HELLO_PAGE = BASE_PATH + "hello.html";
    private static final String NAVIGATION_FROM_TIMEOUT_PAGE =
            BASE_PATH + "navigation_from_timer.html";
    private static final String NAVIGATION_FROM_TIMEOUT_WITH_FALLBACK_PAGE =
            BASE_PATH + "navigation_from_timer_with_fallback.html";
    private static final String NAVIGATION_FROM_TIMEOUT_PARENT_FRAME_PAGE =
            BASE_PATH + "navigation_from_timer_parent_frame.html";
    private static final String NAVIGATION_FROM_USER_GESTURE_PAGE =
            BASE_PATH + "navigation_from_user_gesture.html";
    private static final String NAVIGATION_FROM_USER_GESTURE_PARENT_FRAME_PAGE =
            BASE_PATH + "navigation_from_user_gesture_parent_frame.html";
    private static final String NAVIGATION_FROM_XHR_CALLBACK_PAGE =
            BASE_PATH + "navigation_from_xhr_callback.html";
    private static final String NAVIGATION_FROM_XHR_CALLBACK_PARENT_FRAME_PAGE =
            BASE_PATH + "navigation_from_xhr_callback_parent_frame.html";
    private static final String NAVIGATION_FROM_XHR_CALLBACK_AND_SHORT_TIMEOUT_PAGE =
            BASE_PATH + "navigation_from_xhr_callback_and_short_timeout.html";
    private static final String NAVIGATION_FROM_XHR_CALLBACK_AND_LOST_ACTIVATION_PAGE =
            BASE_PATH + "navigation_from_xhr_callback_lost_activation.html";
    private static final String NAVIGATION_WITH_FALLBACK_URL_PAGE =
            BASE_PATH + "navigation_with_fallback_url.html";
    private static final String FALLBACK_LANDING_PATH = BASE_PATH + "hello.html";
    private static final String OPEN_WINDOW_FROM_USER_GESTURE_PAGE =
            BASE_PATH + "open_window_from_user_gesture.html";
    private static final String OPEN_WINDOW_FROM_LINK_USER_GESTURE_PAGE =
            BASE_PATH + "open_window_from_link_user_gesture.html";
    private static final String OPEN_WINDOW_FROM_SVG_USER_GESTURE_PAGE =
            BASE_PATH + "open_window_from_svg_user_gesture.html";
    private static final String NAVIGATION_FROM_JAVA_REDIRECTION_PAGE =
            BASE_PATH + "navigation_from_java_redirection.html";
    private static final String NAVIGATION_TO_CCT_FROM_INTENT_URI =
            BASE_PATH + "navigation_to_cct_via_intent_uri.html";
    private static final String FALLBACK_URL =
            "https://play.google.com/store/apps/details?id=com.android.chrome";
    private static final String SUBFRAME_REDIRECT_WITH_PLAY_FALLBACK =
            BASE_PATH + "subframe_navigation_with_play_fallback_parent.html";
    private static final String REDIRECT_TO_OTHER_BROWSER =
            BASE_PATH + "redirect_to_other_browser.html";
    private static final String NAVIGATION_FROM_BFCACHE =
            BASE_PATH + "navigation_from_bfcache-1.html";
    private static final String NAVIGATION_FROM_PRERENDER =
            BASE_PATH + "navigation_from_prerender.html";
    private static final String NAVIGATION_FROM_FENCED_FRAME =
            BASE_PATH + "navigation_from_fenced_frame.html";
    private static final String NAVIGATION_FROM_LONG_TIMEOUT =
            BASE_PATH + "navigation_from_long_timeout.html";
    private static final String NAVIGATION_FROM_PAGE_SHOW =
            BASE_PATH + "navigation_from_page_show.html";
    private static final String SUBFRAME_NAVIGATION_PARENT =
            BASE_PATH + "subframe_navigation_parent.html";
    private static final String SUBFRAME_NAVIGATION_PARENT_SANDBOX =
            BASE_PATH + "subframe_navigation_parent_sandbox.html";
    private static final String SUBFRAME_NAVIGATION_PARENT_CSP_SANDBOX =
            BASE_PATH + "subframe_navigation_parent_csp_sandbox.html";
    private static final String SUBFRAME_NAVIGATION_CHILD =
            BASE_PATH + "subframe_navigation_child.html";
    private static final String NAVIGATION_FROM_RENAVIGATE_FRAME =
            BASE_PATH + "renavigate_frame.html";
    private static final String NAVIGATION_FROM_RENAVIGATE_FRAME_WITH_REDIRECT =
            BASE_PATH + "renavigate_frame_with_redirect.html";
    private static final String NAVIGATION_FROM_WINDOW_REDIRECT =
            BASE_PATH + "navigation_from_window_redirect.html";

    private static final String EXTERNAL_APP_URL =
            "intent://test/#Intent;scheme=externalappscheme;end;";

    private static final String OTHER_BROWSER_PACKAGE = "com.other.browser";
    private static final String TRUSTED_CCT_PACKAGE = "com.trusted.cct";

    private static final String EXTERNAL_APP_SCHEME = "externalappscheme";

    @IntDef({NavigationType.SELF, NavigationType.BLANK, NavigationType.TOP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface NavigationType {
        int SELF = 0;
        int BLANK = 1;
        int TOP = 2;
    }

    @IntDef({SandboxType.NONE, SandboxType.FRAME, SandboxType.CSP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SandboxType {
        int NONE = 0;
        int FRAME = 1;
        int CSP = 2;
    }

    @Mock private RedirectHandler mRedirectHandler;

    @Spy private RedirectHandler mSpyRedirectHandler;

    private static class TestTabObserver extends EmptyTabObserver {
        private final CallbackHelper mFinishCallback;
        private final CallbackHelper mDestroyedCallback;
        private final CallbackHelper mFailCallback;
        private final CallbackHelper mLoadCallback;

        TestTabObserver(
                CallbackHelper finishCallback,
                CallbackHelper destroyedCallback,
                CallbackHelper failCallback,
                CallbackHelper loadCallback) {
            mFinishCallback = finishCallback;
            mDestroyedCallback = destroyedCallback;
            mFailCallback = failCallback;
            mLoadCallback = loadCallback;
        }

        @Override
        public void onPageLoadStarted(Tab tab, GURL url) {
            mLoadCallback.notifyCalled();
        }

        @Override
        public void onPageLoadFinished(Tab tab, GURL url) {
            mFinishCallback.notifyCalled();
        }

        @Override
        public void onPageLoadFailed(Tab tab, @NetError int errorCode) {
            mFailCallback.notifyCalled();
        }

        @Override
        public void onDestroyed(Tab tab) {
            // A new tab is destroyed when loading is overridden while opening it.
            mDestroyedCallback.notifyCalled();
        }
    }

    private static ResolveInfo newResolveInfo(String packageName) {
        ActivityInfo ai = new ActivityInfo();
        ai.packageName = packageName;
        ai.name = "Name: " + packageName;
        ai.applicationInfo = new ApplicationInfo();
        ai.exported = true;
        ResolveInfo ri = new ResolveInfo();
        ri.activityInfo = ai;
        return ri;
    }

    private static class TestContext extends ContextWrapper {
        private boolean mResolveToNonBrowserPackage;
        private boolean mResolveToTrustedCaller;
        private String mNonBrowserPackageName;
        private String mHostToMatch;
        private String mSchemeToMatch;
        private IntentFilter mFilterForHostMatch;
        private IntentFilter mFilterForSchemeMatch;

        public TestContext(Context baseContext, String nonBrowserPackageName) {
            super(baseContext);
            mNonBrowserPackageName = nonBrowserPackageName;
        }

        public void setResolveBrowserIntentToNonBrowserPackage(boolean toNonBrowser) {
            mResolveToNonBrowserPackage = toNonBrowser;
        }

        public void setResolveToTrustedCaller(boolean toTrustedCaller) {
            mResolveToTrustedCaller = toTrustedCaller;
        }

        private boolean targetsPlay(Intent intent) {
            if (intent.getPackage() != null
                    && intent.getPackage().equals(ExternalNavigationHandler.PLAY_APP_PACKAGE)) {
                return true;
            }
            if (intent.getScheme() != null && intent.getScheme().equals("market")) return true;
            return false;
        }

        private void setIntentFilterForHost(String host, IntentFilter filter) {
            mHostToMatch = host;
            mFilterForHostMatch = filter;
        }

        private void setIntentFilterForScheme(String scheme, IntentFilter filter) {
            mSchemeToMatch = scheme;
            mFilterForSchemeMatch = filter;
        }

        @Override
        public PackageManager getPackageManager() {
            return new PackageManagerWrapper(super.getPackageManager()) {
                @Override
                public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
                    if ((intent.getPackage() != null
                                    && intent.getPackage().equals(OTHER_BROWSER_PACKAGE))
                            || intent.filterEquals(PackageManagerUtils.BROWSER_INTENT)) {
                        return Arrays.asList(newResolveInfo(OTHER_BROWSER_PACKAGE));
                    }

                    String targetPackage =
                            mResolveToTrustedCaller ? TRUSTED_CCT_PACKAGE : mNonBrowserPackageName;

                    // Behave as though play store is not installed - this matches bot emulator
                    // images.
                    if (targetsPlay(intent)) return null;

                    if (mHostToMatch != null
                            && intent.getData() != null
                            && intent.getData().getHost().equals(mHostToMatch)) {
                        ResolveInfo info = newResolveInfo(targetPackage);
                        info.filter = mFilterForHostMatch;
                        return Arrays.asList(info);
                    }

                    if (mSchemeToMatch != null
                            && intent.getScheme() != null
                            && intent.getScheme().equals(mSchemeToMatch)) {
                        ResolveInfo info = newResolveInfo(targetPackage);
                        info.filter = mFilterForSchemeMatch;
                        return Arrays.asList(info);
                    }

                    return TestContext.super
                            .getPackageManager()
                            .queryIntentActivities(intent, flags);
                }

                @Override
                public ResolveInfo resolveActivity(Intent intent, int flags) {
                    if (intent.getPackage() != null
                            && intent.getPackage().equals(OTHER_BROWSER_PACKAGE)) {
                        if (mResolveToNonBrowserPackage) {
                            return newResolveInfo(mNonBrowserPackageName);
                        }
                        return newResolveInfo(OTHER_BROWSER_PACKAGE);
                    }

                    String targetPackage =
                            mResolveToTrustedCaller ? TRUSTED_CCT_PACKAGE : mNonBrowserPackageName;

                    if (mSchemeToMatch != null
                            && intent.getScheme() != null
                            && intent.getScheme().equals(mSchemeToMatch)) {
                        ResolveInfo info = newResolveInfo(targetPackage);
                        info.filter = mFilterForSchemeMatch;
                        return info;
                    }

                    // Behave as though play store is not installed - this matches bot emulator
                    // images.
                    if (targetsPlay(intent)) return null;

                    return TestContext.super.getPackageManager().resolveActivity(intent, flags);
                }
            };
        }
    }

    private ActivityMonitor mActivityMonitor;
    private EmbeddedTestServer mTestServer;
    private TestContext mTestContext;
    private String mNonBrowserPackageName;

    @Before
    public void setUp() throws Exception {
        mActivityTestRule.getEmbeddedTestServerRule().setServerUsesHttps(true);
        mNonBrowserPackageName = getNonBrowserPackageName();
        mTestContext =
                new TestContext(ContextUtils.getApplicationContext(), mNonBrowserPackageName);
        ContextUtils.initApplicationContextForTests(mTestContext);
        IntentFilter filter = new IntentFilter(Intent.ACTION_VIEW);
        filter.addCategory(Intent.CATEGORY_BROWSABLE);
        filter.addDataScheme(EXTERNAL_APP_SCHEME);
        mActivityMonitor =
                InstrumentationRegistry.getInstrumentation()
                        .addMonitor(
                                filter,
                                new Instrumentation.ActivityResult(Activity.RESULT_OK, null),
                                true);
        mTestServer = mActivityTestRule.getTestServer();
        mTestContext.setIntentFilterForScheme(EXTERNAL_APP_SCHEME, filter);
        ModalDialogView.disableButtonTapProtectionForTesting();
    }

    private Origin createExampleOrigin() {
        org.chromium.url.internal.mojom.Origin origin =
                new org.chromium.url.internal.mojom.Origin();
        origin.scheme = "https";
        origin.host = "example.com";
        origin.port = 80;
        return new Origin(origin);
    }

    private Intent getCustomTabFromChromeIntent(final String url, final boolean markFromChrome) {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent =
                            LaunchIntentDispatcher.createCustomTabActivityIntent(
                                    ApplicationProvider.getApplicationContext(), intent);
                    IntentUtils.addTrustedIntentExtras(intent);
                    return intent;
                });
    }

    private static class TestParams {
        public final String url;
        public final boolean needClick;
        public final boolean shouldLaunchExternalIntent;
        public boolean createsNewTab;
        public String expectedFinalUrl;
        public boolean shouldFailNavigation = true;
        public String clickTargetId;
        public @PageTransition int transition = PageTransition.LINK;
        public boolean willNavigateTwice;
        public boolean willLoadSubframe;

        public TestParams(String url, boolean needClick, boolean shouldLaunchExternalIntent) {
            this.url = url;
            this.needClick = needClick;
            this.shouldLaunchExternalIntent = shouldLaunchExternalIntent;
            expectedFinalUrl = url;
        }
    }

    private OverrideUrlLoadingResult loadUrlAndWaitForIntentUrl(TestParams params)
            throws Exception {
        final CallbackHelper finishCallback = new CallbackHelper();
        final CallbackHelper failCallback = new CallbackHelper();
        final CallbackHelper destroyedCallback = new CallbackHelper();
        final CallbackHelper newTabCallback = new CallbackHelper();
        final CallbackHelper loadCallback = new CallbackHelper();

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();
        final Tab[] latestTabHolder = new Tab[1];
        final InterceptNavigationDelegateImpl[] latestDelegateHolder =
                new InterceptNavigationDelegateImpl[1];

        AtomicReference<OverrideUrlLoadingResult> lastResultValue = new AtomicReference<>();

        latestTabHolder[0] = tab;
        latestDelegateHolder[0] = getInterceptNavigationDelegate(tab);

        Callback<Pair<GURL, OverrideUrlLoadingResult>> resultCallback =
                (Pair<GURL, OverrideUrlLoadingResult> result) -> {
                    if (result.first.getSpec().equals(params.url)) return;
                    // Ignore the NO_OVERRIDE that comes asynchronously after clobbering the tab.
                    if (lastResultValue.get() != null
                            && lastResultValue.get().getResultType()
                                    == OverrideUrlLoadingResultType.OVERRIDE_WITH_NAVIGATE_TAB
                            && result.second.getResultType()
                                    == OverrideUrlLoadingResultType.NO_OVERRIDE) {
                        return;
                    }
                    lastResultValue.set(result.second);
                };

        latestDelegateHolder[0].setResultCallbackForTesting(resultCallback);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(
                            new TestTabObserver(
                                    finishCallback, destroyedCallback, failCallback, loadCallback));

                    TabModelSelectorObserver selectorObserver =
                            new TabModelSelectorObserver() {
                                @Override
                                public void onNewTabCreated(
                                        Tab newTab, @TabCreationState int creationState) {
                                    Assert.assertTrue(params.createsNewTab);
                                    newTabCallback.notifyCalled();
                                    loadCallback.notifyCalled();
                                    newTab.addObserver(
                                            new TestTabObserver(
                                                    finishCallback,
                                                    destroyedCallback,
                                                    failCallback,
                                                    loadCallback));
                                    latestTabHolder[0] = newTab;
                                    latestDelegateHolder[0].setResultCallbackForTesting(null);
                                    latestDelegateHolder[0] =
                                            getInterceptNavigationDelegate(newTab);
                                    latestDelegateHolder[0].setResultCallbackForTesting(
                                            resultCallback);

                                    TestChildFrameNavigationObserver
                                            .createAndAttachToNativeWebContents(
                                                    newTab.getWebContents(),
                                                    failCallback,
                                                    finishCallback,
                                                    loadCallback);
                                }
                            };
                    mActivityTestRule
                            .getActivity()
                            .getTabModelSelector()
                            .addObserver(selectorObserver);

                    TestChildFrameNavigationObserver.createAndAttachToNativeWebContents(
                            tab.getWebContents(), failCallback, finishCallback, loadCallback);
                });

        LoadUrlParams loadParams = new LoadUrlParams(params.url, params.transition);
        if (params.transition == PageTransition.LINK
                || params.transition == PageTransition.FORM_SUBMIT) {
            loadParams.setIsRendererInitiated(true);
            loadParams.setInitiatorOrigin(createExampleOrigin());
        }
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.loadUrl(loadParams);
                });

        int preClickFinishTarget = params.willLoadSubframe ? 2 : 1;
        if (finishCallback.getCallCount() < preClickFinishTarget) {
            finishCallback.waitForCallback(0, preClickFinishTarget, 20, TimeUnit.SECONDS);
        }
        if (params.needClick) {
            int loadCount = loadCallback.getCallCount();
            doClick(params.clickTargetId, tab);
            try {
                // Some tests have a long delay before starting the load.
                loadCallback.waitForCallback(loadCount, 1, 20, TimeUnit.SECONDS);
            } catch (TimeoutException ex) {
                // Non-subframe clicks shouldn't be flaky.
                if (!params.willLoadSubframe) throw ex;
                // Subframe clicks are flaky so re-try them if nothing started loading.
                doClick(params.clickTargetId, tab);
            }
        }

        if (params.willNavigateTwice && finishCallback.getCallCount() < preClickFinishTarget + 1) {
            finishCallback.waitForCallback(preClickFinishTarget, 1, 20, TimeUnit.SECONDS);
        }

        if (params.createsNewTab) {
            newTabCallback.waitForCallback("New Tab was not created.", 0, 1, 20, TimeUnit.SECONDS);
        }

        if (params.shouldFailNavigation) {
            failCallback.waitForCallback("Navigation didn't fail.", 0, 1, 20, TimeUnit.SECONDS);
        }

        boolean hasFallbackUrl =
                params.expectedFinalUrl != null
                        && !TextUtils.equals(params.url, params.expectedFinalUrl);

        int finalFinishTarget =
                preClickFinishTarget + (params.willNavigateTwice || hasFallbackUrl ? 1 : 0);
        if (hasFallbackUrl && finishCallback.getCallCount() < finalFinishTarget) {
            finishCallback.waitForCallback(
                    "Fallback URL is not loaded", finalFinishTarget - 1, 1, 20, TimeUnit.SECONDS);
        }

        // For sub frames, the |loadFailCallback| run through different threads
        // from the ExternalNavigationHandler. As a result, there is no guarantee
        // when url override result would come.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(lastResultValue.get(), Matchers.notNullValue());
                    // Note that we do not distinguish between OVERRIDE_WITH_NAVIGATE_TAB
                    // and NO_OVERRIDE since tab clobbering will eventually lead to NO_OVERRIDE.
                    // in the tab. Rather, we check the final URL to distinguish between
                    // fallback and normal navigation. See crbug.com/487364 for more.
                    Tab latestTab = latestTabHolder[0];
                    if (params.shouldLaunchExternalIntent) {
                        Criteria.checkThat(
                                lastResultValue.get().getResultType(),
                                Matchers.is(
                                        OverrideUrlLoadingResultType
                                                .OVERRIDE_WITH_EXTERNAL_INTENT));
                    } else {
                        Criteria.checkThat(
                                lastResultValue.get().getResultType(),
                                Matchers.not(
                                        OverrideUrlLoadingResultType
                                                .OVERRIDE_WITH_EXTERNAL_INTENT));
                    }
                    if (params.expectedFinalUrl == null) return;
                    Criteria.checkThat(
                            latestTab.getUrl().getSpec(), Matchers.is(params.expectedFinalUrl));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);

        if (params.createsNewTab && params.shouldLaunchExternalIntent) {
            destroyedCallback.waitForCallback(
                    "Intercepted new tab wasn't destroyed.", 0, 1, 20, TimeUnit.SECONDS);
        }

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mActivityMonitor.getHits(),
                            Matchers.is(params.shouldLaunchExternalIntent ? 1 : 0));
                    Criteria.checkThat(
                            finishCallback.getCallCount(), Matchers.is(finalFinishTarget));
                });
        Assert.assertEquals(params.shouldFailNavigation ? 1 : 0, failCallback.getCallCount());

        return lastResultValue.get();
    }

    private void doClick(String targetId, Tab tab) throws Exception {
        if (targetId == null) {
            TouchCommon.singleClickView(tab.getView());
        } else {
            DOMUtils.clickNode(mActivityTestRule.getWebContents(), targetId);
        }
    }

    private static InterceptNavigationDelegateImpl getInterceptNavigationDelegate(Tab tab) {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> InterceptNavigationDelegateTabHelper.get(tab));
    }

    private PropertyModel getCurrentExternalNavigationMessage() throws Exception {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ChromeActivity activity = mActivityTestRule.getActivity();
                    if (activity == null) activity = mCustomTabActivityRule.getActivity();
                    MessageDispatcher messageDispatcher =
                            MessageDispatcherProvider.from(activity.getWindowAndroid());
                    List<MessageStateHandler> messages =
                            MessagesTestHelper.getEnqueuedMessages(
                                    messageDispatcher, MessageIdentifier.EXTERNAL_NAVIGATION);
                    if (messages.isEmpty()) return null;
                    Assert.assertEquals(1, messages.size());
                    return MessagesTestHelper.getCurrentMessage(messages.get(0));
                });
    }

    private void assertMessagePresent() throws Exception {
        PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
        ApplicationInfo applicationInfo = pm.getApplicationInfo(mNonBrowserPackageName, 0);
        CharSequence label = pm.getApplicationLabel(applicationInfo);

        PropertyModel message = getCurrentExternalNavigationMessage();
        Assert.assertNotNull(message);
        assertThat(
                message.get(MessageBannerProperties.TITLE),
                Matchers.containsString(label.toString()));
        assertThat(
                message.get(MessageBannerProperties.DESCRIPTION).toString(),
                Matchers.containsString(label.toString()));
        Assert.assertNotNull(message.get(MessageBannerProperties.ICON));
    }

    private String getSubframeNavigationUrl(
            String subframeTargetUrl,
            @NavigationType int navigationType,
            @SandboxType int sandboxType) {
        // The replace_text parameters for SUBFRAME_NAVIGATION_CHILD, which is loaded in
        // the iframe in SUBFRAME_NAVIGATION_PARENT, have to go through the
        // embedded test server twice and, as such, have to be base64-encoded twice.
        byte[] paramBase64Name = ApiCompatibilityUtils.getBytesUtf8("PARAM_BASE64_NAME");
        byte[] base64ParamSubframeUrl =
                Base64.encode(
                        ApiCompatibilityUtils.getBytesUtf8("PARAM_SUBFRAME_URL"), Base64.URL_SAFE);
        byte[] paramBase64Value = ApiCompatibilityUtils.getBytesUtf8("PARAM_BASE64_VALUE");
        byte[] base64SubframeUrl =
                Base64.encode(
                        ApiCompatibilityUtils.getBytesUtf8(subframeTargetUrl), Base64.URL_SAFE);

        byte[] paramNavType = ApiCompatibilityUtils.getBytesUtf8("PARAM_BLANK");
        byte[] valBlank = ApiCompatibilityUtils.getBytesUtf8("_blank");
        byte[] valTop = ApiCompatibilityUtils.getBytesUtf8("_top");

        String url = SUBFRAME_NAVIGATION_PARENT;
        if (sandboxType == SandboxType.FRAME) {
            url = SUBFRAME_NAVIGATION_PARENT_SANDBOX;
        } else if (sandboxType == SandboxType.CSP) {
            url = SUBFRAME_NAVIGATION_PARENT_CSP_SANDBOX;
        }

        String navType = "";
        if (navigationType == NavigationType.BLANK) {
            navType = Base64.encodeToString(valBlank, Base64.URL_SAFE);
        } else if (navigationType == NavigationType.TOP) {
            navType = Base64.encodeToString(valTop, Base64.URL_SAFE);
        }

        return mTestServer.getURL(
                url
                        + "?replace_text="
                        + Base64.encodeToString(paramBase64Name, Base64.URL_SAFE)
                        + ":"
                        + Base64.encodeToString(base64ParamSubframeUrl, Base64.URL_SAFE)
                        + "&replace_text="
                        + Base64.encodeToString(paramBase64Value, Base64.URL_SAFE)
                        + ":"
                        + Base64.encodeToString(base64SubframeUrl, Base64.URL_SAFE)
                        + "&replace_text="
                        + Base64.encodeToString(paramNavType, Base64.URL_SAFE)
                        + ":"
                        + navType);
    }

    private String getOpenWindowFromLinkUserGestureUrl(String targetUrl) {
        byte[] param = ApiCompatibilityUtils.getBytesUtf8("PARAM_URL");
        byte[] value = ApiCompatibilityUtils.getBytesUtf8(targetUrl);
        return mTestServer.getURL(OPEN_WINDOW_FROM_LINK_USER_GESTURE_PAGE)
                + "?replace_text="
                + Base64.encodeToString(param, Base64.URL_SAFE)
                + ":"
                + Base64.encodeToString(value, Base64.URL_SAFE);
    }

    private String getNonBrowserPackageName() {
        List<PackageInfo> packages =
                ContextUtils.getApplicationContext().getPackageManager().getInstalledPackages(0);
        if (packages == null || packages.size() == 0) {
            return "";
        }

        return packages.get(0).packageName;
    }

    @Test
    @SmallTest
    public void testNavigationFromTimer() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        loadUrlAndWaitForIntentUrl(
                new TestParams(mTestServer.getURL(NAVIGATION_FROM_TIMEOUT_PAGE), false, false));
    }

    @Test
    @SmallTest
    public void testNavigationFromTimerInSubFrame() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params =
                new TestParams(
                        mTestServer.getURL(NAVIGATION_FROM_TIMEOUT_PARENT_FRAME_PAGE),
                        false,
                        false);
        params.willLoadSubframe = true;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @SmallTest
    public void testNavigationFromUserGesture() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        loadUrlAndWaitForIntentUrl(
                new TestParams(mTestServer.getURL(NAVIGATION_FROM_USER_GESTURE_PAGE), true, true));
    }

    @Test
    @SmallTest
    public void testNavigationFromUserGestureInSubFrame() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params =
                new TestParams(
                        mTestServer.getURL(NAVIGATION_FROM_USER_GESTURE_PARENT_FRAME_PAGE),
                        true,
                        true);
        params.willLoadSubframe = true;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @SmallTest
    public void testNavigationFromXHRCallback() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        loadUrlAndWaitForIntentUrl(
                new TestParams(mTestServer.getURL(NAVIGATION_FROM_XHR_CALLBACK_PAGE), true, true));
    }

    @Test
    @SmallTest
    public void testNavigationFromXHRCallbackInSubFrame() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params =
                new TestParams(
                        mTestServer.getURL(NAVIGATION_FROM_XHR_CALLBACK_PARENT_FRAME_PAGE),
                        true,
                        true);
        params.willLoadSubframe = true;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @SmallTest
    public void testNavigationFromXHRCallbackAndShortTimeout() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        loadUrlAndWaitForIntentUrl(
                new TestParams(
                        mTestServer.getURL(NAVIGATION_FROM_XHR_CALLBACK_AND_SHORT_TIMEOUT_PAGE),
                        true,
                        true));
    }

    @Test
    @SmallTest
    public void testNavigationFromXHRCallbackAndLostActivation() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        loadUrlAndWaitForIntentUrl(
                new TestParams(
                        mTestServer.getURL(NAVIGATION_FROM_XHR_CALLBACK_AND_LOST_ACTIVATION_PAGE),
                        true,
                        true));
    }

    @Test
    @SmallTest
    public void testNavigationFromXHRCallbackAndLostActivationLongTimeout() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();
        ThreadUtils.runOnUiThreadBlocking(
                () -> RedirectHandlerTabHelper.swapHandlerFor(tab, mSpyRedirectHandler));
        // This is a little fragile to code changes, but better than waiting 15 real seconds.
        Mockito.doReturn(SystemClock.elapsedRealtime()) // Initial Navigation create
                .doReturn(SystemClock.elapsedRealtime()) // Initial Navigation shouldOverride
                .doReturn(SystemClock.elapsedRealtime()) // XHR Navigation create
                .doReturn(
                        SystemClock.elapsedRealtime()
                                + RedirectHandler.NAVIGATION_CHAIN_TIMEOUT_MILLIS
                                + 1) // xhr callback
                .when(mSpyRedirectHandler)
                .currentRealtime();

        OverrideUrlLoadingResult result =
                loadUrlAndWaitForIntentUrl(
                        new TestParams(
                                mTestServer.getURL(
                                        NAVIGATION_FROM_XHR_CALLBACK_AND_LOST_ACTIVATION_PAGE),
                                true,
                                false));

        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, result.getResultType());

        assertMessagePresent();
    }

    @Test
    @SmallTest
    public void testNavigationWithFallbackURL() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        String fallbackUrl = mTestServer.getURL(FALLBACK_LANDING_PATH);
        String originalUrl =
                mTestServer.getURL(
                        NAVIGATION_WITH_FALLBACK_URL_PAGE
                                + "?replace_text="
                                + Base64.encodeToString(
                                        ApiCompatibilityUtils.getBytesUtf8("PARAM_FALLBACK_URL"),
                                        Base64.URL_SAFE)
                                + ":"
                                + Base64.encodeToString(
                                        ApiCompatibilityUtils.getBytesUtf8(fallbackUrl),
                                        Base64.URL_SAFE));
        TestParams params = new TestParams(originalUrl, true, false);
        params.expectedFinalUrl = fallbackUrl;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @SmallTest
    public void testNavigationWithFallbackURLInSubFrame() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        String fallbackUrl = mTestServer.getURL(FALLBACK_LANDING_PATH);
        String subframeUrl =
                "intent://test/#Intent;scheme=badscheme;S.browser_fallback_url="
                        + fallbackUrl
                        + ";end";
        String originalUrl =
                getSubframeNavigationUrl(subframeUrl, NavigationType.SELF, SandboxType.NONE);

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final CallbackHelper subframeRedirect = new CallbackHelper();
        EmptyTabObserver observer =
                new EmptyTabObserver() {
                    @Override
                    public void onDidStartNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        Assert.assertEquals(originalUrl, navigation.getUrl().getSpec());
                    }

                    @Override
                    public void onDidRedirectNavigation(Tab tab, NavigationHandle navigation) {
                        Assert.assertFalse(navigation.isInPrimaryMainFrame());
                        Assert.assertEquals(fallbackUrl, navigation.getUrl().getSpec());
                        subframeRedirect.notifyCalled();
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(observer);
                });

        // Fallback URL from a subframe will not trigger main navigation.
        TestParams params = new TestParams(originalUrl, true, false);
        params.willLoadSubframe = true;
        params.shouldFailNavigation = false;
        params.willNavigateTwice = true;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_NAVIGATE_TAB, result.getResultType());
        subframeRedirect.waitForOnly();
    }

    @Test
    @SmallTest
    public void testOpenWindowFromUserGesture() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params =
                new TestParams(mTestServer.getURL(OPEN_WINDOW_FROM_USER_GESTURE_PAGE), true, true);
        params.createsNewTab = true;
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @SmallTest
    public void testOpenWindowFromLinkUserGesture() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params =
                new TestParams(getOpenWindowFromLinkUserGestureUrl(EXTERNAL_APP_URL), true, true);
        params.createsNewTab = true;
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @SmallTest
    public void testOpenWindowFromSvgUserGesture() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params =
                new TestParams(
                        mTestServer.getURL(OPEN_WINDOW_FROM_SVG_USER_GESTURE_PAGE), true, true);
        params.createsNewTab = true;
        params.clickTargetId = "link";
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @SmallTest
    public void testRedirectionFromIntentColdNoTask() throws Exception {
        Context context = ContextUtils.getApplicationContext();
        Intent intent =
                new Intent(
                        Intent.ACTION_VIEW,
                        Uri.parse(mTestServer.getURL(NAVIGATION_FROM_JAVA_REDIRECTION_PAGE)));
        intent.setClassName(context, ChromeLauncherActivity.class.getName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        ChromeTabbedActivity activity =
                ApplicationTestUtils.waitForActivityWithClass(
                        ChromeTabbedActivity.class,
                        Stage.CREATED,
                        () -> context.startActivity(intent));
        mActivityTestRule.setActivity(activity);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
        ApplicationTestUtils.waitForActivityState(activity, Stage.DESTROYED);
    }

    @Test
    @SmallTest
    public void testRedirectionFromIntentColdWithTask() throws Exception {
        // Set up task with finished ChromeActivity.
        Context context = ContextUtils.getApplicationContext();
        mActivityTestRule.startMainActivityOnBlankPage();
        mActivityTestRule.getActivity().finish();
        ApplicationTestUtils.waitForActivityState(mActivityTestRule.getActivity(), Stage.DESTROYED);

        // Fire intent into existing task.
        Intent intent =
                new Intent(
                        Intent.ACTION_VIEW,
                        Uri.parse(mTestServer.getURL(NAVIGATION_FROM_JAVA_REDIRECTION_PAGE)));
        intent.setClassName(context, ChromeLauncherActivity.class.getName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        AsyncInitializationActivity.interceptMoveTaskToBackForTesting();
        ChromeTabbedActivity activity =
                ApplicationTestUtils.waitForActivityWithClass(
                        ChromeTabbedActivity.class,
                        Stage.CREATED,
                        () -> context.startActivity(intent));
        mActivityTestRule.setActivity(activity);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
        CriteriaHelper.pollUiThread(
                () -> AsyncInitializationActivity.wasMoveTaskToBackInterceptedForTesting());
    }

    @Test
    @SmallTest
    public void testRedirectionFromIntentWarm() throws Exception {
        Context context = ContextUtils.getApplicationContext();
        mActivityTestRule.startMainActivityOnBlankPage();

        Intent intent =
                new Intent(
                        Intent.ACTION_VIEW,
                        Uri.parse(mTestServer.getURL(NAVIGATION_FROM_JAVA_REDIRECTION_PAGE)));
        intent.setClassName(context, ChromeLauncherActivity.class.getName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        AsyncInitializationActivity.interceptMoveTaskToBackForTesting();
        context.startActivity(intent);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
        CriteriaHelper.pollUiThread(
                () -> AsyncInitializationActivity.wasMoveTaskToBackInterceptedForTesting());
    }

    @Test
    @LargeTest
    public void testCCTRedirectFromIntentUriStaysInChrome_InIncognito() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        // This will cause getActivityTab() in loadUrlAndWaitForIntentUrl to return an incognito tab
        // instead.
        mActivityTestRule.loadUrlInNewTab(
                "chrome://about/",
                /** incognito* */
                true);

        String fallbackUrl = mTestServer.getURL(FALLBACK_LANDING_PATH);
        String fallbackUrlWithoutScheme = fallbackUrl.replace("https://", "");
        String originalUrl =
                mTestServer.getURL(
                        NAVIGATION_TO_CCT_FROM_INTENT_URI
                                + "?replace_text="
                                + Base64.encodeToString(
                                        ApiCompatibilityUtils.getBytesUtf8("PARAM_FALLBACK_URL"),
                                        Base64.URL_SAFE)
                                + ":"
                                + Base64.encodeToString(
                                        ApiCompatibilityUtils.getBytesUtf8(
                                                fallbackUrlWithoutScheme),
                                        Base64.URL_SAFE));
        TestParams params = new TestParams(originalUrl, true, false);
        params.expectedFinalUrl = fallbackUrl;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @LargeTest
    public void testIntentURIWithFileSchemeDoesNothing() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        String targetUrl =
                "intent:///x.mhtml#Intent;package=org.chromium.chrome.tests;"
                        + "action=android.intent.action.VIEW;scheme=file;end;";
        String url = getOpenWindowFromLinkUserGestureUrl(targetUrl);
        TestParams params = new TestParams(url, true, false);
        params.createsNewTab = true;
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @LargeTest
    @DisabledTest(message = "b/361599939")
    public void testIntentURIWithMixedCaseFileSchemeDoesNothing() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        String targetUrl =
                "intent:///x.mhtml#Intent;package=org.chromium.chrome.tests;"
                        + "action=android.intent.action.VIEW;scheme=FiLe;end;";
        String url = getOpenWindowFromLinkUserGestureUrl(targetUrl);
        TestParams params = new TestParams(url, true, false);
        params.createsNewTab = true;
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @LargeTest
    public void testIntentURIWithNoSchemeDoesNothing() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        String targetUrl =
                "intent:///x.mhtml#Intent;package=org.chromium.chrome.tests;"
                        + "action=android.intent.action.VIEW;end;";
        String url = getOpenWindowFromLinkUserGestureUrl(targetUrl);
        TestParams params = new TestParams(url, true, false);
        params.createsNewTab = true;
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @LargeTest
    @DisabledTest(message = "b/361599939")
    public void testIntentURIWithEmptySchemeDoesNothing() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        String targetUrl =
                "intent:///x.mhtml#Intent;package=org.chromium.chrome.tests;"
                        + "action=android.intent.action.VIEW;scheme=;end;";
        String url = getOpenWindowFromLinkUserGestureUrl(targetUrl);
        TestParams params = new TestParams(url, true, false);
        params.createsNewTab = true;
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @LargeTest
    public void testSubframeLoadCannotLaunchPlayApp() throws Exception {
        String fallbackUrl = "https://play.google.com/store/apps/details?id=com.android.chrome";
        String mainUrl = mTestServer.getURL(SUBFRAME_REDIRECT_WITH_PLAY_FALLBACK);
        String redirectUrl = mTestServer.getURL(HELLO_PAGE);
        mActivityTestRule.startMainActivityOnBlankPage();

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final CallbackHelper subframeExternalProtocol = new CallbackHelper();
        final CallbackHelper subframeRedirect = new CallbackHelper();
        EmptyTabObserver observer =
                new EmptyTabObserver() {
                    @Override
                    public void onDidStartNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        Assert.assertEquals(mainUrl, navigation.getUrl().getSpec());
                    }

                    @Override
                    public void onDidRedirectNavigation(Tab tab, NavigationHandle navigation) {
                        Assert.assertFalse(navigation.isInPrimaryMainFrame());
                        Assert.assertEquals(redirectUrl, navigation.getUrl().getSpec());
                        subframeRedirect.notifyCalled();
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(observer);

                    InterceptNavigationDelegateClientImpl client =
                            InterceptNavigationDelegateClientImpl.createForTesting(tab);
                    InterceptNavigationDelegateImpl delegate =
                            new InterceptNavigationDelegateImpl(client) {
                                @Override
                                public GURL handleSubframeExternalProtocol(
                                        GURL escapedUrl,
                                        @PageTransition int transition,
                                        boolean hasUserGesture,
                                        Origin initiatorOrigin) {
                                    GURL target =
                                            super.handleSubframeExternalProtocol(
                                                    escapedUrl,
                                                    transition,
                                                    hasUserGesture,
                                                    initiatorOrigin);
                                    Assert.assertEquals(fallbackUrl, target.getSpec());
                                    subframeExternalProtocol.notifyCalled();
                                    // We can't actually load the play store URL in tests.
                                    return new GURL(redirectUrl);
                                }
                            };
                    client.initializeWithDelegate(delegate);
                    delegate.setExternalNavigationHandler(
                            new ExternalNavigationHandler(new ExternalNavigationDelegateImpl(tab)));
                    delegate.associateWithWebContents(tab.getWebContents());
                    InterceptNavigationDelegateTabHelper.setDelegateForTesting(tab, delegate);
                });

        TestParams params = new TestParams(mainUrl, false, false);
        params.willNavigateTwice = true;
        params.willLoadSubframe = true;
        params.shouldFailNavigation = false;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_NAVIGATE_TAB, result.getResultType());
        subframeExternalProtocol.waitForOnly();
        subframeRedirect.waitForOnly();
    }

    private void runRedirectToOtherBrowserTest(Instrumentation.ActivityResult chooserResult) {
        Context context = ContextUtils.getApplicationContext();
        String targetUrl = getRedirectToOtherBrowserUrl();
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(targetUrl));
        intent.setClassName(context, ChromeLauncherActivity.class.getName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        IntentFilter filter = new IntentFilter(Intent.ACTION_PICK_ACTIVITY);
        Instrumentation.ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation()
                        .addMonitor(filter, chooserResult, true);

        ChromeTabbedActivity activity =
                ApplicationTestUtils.waitForActivityWithClass(
                        ChromeTabbedActivity.class,
                        Stage.CREATED,
                        () -> context.startActivity(intent));
        mActivityTestRule.setActivity(activity);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(monitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
        InstrumentationRegistry.getInstrumentation().removeMonitor(monitor);
    }

    private String getRedirectToOtherBrowserUrl() {
        // Strip off the "https:" for intent scheme formatting.
        String redirectUrl = mTestServer.getURL(HELLO_PAGE).substring(6);
        byte[] param = ApiCompatibilityUtils.getBytesUtf8("PARAM_URL");
        byte[] value = ApiCompatibilityUtils.getBytesUtf8(redirectUrl);
        return mTestServer.getURL(REDIRECT_TO_OTHER_BROWSER)
                + "?replace_text="
                + Base64.encodeToString(param, Base64.URL_SAFE)
                + ":"
                + Base64.encodeToString(value, Base64.URL_SAFE);
    }

    private IntentFilter createHelloIntentFilter() {
        IntentFilter filter = new IntentFilter(Intent.ACTION_VIEW);
        filter.addDataScheme(UrlConstants.HTTPS_SCHEME);
        filter.addCategory(Intent.CATEGORY_BROWSABLE);
        filter.addDataAuthority("*", null);
        filter.addDataPath(HELLO_PAGE, PatternMatcher.PATTERN_LITERAL);
        return filter;
    }

    @Test
    @LargeTest
    public void testRedirectToOtherBrowser_ChooseSelf() throws Exception {
        mTestContext.setResolveBrowserIntentToNonBrowserPackage(false);
        Intent result = new Intent(Intent.ACTION_CREATE_SHORTCUT);

        runRedirectToOtherBrowserTest(
                new Instrumentation.ActivityResult(Activity.RESULT_OK, result));

        // Wait for the target (data) URL to load in the tab.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mActivityTestRule.getActivity().getActivityTab().getUrl().getSpec(),
                            Matchers.is(mTestServer.getURL(HELLO_PAGE)));
                });
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(
                            RedirectHandlerTabHelper.getOrCreateHandlerFor(
                                            mActivityTestRule.getActivity().getActivityTab())
                                    .shouldNotOverrideUrlLoading());
                });
    }

    @Test
    @LargeTest
    public void testRedirectToOtherBrowser_ChooseOther() throws Exception {
        mTestContext.setResolveBrowserIntentToNonBrowserPackage(false);
        IntentFilter filter = createHelloIntentFilter();
        Instrumentation.ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation().addMonitor(filter, null, true);

        Intent result = new Intent(Intent.ACTION_VIEW);
        result.setComponent(new ComponentName(OTHER_BROWSER_PACKAGE, "activity"));

        runRedirectToOtherBrowserTest(
                new Instrumentation.ActivityResult(Activity.RESULT_OK, result));

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(monitor.getHits(), Matchers.is(1));
                });

        InstrumentationRegistry.getInstrumentation().removeMonitor(monitor);
    }

    @Test
    @LargeTest
    public void testRedirectToOtherBrowser_DefaultNonBrowserPackage() throws Exception {
        mTestContext.setResolveBrowserIntentToNonBrowserPackage(true);
        IntentFilter filter = createHelloIntentFilter();
        Instrumentation.ActivityMonitor viewMonitor =
                InstrumentationRegistry.getInstrumentation().addMonitor(filter, null, true);

        Context context = ContextUtils.getApplicationContext();
        String targetUrl = getRedirectToOtherBrowserUrl();
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(targetUrl));
        intent.setClassName(context, ChromeLauncherActivity.class.getName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        IntentFilter filter2 = new IntentFilter(Intent.ACTION_PICK_ACTIVITY);
        Instrumentation.ActivityMonitor pickActivityMonitor =
                InstrumentationRegistry.getInstrumentation().addMonitor(filter2, null, true);

        ChromeTabbedActivity activity =
                ApplicationTestUtils.waitForActivityWithClass(
                        ChromeTabbedActivity.class,
                        Stage.CREATED,
                        () -> context.startActivity(intent));
        mActivityTestRule.setActivity(activity);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(viewMonitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
        Assert.assertEquals(0, pickActivityMonitor.getHits());
        InstrumentationRegistry.getInstrumentation().removeMonitor(pickActivityMonitor);
        InstrumentationRegistry.getInstrumentation().removeMonitor(viewMonitor);
    }

    @Test
    @LargeTest
    @EnableFeatures({"BackForwardCache<Study", "BackForwardCacheNoTimeEviction"})
    @DisableFeatures({"BackForwardCacheMemoryControls"})
    @CommandLineFlags.Add({"force-fieldtrials=Study/Group"})
    @Restriction(Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testNoRedirectWithBFCache() throws Exception {
        final CallbackHelper finishCallback = new CallbackHelper();
        final CallbackHelper syncHelper = new CallbackHelper();
        AtomicReference<NavigationHandle> mLastNavigationHandle = new AtomicReference<>(null);
        EmptyTabObserver observer =
                new EmptyTabObserver() {
                    @Override
                    public void onDidFinishNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        int callCount = syncHelper.getCallCount();
                        mLastNavigationHandle.set(navigation);
                        finishCallback.notifyCalled();
                        try {
                            syncHelper.waitForCallback(callCount);
                        } catch (Exception e) {
                        }
                    }
                };
        String url = mTestServer.getURL(NAVIGATION_FROM_BFCACHE);
        mActivityTestRule.startMainActivityWithURL(url);

        // This test uses the back/forward cache, so return early if it's not enabled.
        if (!ChromeFeatureList.isEnabled(ChromeFeatureList.BACK_FORWARD_CACHE)) return;

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final RedirectHandler spyHandler =
                Mockito.spy(
                        ThreadUtils.runOnUiThreadBlocking(
                                () -> RedirectHandlerTabHelper.getHandlerFor(tab)));

        InterceptNavigationDelegateImpl delegate = getInterceptNavigationDelegate(tab);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(observer);
                    RedirectHandlerTabHelper.swapHandlerFor(tab, spyHandler);
                });

        // Click link to go to second page.
        TouchCommon.singleClickView(tab.getView());
        finishCallback.waitForCallback(0);
        syncHelper.notifyCalled();

        AtomicInteger lastResultValue = new AtomicInteger();
        delegate.setResultCallbackForTesting(
                (Pair<GURL, OverrideUrlLoadingResult> result) -> {
                    if (result.first.getSpec().equals(url)) return;
                    lastResultValue.set(result.second.getResultType());
                });

        // Press back to go back to first page with BFCache.
        ThreadUtils.runOnUiThreadBlocking(() -> mActivityTestRule.getActivity().onBackPressed());
        finishCallback.waitForCallback(1);
        Assert.assertTrue(mLastNavigationHandle.get().isPageActivation());
        // Page activations should clear the RedirectHandler so future navigations aren't part of
        // the same navigation chain.
        Mockito.verify(spyHandler, Mockito.times(1)).clear();
        syncHelper.notifyCalled();

        // Page redirects to intent: URL.
        finishCallback.waitForCallback(2);

        // With RedirectHandler state cleared, this should be treated as a navigation without a
        // user gesture, which will use a Message to ask the user if they would like to follow the
        // external navigation.
        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, lastResultValue.get());
        Assert.assertTrue(mLastNavigationHandle.get().getUrl().getSpec().startsWith("intent://"));
        syncHelper.notifyCalled();

        Assert.assertNotNull(getCurrentExternalNavigationMessage());
    }

    @Test
    @LargeTest
    @EnableFeatures({BlinkFeatures.PRERENDER2})
    @DisableFeatures({BlinkFeatures.PRERENDER2_MEMORY_CONTROLS})
    public void testClearRedirectHandlerOnPageActivation() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final CallbackHelper prerenderFinishCallback = new CallbackHelper();
        WebContentsObserver observer =
                new WebContentsObserver() {
                    @Override
                    public void didStopLoading(GURL url, boolean isKnownValid) {
                        prerenderFinishCallback.notifyCalled();
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.getWebContents().addObserver(observer);
                });

        mActivityTestRule.loadUrl(mTestServer.getURL(NAVIGATION_FROM_PRERENDER));

        prerenderFinishCallback.waitForCallback(0);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    RedirectHandlerTabHelper.swapHandlerFor(tab, mRedirectHandler);
                    tab.getWebContents().removeObserver(observer);
                });

        // Click page to load prerender.
        TouchCommon.singleClickView(tab.getView());

        // Page activations should clear the RedirectHandler so future navigations aren't part of
        // the same navigation chain.
        Mockito.verify(
                        mRedirectHandler,
                        Mockito.timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1))
                .clear();
    }

    @Test
    @LargeTest
    public void testServerRedirectionFromIntent() throws Exception {
        TestWebServer webServer = TestWebServer.start();
        final String redirectTargetUrl =
                "intent://test/#Intent;scheme=" + EXTERNAL_APP_SCHEME + ";end";
        final String redirectUrl = webServer.setRedirect("/302.html", redirectTargetUrl);

        Context context = ContextUtils.getApplicationContext();
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(redirectUrl));
        intent.setClassName(context, ChromeLauncherActivity.class.getName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        ChromeTabbedActivity activity =
                ApplicationTestUtils.waitForActivityWithClass(
                        ChromeTabbedActivity.class,
                        Stage.CREATED,
                        () -> context.startActivity(intent));
        mActivityTestRule.setActivity(activity);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
        ApplicationTestUtils.waitForActivityState(activity, Stage.DESTROYED);
    }

    @Test
    @LargeTest
    @EnableFeatures({
        "FencedFrames<Study,PrivacySandboxAdsAPIsOverride,FencedFramesAPIChanges,FencedFramesDefaultMode"
    })
    @CommandLineFlags.Add({
        "force-fieldtrials=Study/Group",
        "force-fieldtrial-params=Study.Group:implementation_type/mparch"
    })
    public void testNavigationFromFencedFrame() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final CallbackHelper frameFinishCallback = new CallbackHelper();
        WebContentsObserver observer =
                new WebContentsObserver() {
                    @Override
                    public void didStopLoading(GURL url, boolean isKnownValid) {
                        frameFinishCallback.notifyCalled();
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.getWebContents().addObserver(observer);
                });

        try {
            // Note for posterity: This depends on
            // navigation_from_user_gesture.html.mock-http-headers to work.
            mActivityTestRule.loadUrl(mTestServer.getURL(NAVIGATION_FROM_FENCED_FRAME));

            frameFinishCallback.waitForCallback(0);
        } finally {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        tab.getWebContents().removeObserver(observer);
                    });
        }

        // Because fenced frames are now being loaded with a config object, it
        // needs extra time to load the page outside of what the
        // WebContentsObserver is waiting for. Wait for the the fenced frame's
        // navigation to commit before continuing.
        final String fencedFrameUrl = mTestServer.getURL(NAVIGATION_FROM_USER_GESTURE_PAGE);
        RenderFrameHost mainFrame =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> mActivityTestRule.getWebContents().getMainFrame());
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            FencedFrameUtils.getLastFencedFrame(mainFrame, fencedFrameUrl),
                            Matchers.notNullValue());
                });

        // Click page to launch app. There's no easy way to know when an out of process subframe is
        // ready to receive input, even if the document is loaded and javascript runs. If the click
        // fails the first time, try a second time.
        try {
            TouchCommon.singleClickView(tab.getView());

            CriteriaHelper.pollUiThread(
                    () -> {
                        Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                    });
        } catch (Throwable e) {
            TouchCommon.singleClickView(tab.getView());

            CriteriaHelper.pollUiThread(
                    () -> {
                        Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                    });
        }
    }

    @Test
    @Feature("CustomTabFromChrome")
    @LargeTest
    public void testIntentWithRedirectToApp() throws Exception {
        final String redirectUrl = "https://example.com/path";
        final String initialUrl =
                mTestServer.getURL(
                        "/chrome/test/data/android/redirect/js_redirect.html"
                                + "?replace_text="
                                + Base64.encodeToString(
                                        ApiCompatibilityUtils.getBytesUtf8("PARAM_URL"),
                                        Base64.URL_SAFE)
                                + ":"
                                + Base64.encodeToString(
                                        ApiCompatibilityUtils.getBytesUtf8(redirectUrl),
                                        Base64.URL_SAFE));

        IntentFilter filter = new IntentFilter(Intent.ACTION_VIEW);
        filter.addCategory(Intent.CATEGORY_BROWSABLE);
        filter.addDataAuthority("example.com", null);
        filter.addDataScheme("https");
        ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation()
                        .addMonitor(
                                filter,
                                new Instrumentation.ActivityResult(Activity.RESULT_OK, null),
                                true);
        mTestContext.setIntentFilterForHost("example.com", filter);

        AsyncInitializationActivity.interceptMoveTaskToBackForTesting();
        mCustomTabActivityRule.launchActivity(getCustomTabFromChromeIntent(initialUrl, true));

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(monitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
        CriteriaHelper.pollUiThread(
                () -> AsyncInitializationActivity.wasMoveTaskToBackInterceptedForTesting());
    }

    @Test
    @LargeTest
    public void testExternalNavigationMessage() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        TestParams params =
                new TestParams(mTestServer.getURL(NAVIGATION_FROM_LONG_TIMEOUT), true, false);
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, result.getResultType());

        assertMessagePresent();
    }

    @Test
    @LargeTest
    public void testRedirectFromBookmark() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        String url = mTestServer.getURL(NAVIGATION_FROM_TIMEOUT_PAGE);
        TestParams params = new TestParams(url, false, false);
        params.transition = PageTransition.AUTO_BOOKMARK;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, result.getResultType());
        assertMessagePresent();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    TextView button =
                            mActivityTestRule
                                    .getActivity()
                                    .findViewById(R.id.message_primary_button);
                    button.performClick();
                });
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                    Criteria.checkThat(
                            mActivityTestRule.getActivity().getActivityTab().getUrl().getSpec(),
                            Matchers.is("about:blank"));
                });
    }

    @Test
    @LargeTest
    public void testRedirectFromBookmarkWithFallback() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        String fallbackUrl = mTestServer.getURL(FALLBACK_LANDING_PATH);
        String originalUrl =
                mTestServer.getURL(
                        NAVIGATION_FROM_TIMEOUT_WITH_FALLBACK_PAGE
                                + "?replace_text="
                                + Base64.encodeToString(
                                        ApiCompatibilityUtils.getBytesUtf8("PARAM_FALLBACK_URL"),
                                        Base64.URL_SAFE)
                                + ":"
                                + Base64.encodeToString(
                                        ApiCompatibilityUtils.getBytesUtf8(fallbackUrl),
                                        Base64.URL_SAFE));

        TestParams params = new TestParams(originalUrl, false, false);
        params.transition = PageTransition.AUTO_BOOKMARK;
        params.expectedFinalUrl = fallbackUrl;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_NAVIGATE_TAB, result.getResultType());
        Assert.assertNull(getCurrentExternalNavigationMessage());
    }

    @Test
    @LargeTest
    @Restriction(Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testRedirectFromCCTSpeculation() throws Exception {
        final String url = mTestServer.getURL(NAVIGATION_FROM_PAGE_SHOW);
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        Context context = ContextUtils.getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
        final CustomTabsSessionToken token =
                CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        Assert.assertTrue(connection.newSession(token));

        connection.setCanUseHiddenTabForSession(token, true);
        Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(url), null, null));
        CustomTabsTestUtils.ensureCompletedSpeculationForUrl(connection, url);

        // Can't wait for Activity startup as we close so fast the polling is flaky.
        mCustomTabActivityRule.launchActivity(intent);
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    @Test
    @LargeTest
    @EnableFeatures({ExternalIntentsFeatures.TRUSTED_CLIENT_GESTURE_BYPASS_NAME})
    public void testRedirectToTrustedCaller() throws Exception {
        final String url = mTestServer.getURL(HELLO_PAGE);
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        Context context = ContextUtils.getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
        final CustomTabsSessionToken token =
                CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        Assert.assertTrue(connection.newSession(token));
        connection.overridePackageNameForSessionForTesting(token, TRUSTED_CCT_PACKAGE);

        mCustomTabActivityRule.startCustomTabActivityWithIntent(intent);

        final Tab tab = mCustomTabActivityRule.getActivity().getActivityTab();
        ThreadUtils.runOnUiThreadBlocking(
                () -> RedirectHandlerTabHelper.swapHandlerFor(tab, mSpyRedirectHandler));

        mCustomTabActivityRule.loadUrl(
                mTestServer.getURL(NAVIGATION_FROM_XHR_CALLBACK_AND_SHORT_TIMEOUT_PAGE));

        // This is a little fragile to code changes, but better than waiting 15 real seconds.
        Mockito.doReturn(SystemClock.elapsedRealtime()) // XHR Navigation create
                .doReturn(SystemClock.elapsedRealtime()) // XHR callback navigation create
                .doReturn(
                        SystemClock.elapsedRealtime()
                                + RedirectHandler.NAVIGATION_CHAIN_TIMEOUT_MILLIS
                                + 1) // xhr callback
                .doReturn(SystemClock.elapsedRealtime()) // XHR Navigation create
                .doReturn(SystemClock.elapsedRealtime()) // XHR callback navigation create
                .doReturn(
                        SystemClock.elapsedRealtime()
                                + RedirectHandler.NAVIGATION_CHAIN_TIMEOUT_MILLIS
                                + 1) // xhr callback
                .when(mSpyRedirectHandler)
                .currentRealtime();

        TouchCommon.singleClickView(tab.getView());
        // Wait for blocked Message to show.
        CriteriaHelper.pollInstrumentationThread(
                () -> getCurrentExternalNavigationMessage() != null);
        Assert.assertEquals(0, mActivityMonitor.getHits());

        mTestContext.setResolveToTrustedCaller(true);
        TouchCommon.singleClickView(tab.getView());

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                },
                10000L,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    @Test
    @LargeTest
    public void testSubframeNavigationToSelf() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        String targetUrl = mTestServer.getURL(HELLO_PAGE);
        // Strip off the https: from the URL.
        String strippedTargetUrl = targetUrl.substring(6);
        String subframeTarget =
                "intent:"
                        + strippedTargetUrl
                        + "#Intent;scheme=https;package="
                        + ContextUtils.getApplicationContext().getPackageName()
                        + ";S.browser_fallback_url="
                        + "https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.android.chrome"
                        + ";end";

        String originalUrl =
                getSubframeNavigationUrl(subframeTarget, NavigationType.SELF, SandboxType.NONE);

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final CallbackHelper subframeRedirect = new CallbackHelper();
        EmptyTabObserver observer =
                new EmptyTabObserver() {
                    @Override
                    public void onDidStartNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        Assert.assertEquals(originalUrl, navigation.getUrl().getSpec());
                    }

                    @Override
                    public void onDidRedirectNavigation(Tab tab, NavigationHandle navigation) {
                        Assert.assertFalse(navigation.isInPrimaryMainFrame());
                        if (targetUrl.equals(navigation.getUrl().getSpec())) {
                            subframeRedirect.notifyCalled();
                        }
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(observer);
                });

        // Fallback URL from a subframe will not trigger main navigation.
        TestParams params = new TestParams(originalUrl, true, false);
        params.willLoadSubframe = true;
        params.willNavigateTwice = true;
        params.shouldFailNavigation = false;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_NAVIGATE_TAB, result.getResultType());
        subframeRedirect.waitForOnly();
    }

    void doTestIncognitoSubframeExternalNavigation(boolean acceptPrompt) throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        // This will cause getActivityTab() in loadUrlAndWaitForIntentUrl to return an incognito tab
        // instead.
        mActivityTestRule.loadUrlInNewTab(
                "chrome://about/",
                /** incognito* */
                true);

        String fallbackUrl = mTestServer.getURL(FALLBACK_LANDING_PATH);
        String subframeUrl =
                "intent://test/#Intent;scheme=externalappscheme;S.browser_fallback_url="
                        + fallbackUrl
                        + ";end";
        String originalUrl =
                getSubframeNavigationUrl(subframeUrl, NavigationType.SELF, SandboxType.NONE);

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final CallbackHelper subframeRedirect = new CallbackHelper();
        EmptyTabObserver observer =
                new EmptyTabObserver() {
                    @Override
                    public void onDidStartNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        Assert.assertEquals(originalUrl, navigation.getUrl().getSpec());
                    }

                    @Override
                    public void onDidRedirectNavigation(Tab tab, NavigationHandle navigation) {
                        if (acceptPrompt) Assert.fail();
                        Assert.assertFalse(navigation.isInPrimaryMainFrame());
                        if (fallbackUrl.equals(navigation.getUrl().getSpec())) {
                            subframeRedirect.notifyCalled();
                        }
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(observer);
                });

        TestParams params = new TestParams(originalUrl, true, false);
        params.willLoadSubframe = true;
        params.shouldFailNavigation = false;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, result.getResultType());

        if (acceptPrompt) {
            Espresso.onView(withId(R.id.positive_button)).perform(click());
            CriteriaHelper.pollUiThread(
                    () -> {
                        Criteria.checkThat(mActivityMonitor.getHits(), Matchers.is(1));
                        Criteria.checkThat(
                                mActivityTestRule.getActivity().getActivityTab().getUrl().getSpec(),
                                Matchers.is(originalUrl));
                    });
        } else {
            Espresso.onView(withId(R.id.negative_button)).perform(click());
            subframeRedirect.waitForOnly();
            Assert.assertEquals(0, mActivityMonitor.getHits());
        }
    }

    @Test
    @LargeTest
    public void testIncognitoSubframeExternalNavigation_Rejected() throws Exception {
        doTestIncognitoSubframeExternalNavigation(false);
    }

    @Test
    @LargeTest
    public void testIncognitoSubframeExternalNavigation_Accepted() throws Exception {
        doTestIncognitoSubframeExternalNavigation(true);
    }

    @Test
    @LargeTest
    public void testWindowOpenRedirect() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        ChromeActivity activity = mActivityTestRule.getActivity();
        TabModelImpl tabModel = (TabModelImpl) activity.getTabModelSelector().getModel(false);
        GURL url = new GURL(EXTERNAL_APP_URL);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Called when a popup window is allowed.
                    tabModel.openNewTab(
                            activity.getActivityTab(),
                            url,
                            createExampleOrigin(),
                            null,
                            null,
                            WindowOpenDisposition.NEW_FOREGROUND_TAB,
                            true,
                            true);
                });

        assertMessagePresent();
    }

    @Test
    @LargeTest
    @EnableFeatures({ExternalIntentsFeatures.BLOCK_FRAME_RENAVIGATIONS_NAME})
    public void testWindowRenavigation() throws Exception {
        String finalUrl = mTestServer.getURL(HELLO_PAGE);
        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params =
                new TestParams(mTestServer.getURL(NAVIGATION_FROM_RENAVIGATE_FRAME), true, false);
        params.createsNewTab = true;
        params.expectedFinalUrl = finalUrl;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(OverrideUrlLoadingResultType.NO_OVERRIDE, result.getResultType());
        Assert.assertNull(getCurrentExternalNavigationMessage());
    }

    @Test
    @LargeTest
    @EnableFeatures({ExternalIntentsFeatures.BLOCK_FRAME_RENAVIGATIONS_NAME})
    public void testWindowRenavigationServerRedirect() throws Exception {
        String finalUrl = mTestServer.getURL(HELLO_PAGE);
        mActivityTestRule.startMainActivityOnBlankPage();

        TestParams params =
                new TestParams(
                        mTestServer.getURL(NAVIGATION_FROM_RENAVIGATE_FRAME_WITH_REDIRECT),
                        true,
                        false);
        params.createsNewTab = true;
        params.expectedFinalUrl = finalUrl;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);

        Assert.assertEquals(OverrideUrlLoadingResultType.NO_OVERRIDE, result.getResultType());
        Assert.assertNull(getCurrentExternalNavigationMessage());
    }

    @Test
    @LargeTest
    @EnableFeatures({ExternalIntentsFeatures.BLOCK_FRAME_RENAVIGATIONS_NAME})
    public void testWindowServerRedirect() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params =
                new TestParams(mTestServer.getURL(NAVIGATION_FROM_WINDOW_REDIRECT), true, true);
        params.createsNewTab = true;
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @LargeTest
    public void testNavigateTopFrame() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        String subframeUrl = "intent://test/#Intent;scheme=externalappscheme;end";
        String originalUrl =
                getSubframeNavigationUrl(subframeUrl, NavigationType.TOP, SandboxType.NONE);

        TestParams params = new TestParams(originalUrl, true, true);
        params.willLoadSubframe = true;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @LargeTest
    @EnableFeatures({ExternalIntentsFeatures.BLOCK_INTENTS_TO_SELF_NAME})
    public void testIntentToSelf() throws Exception {
        String targetUrl = mTestServer.getURL(HELLO_PAGE);
        // Strip off the https: from the URL.
        String strippedTargetUrl = targetUrl.substring(6);
        String link =
                "intent:"
                        + strippedTargetUrl
                        + "#Intent;scheme=https;package="
                        + ContextUtils.getApplicationContext().getPackageName()
                        + ";end";

        byte[] paramName = ApiCompatibilityUtils.getBytesUtf8("PARAM_SUBFRAME_URL");
        byte[] paramValue = ApiCompatibilityUtils.getBytesUtf8(link);

        String url =
                mTestServer.getURL(
                        SUBFRAME_NAVIGATION_CHILD
                                + "?replace_text="
                                + Base64.encodeToString(paramName, Base64.URL_SAFE)
                                + ":"
                                + Base64.encodeToString(paramValue, Base64.URL_SAFE));

        mActivityTestRule.startMainActivityOnBlankPage();
        TestParams params = new TestParams(url, true, false);
        params.willNavigateTwice = true;
        params.expectedFinalUrl = null;
        loadUrlAndWaitForIntentUrl(params);
    }

    @Test
    @LargeTest
    @EnableFeatures({ExternalIntentsFeatures.BLOCK_INTENTS_TO_SELF_NAME})
    public void testIntentToSelfWithFallback() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        String targetUrl = mTestServer.getURL(HELLO_PAGE);
        // Strip off the https: from the URL.
        String strippedTargetUrl = targetUrl.substring(6);
        String subframeTarget =
                "intent:"
                        + strippedTargetUrl
                        + "#Intent;scheme=https;package="
                        + ContextUtils.getApplicationContext().getPackageName()
                        + ";S.browser_fallback_url="
                        + "https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.android.chrome"
                        + ";end";

        String originalUrl =
                getSubframeNavigationUrl(subframeTarget, NavigationType.BLANK, SandboxType.NONE);

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final AtomicInteger navCount = new AtomicInteger(0);
        EmptyTabObserver observer =
                new EmptyTabObserver() {
                    @Override
                    public void onDidStartNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        int count = navCount.getAndIncrement();
                        if (count == 0) {
                            Assert.assertEquals(originalUrl, navigation.getUrl().getSpec());
                        } else if (count == 1) {
                            Assert.assertEquals(subframeTarget, navigation.getUrl().getSpec());
                        } else if (count == 2) {
                            Assert.assertEquals(targetUrl, navigation.getUrl().getSpec());
                        } else {
                            Assert.fail();
                        }
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(observer);
                });

        TestParams params = new TestParams(originalUrl, true, false);
        params.createsNewTab = true;
        params.expectedFinalUrl = targetUrl;
        params.willLoadSubframe = true;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);
        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_NAVIGATE_TAB, result.getResultType());
    }

    private void doTestIntentToSelfWithFallback_Sandboxed(boolean useCSP) throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();

        String targetUrl = mTestServer.getURL(HELLO_PAGE);
        // Strip off the https: from the URL.
        String strippedTargetUrl = targetUrl.substring(6);
        String subframeTarget =
                "intent:"
                        + strippedTargetUrl
                        + "#Intent;scheme=https;package="
                        + ContextUtils.getApplicationContext().getPackageName()
                        + ";S.browser_fallback_url="
                        + "https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.android.chrome"
                        + ";end";

        @SandboxType int sandboxType = useCSP ? SandboxType.CSP : SandboxType.FRAME;
        String originalUrl =
                getSubframeNavigationUrl(subframeTarget, NavigationType.BLANK, sandboxType);

        final Tab tab = mActivityTestRule.getActivity().getActivityTab();

        final AtomicInteger navCount = new AtomicInteger(0);
        EmptyTabObserver observer =
                new EmptyTabObserver() {
                    @Override
                    public void onDidStartNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        int count = navCount.getAndIncrement();
                        if (count == 0) {
                            Assert.assertEquals(originalUrl, navigation.getUrl().getSpec());
                        } else if (count == 1) {
                            Assert.assertEquals(subframeTarget, navigation.getUrl().getSpec());
                        } else {
                            Assert.fail();
                        }
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(observer);
                });

        TestParams params = new TestParams(originalUrl, true, false);
        params.createsNewTab = true;
        params.willLoadSubframe = true;
        params.expectedFinalUrl = null;
        OverrideUrlLoadingResult result = loadUrlAndWaitForIntentUrl(params);
        // Navigation to self is blocked, ExternalNavigationHandler asks to navigate to the
        // fallback URL.
        Assert.assertEquals(
                OverrideUrlLoadingResultType.OVERRIDE_WITH_NAVIGATE_TAB, result.getResultType());
        // Fallback URL is blocked by InterceptNavigationDelegateImpl, no URL is loading and the
        // final URL is the subframe's target.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Tab newTab = mActivityTestRule.getActivity().getActivityTab();
                    Assert.assertEquals(subframeTarget, newTab.getUrl().getSpec());
                    Assert.assertFalse(newTab.getWebContents().isLoading());
                });
    }

    // Ensures that for a sandboxed main frame, we block both intents to ourself, and fallback URLs
    // that would escape the sandbox by clobbering the main frame.
    @Test
    @LargeTest
    @EnableFeatures({ExternalIntentsFeatures.BLOCK_INTENTS_TO_SELF_NAME})
    public void testIntentToSelfWithFallback_Sandboxed() throws Exception {
        doTestIntentToSelfWithFallback_Sandboxed(false);
    }

    // Same as testIntentToSelfWithFallback_Sandboxed but with CSP sandbox.
    @Test
    @LargeTest
    @EnableFeatures({ExternalIntentsFeatures.BLOCK_INTENTS_TO_SELF_NAME})
    public void testIntentToSelfWithFallback_CSPSandboxed() throws Exception {
        doTestIntentToSelfWithFallback_Sandboxed(true);
    }
}