chromium/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/partialcustomtab/PartialCustomTabBottomSheetStrategyTest.java

// Copyright 2021 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.features.partialcustomtab;

import static androidx.browser.customtabs.CustomTabsCallback.ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET;
import static androidx.browser.customtabs.CustomTabsCallback.ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET_MAXIMIZED;
import static androidx.browser.customtabs.CustomTabsCallback.ACTIVITY_LAYOUT_STATE_FULL_SCREEN;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import static org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabBottomSheetStrategy.BOTTOM_SHEET_MAX_WIDTH_DP_LANDSCAPE;
import static org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabTestRule.DEVICE_HEIGHT;
import static org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabTestRule.DEVICE_HEIGHT_LANDSCAPE;
import static org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabTestRule.DEVICE_WIDTH;
import static org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabTestRule.FULL_HEIGHT;
import static org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabTestRule.NAVBAR_HEIGHT;
import static org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabTestRule.STATUS_BAR_HEIGHT;

import android.animation.Animator.AnimatorListener;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Build;
import android.os.Looper;
import android.os.SystemClock;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.BaseSwitches;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.customtabs.features.partialcustomtab.ContentGestureListener.GestureState;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar.HandleStrategy;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.components.embedder_support.view.ContentView;

import java.util.concurrent.TimeUnit;

