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

import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_ACTIVITY_SIDE_SHEET_BREAKPOINT_DP;
import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_ACTIVITY_SIDE_SHEET_ENABLE_MAXIMIZATION;
import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_INITIAL_ACTIVITY_HEIGHT_PX;
import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_INITIAL_ACTIVITY_WIDTH_PX;
import static androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
import static org.chromium.chrome.browser.customtabs.CustomTabsTestUtils.createTestBitmap;
import static org.chromium.components.content_settings.PrefNames.COOKIE_CONTROLS_MODE;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.app.Activity;
import android.app.Instrumentation;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.SystemClock;
import android.provider.Browser;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.RemoteViews;
import android.widget.TextView;

import androidx.annotation.DrawableRes;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.ContentFrameLayout;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.runner.lifecycle.Stage;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.BuildInfo;
import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
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.CriteriaNotSatisfiedException;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.UserActionTester;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.AppHooksImpl;
import org.chromium.chrome.browser.ChromeApplicationImpl;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.LaunchIntentDispatcher;
import org.chromium.chrome.browser.TabsOpenedFromExternalAppTest;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.back_press.BackPressManager;
import org.chromium.chrome.browser.browserservices.SessionDataHolder;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.verification.ChromeOriginVerifier;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.contextmenu.ContextMenuCoordinator;
import org.chromium.chrome.browser.customtabs.CustomTabActivityLifecycleUmaTracker.ClientIdentifierType;
import org.chromium.chrome.browser.customtabs.CustomTabsIntentTestUtils.OnFinishedForTest;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityNavigationController.FinishReason;
import org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabBaseStrategy;
import org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabDisplayManager;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.history.BrowsingHistoryBridge;
import org.chromium.chrome.browser.history.HistoryItem;
import org.chromium.chrome.browser.history.TestBrowsingHistoryObserver;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.page_load_metrics.PageLoadMetrics;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.Tab.LoadUrlResult;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabTestUtils;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.test.ScreenShooter;
import org.chromium.chrome.browser.toolbar.menu_button.MenuButton;
import org.chromium.chrome.browser.ui.appmenu.AppMenuCoordinator;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
import org.chromium.chrome.test.AutomotiveContextWrapperTestRule;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ActivityTestUtils;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.browser.contextmenu.ContextMenuUtils;
import org.chromium.components.browser_ui.widget.CoordinatorLayoutForPointer;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.content_settings.CookieControlsMode;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.components.page_info.PageInfoController;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.ClickUtils;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.JavaScriptUtils;
import org.chromium.content_public.common.ContentSwitches;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.util.TestWebServer;
import org.chromium.ui.mojom.WindowOpenDisposition;
import org.chromium.ui.test.util.BlankUiTestActivity;
import org.chromium.ui.test.util.DeviceRestriction;
import org.chromium.ui.test.util.UiRestriction;
import org.chromium.ui.util.ColorUtils;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/** Instrumentation tests for app menu, context menu, and toolbar of a {@link CustomTabActivity}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@DoNotBatch(
        reason =
                "Some tests are Testing CCT start up behavior. "
                        + "Unit test conversion tracked in crbug.com/1217031")
public class CustomTabActivityTest {
    private static final int TIMEOUT_PAGE_LOAD_SECONDS = 10;
    private static final String TEST_PACKAGE = "org.chromium.chrome.tests";
    private static final String TEST_PAGE = "/chrome/test/data/android/google.html";
    private static final String TEST_PAGE_2 = "/chrome/test/data/android/test.html";
    private static final String FRAGMENT_TEST_PAGE = "/chrome/test/data/android/fragment.html";
    private static final String TARGET_BLANK_TEST_PAGE =
            "/chrome/test/data/android/cct_target_blank.html";
    private static final String ONLOAD_TITLE_CHANGE =
            "<!DOCTYPE html><html><body>"
                    + "    <script>"
                    + "        window.onload = function () {"
                    + "            document.title = \"nytimes.com\";"
                    + "        }"
                    + "   </script>"
                    + "</body></html>";
    private static final String DELAYED_TITLE_CHANGE =
            "<!DOCTYPE html><html><body>"
                    + "    <script>"
                    + "        window.onload = function () {"
                    + "           setTimeout(function (){ document.title = \"nytimes.com\"}, 200);"
                    + "        }"
                    + "   </script>"
                    + "</body></html>";

    @Rule
    public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();

    @Rule
    public ChromeTabbedActivityTestRule mChromeTabbedActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public AutomotiveContextWrapperTestRule mAutomotiveRule =
            new AutomotiveContextWrapperTestRule();

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

    private static int sIdToIncrement = 1;

    private String mTestPage;
    private String mTestPage2;
    private EmbeddedTestServer mTestServer;
    private CustomTabsConnection mConnectionToCleanup;

    @Captor ArgumentCaptor<Intent> mIntentCaptor;

    private class CustomTabsExtraCallbackHelper<T> extends CallbackHelper {
        private T mValue;

        public T getValue() {
            assert getCallCount() > 0;
            return mValue;
        }

        public void notifyCalled(T value) {
            mValue = value;
            notifyCalled();
        }
    }

    @Rule public final ScreenShooter mScreenShooter = new ScreenShooter();

    @Before
    public void setUp() throws Exception {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(true));
        mAutomotiveRule.setIsAutomotive(false);
        Context appContext = getInstrumentation().getTargetContext().getApplicationContext();
        mTestServer = EmbeddedTestServer.createAndStartServer(appContext);
        mTestPage = mTestServer.getURL(TEST_PAGE);
        mTestPage2 = mTestServer.getURL(TEST_PAGE_2);
        LibraryLoader.getInstance().ensureInitialized();
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false));
        SharedPreferencesManager pref = ChromeSharedPreferences.getInstance();
        pref.removeKey(ChromePreferenceKeys.CUSTOM_TABS_LAST_TASK_ID);
        pref.removeKey(ChromePreferenceKeys.CUSTOM_TABS_LAST_URL);
        pref.removeKey(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLIENT_PACKAGE);

        ChromeSharedPreferences.getInstance()
                .removeKey(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLOSE_TAB_INTERACTION);
        ChromeSharedPreferences.getInstance()
                .removeKey(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLOSE_TIMESTAMP);

        // finish() is called on a non-UI thread by the testing harness. Must hide the menu
        // first, otherwise the UI is manipulated on a non-UI thread.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (getActivity() == null) return;
                    AppMenuCoordinator coordinator =
                            mCustomTabActivityTestRule.getAppMenuCoordinator();
                    // CCT doesn't always have a menu (ex. in the media viewer).
                    if (coordinator == null) return;
                    AppMenuHandler handler = coordinator.getAppMenuHandler();
                    if (handler != null) handler.hideAppMenu();
                });

        if (mConnectionToCleanup != null) {
            CustomTabsTestUtils.cleanupSessions(mConnectionToCleanup);
        }
    }

    private CustomTabActivity getActivity() {
        return mCustomTabActivityTestRule.getActivity();
    }

    /**
     * @see CustomTabsIntentTestUtils#createMinimalCustomTabIntent(Context, String).
     */
    private Intent createMinimalCustomTabIntent() {
        return CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                ApplicationProvider.getApplicationContext(), mTestPage);
    }

    @Test
    @SmallTest
    public void testAllowedHeadersReceivedWhenConnectionVerified() throws Exception {
        final Context context = getInstrumentation().getTargetContext().getApplicationContext();
        final Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage2);

        Bundle headers = new Bundle();
        headers.putString("bearer-token", "Some token");
        headers.putString("redirect-url", "https://www.google.com");
        intent.putExtra(Browser.EXTRA_HEADERS, headers);

        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(token, "app1");
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        ChromeOriginVerifier.addVerificationOverride(
                                "app1",
                                Origin.create(intent.getData()),
                                CustomTabsService.RELATION_USE_AS_ORIGIN));

        final CustomTabsSessionToken session = warmUpAndLaunchUrlWithSession(intent);
        assertEquals(getActivity().getIntentDataProvider().getSession(), session);

        final Tab tab = getActivity().getActivityTab();
        final CallbackHelper pageLoadFinishedHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(
                            new EmptyTabObserver() {
                                @Override
                                public void onLoadUrl(
                                        Tab tab,
                                        LoadUrlParams params,
                                        LoadUrlResult loadUrlResult) {
                                    assertTrue(
                                            params.getVerbatimHeaders()
                                                    .contains("bearer-token: Some token"));
                                    assertTrue(
                                            params.getVerbatimHeaders()
                                                    .contains(
                                                            "redirect-url:"
                                                                    + " https://www.google.com"));
                                    pageLoadFinishedHelper.notifyCalled();
                                }
                            });
                });

        ThreadUtils.runOnUiThreadBlocking(() -> getSessionDataHolder().handleIntent(intent));
        pageLoadFinishedHelper.waitForCallback(0);
    }

    private void addToolbarColorToIntent(Intent intent, int color) {
        intent.putExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, color);
    }

    private Bundle makeBottomBarBundle(int id, Bitmap icon, String description) {
        Bundle bundle = new Bundle();
        PendingIntent pi =
                PendingIntent.getBroadcast(
                        ApplicationProvider.getApplicationContext(),
                        0,
                        new Intent(),
                        IntentUtils.getPendingIntentMutabilityFlag(true));

        bundle.putInt(CustomTabsIntent.KEY_ID, sIdToIncrement++);
        bundle.putString(CustomTabsIntent.KEY_DESCRIPTION, description);
        bundle.putParcelable(CustomTabsIntent.KEY_PENDING_INTENT, pi);
        bundle.putParcelable(CustomTabsIntent.KEY_ICON, icon);
        return bundle;
    }

    private Bundle makeUpdateVisualsBundle(int id, Bitmap icon, String description) {
        Bundle bundle = new Bundle();
        bundle.putInt(CustomTabsIntent.KEY_ID, id);
        bundle.putParcelable(CustomTabsIntent.KEY_ICON, icon);
        bundle.putString(CustomTabsIntent.KEY_DESCRIPTION, description);
        return bundle;
    }

    private void openAppMenuAndAssertMenuShown() {
        CustomTabsTestUtils.openAppMenuAndAssertMenuShown(mCustomTabActivityTestRule.getActivity());
    }

    private Bitmap createVectorDrawableBitmap(@DrawableRes int resId, int widthDp, int heightDp) {
        Context context = ApplicationProvider.getApplicationContext();
        Drawable vectorDrawable = AppCompatResources.getDrawable(context, resId);
        Bitmap bitmap = createTestBitmap(widthDp, heightDp);
        Canvas canvas = new Canvas(bitmap);
        float density = context.getResources().getDisplayMetrics().density;
        int widthPx = (int) (density * widthDp);
        int heightPx = (int) (density * heightDp);
        vectorDrawable.setBounds(0, 0, widthPx, heightPx);
        vectorDrawable.draw(canvas);
        return bitmap;
    }

    private void setCanUseHiddenTabForSession(
            CustomTabsConnection connection, CustomTabsSessionToken token, boolean useHiddenTab) {
        assert mConnectionToCleanup == null || mConnectionToCleanup == connection;
        // Save the connection. In case the hidden tab is not consumed by the test, ensure that it
        // is properly cleaned up after the test.
        mConnectionToCleanup = connection;
        connection.setCanUseHiddenTabForSession(token, useHiddenTab);
    }

    @Test
    @SmallTest
    public void testContextMenuEntriesBeforeFirstRun() throws Exception {
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(createMinimalCustomTabIntent());
        // Mark the first run as not completed. This has to be done after we start the intent,
        // otherwise we are going to hit the FRE.
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false));

        // The context menu for images should not be built when the first run is not completed.
        ContextMenuCoordinator imageMenu =
                ContextMenuUtils.openContextMenu(
                        mCustomTabActivityTestRule.getActivity().getActivityTab(), "logo");
        assertNull(
                "Context menu for images should not be built when first run is not finished.",
                imageMenu);

        // Options on the context menu for links should be limited when the first run is not
        // completed.
        ContextMenuCoordinator linkMenu =
                ContextMenuUtils.openContextMenu(
                        mCustomTabActivityTestRule.getActivity().getActivityTab(), "aboutLink");
        final int expectedMenuItems = 4;
        Assert.assertEquals(
                "Menu item count does not match expectation.",
                expectedMenuItems,
                linkMenu.getCount());

        Assert.assertNotNull(linkMenu.findItem(R.id.contextmenu_copy_link_text));
        Assert.assertNotNull(linkMenu.findItem(R.id.contextmenu_copy_link_address));
    }

    /**
     * Test whether the color of the toolbar is correctly customized. For L or later releases,
     * status bar color is also tested.
     */
    @Test
    @SmallTest
    @Feature({"StatusBar"})
    public void testToolbarColor() {
        Intent intent = createMinimalCustomTabIntent();
        final int expectedColor = Color.RED;
        addToolbarColorToIntent(intent, expectedColor);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        View toolbarView = mCustomTabActivityTestRule.getActivity().findViewById(R.id.toolbar);
        Assert.assertTrue(
                "A custom tab toolbar is never shown", toolbarView instanceof CustomTabToolbar);
        CustomTabToolbar toolbar = (CustomTabToolbar) toolbarView;
        assertEquals(expectedColor, toolbar.getBackground().getColor());
        Assert.assertFalse(
                mCustomTabActivityTestRule
                        .getActivity()
                        .getToolbarManager()
                        .getLocationBarModelForTesting()
                        .shouldEmphasizeHttpsScheme());
        // Status bar color is constantly black on automotive. See b/285208454.
        int expectedStatusBarColor =
                BuildInfo.getInstance().isAutomotive ? Color.BLACK : expectedColor;
        assertEquals(
                expectedStatusBarColor,
                mCustomTabActivityTestRule.getActivity().getWindow().getStatusBarColor());
        // TODO(crbug.com/40588353): Use helper class to determine whether dark status icons
        // are supported.

        MenuButton menuButtonView = toolbarView.findViewById(R.id.menu_button_wrapper);
        assertEquals(
                ColorUtils.shouldUseLightForegroundOnBackground(expectedColor)
                        ? BrandedColorScheme.DARK_BRANDED_THEME
                        : BrandedColorScheme.LIGHT_BRANDED_THEME,
                menuButtonView.getBrandedColorSchemeForTesting());
    }

    /**
     * Test if an action button is shown with correct image and size, and clicking it sends the
     * correct {@link PendingIntent}.
     */
    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @DisabledTest(message = "https://crbug.com/40898152")
    public void testActionButton() throws TimeoutException {
        Bitmap expectedIcon = createVectorDrawableBitmap(R.drawable.ic_credit_card_black, 77, 48);
        Intent intent = createMinimalCustomTabIntent();
        final PendingIntent pi =
                CustomTabsIntentTestUtils.addActionButtonToIntent(
                        intent, expectedIcon, "Good test", sIdToIncrement++);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        final OnFinishedForTest onFinished = new OnFinishedForTest(pi);
        getActivity()
                .getComponent()
                .resolveToolbarCoordinator()
                .setCustomButtonPendingIntentOnFinishedForTesting(onFinished);

        View toolbarView = mCustomTabActivityTestRule.getActivity().findViewById(R.id.toolbar);
        Assert.assertTrue(
                "A custom tab toolbar is never shown", toolbarView instanceof CustomTabToolbar);
        CustomTabToolbar toolbar = (CustomTabToolbar) toolbarView;
        final ImageButton actionButton = toolbar.getCustomActionButtonForTest(0);

        Assert.assertNotNull(actionButton);
        Assert.assertNotNull(actionButton.getDrawable());
        Assert.assertTrue(
                "Action button's background is not a BitmapDrawable.",
                actionButton.getDrawable() instanceof BitmapDrawable);

        Assert.assertTrue(
                "Action button does not have the correct bitmap.",
                expectedIcon.sameAs(((BitmapDrawable) actionButton.getDrawable()).getBitmap()));

        mScreenShooter.shoot("Action Buttons");

        ThreadUtils.runOnUiThreadBlocking((Runnable) actionButton::performClick);

        onFinished.waitForCallback("Pending Intent was not sent.");
        assertThat(onFinished.getCallbackIntent().getDataString(), equalTo(mTestPage));
    }

    /**
     * Test if an action button is shown with correct image and size, and clicking it sends the
     * correct {@link PendingIntent}.
     */
    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @DisabledTest(message = "https://crbug.com/40898152")
    public void testMultipleActionButtons() throws TimeoutException {
        Bitmap expectedIcon1 = createVectorDrawableBitmap(R.drawable.ic_content_copy_black, 48, 48);
        Bitmap expectedIcon2 =
                createVectorDrawableBitmap(R.drawable.ic_email_googblue_36dp, 48, 48);
        Intent intent = createMinimalCustomTabIntent();

        // Mark the intent as trusted so it can show more than one action button.
        IntentUtils.addTrustedIntentExtras(intent);
        Assert.assertTrue(IntentHandler.wasIntentSenderChrome(intent));

        ArrayList<Bundle> toolbarItems = new ArrayList<>(2);
        final PendingIntent pi1 =
                PendingIntent.getBroadcast(
                        ApplicationProvider.getApplicationContext(),
                        0,
                        new Intent(),
                        IntentUtils.getPendingIntentMutabilityFlag(true));
        final OnFinishedForTest onFinished1 = new OnFinishedForTest(pi1);
        toolbarItems.add(
                CustomTabsIntentTestUtils.makeToolbarItemBundle(
                        expectedIcon1, "Good test", pi1, sIdToIncrement++));
        final PendingIntent pi2 =
                PendingIntent.getBroadcast(
                        ApplicationProvider.getApplicationContext(),
                        1,
                        new Intent(),
                        IntentUtils.getPendingIntentMutabilityFlag(true));
        assertThat(pi2, not(equalTo(pi1)));
        final OnFinishedForTest onFinished2 = new OnFinishedForTest(pi2);
        toolbarItems.add(
                CustomTabsIntentTestUtils.makeToolbarItemBundle(
                        expectedIcon2, "Even gooder test", pi2, sIdToIncrement++));
        intent.putParcelableArrayListExtra(CustomTabsIntent.EXTRA_TOOLBAR_ITEMS, toolbarItems);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        // Forward the onFinished event to both objects.
        getActivity()
                .getComponent()
                .resolveToolbarCoordinator()
                .setCustomButtonPendingIntentOnFinishedForTesting(
                        (pendingIntent, openedIntent, resultCode, resultData, resultExtras) -> {
                            onFinished1.onSendFinished(
                                    pendingIntent,
                                    openedIntent,
                                    resultCode,
                                    resultData,
                                    resultExtras);
                            onFinished2.onSendFinished(
                                    pendingIntent,
                                    openedIntent,
                                    resultCode,
                                    resultData,
                                    resultExtras);
                        });

        View toolbarView = mCustomTabActivityTestRule.getActivity().findViewById(R.id.toolbar);
        Assert.assertTrue(
                "A custom tab toolbar is never shown", toolbarView instanceof CustomTabToolbar);
        CustomTabToolbar toolbar = (CustomTabToolbar) toolbarView;
        final ImageButton actionButton = toolbar.getCustomActionButtonForTest(1);

        Assert.assertNotNull("Action button not found", actionButton);
        Assert.assertNotNull(actionButton.getDrawable());
        Assert.assertTrue(
                "Action button's background is not a BitmapDrawable.",
                actionButton.getDrawable() instanceof BitmapDrawable);

        Assert.assertTrue(
                "Action button does not have the correct bitmap.",
                expectedIcon1.sameAs(((BitmapDrawable) actionButton.getDrawable()).getBitmap()));

        mScreenShooter.shoot("Multiple Action Buttons");

        ThreadUtils.runOnUiThreadBlocking((Runnable) actionButton::performClick);

        onFinished1.waitForCallback("Pending Intent was not sent.");
        assertThat(onFinished1.getCallbackIntent().getDataString(), equalTo(mTestPage));
        assertNull(onFinished2.getCallbackIntent());

        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        int id = toolbarItems.get(0).getInt(CustomTabsIntent.KEY_ID);
        Bundle updateActionButtonBundle =
                makeUpdateVisualsBundle(id, expectedIcon2, "Bestest testest");
        Bundle updateVisualsBundle = new Bundle();
        updateVisualsBundle.putParcelableArrayList(
                CustomTabsIntent.EXTRA_TOOLBAR_ITEMS,
                new ArrayList<>(Arrays.asList(updateActionButtonBundle)));
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        Assert.assertTrue(connection.updateVisuals(token, updateVisualsBundle));

        assertEquals("Bestest testest", actionButton.getContentDescription());
    }

    /**
     * Test the case that the action button should not be shown, given a bitmap with unacceptable
     * height/width ratio.
     */
    @Test
    @SmallTest
    public void testActionButtonBadRatio() {
        Bitmap expectedIcon = createTestBitmap(60, 20);
        Intent intent = createMinimalCustomTabIntent();
        CustomTabsIntentTestUtils.setShareState(intent, CustomTabsIntent.SHARE_STATE_OFF);
        CustomTabsIntentTestUtils.addActionButtonToIntent(
                intent, expectedIcon, "Good test", sIdToIncrement++);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        View toolbarView = mCustomTabActivityTestRule.getActivity().findViewById(R.id.toolbar);
        Assert.assertTrue(
                "A custom tab toolbar is never shown", toolbarView instanceof CustomTabToolbar);
        CustomTabToolbar toolbar = (CustomTabToolbar) toolbarView;
        final ImageButton actionButton = toolbar.getCustomActionButtonForTest(0);

        assertNull("Action button should not be shown", actionButton);

        BrowserServicesIntentDataProvider dataProvider = getActivity().getIntentDataProvider();
        assertThat(dataProvider.getCustomButtonsOnToolbar(), is(empty()));
    }

    @Test
    @SmallTest
    public void testBottomBar() {
        final int numItems = 3;
        final Bitmap expectedIcon = createTestBitmap(48, 24);
        final int barColor = Color.GREEN;

        Intent intent = createMinimalCustomTabIntent();
        ArrayList<Bundle> bundles = new ArrayList<>();
        for (int i = 1; i <= numItems; i++) {
            Bundle bundle = makeBottomBarBundle(i, expectedIcon, Integer.toString(i));
            bundles.add(bundle);
        }
        intent.putExtra(CustomTabsIntent.EXTRA_TOOLBAR_ITEMS, bundles);
        intent.putExtra(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR, barColor);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        ViewGroup bottomBar =
                mCustomTabActivityTestRule
                        .getActivity()
                        .findViewById(R.id.custom_tab_bottom_bar_wrapper);
        Assert.assertNotNull(bottomBar);
        Assert.assertTrue(
                "Bottom Bar wrapper is not visible.",
                bottomBar.getVisibility() == View.VISIBLE
                        && bottomBar.getHeight() > 0
                        && bottomBar.getWidth() > 0);
        assertEquals(
                "Bottom Bar showing incorrect number of buttons.",
                numItems,
                bottomBar.getChildCount());
        assertEquals(
                "Bottom bar not showing correct color",
                barColor,
                ((ColorDrawable) bottomBar.getBackground()).getColor());
        for (int i = 0; i < numItems; i++) {
            ImageButton button = (ImageButton) bottomBar.getChildAt(i);
            Assert.assertTrue(
                    "Bottom Bar button does not have the correct bitmap.",
                    expectedIcon.sameAs(((BitmapDrawable) button.getDrawable()).getBitmap()));
            Assert.assertTrue(
                    "Bottom Bar button is not visible.",
                    button.getVisibility() == View.VISIBLE
                            && button.getHeight() > 0
                            && button.getWidth() > 0);
            assertEquals(
                    "Bottom Bar button does not have correct content description",
                    Integer.toString(i + 1),
                    button.getContentDescription());
        }
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    public void testRemoteViews() {
        Intent intent = createMinimalCustomTabIntent();

        Bitmap expectedIcon = createVectorDrawableBitmap(R.drawable.ic_credit_card_black, 77, 48);
        CustomTabsIntentTestUtils.addActionButtonToIntent(
                intent, expectedIcon, "Good test", sIdToIncrement++);

        // Create a RemoteViews. The layout used here is pretty much arbitrary, but with the
        // constraint that a) it already exists in production code, and b) it only contains views
        // with the @RemoteView annotation.
        RemoteViews remoteViews =
                new RemoteViews(
                        ApplicationProvider.getApplicationContext().getPackageName(),
                        R.layout.share_sheet_item);
        remoteViews.setTextViewText(R.id.text, "Kittens!");
        remoteViews.setTextViewText(R.id.display_new, "So fluffy");
        remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_email_googblue_36dp);
        intent.putExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS, remoteViews);
        intent.putExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_VIEW_IDS, new int[] {R.id.icon});
        PendingIntent pi2 =
                PendingIntent.getBroadcast(
                        ApplicationProvider.getApplicationContext(),
                        0,
                        new Intent(),
                        IntentUtils.getPendingIntentMutabilityFlag(true));
        intent.putExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_PENDINGINTENT, pi2);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        mScreenShooter.shoot("Remote Views");
    }

    @Test
    @SmallTest
    public void testLaunchWithSession() throws Exception {
        CustomTabsSessionToken session = warmUpAndLaunchUrlWithSession();
        assertEquals(getActivity().getIntentDataProvider().getSession(), session);
    }

    @Test
    @SmallTest
    public void testRecordRetainableSession_WithCctSession() throws Exception {
        Activity emptyActivity = startBlankUiTestActivity();

        // Write shared pref as it there's a previous CCT launch with sessions.
        SharedPreferencesManager pref = ChromeSharedPreferences.getInstance();
        pref.writeString(ChromePreferenceKeys.CUSTOM_TABS_LAST_URL, mTestPage);
        pref.writeString(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLIENT_PACKAGE, TEST_PACKAGE);
        pref.writeInt(ChromePreferenceKeys.CUSTOM_TABS_LAST_TASK_ID, emptyActivity.getTaskId());
        pref.writeLong(
                ChromePreferenceKeys.CUSTOM_TABS_LAST_CLOSE_TIMESTAMP, SystemClock.uptimeMillis());
        pref.writeBoolean(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLOSE_TAB_INTERACTION, true);

        Intent intent =
                CustomTabsIntentTestUtils.createCustomTabIntent(
                        ApplicationProvider.getApplicationContext(),
                        mTestPage,
                        false,
                        builder -> {});
        CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        connection.newSession(token);
        intent.setData(Uri.parse(mTestPage));

        CustomTabActivity cctActivity =
                ApplicationTestUtils.waitForActivityWithClass(
                        CustomTabActivity.class,
                        Stage.CREATED,
                        () -> emptyActivity.startActivity(intent));
        mCustomTabActivityTestRule.setActivity(cctActivity);
        mCustomTabActivityTestRule.waitForActivityCompletelyLoaded();

        assertLastLaunchedClientAppRecorded(
                ClientIdentifierType.PACKAGE_NAME,
                TEST_PACKAGE,
                mTestPage,
                cctActivity.getTaskId(),
                true);
    }

    @Test
    @SmallTest
    public void testRecordRetainableSession_WithoutWarmupAndSession() {
        Context context = ApplicationProvider.getApplicationContext();
        Activity emptyActivity = startBlankUiTestActivity();

        Intent intent =
                CustomTabsIntentTestUtils.createCustomTabIntent(
                                context, mTestPage, /* launchAsNewTask= */ false, builder -> {})
                        .putExtra(IntentHandler.EXTRA_ACTIVITY_REFERRER, context.getPackageName());

        CustomTabActivity cctActivity =
                ApplicationTestUtils.waitForActivityWithClass(
                        CustomTabActivity.class,
                        Stage.CREATED,
                        () -> emptyActivity.startActivity(intent));
        mCustomTabActivityTestRule.setActivity(cctActivity);
        mCustomTabActivityTestRule.waitForActivityCompletelyLoaded();

        assertLastLaunchedClientAppRecorded(
                ClientIdentifierType.REFERRER, "", mTestPage, cctActivity.getTaskId(), false);

        Activity activity = getActivity();
        activity.finish();
        ApplicationTestUtils.waitForActivityState(activity, Stage.DESTROYED);

        // Write shared prefs as it the last CCT session has saw tab interactions.
        SharedPreferencesManager pref = ChromeSharedPreferences.getInstance();
        pref.writeBoolean(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLOSE_TAB_INTERACTION, true);

        // Start another CCT with same intent right away.
        Intent newIntent = new Intent(intent);
        cctActivity =
                ApplicationTestUtils.waitForActivityWithClass(
                        CustomTabActivity.class,
                        Stage.CREATED,
                        () -> emptyActivity.startActivity(newIntent));
        mCustomTabActivityTestRule.setActivity(cctActivity);
        mCustomTabActivityTestRule.waitForActivityCompletelyLoaded();
        assertLastLaunchedClientAppRecorded(
                ClientIdentifierType.REFERRER, "", mTestPage, getActivity().getTaskId(), true);
    }

    @Test
    @SmallTest
    @DisabledTest(message = "https://crbug.com/1308065")
    public void testLoadNewUrlWithSession() throws Exception {
        final Context context = ApplicationProvider.getApplicationContext();
        final Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        CustomTabsSessionToken session = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        warmUpAndLaunchUrlWithSession(intent);
        assertEquals(getActivity().getIntentDataProvider().getSession(), session);
        Assert.assertFalse(
                "CustomTabContentHandler handled intent with wrong session",
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return getSessionDataHolder()
                                    .handleIntent(
                                            CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                                                    context, mTestPage2));
                        }));
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            ChromeTabUtils.getUrlStringOnUiThread(getActivity().getActivityTab()),
                            is(mTestPage));
                });
        Assert.assertTrue(
                "CustomTabContentHandler can't handle intent with same session",
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            intent.setData(Uri.parse(mTestPage2));
                            return getSessionDataHolder().handleIntent(intent);
                        }));
        final Tab tab = getActivity().getActivityTab();
        final CallbackHelper pageLoadFinishedHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(
                            new EmptyTabObserver() {
                                @Override
                                public void onPageLoadFinished(Tab tab, GURL url) {
                                    pageLoadFinishedHelper.notifyCalled();
                                }
                            });
                });
        pageLoadFinishedHelper.waitForCallback(0);
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            ChromeTabUtils.getUrlStringOnUiThread(getActivity().getActivityTab()),
                            is(mTestPage2));
                });
    }

    @Test
    @SmallTest
    public void testCreateNewTab() throws Exception {
        final String testUrl =
                mTestServer.getURL("/chrome/test/data/android/customtabs/test_window_open.html");
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                        ApplicationProvider.getApplicationContext(), testUrl));
        final TabModelSelector tabSelector =
                mCustomTabActivityTestRule.getActivity().getTabModelSelector();

        final CallbackHelper openTabHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tabSelector
                            .getModel(false)
                            .addObserver(
                                    new TabModelObserver() {
                                        @Override
                                        public void didAddTab(
                                                Tab tab,
                                                @TabLaunchType int type,
                                                @TabCreationState int creationState,
                                                boolean markedForSelection) {
                                            openTabHelper.notifyCalled();
                                        }
                                    });
                });
        DOMUtils.clickNode(mCustomTabActivityTestRule.getWebContents(), "new_window");

        openTabHelper.waitForCallback(0, 1);
        assertEquals(
                "A new tab should have been created.", 2, tabSelector.getModel(false).getCount());
    }

    @Test
    @SmallTest
    public void testReferrerAddedAutomatically() throws Exception {
        final Context context = getInstrumentation().getTargetContext().getApplicationContext();
        final Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage2);
        final CustomTabsSessionToken session = warmUpAndLaunchUrlWithSession(intent);
        assertEquals(getActivity().getIntentDataProvider().getSession(), session);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        String packageName = context.getPackageName();
        final String referrer =
                IntentHandler.constructValidReferrerForAuthority(packageName).getUrl();
        assertEquals(referrer, connection.getDefaultReferrerForSession(session).getUrl());

        final Tab tab = getActivity().getActivityTab();
        final CallbackHelper pageLoadFinishedHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(
                            new EmptyTabObserver() {
                                @Override
                                public void onLoadUrl(
                                        Tab tab,
                                        LoadUrlParams params,
                                        LoadUrlResult loadUrlResult) {
                                    assertEquals(referrer, params.getReferrer().getUrl());
                                }

                                @Override
                                public void onPageLoadFinished(Tab tab, GURL url) {
                                    pageLoadFinishedHelper.notifyCalled();
                                }
                            });
                });
        Assert.assertTrue(
                "CustomTabContentHandler can't handle intent with same session",
                ThreadUtils.runOnUiThreadBlocking(
                        () -> getSessionDataHolder().handleIntent(intent)));
        pageLoadFinishedHelper.waitForCallback(0);
    }

    @Test
    @SmallTest
    public void testVerifiedReferrer() throws Exception {
        final Context context = getInstrumentation().getTargetContext().getApplicationContext();
        final Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage2);
        String referrer = "https://example.com";
        intent.putExtra(Intent.EXTRA_REFERRER_NAME, referrer);
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(token, "app1");
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        ChromeOriginVerifier.addVerificationOverride(
                                "app1",
                                Origin.create(referrer),
                                CustomTabsService.RELATION_USE_AS_ORIGIN));

        final CustomTabsSessionToken session = warmUpAndLaunchUrlWithSession(intent);
        assertEquals(getActivity().getIntentDataProvider().getSession(), session);

        final Tab tab = getActivity().getActivityTab();
        final CallbackHelper pageLoadFinishedHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tab.addObserver(
                            new EmptyTabObserver() {
                                @Override
                                public void onLoadUrl(
                                        Tab tab,
                                        LoadUrlParams params,
                                        LoadUrlResult loadUrlResult) {
                                    assertEquals(referrer, params.getReferrer().getUrl());
                                }

                                @Override
                                public void onPageLoadFinished(Tab tab, GURL url) {
                                    pageLoadFinishedHelper.notifyCalled();
                                }
                            });
                });
        Assert.assertTrue(
                "CustomTabContentHandler can't handle intent with same session",
                ThreadUtils.runOnUiThreadBlocking(
                        () -> getSessionDataHolder().handleIntent(intent)));
        pageLoadFinishedHelper.waitForCallback(0);
    }

    /** Tests that the navigation callbacks are sent. */
    @Test
    @SmallTest
    public void testCallbacksAreSent() throws Exception {
        final Semaphore navigationStartSemaphore = new Semaphore(0);
        final Semaphore navigationFinishedSemaphore = new Semaphore(0);
        CustomTabsSession session =
                CustomTabsTestUtils.bindWithCallback(
                                new CustomTabsCallback() {
                                    @Override
                                    public void onNavigationEvent(
                                            int navigationEvent, Bundle extras) {
                                        Assert.assertNotEquals(
                                                CustomTabsCallback.NAVIGATION_FAILED,
                                                navigationEvent);
                                        Assert.assertNotEquals(
                                                CustomTabsCallback.NAVIGATION_ABORTED,
                                                navigationEvent);
                                        if (navigationEvent
                                                == CustomTabsCallback.NAVIGATION_STARTED) {
                                            navigationStartSemaphore.release();
                                        } else if (navigationEvent
                                                == CustomTabsCallback.NAVIGATION_FINISHED) {
                                            navigationFinishedSemaphore.release();
                                        }
                                    }
                                })
                        .session;
        Intent intent = new CustomTabsIntent.Builder(session).build().intent;
        intent.setData(Uri.parse(mTestPage));
        intent.setComponent(
                new ComponentName(
                        ApplicationProvider.getApplicationContext(), ChromeLauncherActivity.class));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        Assert.assertTrue(
                navigationStartSemaphore.tryAcquire(TIMEOUT_PAGE_LOAD_SECONDS, TimeUnit.SECONDS));
        Assert.assertTrue(
                navigationFinishedSemaphore.tryAcquire(
                        TIMEOUT_PAGE_LOAD_SECONDS, TimeUnit.SECONDS));
    }

    /** Tests that page load metrics are sent. */
    @Test
    @MediumTest
    public void testPageLoadMetricsAreSent() throws Exception {
        checkPageLoadMetrics(true);
    }

    /** Tests that page load metrics are not sent when the client is not allowlisted. */
    @Test
    @MediumTest
    public void testPageLoadMetricsAreNotSentByDefault() throws Exception {
        checkPageLoadMetrics(false);
    }

    private static void assertSuffixedHistogramTotalCount(long expected, String histogramPrefix) {
        for (String suffix : new String[] {".ZoomedIn", ".ZoomedOut"}) {
            assertEquals(
                    expected,
                    RecordHistogram.getHistogramTotalCountForTesting(histogramPrefix + suffix));
        }
    }

    /**
     * Tests that one navigation in a custom tab records the histograms reflecting time from intent
     * to first navigation start/commit.
     */
    @Test
    @SmallTest
    public void testNavigationHistogramsRecorded() throws Exception {
        String startHistogramPrefix = "CustomTabs.IntentToFirstNavigationStartTime";
        String commitHistogramPrefix = "CustomTabs.IntentToFirstCommitNavigationTime3";
        assertSuffixedHistogramTotalCount(0, startHistogramPrefix);
        assertSuffixedHistogramTotalCount(0, commitHistogramPrefix);

        final Semaphore semaphore = new Semaphore(0);
        CustomTabsSession session =
                CustomTabsTestUtils.bindWithCallback(
                                new CustomTabsCallback() {
                                    @Override
                                    public void onNavigationEvent(
                                            int navigationEvent, Bundle extras) {
                                        if (navigationEvent
                                                == CustomTabsCallback.NAVIGATION_FINISHED) {
                                            semaphore.release();
                                        }
                                    }
                                })
                        .session;
        Intent intent = new CustomTabsIntent.Builder(session).build().intent;
        intent.setData(Uri.parse(mTestPage));
        intent.setComponent(
                new ComponentName(
                        ApplicationProvider.getApplicationContext(), ChromeLauncherActivity.class));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        Assert.assertTrue(semaphore.tryAcquire(TIMEOUT_PAGE_LOAD_SECONDS, TimeUnit.SECONDS));

        assertSuffixedHistogramTotalCount(1, startHistogramPrefix);
        assertSuffixedHistogramTotalCount(1, commitHistogramPrefix);
    }

    /** Tests that TITLE_ONLY state works as expected with a title getting set onload. */
    @Test
    @SmallTest
    public void testToolbarTitleOnlyStateWithProperTitle() throws Exception {
        TestWebServer webServer = TestWebServer.start();
        final String url = webServer.setResponse("/test.html", ONLOAD_TITLE_CHANGE, null);
        hideDomainAndEnsureTitleIsSet(url, false, "nytimes.com");
        webServer.shutdown();
    }

    /**
     * Tests that TITLE_ONLY state works as expected with a title getting set during prerendering.
     */
    @Test
    @SmallTest
    public void testToolbarTitleOnlyStateWithProperTitleHiddenTab() throws Exception {
        TestWebServer webServer = TestWebServer.start();
        final String url = webServer.setResponse("/test.html", ONLOAD_TITLE_CHANGE, null);
        hideDomainAndEnsureTitleIsSet(url, true, "nytimes.com");
        webServer.shutdown();
    }

    /**
     * Tests that TITLE_ONLY state works as expected with a title getting set delayed after load.
     */
    @Test
    @SmallTest
    public void testToolbarTitleOnlyStateWithDelayedTitle() throws Exception {
        TestWebServer webServer = TestWebServer.start();
        final String url = webServer.setResponse("/test.html", DELAYED_TITLE_CHANGE, null);
        hideDomainAndEnsureTitleIsSet(url, false, "nytimes.com");
        webServer.shutdown();
    }

    private void hideDomainAndEnsureTitleIsSet(
            final String url, boolean useHiddenTab, final String expectedTitle) throws Exception {
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        Context context = ApplicationProvider.getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
        intent.putExtra(
                CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.SHOW_PAGE_TITLE);
        final CustomTabsSessionToken token =
                CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        Assert.assertTrue(connection.newSession(token));
        connection.mClientManager.setHideDomainForSession(token, true);

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

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        CriteriaHelper.pollUiThread(
                () -> {
                    final Tab currentTab =
                            mCustomTabActivityTestRule.getActivity().getActivityTab();
                    Criteria.checkThat(ChromeTabUtils.getUrlStringOnUiThread(currentTab), is(url));
                });
        CriteriaHelper.pollUiThread(
                () -> {
                    CustomTabToolbar toolbar =
                            mCustomTabActivityTestRule.getActivity().findViewById(R.id.toolbar);
                    TextView titleBar = toolbar.findViewById(R.id.title_bar);
                    Criteria.checkThat(titleBar, Matchers.notNullValue());
                    Criteria.checkThat(titleBar.isShown(), is(true));
                    Criteria.checkThat(titleBar.getText().toString(), is(expectedTitle));
                });
    }

    /**
     * Tests that when we use a pre-created renderer, the page loaded is the only one in the
     * navigation history.
     */
    @Test
    @SmallTest
    public void testPrecreatedRenderer() throws Exception {
        CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        Context context = ApplicationProvider.getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        Assert.assertTrue(connection.newSession(token));
        // Forcing no hidden tab implies falling back to simply creating a spare WebContents.
        setCanUseHiddenTabForSession(connection, token, false);
        Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(mTestPage), null, null));
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    final Tab currentTab =
                            mCustomTabActivityTestRule.getActivity().getActivityTab();
                    Criteria.checkThat(
                            ChromeTabUtils.getUrlStringOnUiThread(currentTab), is(mTestPage));
                });

        Assert.assertFalse(mCustomTabActivityTestRule.getActivity().getActivityTab().canGoBack());
        Assert.assertFalse(
                mCustomTabActivityTestRule.getActivity().getActivityTab().canGoForward());

        List<HistoryItem> history =
                getHistory(mCustomTabActivityTestRule.getActivity().getActivityTab());
        assertEquals(1, history.size());
        assertEquals(mTestPage, history.get(0).getUrl().getSpec());
    }

    /** Tests that calling warmup() is optional without prerendering. */
    @Test
    @SmallTest
    public void testMayLaunchUrlWithoutWarmupNoSpeculation() {
        mayLaunchUrlWithoutWarmup(false);
    }

    /** Tests that calling mayLaunchUrl() without warmup() succeeds. */
    @Test
    @SmallTest
    public void testMayLaunchUrlWithoutWarmupHiddenTab() {
        mayLaunchUrlWithoutWarmup(true);
    }

    /**
     * Tests that launching a regular Chrome tab after warmup() gives the right layout.
     *
     * <p>About the restrictions and switches: No FRE and no document mode to get a
     * ChromeTabbedActivity, and no tablets to have the tab switcher button.
     *
     * <p>Non-regression test for crbug.com/547121. @SmallTest Disabled for flake:
     * https://crbug.com/692025.
     */
    @Test
    @DisabledTest(message = "crbug.com/692025")
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    @CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
    public void testWarmupAndLaunchRegularChrome() throws Exception {
        CustomTabsTestUtils.warmUpAndWait();
        Intent intent =
                new Intent(
                        ApplicationProvider.getApplicationContext(), ChromeLauncherActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        Instrumentation.ActivityMonitor monitor =
                getInstrumentation().addMonitor(ChromeTabbedActivity.class.getName(), null, false);
        Activity activity = getInstrumentation().startActivitySync(intent);
        Assert.assertNotNull("Main activity did not start", activity);
        ChromeTabbedActivity tabbedActivity =
                (ChromeTabbedActivity)
                        monitor.waitForActivityWithTimeout(
                                ChromeActivityTestRule.getActivityStartTimeoutMs());
        Assert.assertNotNull("ChromeTabbedActivity did not start", tabbedActivity);
        Assert.assertNotNull(
                "Should have a tab switcher button.",
                tabbedActivity.findViewById(R.id.tab_switcher_button));
    }

    /**
     * Tests that launching a Custom Tab after warmup() gives the right layout.
     *
     * <p>Non-regression test for crbug.com/547121.
     */
    @Test
    @SmallTest
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    @CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
    public void testWarmupAndLaunchRightToolbarLayout() throws Exception {
        CustomTabsTestUtils.warmUpAndWait();
        mCustomTabActivityTestRule.startActivityCompletely(createMinimalCustomTabIntent());
        assertNull(
                "Should not have a tab switcher button.",
                getActivity().findViewById(R.id.tab_switcher_button));
    }

    /**
     * Tests the following scenario: - warmup() +
     * mayLaunchUrl("http://example.com/page.html#first-fragment") -
     * loadUrl("http://example.com/page.html#other-fragment")
     *
     * <p>The expected behavior is that the hidden tab shouldn't be dropped, and that the fragment
     * is updated.
     */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHiddenTabAndChangingFragmentIgnoreFragments() throws Exception {
        startHiddenTabAndChangeFragment(true, true);
    }

    /** Same as above, but the hidden tab matching should not ignore the fragment. */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHiddenTabAndChangingFragmentDontIgnoreFragments() throws Exception {
        startHiddenTabAndChangeFragment(false, true);
    }

    /** Same as above, hidden tab matching ignores the fragment, don't wait. */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    @DisabledTest(message = "https://crbug.com/1148544")
    public void testHiddenTabAndChangingFragmentDontWait() throws Exception {
        startHiddenTabAndChangeFragment(true, false);
    }

    /** Same as above, hidden tab matching doesn't ignore the fragment, don't wait. */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHiddenTabAndChangingFragmentDontWaitDrop() throws Exception {
        startHiddenTabAndChangeFragment(false, false);
    }

    @Test
    @SmallTest
    public void testParallelRequest() throws Exception {
        String url = mTestServer.getURL("/echoheader?Cookie");
        Uri requestUri = Uri.parse(mTestServer.getURL("/set-cookie?acookie"));

        final Context context = ApplicationProvider.getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
        final CustomTabsSessionToken token =
                CustomTabsSessionToken.getSessionTokenFromIntent(intent);

        // warmup(), create session, allow parallel requests, allow origin.
        CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        final Origin origin = Origin.create(requestUri);
        Assert.assertTrue(connection.newSession(token));
        connection.mClientManager.setAllowParallelRequestForSession(token, true);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ChromeOriginVerifier.addVerificationOverride(
                            context.getPackageName(),
                            origin,
                            CustomTabsService.RELATION_USE_AS_ORIGIN);
                });

        intent.putExtra(CustomTabsConnection.PARALLEL_REQUEST_URL_KEY, requestUri);
        intent.putExtra(
                CustomTabsConnection.PARALLEL_REQUEST_REFERRER_KEY, Uri.parse(origin.toString()));
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();
        String content =
                JavaScriptUtils.executeJavaScriptAndWaitForResult(
                        tab.getWebContents(), "document.body.textContent");
        assertEquals("\"acookie\"", content);
    }

    /**
     * Tests the following scenario: - warmup() +
     * mayLaunchUrl("http://example.com/page.html#first-fragment") -
     * loadUrl("http://example.com/page.html#other-fragment")
     *
     * <p>There are two parameters changing the behavior:
     *
     * @param ignoreFragments Whether the hidden tab should be kept.
     * @param wait Whether to wait for the hidden tab to load.
     */
    private void startHiddenTabAndChangeFragment(boolean ignoreFragments, boolean wait)
            throws Exception {
        String testUrl = mTestServer.getURL(FRAGMENT_TEST_PAGE);
        String initialFragment = "#test";
        String initialUrl = testUrl + initialFragment;
        String fragment = "#yeah";
        String urlWithFragment = testUrl + fragment;

        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, urlWithFragment);
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        connection.newSession(token);
        connection.setIgnoreUrlFragmentsForSession(token, ignoreFragments);
        setCanUseHiddenTabForSession(connection, token, true);
        Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(initialUrl), null, null));

        if (wait) {
            CustomTabsTestUtils.ensureCompletedSpeculationForUrl(connection, initialUrl);
        }

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        final Tab tab = getActivity().getActivityTab();

        if (wait) {
            ElementContentCriteria initialVisibilityCriteria =
                    new ElementContentCriteria(
                            tab, "visibility", ignoreFragments ? "hidden" : "visible");
            ElementContentCriteria initialFragmentCriteria =
                    new ElementContentCriteria(
                            tab, "initial-fragment", ignoreFragments ? initialFragment : fragment);
            ElementContentCriteria fragmentCriteria =
                    new ElementContentCriteria(tab, "fragment", fragment);
            // The tab hasn't been reloaded.
            CriteriaHelper.pollInstrumentationThread(initialVisibilityCriteria, 2000, 200);
            // No reload (initial fragment is correct).
            CriteriaHelper.pollInstrumentationThread(initialFragmentCriteria, 2000, 200);
            if (ignoreFragments) {
                CriteriaHelper.pollInstrumentationThread(fragmentCriteria, 2000, 200);
            }
        } else {
            CriteriaHelper.pollInstrumentationThread(
                    new ElementContentCriteria(tab, "initial-fragment", fragment), 2000, 200);
        }

        Assert.assertFalse(tab.canGoForward());
        Assert.assertFalse(tab.canGoBack());

        // TODO(crbug.com/40276951):
        // Fragment misses will trigger two history entries
        // - url#speculated and url#actual are both inserted
        // This should ideally not be the case.
    }

    /**
     * Test whether the url shown on hidden tab gets updated from about:blank when it completes in
     * the background.
     */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHiddenTabCorrectUrl() throws Exception {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
        connection.newSession(token);
        setCanUseHiddenTabForSession(connection, token, true);
        Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(mTestPage), null, null));
        CustomTabsTestUtils.ensureCompletedSpeculationForUrl(connection, mTestPage);

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage));
        assertEquals(
                Uri.parse(mTestPage).getHost() + ":" + Uri.parse(mTestPage).getPort(),
                ((EditText) getActivity().findViewById(R.id.url_bar)).getText().toString());
    }

    /** Test that hidden tab speculation is not performed if 3rd party cookies are blocked. */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
    public void testHiddenTabThirdPartyCookiesBlocked() throws Exception {
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        final CustomTabsSessionToken token =
                CustomTabsSessionToken.createMockSessionTokenForTesting();
        connection.newSession(token);
        setCanUseHiddenTabForSession(connection, token, true);
        connection.warmup(0);

        // Needs the browser process to be initialized.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PrefService prefs = UserPrefs.get(ProfileManager.getLastUsedRegularProfile());
                    int old_block_pref = prefs.getInteger(COOKIE_CONTROLS_MODE);
                    prefs.setInteger(COOKIE_CONTROLS_MODE, CookieControlsMode.OFF);
                    Assert.assertTrue(connection.maySpeculate(token));
                    prefs.setInteger(COOKIE_CONTROLS_MODE, CookieControlsMode.BLOCK_THIRD_PARTY);
                    Assert.assertFalse(connection.maySpeculate(token));
                    prefs.setInteger(COOKIE_CONTROLS_MODE, old_block_pref);
                });
    }

    /** Test whether invalid urls are avoided for hidden tab. */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHiddenTabInvalidUrl() throws Exception {
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
        connection.newSession(token);
        setCanUseHiddenTabForSession(connection, token, true);
        Assert.assertFalse(
                connection.mayLaunchUrl(token, Uri.parse("chrome://version"), null, null));
    }

    /**
     * Tests that the activity knows there is already a child process when warmup() has been called.
     */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testAllocateChildConnectionWithWarmup() throws Exception {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
        connection.newSession(token);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage));
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        Assert.assertFalse(
                                "Warmup() should have allocated a child connection",
                                getActivity().shouldAllocateChildConnection()));
    }

    /** Tests that the activity knows there is no child process. */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testAllocateChildConnectionNoWarmup() {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        final CustomTabsConnection connection = CustomTabsConnection.getInstance();
        CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
        connection.newSession(token);

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage2));
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        Assert.assertTrue(
                                "No spare renderer available, should allocate a child connection.",
                                getActivity().shouldAllocateChildConnection()));
    }

    /** Tests that the activity knows there is already a child process with a hidden tab. */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testAllocateChildConnectionWithHiddenTab() throws Exception {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
        connection.newSession(token);
        setCanUseHiddenTabForSession(connection, token, true);
        Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(mTestPage), null, null));
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage));
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        Assert.assertFalse(
                                "Prerendering should have allocated a child connection",
                                getActivity().shouldAllocateChildConnection()));
    }

    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testRecreateSpareRendererOnTabClose() throws Exception {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        CustomTabsTestUtils.warmUpAndWait();

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertFalse(WarmupManager.getInstance().hasSpareWebContents());
                    final CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
                    activity.getComponent()
                            .resolveNavigationController()
                            .finish(FinishReason.OTHER);
                });
        if (ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_PREWARM_TAB)) {
            CriteriaHelper.pollUiThread(
                    () ->
                            WarmupManager.getInstance()
                                    .hasSpareTab(ProfileManager.getLastUsedRegularProfile()),
                    "No new spare tab");
        } else {
            CriteriaHelper.pollUiThread(
                    () -> WarmupManager.getInstance().hasSpareWebContents(),
                    "No new spare renderer");
        }
    }

    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testInteractionRecordedOnClose() throws Exception {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    final CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
                    activity.finish();
                });
        CriteriaHelper.pollUiThread(() -> getActivity().isDestroyed());

        Assert.assertTrue(
                "CUSTOM_TABS_LAST_CLOSE_TAB_INTERACTION not recorded.",
                ChromeSharedPreferences.getInstance()
                        .contains(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLOSE_TAB_INTERACTION));
        Assert.assertTrue(
                "CUSTOM_TABS_LAST_CLOSE_TIMESTAMP not recorded.",
                ChromeSharedPreferences.getInstance()
                        .contains(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLOSE_TIMESTAMP));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramTotalCountForTesting(
                        "CustomTabs.HadInteractionOnClose.Form"));
    }

    /**
     * Tests that hidden tab accepts a referrer, and that this is not lost when launching the Custom
     * Tab.
     */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHiddenTabWithReferrer() throws Exception {
        String referrer = "android-app://com.foo.me/";
        maybeSpeculateAndLaunchWithReferrers(
                mTestServer.getURL(FRAGMENT_TEST_PAGE), true, referrer, referrer);

        Tab tab = getActivity().getActivityTab();
        // The tab hasn't been reloaded.
        CriteriaHelper.pollInstrumentationThread(
                new ElementContentCriteria(tab, "visibility", "hidden"), 2000, 200);
        // The Referrer is correctly set.
        CriteriaHelper.pollInstrumentationThread(
                new TabsOpenedFromExternalAppTest.ReferrerCriteria(tab, referrer), 2000, 200);
    }

    /**
     * Tests that hidden tab accepts a referrer, and that this is dropped when the tab is launched
     * with a mismatched referrer.
     */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHiddenTabWithMismatchedReferrers() throws Exception {
        String prerenderReferrer = "android-app://com.foo.me/";
        String launchReferrer = "android-app://com.foo.me.i.changed.my.mind/";
        maybeSpeculateAndLaunchWithReferrers(
                mTestServer.getURL(FRAGMENT_TEST_PAGE), true, prerenderReferrer, launchReferrer);

        Tab tab = getActivity().getActivityTab();
        // Prerender has been dropped.
        CriteriaHelper.pollInstrumentationThread(
                new ElementContentCriteria(tab, "visibility", "visible"), 2000, 200);
        // The Referrer is correctly set.
        CriteriaHelper.pollInstrumentationThread(
                new TabsOpenedFromExternalAppTest.ReferrerCriteria(tab, launchReferrer), 2000, 200);
    }

    /** Tests that a client can set a referrer, without speculating. */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testClientCanSetReferrer() throws Exception {
        String referrerUrl = "android-app://com.foo.me/";
        maybeSpeculateAndLaunchWithReferrers(mTestPage, false, null, referrerUrl);

        Tab tab = getActivity().getActivityTab();
        // The Referrer is correctly set.
        CriteriaHelper.pollInstrumentationThread(
                new TabsOpenedFromExternalAppTest.ReferrerCriteria(tab, referrerUrl), 2000, 200);
    }

    @Test
    @MediumTest
    @DisabledTest(message = "see crbug.com/1361534")
    public void testLaunchIncognitoURL() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        final CustomTabActivity cctActivity = mCustomTabActivityTestRule.getActivity();
        final CallbackHelper mCctHiddenCallback = new CallbackHelper();
        final CallbackHelper mTabbedModeShownCallback = new CallbackHelper();
        final AtomicReference<ChromeTabbedActivity> tabbedActivity = new AtomicReference<>();

        ActivityStateListener listener =
                (activity, newState) -> {
                    if (activity == cctActivity
                            && (newState == ActivityState.STOPPED
                                    || newState == ActivityState.DESTROYED)) {
                        mCctHiddenCallback.notifyCalled();
                    }

                    if (activity instanceof ChromeTabbedActivity
                            && newState == ActivityState.RESUMED) {
                        mTabbedModeShownCallback.notifyCalled();
                        tabbedActivity.set((ChromeTabbedActivity) activity);
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> ApplicationStatus.registerStateListenerForAllActivities(listener));

        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    TabTestUtils.openNewTab(
                            cctActivity.getActivityTab(),
                            new GURL("about:blank"),
                            null,
                            null,
                            WindowOpenDisposition.OFF_THE_RECORD,
                            false);
                });

        mCctHiddenCallback.waitForCallback("CCT not hidden.", 0);
        mTabbedModeShownCallback.waitForCallback("Tabbed mode not shown.", 0);

        CriteriaHelper.pollUiThread(() -> tabbedActivity.get().areTabModelsInitialized());

        CriteriaHelper.pollUiThread(
                () -> {
                    Tab tab = tabbedActivity.get().getActivityTab();
                    Criteria.checkThat("Tab is null", tab, Matchers.notNullValue());
                    Criteria.checkThat("Incognito tab not selected", tab.isIncognito(), is(true));
                    Criteria.checkThat(
                            "Wrong URL loaded in incognito tab",
                            ChromeTabUtils.getUrlStringOnUiThread(tab),
                            is("about:blank"));
                });

        ThreadUtils.runOnUiThreadBlocking(
                () -> ApplicationStatus.unregisterActivityStateListener(listener));
    }

    /** Maybe prerenders a URL with a referrer, then launch it with another one. */
    private void maybeSpeculateAndLaunchWithReferrers(
            String url, boolean useHiddenTab, String speculationReferrer, String launchReferrer)
            throws Exception {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
        if (useHiddenTab) {
            CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
            CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
            connection.newSession(token);
            setCanUseHiddenTabForSession(connection, token, true);
            Bundle extras = null;
            if (speculationReferrer != null) {
                extras = new Bundle();
                extras.putParcelable(Intent.EXTRA_REFERRER, Uri.parse(speculationReferrer));
            }
            Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(url), extras, null));
            CustomTabsTestUtils.ensureCompletedSpeculationForUrl(connection, url);
        }

        if (launchReferrer != null) {
            intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(launchReferrer));
        }
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
    }

    /**
     * Test intended to verify the way we test history is correct. Start an activity and then
     * navigate to a different url. We test NavigationController behavior through canGoBack/Forward
     * as well as browser history through an HistoryProvider.
     */
    @Test
    @SmallTest
    public void testHistoryNoSpeculation() throws Exception {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        final Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();
        ThreadUtils.runOnUiThreadBlocking(
                (Runnable) () -> tab.loadUrl(new LoadUrlParams(mTestPage2)));
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage2);

        Assert.assertTrue(tab.canGoBack());
        Assert.assertFalse(tab.canGoForward());

        List<HistoryItem> history = getHistory(tab);
        assertEquals(2, history.size());
        assertEquals(mTestPage2, history.get(0).getUrl().getSpec());
        assertEquals(mTestPage, history.get(1).getUrl().getSpec());
    }

    /**
     * The following test that history only has a single final page after speculation, whether it
     * was a hit or a miss.
     */
    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHistoryAfterHiddenTabHit() throws Exception {
        verifyHistoryAfterHiddenTab(true);
    }

    @Test
    @SmallTest
    @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    public void testHistoryAfterHiddenTabMiss() throws Exception {
        verifyHistoryAfterHiddenTab(false);
    }

    @Test
    @SmallTest
    @DisabledTest(message = "https://crbug.com/1148544")
    public void closeButton_navigatesToLandingPage() throws TimeoutException {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                        context, mTestServer.getURL(TARGET_BLANK_TEST_PAGE));
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
        Assert.assertEquals(activity.getCurrentTabModel().getCount(), 1);

        // The link we click will take us to another page. Set the initial page as the landing page.
        activity.getComponent()
                .resolveNavigationController()
                .setLandingPageOnCloseCriterion(url -> url.contains(TARGET_BLANK_TEST_PAGE));

        DOMUtils.clickNode(activity.getActivityTab().getWebContents(), "target_blank_link");
        CriteriaHelper.pollUiThread(
                () -> Criteria.checkThat(activity.getCurrentTabModel().getCount(), is(2)));

        ClickUtils.clickButton(activity.findViewById(R.id.close_button));

        CriteriaHelper.pollUiThread(
                () -> Criteria.checkThat(activity.getCurrentTabModel().getCount(), is(1)));
        assertFalse(activity.isFinishing());
    }

    @Test
    @SmallTest
    public void closeButton_closesActivityIfNoLandingPage() throws TimeoutException {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                        context, mTestServer.getURL(TARGET_BLANK_TEST_PAGE));
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
        Assert.assertEquals(activity.getCurrentTabModel().getCount(), 1);

        DOMUtils.clickNode(activity.getActivityTab().getWebContents(), "target_blank_link");
        CriteriaHelper.pollUiThread(
                () -> Criteria.checkThat(activity.getCurrentTabModel().getCount(), is(2)));

        ClickUtils.clickButton(activity.findViewById(R.id.close_button));

        CriteriaHelper.pollUiThread(activity::isFinishing);
    }

    // The flag is necessary to ensure the experiment id 101 is honored.
    @Test
    @SmallTest
    @CommandLineFlags.Add({
        "enable-features=ExternalExperimentAllowlist:101/x",
    })
    public void testExperimentIds() throws Exception {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        intent.setData(Uri.parse(mTestPage));
        int[] ids = {101};
        intent.putExtra(CustomTabIntentDataProvider.EXPERIMENT_IDS, ids);
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(
                token, "com.google.android.googlequicksearchbox");
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    assertTrue(CustomTabsTestUtils.hasVariationId(101));
                });
    }

    // The flag is necessary to ensure the experiment id 101 is honored.
    @Test
    @SmallTest
    @CommandLineFlags.Add({
        "enable-features=ExternalExperimentAllowlist:101/x",
    })
    public void testExperimentIdsFromMayLaunchUrl() throws Exception {
        CallbackHelper warmupHelper = new CallbackHelper();
        CustomTabsSession session =
                CustomTabsTestUtils.bindWithCallback(
                                new CustomTabsCallback() {
                                    @Override
                                    public void onWarmupCompleted(Bundle extras) {
                                        warmupHelper.notifyCalled();
                                    }
                                })
                        .session;
        Intent intent = new CustomTabsIntent.Builder(session).build().intent;
        intent.setData(Uri.parse(mTestPage));
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(
                token, "com.google.android.googlequicksearchbox");
        Bundle extrasBundle = new Bundle();
        int[] ids = {101};
        extrasBundle.putIntArray(CustomTabIntentDataProvider.EXPERIMENT_IDS, ids);
        Assert.assertTrue(
                connection.mayLaunchUrl(
                        token, Uri.parse("https://www.google.com"), extrasBundle, null));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    assertTrue(CustomTabsTestUtils.hasVariationId(101));
                });

        warmupHelper.waitForCallback(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    WarmupManager.getInstance().destroySpareTab();
                });
    }

    @Test
    @SmallTest
    @CommandLineFlags.Add({
        ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1",
        "ignore-certificate-errors",
        "ignore-google-port-numbers"
    })
    public void testMayLaunchUrlAddsClientDataHeader() throws Exception {
        TestWebServer webServer = TestWebServer.start();
        webServer.setServerHost("www.google.com");
        final String expectedHeader = "test-header";
        String url = webServer.setResponse("/ok.html", "<html>ok</html>", null);
        AppHooks.setInstanceForTesting(
                new AppHooksImpl() {
                    @Override
                    public CustomTabsConnection createCustomTabsConnection() {
                        return new CustomTabsConnection() {
                            @Override
                            public void setClientDataHeaderForNewTab(
                                    CustomTabsSessionToken session, WebContents webContents) {
                                setClientDataHeader(webContents, expectedHeader);
                            }
                        };
                    }
                });
        CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        connection.newSession(token);
        setCanUseHiddenTabForSession(connection, token, true);
        Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(url), null, null));
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            connection.getHiddenTabForTesting(), Matchers.notNullValue());
                });
        Tab hiddenTab =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return connection.getHiddenTabForTesting();
                        });
        ChromeTabUtils.waitForTabPageLoaded(hiddenTab, url);
        String actualHeader = webServer.getLastRequest("/ok.html").headerValue("X-CCT-Client-Data");
        assertEquals(expectedHeader, actualHeader);
        webServer.shutdown();
    }

    @Test
    @SmallTest
    public void testCanHideBrowserControls_notPartial() throws Exception {
        CustomTabsSessionToken session = warmUpAndLaunchUrlWithSession();
        assertEquals(getActivity().getIntentDataProvider().getSession(), session);
        assertOverlayPanelCanHideAndroidBrowserControls(true);
    }

    @Test
    @SmallTest
    @Restriction(DeviceRestriction.RESTRICTION_TYPE_NON_AUTO)
    @EnableFeatures({ChromeFeatureList.CCT_RESIZABLE_FOR_THIRD_PARTIES})
    public void testLaunchPartialCustomTabActivity_BottomSheet() throws Exception {
        doTestLaunchPartialCustomTabWithInitialHeight();
    }

    private void doTestLaunchPartialCustomTabWithInitialHeight() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(token, "org.chromium.testapp");
        intent.putExtra(EXTRA_INITIAL_ACTIVITY_HEIGHT_PX, 50);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        // A Normal CCT height is set to MATCH_PARENT while Partial CCT has non-zero value.
        int fullHeight = ViewGroup.LayoutParams.MATCH_PARENT;
        WindowManager.LayoutParams attrs = getActivity().getWindow().getAttributes();
        assertNotEquals("The window should have non-full height", fullHeight, attrs.height);

        assert (mCustomTabActivityTestRule.getActivity().getRootUiCoordinatorForTesting()
                instanceof BaseCustomTabRootUiCoordinator);
        BaseCustomTabRootUiCoordinator coordinator =
                (BaseCustomTabRootUiCoordinator)
                        mCustomTabActivityTestRule.getActivity().getRootUiCoordinatorForTesting();

        PartialCustomTabDisplayManager displayManager =
                (PartialCustomTabDisplayManager) coordinator.getCustomTabSizeStrategyForTesting();
        assertEquals(
                "Incorrect strategy type with display manager.",
                PartialCustomTabBaseStrategy.PartialCustomTabType.BOTTOM_SHEET,
                displayManager.getActiveStrategyType());

        // Verify the hierarchy of the enclosing layouts that PCCT relies on for its operation.
        CallbackHelper eventHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    getActivity()
                            .getCompositorViewHolderSupplier()
                            .addObserver(
                                    cvh -> {
                                        if (cvh == null) return;
                                        assertTrue(
                                                "CoordinatorLayoutForPointer should be the parent"
                                                        + " of CompositorViewHolder",
                                                cvh.getParent()
                                                        instanceof CoordinatorLayoutForPointer);
                                        assertTrue(
                                                "ContentFrameLayout should be the parent of"
                                                        + " CoodinatorLayoutForPointer",
                                                cvh.getParent().getParent()
                                                        instanceof ContentFrameLayout);
                                        assertNotEquals(
                                                "The window should have non-zero y", 0, attrs.y);
                                        eventHelper.notifyCalled();
                                    });
                });
        eventHelper.waitForCallback(0);
    }

    @Test
    @SmallTest
    @Restriction(DeviceRestriction.RESTRICTION_TYPE_NON_AUTO)
    @EnableFeatures({ChromeFeatureList.CCT_RESIZABLE_FOR_THIRD_PARTIES})
    @MinAndroidSdkLevel(Build.VERSION_CODES.O_MR1)
    public void testLaunchPartialCustomTabActivity_SideSheet() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(token, "org.chromium.testapp");
        intent.putExtra(EXTRA_ACTIVITY_SIDE_SHEET_BREAKPOINT_DP, 100);
        intent.putExtra(EXTRA_INITIAL_ACTIVITY_WIDTH_PX, 300);
        intent.putExtra(EXTRA_ACTIVITY_SIDE_SHEET_ENABLE_MAXIMIZATION, true);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        rotateCustomTabActivity(
                mCustomTabActivityTestRule.getActivity(), Layout.Orientation.LANDSCAPE);

        assert (mCustomTabActivityTestRule.getActivity().getRootUiCoordinatorForTesting()
                instanceof BaseCustomTabRootUiCoordinator);
        BaseCustomTabRootUiCoordinator coordinator =
                (BaseCustomTabRootUiCoordinator)
                        mCustomTabActivityTestRule.getActivity().getRootUiCoordinatorForTesting();
        assert (coordinator.getCustomTabSizeStrategyForTesting()
                instanceof PartialCustomTabDisplayManager);
        PartialCustomTabDisplayManager displayManager =
                (PartialCustomTabDisplayManager) coordinator.getCustomTabSizeStrategyForTesting();
        if (displayManager.getActiveStrategyType()
                != PartialCustomTabBaseStrategy.PartialCustomTabType.SIDE_SHEET) {
            return;
        }

        onViewWaiting(allOf(withId(R.id.custom_tabs_sidepanel_maximize), isDisplayed()));
        View toolbarView = mCustomTabActivityTestRule.getActivity().findViewById(R.id.toolbar);
        CustomTabToolbar toolbar = (CustomTabToolbar) toolbarView;
        ImageButton maximizeButton = toolbar.getMaximizeButtonForTest();
        Assert.assertNotNull(maximizeButton);

        // A Normal CCT width is set to MATCH_PARENT while Partial CCT has non-zero value.
        int fullWidth = ViewGroup.LayoutParams.MATCH_PARENT;
        WindowManager.LayoutParams attrs = getActivity().getWindow().getAttributes();
        assertNotEquals("The window should have non-full width", fullWidth, attrs.width);

        int maxWidth = getActivity().getResources().getDisplayMetrics().widthPixels;
        ThreadUtils.runOnUiThreadBlocking((Runnable) maximizeButton::performClick);
        assertEquals("The window should be max width", maxWidth, attrs.width);

        ThreadUtils.runOnUiThreadBlocking((Runnable) maximizeButton::performClick);
        onViewWaiting(allOf(withId(R.id.custom_tabs_sidepanel_maximize), isDisplayed()));

        assertNotEquals("The window should not be max width", maxWidth, attrs.width);
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.CCT_RESIZABLE_FOR_THIRD_PARTIES})
    // Screen rotation is not relevant on automotive.
    @Restriction(DeviceRestriction.RESTRICTION_TYPE_NON_AUTO)
    @MinAndroidSdkLevel(Build.VERSION_CODES.O_MR1)
    // Bug in O that's been fixed in 8.1
    // https://issuetracker.google.com/issues/68427483
    public void testLaunchPartialCustomTabActivity_Transition() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(token, "org.chromium.testapp");
        intent.putExtra(EXTRA_ACTIVITY_SIDE_SHEET_BREAKPOINT_DP, 600);
        intent.putExtra(EXTRA_INITIAL_ACTIVITY_HEIGHT_PX, 300);
        intent.putExtra(EXTRA_INITIAL_ACTIVITY_WIDTH_PX, 300);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        rotateCustomTabActivity(
                mCustomTabActivityTestRule.getActivity(), Layout.Orientation.PORTRAIT);

        DisplayMetrics displayMetrics = getActivity().getResources().getDisplayMetrics();
        int devicePortraitWidthDp = (int) (displayMetrics.widthPixels / displayMetrics.density);
        int deviceLandscapeWidthDp = (int) (displayMetrics.heightPixels / displayMetrics.density);

        assert (mCustomTabActivityTestRule.getActivity().getRootUiCoordinatorForTesting()
                instanceof BaseCustomTabRootUiCoordinator);
        BaseCustomTabRootUiCoordinator coordinator =
                (BaseCustomTabRootUiCoordinator)
                        mCustomTabActivityTestRule.getActivity().getRootUiCoordinatorForTesting();
        assert (coordinator.getCustomTabSizeStrategyForTesting()
                instanceof PartialCustomTabDisplayManager);
        PartialCustomTabDisplayManager displayManager =
                (PartialCustomTabDisplayManager) coordinator.getCustomTabSizeStrategyForTesting();

        assertSideSheetOrBottomSheet(displayManager, devicePortraitWidthDp);
        rotateCustomTabActivity(
                mCustomTabActivityTestRule.getActivity(), Layout.Orientation.LANDSCAPE);
        assertSideSheetOrBottomSheet(displayManager, deviceLandscapeWidthDp);
        rotateCustomTabActivity(
                mCustomTabActivityTestRule.getActivity(), Layout.Orientation.PORTRAIT);
        assertSideSheetOrBottomSheet(displayManager, devicePortraitWidthDp);
    }

    @Test
    @SmallTest
    @Restriction(DeviceRestriction.RESTRICTION_TYPE_NON_AUTO)
    @EnableFeatures({ChromeFeatureList.CCT_RESIZABLE_FOR_THIRD_PARTIES})
    public void testLaunchPartialCustomTabActivity_FullSize() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsConnection.getInstance();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(token, "org.chromium.testapp");
        intent.putExtra(EXTRA_INITIAL_ACTIVITY_HEIGHT_PX, 300);
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        assert (mCustomTabActivityTestRule.getActivity().getRootUiCoordinatorForTesting()
                instanceof BaseCustomTabRootUiCoordinator);
        BaseCustomTabRootUiCoordinator coordinator =
                (BaseCustomTabRootUiCoordinator)
                        mCustomTabActivityTestRule.getActivity().getRootUiCoordinatorForTesting();
        assert (coordinator.getCustomTabSizeStrategyForTesting()
                instanceof PartialCustomTabDisplayManager);
        PartialCustomTabDisplayManager displayManager =
                (PartialCustomTabDisplayManager) coordinator.getCustomTabSizeStrategyForTesting();
        int fullWidth = ViewGroup.LayoutParams.MATCH_PARENT;
        int fullHeight = ViewGroup.LayoutParams.MATCH_PARENT;
        WindowManager.LayoutParams attrs = getActivity().getWindow().getAttributes();
        assertEquals("The window should have full width", fullWidth, attrs.width);
        assertEquals("The window should have full height", fullHeight, attrs.height);
        assertEquals(
                "Incorrect strategy type",
                displayManager.getActiveStrategyType(),
                PartialCustomTabBaseStrategy.PartialCustomTabType.FULL_SIZE);
    }

    @Test
    @SmallTest
    @Restriction(DeviceRestriction.RESTRICTION_TYPE_NON_AUTO)
    @EnableFeatures({ChromeFeatureList.CCT_RESIZABLE_FOR_THIRD_PARTIES})
    public void testLaunchPartialCustomTabActivity_startActivityForResult() {
        CustomTabsIntent customTabsIntent =
                new CustomTabsIntent.Builder().setInitialActivityHeightPx(200).build();
        Intent intent = customTabsIntent.intent;
        intent.setData(Uri.parse("https://example.com"));
        String packageName = ContextUtils.getApplicationContext().getPackageName();
        intent.setPackage(packageName);
        intent.putExtra(EXTRA_INITIAL_ACTIVITY_WIDTH_PX, 300);
        intent.putExtra(EXTRA_ACTIVITY_SIDE_SHEET_BREAKPOINT_DP, 600);
        mChromeTabbedActivityTestRule.startMainActivityOnBlankPage();

        CustomTabActivity resultActivity =
                ApplicationTestUtils.waitForActivityWithClass(
                        CustomTabActivity.class,
                        Stage.CREATED,
                        () -> {
                            mChromeTabbedActivityTestRule
                                    .getActivity()
                                    .startActivityForResult(intent, 0);
                        });

        rotateCustomTabActivity(resultActivity, Layout.Orientation.PORTRAIT);

        DisplayMetrics displayMetrics = resultActivity.getResources().getDisplayMetrics();
        int devicePortraitWidthDp = (int) (displayMetrics.widthPixels / displayMetrics.density);

        assert (resultActivity.getRootUiCoordinatorForTesting()
                instanceof BaseCustomTabRootUiCoordinator);
        BaseCustomTabRootUiCoordinator coordinator =
                (BaseCustomTabRootUiCoordinator) resultActivity.getRootUiCoordinatorForTesting();
        assert (coordinator.getCustomTabSizeStrategyForTesting()
                instanceof PartialCustomTabDisplayManager);
        PartialCustomTabDisplayManager displayManager =
                (PartialCustomTabDisplayManager) coordinator.getCustomTabSizeStrategyForTesting();

        assertSideSheetOrBottomSheet(displayManager, devicePortraitWidthDp);

        Drawable windowBackground = resultActivity.getWindow().getDecorView().getBackground();
        int windowBackgroundColor = ((ColorDrawable) windowBackground).getColor();
        boolean isTranslucent = Color.alpha(windowBackgroundColor) < 255;
        assertTrue(isTranslucent);
    }

    private void doOpaqueOriginTest(boolean prefetch) throws Exception {
        TestWebServer webServer = TestWebServer.start();
        String url = webServer.setResponse("/ok.html", "<html>ok</html>", null);
        CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, url);
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        connection.newSession(token);

        if (prefetch) {
            setCanUseHiddenTabForSession(connection, token, true);
            Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(url), null, null));
            CriteriaHelper.pollUiThread(
                    () -> {
                        Criteria.checkThat(
                                connection.getHiddenTabForTesting(), Matchers.notNullValue());
                    });
            Tab hiddenTab =
                    ThreadUtils.runOnUiThreadBlocking(
                            () -> {
                                return connection.getHiddenTabForTesting();
                            });
            ChromeTabUtils.waitForTabPageLoaded(hiddenTab, url);
        } else {
            mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        }
        String actualHeader = webServer.getLastRequest("/ok.html").headerValue("Sec-Fetch-Site");
        assertEquals("cross-site", actualHeader);
        webServer.shutdown();
    }

    @Test
    @LargeTest
    public void testOpaqueOriginFromPrefetch_Enabled() throws Exception {
        doOpaqueOriginTest(true);
    }

    @Test
    @LargeTest
    public void testOpaqueOriginFromIntent_Enabled() throws Exception {
        doOpaqueOriginTest(false);
    }

    /** Asserts that the Overlay Panel is set to allow or not allow ever hiding the Toolbar. */
    private void assertOverlayPanelCanHideAndroidBrowserControls(boolean canEverHide) {
        // Wait for CS to get initialized.
        CriteriaHelper.pollUiThread(
                () -> getActivity().getContextualSearchManagerForTesting() != null);

        // The toolbar cannot go away for Partial Height Custom Tabs, but can for full height ones.
        CriteriaHelper.pollUiThread(
                () ->
                        getActivity()
                                        .getContextualSearchManagerForTesting()
                                        .getCanHideAndroidBrowserControls()
                                == canEverHide);
    }

    private void verifyHistoryAfterHiddenTab(boolean speculationWasAHit) throws Exception {
        String speculationUrl = mTestPage;
        String navigationUrl = speculationWasAHit ? mTestPage : mTestPage2;
        final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, navigationUrl);
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        connection.newSession(token);
        setCanUseHiddenTabForSession(connection, token, true);

        Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(speculationUrl), null, null));
        CustomTabsTestUtils.ensureCompletedSpeculationForUrl(connection, speculationUrl);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        Tab tab = getActivity().getActivityTab();
        Assert.assertFalse(tab.canGoBack());
        Assert.assertFalse(tab.canGoForward());

        List<HistoryItem> history = getHistory(tab);
        assertEquals(1, history.size());
        assertEquals(navigationUrl, history.get(0).getUrl().getSpec());
    }

    private void mayLaunchUrlWithoutWarmup(boolean useHiddenTab) {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        CustomTabsConnection connection = CustomTabsTestUtils.setUpConnection();
        CustomTabsSessionToken token = CustomTabsSessionToken.createMockSessionTokenForTesting();
        connection.newSession(token);
        Bundle extras = null;
        setCanUseHiddenTabForSession(connection, token, useHiddenTab);
        Assert.assertTrue(connection.mayLaunchUrl(token, Uri.parse(mTestPage), extras, null));
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage));
        Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();
        assertEquals(mTestPage, ChromeTabUtils.getUrlStringOnUiThread(tab));
    }

    private void checkPageLoadMetrics(boolean allowMetrics) throws TimeoutException {
        CustomTabsExtraCallbackHelper<Long> firstContentfulPaintCallback =
                new CustomTabsExtraCallbackHelper<>();
        CustomTabsExtraCallbackHelper<Long> largestContentfulPaintCallback =
                new CustomTabsExtraCallbackHelper<>();
        CustomTabsExtraCallbackHelper<Long> loadEventStartCallback =
                new CustomTabsExtraCallbackHelper<>();
        CustomTabsExtraCallbackHelper<Float> layoutShiftScoreCallback =
                new CustomTabsExtraCallbackHelper<>();
        CustomTabsExtraCallbackHelper<Boolean> sawNetworkQualityEstimatesCallback =
                new CustomTabsExtraCallbackHelper<>();

        final AtomicReference<Long> activityStartTimeMs = new AtomicReference<>(-1L);

        CustomTabsCallback cb =
                new CustomTabsCallback() {
                    @Override
                    public void extraCallback(String callbackName, Bundle args) {
                        if (callbackName.equals(CustomTabsConnection.ON_WARMUP_COMPLETED)) return;

                        // Check if the callback name is either the Bottom Bar Scroll, or Page Load
                        // Metrics.
                        // See https://crbug.com/963538 for why it might be either.
                        if (!CustomTabsConnection.BOTTOM_BAR_SCROLL_STATE_CALLBACK.equals(
                                callbackName)) {
                            assertEquals(
                                    CustomTabsConnection.PAGE_LOAD_METRICS_CALLBACK, callbackName);
                        }
                        if (-1 != args.getLong(PageLoadMetrics.EFFECTIVE_CONNECTION_TYPE, -1)) {
                            sawNetworkQualityEstimatesCallback.notifyCalled(true);
                        }

                        float layoutShiftScoreValue =
                                args.getFloat(PageLoadMetrics.LAYOUT_SHIFT_SCORE, -1f);
                        if (layoutShiftScoreValue >= 0f) {
                            layoutShiftScoreCallback.notifyCalled(layoutShiftScoreValue);
                        }

                        long navigationStart = args.getLong(PageLoadMetrics.NAVIGATION_START, -1);
                        if (navigationStart == -1) {
                            // Untested metric callback.
                            return;
                        }
                        long current = SystemClock.uptimeMillis();
                        Assert.assertTrue(navigationStart <= current);
                        Assert.assertTrue(navigationStart >= activityStartTimeMs.get());

                        long firstContentfulPaint =
                                args.getLong(PageLoadMetrics.FIRST_CONTENTFUL_PAINT, -1);
                        if (firstContentfulPaint > 0) {
                            Assert.assertTrue(firstContentfulPaint <= (current - navigationStart));
                            firstContentfulPaintCallback.notifyCalled(firstContentfulPaint);
                        }

                        long largestContentfulPaint =
                                args.getLong(PageLoadMetrics.LARGEST_CONTENTFUL_PAINT, -1);
                        if (largestContentfulPaint > 0) {
                            Assert.assertTrue(
                                    largestContentfulPaint <= (current - navigationStart));
                            largestContentfulPaintCallback.notifyCalled(largestContentfulPaint);
                        }

                        long loadEventStart = args.getLong(PageLoadMetrics.LOAD_EVENT_START, -1);
                        if (loadEventStart > 0) {
                            Assert.assertTrue(loadEventStart <= (current - navigationStart));
                            loadEventStartCallback.notifyCalled(loadEventStart);
                        }
                    }
                };

        CustomTabsSession session = CustomTabsTestUtils.bindWithCallback(cb).session;
        Intent intent = new CustomTabsIntent.Builder(session).build().intent;

        if (allowMetrics) {
            CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
            CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
            connection.mClientManager.setShouldGetPageLoadMetricsForSession(token, true);
        }

        intent.setData(Uri.parse(mTestPage));
        intent.setComponent(
                new ComponentName(
                        ApplicationProvider.getApplicationContext(), ChromeLauncherActivity.class));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        activityStartTimeMs.set(SystemClock.uptimeMillis());
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        if (allowMetrics) {
            firstContentfulPaintCallback.waitForCallback(0);
            loadEventStartCallback.waitForCallback(0);
            sawNetworkQualityEstimatesCallback.waitForCallback(0);

            assertTrue(firstContentfulPaintCallback.getValue() > 0);
            assertTrue(firstContentfulPaintCallback.getValue() > 0);
            assertTrue(sawNetworkQualityEstimatesCallback.getValue());
        } else {
            try {
                firstContentfulPaintCallback.waitForCallback(0);
                assertTrue(firstContentfulPaintCallback.getValue() > 0);
            } catch (TimeoutException e) {
                // Expected.
            }
            assertEquals(0, firstContentfulPaintCallback.getCallCount());

            try {
                largestContentfulPaintCallback.waitForCallback(0);
                assertTrue(largestContentfulPaintCallback.getValue() > 0);
            } catch (TimeoutException e) {
                // Expected.
            }

            assertEquals(0, largestContentfulPaintCallback.getCallCount());
        }

        // Navigate to a new page, as metrics like LCP are only reported at the end of the page load
        // lifetime.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    final CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
                    activity.getComponent()
                            .resolveNavigationController()
                            .navigate(new LoadUrlParams("about:blank"), intent);
                });

        if (allowMetrics) {
            largestContentfulPaintCallback.waitForCallback(0, 1, 15, TimeUnit.SECONDS);
            assertTrue((long) largestContentfulPaintCallback.getValue() > 0);

            layoutShiftScoreCallback.waitForCallback(0);
            assertNotNull(layoutShiftScoreCallback.getValue());
        }
    }

    private CustomTabsSessionToken warmUpAndLaunchUrlWithSession(Intent intentWithSession)
            throws Exception {
        CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        CustomTabsSessionToken token =
                CustomTabsSessionToken.getSessionTokenFromIntent(intentWithSession);
        connection.newSession(token);
        intentWithSession.setData(Uri.parse(mTestPage));
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intentWithSession);
        return token;
    }

    private CustomTabsSessionToken warmUpAndLaunchUrlWithSession() throws Exception {
        return warmUpAndLaunchUrlWithSession(
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                        ApplicationProvider.getApplicationContext(), mTestPage));
    }

    private Activity startBlankUiTestActivity() {
        Context context = ApplicationProvider.getApplicationContext();
        Intent emptyIntent = new Intent(context, BlankUiTestActivity.class);
        emptyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        return getInstrumentation().startActivitySync(emptyIntent);
    }

    private static class ElementContentCriteria implements Runnable {
        private final Tab mTab;
        private final String mJsFunction;
        private final String mExpected;

        public ElementContentCriteria(Tab tab, String elementId, String expected) {
            mTab = tab;
            mExpected = "\"" + expected + "\"";
            mJsFunction =
                    "(function () { return document.getElementById(\""
                            + elementId
                            + "\").innerHTML; })()";
        }

        @Override
        public void run() {
            String value = null;
            try {
                String jsonText =
                        JavaScriptUtils.executeJavaScriptAndWaitForResult(
                                mTab.getWebContents(), mJsFunction);
                if (jsonText.equalsIgnoreCase("null")) jsonText = "";
                value = jsonText;
            } catch (TimeoutException e) {
                throw new CriteriaNotSatisfiedException(e);
            }
            Criteria.checkThat("Page element is not as expected.", value, is(mExpected));
        }
    }

    private static List<HistoryItem> getHistory(Tab tab) throws TimeoutException {
        final TestBrowsingHistoryObserver historyObserver = new TestBrowsingHistoryObserver();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Profile profile = Profile.fromWebContents(tab.getWebContents());
                    BrowsingHistoryBridge historyService = new BrowsingHistoryBridge(profile);
                    historyService.setObserver(historyObserver);
                    String historyQueryFilter = "";
                    historyService.queryHistory(historyQueryFilter, null);
                });
        historyObserver.getQueryCallback().waitForCallback(0);
        return historyObserver.getHistoryQueryResults();
    }

    private SessionDataHolder getSessionDataHolder() {
        return ChromeApplicationImpl.getComponent().resolveSessionDataHolder();
    }

    private void assertLastLaunchedClientAppRecorded(
            String histogramSuffix,
            String clientPackage,
            String url,
            int taskId,
            boolean umaRecorded) {
        SharedPreferencesManager pref = ChromeSharedPreferences.getInstance();
        String histogramName =
                "CustomTabs.RetainableSessionsV2.TimeBetweenLaunch" + histogramSuffix;

        Assert.assertEquals(
                "Client package name in shared pref is different.",
                clientPackage,
                pref.readString(ChromePreferenceKeys.CUSTOM_TABS_LAST_CLIENT_PACKAGE, ""));
        Assert.assertEquals(
                "Url in shared pref is different.",
                url,
                pref.readString(ChromePreferenceKeys.CUSTOM_TABS_LAST_URL, ""));
        Assert.assertEquals(
                "Task id in shared pref is different.",
                taskId,
                pref.readInt(ChromePreferenceKeys.CUSTOM_TABS_LAST_TASK_ID, 0));
        Assert.assertEquals(
                String.format("<%s> not recorded correctly.", histogramName),
                umaRecorded ? 1 : 0,
                RecordHistogram.getHistogramTotalCountForTesting(histogramName));
    }

    @Test
    @SmallTest
    public void doesNotLaunchJavaScriptUrls_dispatchToCustomTabActivity() {
        Context context = ApplicationProvider.getApplicationContext();
        String javaScriptUrl = "javascript: alert('Hello');";

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Intent intent =
                            CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                                    context, javaScriptUrl);

                    Activity activity = Mockito.mock(Activity.class);
                    @LaunchIntentDispatcher.Action
                    int result =
                            LaunchIntentDispatcher.dispatchToCustomTabActivity(activity, intent);
                    assertEquals(LaunchIntentDispatcher.Action.CONTINUE, result);
                    verify(activity, never()).startActivity(any(), any());
                });
    }

    @Test
    @SmallTest
    public void doesNotLaunchJavaScriptUrls_dispatch() {
        Context context = ApplicationProvider.getApplicationContext();
        String javaScriptUrl = "javascript: alert('Hello');";

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Intent intent =
                            CustomTabsIntentTestUtils.createMinimalCustomTabIntent(
                                    context, javaScriptUrl);

                    Activity activity = Mockito.mock(Activity.class);
                    @LaunchIntentDispatcher.Action
                    int result = LaunchIntentDispatcher.dispatch(activity, intent);
                    assertEquals(LaunchIntentDispatcher.Action.FINISH_ACTIVITY, result);
                    verify(activity, never()).startActivity(any(), any());
                });
    }

    @Test
    @SmallTest
    public void testCallingActivityExtra() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Intent intent = createMinimalCustomTabIntent();
                    intent.putExtra(IntentHandler.EXTRA_CALLING_ACTIVITY_PACKAGE, "spoofed");
                    Activity activity = Mockito.mock(Activity.class);

                    when(activity.getCallingPackage()).thenReturn("com.foo.bar");
                    PowerManager powerManager =
                            (PowerManager)
                                    ContextUtils.getApplicationContext()
                                            .getSystemService(Context.POWER_SERVICE);
                    when(activity.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager);

                    LaunchIntentDispatcher.dispatch(activity, intent);
                    verify(activity, times(1)).startActivity(mIntentCaptor.capture(), any());

                    Assert.assertEquals(
                            "Calling activity package incorrect.",
                            "com.foo.bar",
                            mIntentCaptor
                                    .getValue()
                                    .getStringExtra(IntentHandler.EXTRA_CALLING_ACTIVITY_PACKAGE));
                });
    }

    @Test
    @SmallTest
    public void testCallingActivityExtra_NotSet() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Intent intent = createMinimalCustomTabIntent();
                    intent.putExtra(IntentHandler.EXTRA_CALLING_ACTIVITY_PACKAGE, "spoofed");
                    Activity activity = Mockito.mock(Activity.class);
                    PowerManager powerManager =
                            (PowerManager)
                                    ContextUtils.getApplicationContext()
                                            .getSystemService(Context.POWER_SERVICE);
                    when(activity.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager);

                    LaunchIntentDispatcher.dispatch(activity, intent);
                    verify(activity, times(1)).startActivity(mIntentCaptor.capture(), any());

                    Assert.assertNull(
                            "Calling activity package shouldn't be set.",
                            mIntentCaptor
                                    .getValue()
                                    .getStringExtra(IntentHandler.EXTRA_CALLING_ACTIVITY_PACKAGE));
                });
    }

    @Test
    @SmallTest
    public void testAppMenuDoesNotShowReadAloud() {
        // TODO(b/292134067): Update when Read Aloud supports Custom Tabs.
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(createMinimalCustomTabIntent());
        openAppMenuAndAssertMenuShown();
        Assert.assertNull(
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.readaloud_menu_id));
    }

    @Test
    @SmallTest
    @EnableFeatures(ChromeFeatureList.BACK_GESTURE_REFACTOR)
    public void testBackPressNavigationFailure_WithRecover() {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        final Tab tab = getActivity().getActivityTab();
        BackPressManager.TAB_HISTORY_RECOVER.setForTesting(true);

        ThreadUtils.runOnUiThreadBlocking(
                (Runnable) () -> tab.loadUrl(new LoadUrlParams(mTestPage2)));
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage2);

        BackPressHandler navigationHandler =
                getActivity()
                        .getBackPressManagerForTesting()
                        .getHandlersForTesting()[BackPressHandler.Type.TAB_HISTORY];
        ObservableSupplierImpl<Boolean> handleBackPressChangedSupplier =
                (ObservableSupplierImpl<Boolean>)
                        (navigationHandler.getHandleBackPressChangedSupplier());
        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.BackPress.Failure",
                        BackPressManager.getHistogramValue(
                                BackPressHandler.Type.MINIMIZE_APP_AND_CLOSE_TAB));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    handleBackPressChangedSupplier.set(false);
                    try {
                        getActivity().getOnBackPressedDispatcher().onBackPressed();
                    } catch (AssertionError ignored) {
                        if (!ignored.getMessage().contains("-1")) throw ignored;
                    }
                });

        histogramWatcher.assertExpected("Failure should be recorded");
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "Tab should be navigated when tab handler fails",
                            ChromeTabUtils.getUrlStringOnUiThread(getActivity().getActivityTab()),
                            is(mTestPage));
                });
        BackPressManager.TAB_HISTORY_RECOVER.setForTesting(false);
    }

    @Test
    @SmallTest
    @EnableFeatures(ChromeFeatureList.BACK_GESTURE_REFACTOR)
    public void testBackPressNavigationFailure_WithoutRecover() {
        Context context = getInstrumentation().getTargetContext().getApplicationContext();
        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        final Tab tab = getActivity().getActivityTab();
        BackPressManager.TAB_HISTORY_RECOVER.setForTesting(false);

        ThreadUtils.runOnUiThreadBlocking(
                (Runnable) () -> tab.loadUrl(new LoadUrlParams(mTestPage2)));
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage2);

        BackPressHandler navigationHandler =
                getActivity()
                        .getBackPressManagerForTesting()
                        .getHandlersForTesting()[BackPressHandler.Type.TAB_HISTORY];
        ObservableSupplierImpl<Boolean> handleBackPressChangedSupplier =
                (ObservableSupplierImpl<Boolean>)
                        (navigationHandler.getHandleBackPressChangedSupplier());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    handleBackPressChangedSupplier.set(false);
                    try {
                        getActivity().getOnBackPressedDispatcher().onBackPressed();
                    } catch (AssertionError ignored) {
                        if (!ignored.getMessage().contains("-1")) throw ignored;
                    }
                });

        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "Tab should not be navigated when tab handler fails without recover",
                            ChromeTabUtils.getUrlStringOnUiThread(getActivity().getActivityTab()),
                            is(mTestPage2));
                });
    }

    @Test
    @SmallTest
    public void disableShareEntriesForAutomotive() {
        mAutomotiveRule.setIsAutomotive(true);
        Intent intent = createMinimalCustomTabIntent();
        CustomTabsIntentTestUtils.setShareState(intent, SHARE_STATE_ON);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        ViewGroup toolbarButtons =
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.action_buttons);
        Assert.assertEquals(
                "No action buttons should be added.", 0, toolbarButtons.getChildCount());

        openAppMenuAndAssertMenuShown();
        Assert.assertNull(
                "Share option should be hidden.",
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.share_row_menu_id));
    }

    @Test
    @MediumTest
    public void omniboxInCCT_testInteractiveOmniboxOnEligibleCCTs() throws Exception {
        // Permit Omnibox for any upcoming intent(s).
        var connection = Mockito.spy(CustomTabsConnection.getInstance());
        doReturn(true).when(connection).shouldEnableOmniboxForIntent(any());
        CustomTabsConnection.setInstanceForTesting(connection);

        Intent intent = createMinimalCustomTabIntent();
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        var tab = getActivity().getActivityTab();
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage);

        var titleBar =
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.title_url_container);
        Assert.assertTrue(titleBar.hasOnClickListeners());

        var url = mCustomTabActivityTestRule.getActivity().findViewById(R.id.url_bar);
        Assert.assertNotNull(
                "Url bar should have an accessibility delegate.", url.getAccessibilityDelegate());

        UserActionTester userActionTester = new UserActionTester();
        ThreadUtils.runOnUiThreadBlocking(() -> titleBar.performClick());
        assertThat(userActionTester.getActions(), Matchers.hasItem("CustomTabs.OmniboxClicked"));
    }

    @Test
    @MediumTest
    public void omniboxInCCT_testInteractiveOmniboxOnEligibleCCTsWithAlternateHandler()
            throws Exception {
        var connection = Mockito.spy(CustomTabsConnection.getInstance());
        // Permit Omnibox for any upcoming intent(s).
        doReturn(true).when(connection).shouldEnableOmniboxForIntent(any());
        Consumer<Tab> mockConsumer = Mockito.mock(Consumer.class);
        // Provide an alternate tap handler on any upcoming intent(s).
        doReturn(mockConsumer).when(connection).getAlternateOmniboxTapHandler(any());
        CustomTabsConnection.setInstanceForTesting(connection);

        Intent intent = createMinimalCustomTabIntent();
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        var tab = getActivity().getActivityTab();
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage);

        var titleBar =
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.title_url_container);
        Assert.assertTrue(titleBar.hasOnClickListeners());

        var url = mCustomTabActivityTestRule.getActivity().findViewById(R.id.url_bar);
        Assert.assertNotNull(
                "Url bar should have an accessibility delegate.", url.getAccessibilityDelegate());

        UserActionTester userActionTester = new UserActionTester();
        ThreadUtils.runOnUiThreadBlocking(() -> titleBar.performClick());
        assertThat(userActionTester.getActions(), Matchers.hasItem("CustomTabs.OmniboxClicked"));
        verify(mockConsumer).accept(tab);
    }

    @Test
    @MediumTest
    public void omniboxInCCT_testNonInteractiveOmniboxWhenIntentNotEligible() {
        // By default, omnibox in CCT is not permitted and no stubbing is necessary.
        Intent intent = createMinimalCustomTabIntent();
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        var tab = getActivity().getActivityTab();
        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage);

        var titleBar =
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.title_url_container);
        Assert.assertFalse(titleBar.hasOnClickListeners());
    }

    @Test
    @MediumTest
    @EnableFeatures(ChromeFeatureList.CCT_NESTED_SECURITY_ICON)
    @MinAndroidSdkLevel(VERSION_CODES.R)
    public void titleAndUrlActionTest() throws ExecutionException {
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(createMinimalCustomTabIntent());

        var titleBar =
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.title_url_container);
        Assert.assertTrue(
                "Title bar should have a click listener.", titleBar.hasOnClickListeners());
        Assert.assertTrue(
                "Title bar should have a long press listener.", titleBar.hasOnLongClickListeners());

        var title = mCustomTabActivityTestRule.getActivity().findViewById(R.id.title_bar);
        Assert.assertNotNull(
                "Title should have an accessibility delegate.", title.getAccessibilityDelegate());

        var url = mCustomTabActivityTestRule.getActivity().findViewById(R.id.url_bar);
        Assert.assertNotNull(
                "Url bar should have an accessibility delegate.", url.getAccessibilityDelegate());

        assertNull(
                "Page info hasn't been shown, so PageInfoController should be null.",
                PageInfoController.getLastPageInfoControllerForTesting());
        ThreadUtils.runOnUiThreadBlocking(() -> titleBar.performClick());
        assertNotNull(
                "Page info should have been shown.",
                PageInfoController.getLastPageInfoControllerForTesting());
    }

    private void rotateCustomTabActivity(CustomTabActivity activity, int orientation) {
        ActivityTestUtils.rotateActivityToOrientation(activity, orientation);
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            activity.getResources().getConfiguration().orientation,
                            is(orientation));
                });
    }

    private void assertSideSheetOrBottomSheet(
            PartialCustomTabDisplayManager displayManager, int deviceWidthDp) {
        if (deviceWidthDp >= 600) {
            assertEquals(
                    "Incorrect strategy type",
                    PartialCustomTabBaseStrategy.PartialCustomTabType.SIDE_SHEET,
                    displayManager.getActiveStrategyType());
        } else {
            assertEquals(
                    "Incorrect strategy type",
                    PartialCustomTabBaseStrategy.PartialCustomTabType.BOTTOM_SHEET,
                    displayManager.getActiveStrategyType());
        }
    }
}