/** Tests for {@link PartialCustomTabHandleStrategy}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {PartialCustomTabTestRule.ShadowSemanticColorUtils.class})
@EnableFeatures({ChromeFeatureList.CCT_RESIZABLE_FOR_THIRD_PARTIES})
@LooperMode(Mode.PAUSED)
public class PartialCustomTabBottomSheetStrategyTest {
    @Rule public final PartialCustomTabTestRule mPCCTTestRule = new PartialCustomTabTestRule();

    private static final int INITIAL_HEIGHT = DEVICE_HEIGHT / 2 - NAVBAR_HEIGHT;

    private static final int FIND_TOOLBAR_COLOR = 3755;
    private static final int PCCT_TOOLBAR_COLOR = 12111;

    private boolean mFullscreen;

    private PartialCustomTabBottomSheetStrategy createPcctBackgroundDisabled() {
        BrowserServicesIntentDataProvider intentData = mPCCTTestRule.mIntentData;
        when(intentData.getInitialActivityHeight()).thenReturn(500);
        PartialCustomTabBottomSheetStrategy pcct =
                new PartialCustomTabBottomSheetStrategy(
                        mPCCTTestRule.mActivity,
                        mPCCTTestRule.mIntentData,
                        () -> mPCCTTestRule.mTouchEventProvider,
                        () -> mPCCTTestRule.mTab,
                        mPCCTTestRule.mOnResizedCallback,
                        mPCCTTestRule.mOnActivityLayoutCallback,
                        mPCCTTestRule.mActivityLifecycleDispatcher,
                        mPCCTTestRule.mFullscreenManager,
                        /* isTablet= */ false,
                        /* startMaximized= */ false,
                        mPCCTTestRule.mHandleStrategyFactory);
        pcct.setMockViewForTesting(
                mPCCTTestRule.mNavbar,
                mPCCTTestRule.mSpinnerView,
                mPCCTTestRule.mSpinner,
                mPCCTTestRule.mToolbarView,
                mPCCTTestRule.mToolbarCoordinator,
                mPCCTTestRule.mHandleStrategyFactory);
        return pcct;
    }

    private PartialCustomTabBottomSheetStrategy createPcctAtHeight(int heightPx) {
        return createPcctAtHeight(heightPx, false);
    }

    private PartialCustomTabBottomSheetStrategy createPcctAtHeight(
            int heightPx, boolean isFixedHeight) {
        BrowserServicesIntentDataProvider intentData = mPCCTTestRule.mIntentData;
        when(intentData.getInitialActivityHeight()).thenReturn(heightPx);
        when(intentData.isPartialCustomTabFixedHeight()).thenReturn(isFixedHeight);
        when(intentData.canInteractWithBackground()).thenReturn(true);
        PartialCustomTabBottomSheetStrategy pcct =
                new PartialCustomTabBottomSheetStrategy(
                        mPCCTTestRule.mActivity,
                        mPCCTTestRule.mIntentData,
                        () -> mPCCTTestRule.mTouchEventProvider,
                        () -> mPCCTTestRule.mTab,
                        mPCCTTestRule.mOnResizedCallback,
                        mPCCTTestRule.mOnActivityLayoutCallback,
                        mPCCTTestRule.mActivityLifecycleDispatcher,
                        mPCCTTestRule.mFullscreenManager,
                        /* isTablet= */ false,
                        /* startMaxmized= */ false,
                        mPCCTTestRule.mHandleStrategyFactory);
        pcct.setMockViewForTesting(
                mPCCTTestRule.mNavbar,
                mPCCTTestRule.mSpinnerView,
                mPCCTTestRule.mSpinner,
                mPCCTTestRule.mToolbarView,
                mPCCTTestRule.mToolbarCoordinator,
                mPCCTTestRule.mHandleStrategyFactory);
        return pcct;
    }

    @Test
    public void create_heightIsCappedToHalfOfDeviceHeight() {
        createPcctAtHeight(500);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());
    }

    @Test
    public void create_largeInitialHeight() {
        createPcctAtHeight(5000);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsFullHeight(mPCCTTestRule.getWindowAttributes());
    }

    @Test
    public void create_heightIsCappedToDeviceHeight() {
        createPcctAtHeight(DEVICE_HEIGHT + 100);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsFullHeight(mPCCTTestRule.getWindowAttributes());
    }

    private void doTestHeightWithStatusBar() {
        when(mPCCTTestRule.mContentFrame.getHeight())
                .thenReturn(DEVICE_HEIGHT - NAVBAR_HEIGHT - STATUS_BAR_HEIGHT);
        createPcctAtHeight(DEVICE_HEIGHT + 100);
        mPCCTTestRule.verifyWindowFlagsSet();
        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.R)
    public void create_maxHeightWithStatusBar_R() {
        configureStatusBarHeightForR();
        doTestHeightWithStatusBar();
        assertTabBelowStatusBar(mPCCTTestRule.mAttributeResults.get(0));
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.Q)
    public void create_maxHeightWithStatusBar_Q() {
        configureStatusBarHeightForQ();
        doTestHeightWithStatusBar();
        assertTabBelowStatusBar(mPCCTTestRule.mAttributeResults.get(0));
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.R)
    public void create_maxHeightWithStatusBar_landscape_R() {
        configureStatusBarHeightForR();
        mPCCTTestRule.configLandscapeMode();
        doTestHeightWithStatusBar();
        assertEquals(
                DEVICE_HEIGHT - NAVBAR_HEIGHT - STATUS_BAR_HEIGHT,
                mPCCTTestRule.mAttributeResults.get(0).height);
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.Q)
    public void create_maxHeightWithStatusBar_landscape_Q() {
        configureStatusBarHeightForQ();
        mPCCTTestRule.configLandscapeMode();
        doTestHeightWithStatusBar();
        assertEquals(
                DEVICE_HEIGHT - NAVBAR_HEIGHT - STATUS_BAR_HEIGHT,
                mPCCTTestRule.mAttributeResults.get(0).height);
    }

    @Test
    public void create_landscapeOrientation() {
        int pcctHeight = 800;
        mPCCTTestRule.configLandscapeMode();
        createPcctAtHeight(pcctHeight);
        mPCCTTestRule.verifyWindowFlagsSet();

        // Full height when in landscape mode.
        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertEquals(pcctHeight, mPCCTTestRule.getWindowAttributes().height);
    }

    @Test
    public void create_backgroundAppDisabledPortrait() {
        createPcctBackgroundDisabled();

        verify(mPCCTTestRule.mWindow).addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
        verify(mPCCTTestRule.mWindow).clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
    }

    private static MotionEvent event(long ts, int action, int ypos) {
        return MotionEvent.obtain(ts, ts, action, DEVICE_WIDTH / 2, ypos, 0);
    }

    private static void actionDown(HandleStrategy strategy, long ts, int ypos) {
        strategy.onTouchEvent(event(ts, MotionEvent.ACTION_DOWN, ypos));
    }

    private static void actionMove(HandleStrategy strategy, long ts, int ypos) {
        strategy.onTouchEvent(event(ts, MotionEvent.ACTION_MOVE, ypos));
    }

    private static void actionUp(HandleStrategy strategy, long ts, int ypos) {
        strategy.onTouchEvent(event(ts, MotionEvent.ACTION_UP, ypos));
    }

    /**
     * Simulate dragging the tab and lifting the finger at the end.
     * @param handleStrategy {@link PartialCustomTabHandleStrategy} object.
     * @param ypos Series of y positions simulating the events.
     * @return Window attributes after the dragging finishes.
     */
    private WindowManager.LayoutParams dragTab(HandleStrategy handleStrategy, int... ypos) {
        int npos = ypos.length;
        assert npos >= 2;
        long timestamp = SystemClock.uptimeMillis();

        // ACTION_DOWN -> ACTION_MOVE * (npos-1) -> ACTION_UP
        actionDown(handleStrategy, timestamp, ypos[0]);
        for (int i = 1; i < npos; ++i) actionMove(handleStrategy, timestamp, ypos[i]);
        actionUp(handleStrategy, timestamp, ypos[npos - 1]);

        // Wait animation to finish.
        shadowOf(Looper.getMainLooper()).idle();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        int length = mPCCTTestRule.mAttributeResults.size();
        assertTrue(length > 1);
        return mPCCTTestRule.mAttributeResults.get(length - 1);
    }

    private void assertMotionEventIgnored(HandleStrategy handleStrategy) {
        assertFalse(
                handleStrategy.onInterceptTouchEvent(
                        event(SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 1500)));
    }

    private void assertTabIsAtInitialPos(WindowManager.LayoutParams attrs) {
        assertEquals(INITIAL_HEIGHT + NAVBAR_HEIGHT, attrs.y);
    }

    private void assertTabIsFullHeight(WindowManager.LayoutParams attrs) {
        assertEquals(FULL_HEIGHT, attrs.height);
    }

    private void assertTabBelowStatusBar(WindowManager.LayoutParams attrs) {
        assertEquals(FULL_HEIGHT - STATUS_BAR_HEIGHT, attrs.height);
    }

    private void disableSpinnerAnimation() {
        // Disable animation for the mock spinner view.
        doAnswer(
                        invocation -> {
                            AnimatorListener listener = invocation.getArgument(0);
                            listener.onAnimationEnd(null);
                            return mPCCTTestRule.mViewAnimator;
                        })
                .when(mPCCTTestRule.mViewAnimator)
                .setListener(any(AnimatorListener.class));
    }

    private WindowManager.LayoutParams getWindowAttributes() {
        return mPCCTTestRule.mAttributeResults.get(mPCCTTestRule.mAttributeResults.size() - 1);
    }

    private void configureStatusBarHeightForR() {
        // Setup for R+
        WindowMetrics windowMetric = Mockito.mock(WindowMetrics.class);
        WindowInsets windowInsets = Mockito.mock(WindowInsets.class);

        doReturn(windowMetric).when(mPCCTTestRule.mWindowManager).getCurrentWindowMetrics();
        doReturn(windowInsets).when(windowMetric).getWindowInsets();
        doReturn(
                        new Rect(
                                0,
                                0,
                                mPCCTTestRule.mRealMetrics.widthPixels,
                                mPCCTTestRule.mRealMetrics.heightPixels))
                .when(windowMetric)
                .getBounds();
        doReturn(Insets.of(0, STATUS_BAR_HEIGHT, 0, 0))
                .when(windowInsets)
                .getInsets(eq(WindowInsets.Type.statusBars()));
        doReturn(Insets.of(0, 0, 0, NAVBAR_HEIGHT))
                .when(windowInsets)
                .getInsets(eq(WindowInsets.Type.navigationBars()));
        doReturn(Insets.of(0, 0, 0, NAVBAR_HEIGHT))
                .when(windowInsets)
                .getInsets(
                        eq(WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout()));
    }

    private void configureStatusBarHeightForQ() {
        // Setup for Q-
        int statusBarId = 54321;
        doReturn(statusBarId)
                .when(mPCCTTestRule.mResources)
                .getIdentifier(eq("status_bar_height"), any(), any());
        doReturn(STATUS_BAR_HEIGHT)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(statusBarId));
    }

    @Test
    public void setTitleWhenLaunched() {
        final String title = "BottomSheet";
        var coordinator = mPCCTTestRule.mCoordinatorLayout;
        doReturn(false).when(coordinator).isAttachedToWindow();
        doReturn(title).when(mPCCTTestRule.mResources).getString(anyInt());
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        var listener = mPCCTTestRule.mAttachStateChangeListener;
        verify(coordinator).addOnAttachStateChangeListener(listener.capture());
        listener.getValue().onViewAttachedToWindow(null);
        verify(mPCCTTestRule.mWindow).setTitle(eq(title));
        verify(coordinator).removeOnAttachStateChangeListener(listener.getValue());
        clearInvocations(coordinator);

        // Once attached, the title is not set again.
        doReturn(true).when(coordinator).isAttachedToWindow();
        strategy.onPostInflationStartup();
        verify(coordinator, never()).addOnAttachStateChangeListener(any());
    }

    @Test
    public void moveFromTop() {
        // Drag to the top
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        // Drag to the top.
        assertTabIsFullHeight(dragTab(handleStrategy, 1500, 1000, 500));

        // Drag down a little -> slide back to the top.
        assertTabIsFullHeight(dragTab(handleStrategy, 50, 100, 150));

        // Drag down enough -> slide to the initial position.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 50, 650, 1300));
    }

    @Test
    public void moveFromInitialHeight() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());

        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        // Drag up slightly -> slide back to the initial height.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 1500, 1450, 1400));

        // Drag down slightly -> slide back to the initial height.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 1500, 1550, 1600));
    }

    @Test
    public void moveUpThenDown() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);

        verify(mPCCTTestRule.mWindow).addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
        verify(mPCCTTestRule.mWindow).clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        // Shake the tab from the initial position slightly -> back to the initial height.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 1500, 1450, 1600));
    }

    @Test
    public void moveUp_landscapeOrientationUnresizable() {
        mPCCTTestRule.configLandscapeMode();
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(800);
        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();
        assertMotionEventIgnored(handleStrategy);
    }

    @Test
    public void moveUp_multiwindowModeUnresizable() {
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(800);
        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();
        assertMotionEventIgnored(handleStrategy);
    }

    @Test
    public void moveDownToDismiss() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        final boolean[] finishRunnable = {false};
        strategy.handleCloseAnimation(() -> finishRunnable[0] = true);
        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();
        dragTab(handleStrategy, INITIAL_HEIGHT, DEVICE_HEIGHT - 400);
        assertTrue("FinnishRunnable should be called.", finishRunnable[0]);
    }

    @Test
    public void showSpinnerOnDragUpOnly() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);

        verify(mPCCTTestRule.mWindow).addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
        verify(mPCCTTestRule.mWindow).clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.GONE);

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        long timestamp = SystemClock.uptimeMillis();
        actionDown(handleStrategy, timestamp, 1500);
        actionMove(handleStrategy, timestamp, 1450);

        // Verify the spinner is visible.
        verify(mPCCTTestRule.mSpinnerView).setVisibility(View.VISIBLE);
        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.VISIBLE);
        clearInvocations(mPCCTTestRule.mSpinnerView);

        actionUp(handleStrategy, timestamp, 1450);

        // Wait animation to finish.
        shadowOf(Looper.getMainLooper()).idle();
        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.GONE);

        // Now the tab is full-height. Start dragging down.
        actionDown(handleStrategy, timestamp, 500);
        actionMove(handleStrategy, timestamp, 650);

        // Verify the spinner remained invisible.
        verify(mPCCTTestRule.mSpinnerView, never()).setVisibility(anyInt());
    }

    @Test
    public void hideSpinnerWhenReachingFullHeight() {
        disableSpinnerAnimation();
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);

        verify(mPCCTTestRule.mWindow).addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
        verify(mPCCTTestRule.mWindow).clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.GONE);

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        long timestamp = SystemClock.uptimeMillis();
        actionDown(handleStrategy, timestamp, 1500);
        actionMove(handleStrategy, timestamp, 1450);

        // Verify the spinner is visible.
        verify(mPCCTTestRule.mSpinnerView).setVisibility(View.VISIBLE);
        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.VISIBLE);
        clearInvocations(mPCCTTestRule.mSpinnerView);

        // Verify the spinner remains invisible after the tab reaches the top.
        int topY = strategy.getFullyExpandedY();
        actionMove(handleStrategy, timestamp, topY);
        verify(mPCCTTestRule.mSpinnerView).setVisibility(View.GONE);
        clearInvocations(mPCCTTestRule.mSpinnerView);

        actionMove(handleStrategy, timestamp, topY + 200);
        verify(mPCCTTestRule.mSpinnerView, never()).setVisibility(anyInt());
    }

    @Test
    public void hideSpinnerWhenDraggingDown() {
        disableSpinnerAnimation();
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);

        verify(mPCCTTestRule.mWindow).addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
        verify(mPCCTTestRule.mWindow).clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.GONE);

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        long timestamp = SystemClock.uptimeMillis();
        actionDown(handleStrategy, timestamp, INITIAL_HEIGHT - 100);
        actionMove(handleStrategy, timestamp, INITIAL_HEIGHT - 150);

        // Verify the spinner is visible.
        verify(mPCCTTestRule.mSpinnerView).setVisibility(View.VISIBLE);
        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.VISIBLE);
        clearInvocations(mPCCTTestRule.mSpinnerView);

        // Drag below the initial height.
        actionMove(handleStrategy, timestamp, INITIAL_HEIGHT + 100);

        // Verify the spinner goes invisible.
        verify(mPCCTTestRule.mSpinnerView).setVisibility(View.GONE);
    }

    @Test
    public void hideSpinnerEarly() {
        // Test hiding spinner early (500ms after showing) when there is no glitch at
        // the end of draggin action.
        disableSpinnerAnimation();
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.GONE);

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        long timestamp = SystemClock.uptimeMillis();
        actionDown(handleStrategy, timestamp, INITIAL_HEIGHT - 100);
        actionMove(handleStrategy, timestamp, INITIAL_HEIGHT - 150);

        // Verify the spinner is visible.
        verify(mPCCTTestRule.mSpinnerView).setVisibility(View.VISIBLE);
        when(mPCCTTestRule.mSpinnerView.getVisibility()).thenReturn(View.VISIBLE);
        clearInvocations(mPCCTTestRule.mSpinnerView);

        long timeOut = PartialCustomTabBottomSheetStrategy.SPINNER_TIMEOUT_MS;
        shadowOf(Looper.getMainLooper()).idleFor(timeOut, TimeUnit.MILLISECONDS);

        // Verify the spinner goes invisible after the specified timeout.
        verify(mPCCTTestRule.mSpinnerView).setVisibility(View.GONE);
    }

    @Test
    public void expandToFullHeightOnShowingKeyboard() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        verify(mPCCTTestRule.mOnActivityLayoutCallback)
                .onActivityLayout(
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        eq(ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET));
        clearInvocations(mPCCTTestRule.mOnActivityLayoutCallback);

        assertTabIsAtInitialPos(mPCCTTestRule.mAttributeResults.get(0));

        strategy.onShowSoftInput(() -> {});
        shadowOf(Looper.getMainLooper()).idle();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        final int length = mPCCTTestRule.mAttributeResults.size();
        assertTrue(length > 1);

        // Verify that the tab expands to full height.
        assertTabIsFullHeight(mPCCTTestRule.mAttributeResults.get(length - 1));
        PartialCustomTabTestRule.waitForAnimationToFinish();
        verify(mPCCTTestRule.mOnResizedCallback).onResized(eq(FULL_HEIGHT), anyInt());
        clearInvocations(mPCCTTestRule.mOnResizedCallback);
        verify(mPCCTTestRule.mOnActivityLayoutCallback)
                .onActivityLayout(
                        eq(0),
                        eq(0),
                        eq(DEVICE_WIDTH),
                        eq(DEVICE_HEIGHT - NAVBAR_HEIGHT),
                        eq(ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET_MAXIMIZED));
        clearInvocations(mPCCTTestRule.mOnActivityLayoutCallback);
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.R)
    public void fixedHeightReactsTosoftKeyboard() {
        configureStatusBarHeightForR();
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500, true);
        assertTabIsAtInitialPos(getWindowAttributes());

        strategy.onShowSoftInput(() -> {});
        PartialCustomTabTestRule.waitForAnimationToFinish();
        // assertTabBelowStatusBar instead of assertTabIsFullHeight since
        // the height in mock is configured to return the device height minus
        // both navbar + status on R, which is more correct. By default on
        // other builds, status bar height was zero, thus ignored as it was
        // insignificant for tests.
        assertTabBelowStatusBar(getWindowAttributes());

        strategy.onImeStateChanged(/* imeVisible= */ true);
        assertTabBelowStatusBar(getWindowAttributes());

        strategy.onImeStateChanged(/* imeVisible= */ false);
        PartialCustomTabTestRule.waitForAnimationToFinish();
        assertTabIsAtInitialPos(getWindowAttributes());
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.P)
    public void fixedHeightReactsToSoftKeyboardBelowR() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500, true);
        assertTabIsAtInitialPos(getWindowAttributes());

        strategy.onShowSoftInput(() -> {});
        PartialCustomTabTestRule.waitForAnimationToFinish();
        assertTabIsFullHeight(getWindowAttributes());

        strategy.onImeStateChanged(/* imeVisible= */ true);
        assertTabIsFullHeight(getWindowAttributes());

        strategy.onImeStateChanged(/* imeVisible= */ false);
        PartialCustomTabTestRule.waitForAnimationToFinish();
        assertTabIsAtInitialPos(getWindowAttributes());
    }

    @Test
    public void moveUpFixedHeight() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500, true);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        long timestamp = SystemClock.uptimeMillis();

        // Try to drag up and verify that the location does not change.
        actionDown(handleStrategy, timestamp, INITIAL_HEIGHT - 100);
        actionMove(handleStrategy, timestamp, INITIAL_HEIGHT - 250);
        assertTabIsAtInitialPos(getWindowAttributes());
    }

    @Test
    public void moveUpFixedHeightWithFling() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500, true);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        long timestamp = SystemClock.uptimeMillis();

        // Drag down and then fling up hard, verify that the tab doesn't go up to the top.
        actionDown(handleStrategy, timestamp, INITIAL_HEIGHT + 100);
        strategy.onDragEnd(-FULL_HEIGHT * 2); // Mighty up-flinging
        assertTabIsAtInitialPos(getWindowAttributes());
    }

    @Test
    public void moveDownFixedHeight() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500, true);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        // Try to drag down and check that it returns to the initial height.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 1500, 1550, 1600));
    }

    @Test
    public void dragHandlebarInvisibleFixedHeight() {
        createPcctAtHeight(500, true);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(2, mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        verify(mPCCTTestRule.mDragHandlebar).setVisibility(View.GONE);
    }

    @Test
    public void invokeResizeCallbackExpansion() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(
                "mPCCTTestRule.mAttributeResults should have exactly 2 elements, one for "
                        + "setting the height and one for setting the width.",
                2,
                mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        // Drag to the top.
        assertTabIsFullHeight(dragTab(handleStrategy, 1500, 1000, 0));

        // invokeResizeCallback() should have been called and MANUAL_EXPANSION logged once.
    }

    @Test
    public void invokeResizeCallbackMinimization() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(
                "mPCCTTestRule.mAttributeResults should have exactly 2 elements, one for "
                        + "setting the height and one for setting the width.",
                2,
                mPCCTTestRule.mAttributeResults.size());
        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        // Drag to the top so it can be minimized in the next step.
        assertTabIsFullHeight(dragTab(handleStrategy, 1500, 1000, 0));

        // Drag down enough -> slide to the initial position.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 50, 650, 1300));

        // invokeResizeCallback() should have been called and MANUAL_MINIMIZATION logged once.
    }

    @Test
    public void callbackWhenHeightResized() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        assertTabIsAtInitialPos(mPCCTTestRule.mAttributeResults.get(0));
        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        // Slide back to the initial height -> no resize happens.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 1500, 1450, 1400));
        verify(mPCCTTestRule.mOnResizedCallback, never()).onResized(anyInt(), anyInt());

        // Drag to the top -> resized.
        assertTabIsFullHeight(dragTab(handleStrategy, 1500, 1000, 500));
        verify(mPCCTTestRule.mOnResizedCallback).onResized(eq(FULL_HEIGHT), anyInt());
        clearInvocations(mPCCTTestRule.mOnResizedCallback);

        // Slide back to the top -> no resize happens.
        assertTabIsFullHeight(dragTab(handleStrategy, 50, 100, 150));
        verify(mPCCTTestRule.mOnResizedCallback, never()).onResized(anyInt(), anyInt());

        // Drag to the initial height -> resized.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 50, 650, 1300));
        verify(mPCCTTestRule.mOnResizedCallback).onResized(eq(INITIAL_HEIGHT), anyInt());
    }

    @Test
    public void verifyNavigationBarHeightInMultiWindowMode() {
        mPCCTTestRule.setupDisplayMetricsInMultiWindowMode();
        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        assertEquals(0, strategy.getNavbarHeightForTesting());
    }

    @Config(sdk = Build.VERSION_CODES.Q)
    @Test
    public void enterAndExitHtmlFullscreen() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        strategy.createHandleStrategyForTesting();
        verify(mPCCTTestRule.mOnActivityLayoutCallback)
                .onActivityLayout(
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        eq(ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET));
        clearInvocations(mPCCTTestRule.mOnActivityLayoutCallback);

        assertFalse(getWindowAttributes().isFullscreen());
        int height = getWindowAttributes().height;
        doReturn(47)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(R.dimen.custom_tabs_shadow_offset));

        strategy.setFullscreenSupplierForTesting(() -> mFullscreen);

        mFullscreen = true;
        strategy.onEnterFullscreen(null, null);
        PartialCustomTabTestRule.waitForAnimationToFinish();
        assertTrue(getWindowAttributes().isFullscreen());
        assertEquals("Shadow should be removed.", 0, strategy.getShadowOffsetForTesting());
        verify(mPCCTTestRule.mOnResizedCallback).onResized(eq(DEVICE_HEIGHT), eq(DEVICE_WIDTH));
        clearInvocations(mPCCTTestRule.mOnResizedCallback);
        verify(mPCCTTestRule.mOnActivityLayoutCallback)
                .onActivityLayout(
                        eq(0),
                        eq(0),
                        eq(DEVICE_WIDTH),
                        eq(DEVICE_HEIGHT),
                        eq(ACTIVITY_LAYOUT_STATE_FULL_SCREEN));
        clearInvocations(mPCCTTestRule.mOnActivityLayoutCallback);

        mFullscreen = false;
        strategy.onExitFullscreen(null);
        PartialCustomTabTestRule.waitForAnimationToFinish();
        assertFalse(getWindowAttributes().isFullscreen());
        assertEquals(height, getWindowAttributes().height);
        assertNotEquals("Shadow should be restored.", 0, strategy.getShadowOffsetForTesting());
        verify(mPCCTTestRule.mOnResizedCallback).onResized(eq(height), anyInt());
        clearInvocations(mPCCTTestRule.mOnResizedCallback);
        verify(mPCCTTestRule.mOnActivityLayoutCallback)
                .onActivityLayout(
                        eq(0),
                        eq(DEVICE_HEIGHT - INITIAL_HEIGHT - NAVBAR_HEIGHT),
                        eq(DEVICE_WIDTH),
                        eq(DEVICE_HEIGHT - NAVBAR_HEIGHT),
                        eq(ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET));
        clearInvocations(mPCCTTestRule.mOnActivityLayoutCallback);
    }

    @Test
    public void dragToTheSameInitialY() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        mPCCTTestRule.verifyWindowFlagsSet();

        assertEquals(
                "mPCCTTestRule.mAttributeResults should have exactly 2 elements, one for "
                        + "setting the height and one for setting the width.",
                2,
                mPCCTTestRule.mAttributeResults.size());

        assertTabIsAtInitialPos(mPCCTTestRule.getWindowAttributes());

        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();

        // Drag tab slightly but actionDown and actionUp will be performed at the same Y.
        // The tab should remain open.
        assertTabIsAtInitialPos(dragTab(handleStrategy, 1500, 1450, 1500));
    }

    @Test
    public void dragBarMatchesFindToolbarInColor() {
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(500);
        strategy.setToolbarColorForTesting(PCCT_TOOLBAR_COLOR);
        doReturn(FIND_TOOLBAR_COLOR)
                .when(mPCCTTestRule.mActivity)
                .getColor(eq(R.color.find_in_page_background_color));
        doReturn(mPCCTTestRule.mDragBarBackground).when(mPCCTTestRule.mDragBar).getBackground();

        strategy.onFindToolbarShown();
        verify(mPCCTTestRule.mDragBarBackground).setColor(FIND_TOOLBAR_COLOR);

        strategy.onFindToolbarHidden();
        verify(mPCCTTestRule.mDragBarBackground).setColor(PCCT_TOOLBAR_COLOR);
    }

    @Config(sdk = Build.VERSION_CODES.Q)
    @Test
    public void noTopShadowAtFullHeight() {
        doReturn(47)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(R.dimen.custom_tabs_shadow_offset));
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(800);
        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();
        assertNotEquals(
                "Top margin should be non-zero for the shadow",
                0,
                mPCCTTestRule.mLayoutParams.topMargin);

        dragTab(handleStrategy, 1500, 1000, 500);
        assertEquals(
                "There should be no top shadow at full height",
                0,
                mPCCTTestRule.mLayoutParams.topMargin);
    }

    @Config(sdk = Build.VERSION_CODES.Q)
    @Test
    public void sideShadowsWith900dpBottomSheet() {
        doReturn(8)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(R.dimen.custom_tabs_shadow_offset));

        mPCCTTestRule.configPortraitMode();
        mPCCTTestRule.mRealMetrics.widthPixels = 700;
        mPCCTTestRule.mDisplaySize.x = 700;
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(800);
        HandleStrategy handleStrategy = strategy.createHandleStrategyForTesting();
        assertEquals(
                "Left margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);
        assertEquals(
                "Right margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);

        mPCCTTestRule.configLandscapeMode();
        strategy = createPcctAtHeight(800);
        handleStrategy = strategy.createHandleStrategyForTesting();

        assertNotEquals(
                "Left margin should be non-zero for the shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);
        assertNotEquals(
                "Right margin should be non-zero for the shadow",
                0,
                mPCCTTestRule.mLayoutParams.rightMargin);

        // Drag to the top
        assertEquals(DEVICE_HEIGHT_LANDSCAPE, dragTab(handleStrategy, 1500, 1000, 0).height);

        assertNotEquals(
                "Left margin should be non-zero for the shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);
        assertNotEquals(
                "Right margin should be non-zero for the shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);
    }

    @Test
    public void noTopShadowFullHeightBottomSheet() {
        doReturn(8)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(R.dimen.custom_tabs_shadow_offset));

        mPCCTTestRule.configPortraitMode();
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(3000);
        strategy.createHandleStrategyForTesting();
        assertEquals(
                "Top margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.topMargin);

        mPCCTTestRule.configLandscapeMode();
        strategy = createPcctAtHeight(3000);
        strategy.createHandleStrategyForTesting();
        assertEquals(
                "Top margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.topMargin);
    }

    @Test
    public void largeDeviceInPortrait_screenWidth() {
        doReturn(8)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(R.dimen.custom_tabs_shadow_offset));
        mPCCTTestRule.configPortraitMode();
        mPCCTTestRule.mRealMetrics.widthPixels = 6000;
        mPCCTTestRule.mRealMetrics.heightPixels = 9500;
        mPCCTTestRule.mDisplaySize.x = 6000;
        mPCCTTestRule.mDisplaySize.y = 9500;
        createPcctAtHeight(5000);
        assertEquals(
                "Left margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);
        assertEquals(
                "Right margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.rightMargin);
        assertEquals(
                "Bottom sheet width should be the screen width", 6000, getWindowAttributes().width);
    }

    @Config(sdk = Build.VERSION_CODES.Q)
    @Test
    public void largeDeviceInLandscape_900dpWidth() {
        doReturn(8)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(R.dimen.custom_tabs_shadow_offset));
        mPCCTTestRule.configLandscapeMode();
        mPCCTTestRule.mRealMetrics.widthPixels = 9500;
        mPCCTTestRule.mRealMetrics.heightPixels = 6000;
        mPCCTTestRule.mDisplaySize.x = 6000;
        mPCCTTestRule.mDisplaySize.y = 9500;
        createPcctAtHeight(5000);
        assertNotEquals(
                "Left margin should not be zero because there is a shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);
        assertNotEquals(
                "Right margin should not be zero because there is a shadow",
                0,
                mPCCTTestRule.mLayoutParams.rightMargin);
        assertEquals(
                "Bottom sheet width should be 900dp",
                BOTTOM_SHEET_MAX_WIDTH_DP_LANDSCAPE * mPCCTTestRule.getDisplayDensity(),
                getWindowAttributes().width,
                0.01f);
    }

    @CommandLineFlags.Add({BaseSwitches.ENABLE_LOW_END_DEVICE_MODE})
    @Test
    public void useDividerLine_LowEndDevice() {
        doReturn(8)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(R.dimen.custom_tabs_shadow_offset));
        mPCCTTestRule.configPortraitMode();
        createPcctAtHeight(1500);

        assertEquals(
                "Top margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.topMargin);

        // 900 dp landscape bottom sheet
        mPCCTTestRule.configLandscapeMode();
        createPcctAtHeight(3000);
        assertEquals(
                "Right margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.rightMargin);
        assertEquals(
                "Left margin should not be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);
    }

    @Config(sdk = Build.VERSION_CODES.P)
    @Test
    public void useDividerLine_OldOS() {
        doReturn(8)
                .when(mPCCTTestRule.mResources)
                .getDimensionPixelSize(eq(R.dimen.custom_tabs_shadow_offset));
        mPCCTTestRule.configPortraitMode();
        createPcctAtHeight(1500);

        assertEquals(
                "Top margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.topMargin);

        // 900 dp landscape bottom sheet
        mPCCTTestRule.configLandscapeMode();
        createPcctAtHeight(3000);
        assertEquals(
                "Right margin should be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.rightMargin);
        assertEquals(
                "Left margin should not be zero because there is no shadow",
                0,
                mPCCTTestRule.mLayoutParams.leftMargin);
    }

    @Test
    public void expandToFullHeightOnFindInPage() {
        mPCCTTestRule.configPortraitMode();
        PartialCustomTabBottomSheetStrategy strategy = createPcctAtHeight(800);
        verify(mPCCTTestRule.mOnActivityLayoutCallback)
                .onActivityLayout(
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        anyInt(),
                        eq(ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET));
        clearInvocations(mPCCTTestRule.mOnActivityLayoutCallback);
        doReturn(mPCCTTestRule.mDragBarBackground).when(mPCCTTestRule.mDragBar).getBackground();
        strategy.onFindToolbarShown();
        PartialCustomTabTestRule.waitForAnimationToFinish();

        assertTabIsFullHeight(getWindowAttributes());
        verify(mPCCTTestRule.mOnResizedCallback).onResized(eq(FULL_HEIGHT), anyInt());
        clearInvocations(mPCCTTestRule.mOnResizedCallback);
        verify(mPCCTTestRule.mOnActivityLayoutCallback)
                .onActivityLayout(
                        eq(0),
                        eq(0),
                        anyInt(),
                        anyInt(),
                        eq(ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET_MAXIMIZED));
        clearInvocations(mPCCTTestRule.mOnActivityLayoutCallback);

        strategy.onFindToolbarHidden();
        PartialCustomTabTestRule.waitForAnimationToFinish();

        assertTabIsAtInitialPos(getWindowAttributes());
        verify(mPCCTTestRule.mOnResizedCallback).onResized(eq(INITIAL_HEIGHT), anyInt());
        clearInvocations(mPCCTTestRule.mOnResizedCallback);
        verify(mPCCTTestRule.mOnActivityLayoutCallback)
                .onActivityLayout(
                        eq(0),
                        eq(DEVICE_HEIGHT - INITIAL_HEIGHT - NAVBAR_HEIGHT),
                        eq(DEVICE_WIDTH),
                        eq(DEVICE_HEIGHT - NAVBAR_HEIGHT),
                        eq(ACTIVITY_LAYOUT_STATE_BOTTOM_SHEET));
        clearInvocations(mPCCTTestRule.mOnActivityLayoutCallback);
    }

    @Test
    public void contentScrollMayResizeTab() {
        var intentData = mPCCTTestRule.mIntentData;
        when(intentData.contentScrollMayResizeTab()).thenReturn(true);
        ContentView contentView = Mockito.mock(ContentView.class);
        when(mPCCTTestRule.mTab.getContentView()).thenReturn(contentView);

        var strategy = createPcctAtHeight(500);
        GestureDetector detector = Mockito.mock(GestureDetector.class);
        ContentGestureListener listener = Mockito.mock(ContentGestureListener.class);
        strategy.setGestureObjectsForTesting(detector, listener);

        MotionEvent e = Mockito.mock(MotionEvent.class);
        when(e.getActionMasked()).thenReturn(MotionEvent.ACTION_DOWN);
        when(listener.getState()).thenReturn(GestureState.NONE);

        // At initial state (none) -> down event forwarded to GestureDetector
        assertFalse(strategy.onInterceptTouchEvent(e));
        verify(detector).onTouchEvent(e);
        clearInvocations(detector);

        strategy.onTouchEvent(e);
        verify(detector, never()).onTouchEvent(e);

        // At content-scroll state -> forward events to contentview
        when(listener.getState()).thenReturn(GestureState.SCROLL_CONTENT);
        when(e.getActionMasked()).thenReturn(MotionEvent.ACTION_MOVE);
        strategy.onTouchEvent(e);
        verify(detector).onTouchEvent(e);
        verify(contentView).onTouchEvent(e);

        clearInvocations(detector);
        clearInvocations(contentView);

        // Lift up finger -> release
        when(e.getActionMasked()).thenReturn(MotionEvent.ACTION_UP);
        strategy.onTouchEvent(e);
        verify(detector).onTouchEvent(e);
        verify(contentView).onTouchEvent(e);
        verify(listener).doNonFlingRelease();
    }
}