// Copyright 2016 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.compositor.bottombar;
import android.app.Activity;
import android.content.Context;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseActivityTestRule;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.OverlayPanelEventFilter;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.IntentRequestTracker;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.test.util.BlankUiTestActivity;
/** Class responsible for testing the OverlayPanelEventFilter. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
public class OverlayPanelEventFilterTest {
private static final float PANEL_ALMOST_MAXIMIZED_OFFSET_Y_DP = 50.f;
private static final float BAR_HEIGHT_DP = 100.f;
private static final float LAYOUT_WIDTH_DP = 600.f;
private static final float LAYOUT_HEIGHT_DP = 800.f;
// A small value used to check whether two floats are almost equal.
private static final float EPSILON = 1e-04f;
private static final int MOCK_TOOLBAR_HEIGHT = 100;
@ClassRule
public static BaseActivityTestRule<BlankUiTestActivity> activityTestRule =
new BaseActivityTestRule<>(BlankUiTestActivity.class);
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock private LayoutManagerImpl mLayoutManager;
@Mock private BrowserControlsStateProvider mBrowserControlsStateProvider;
@Mock private ViewGroup mCompositorViewHolder;
@Mock private Profile mProfile;
@Mock private Tab mTab;
@Mock private OverlayPanelContentDelegate mOverlayPanelContentDelegate;
@Mock private OverlayPanelContentProgressObserver mOverlayPanelContentProgressObserver;
private float mTouchSlopDp;
private float mDpToPx;
private float mAlmostMaximizedContentOffsetYDp;
private float mMaximizedContentOffsetYDp;
private float mContentVerticalScroll;
private boolean mWasTapDetectedOnContent;
private boolean mWasScrollDetectedOnContent;
private MockOverlayPanel mPanel;
private OverlayPanelEventFilterWrapper mEventFilter;
private boolean mShouldLockHorizontalMotionInContent;
private MotionEvent mEventPropagatedToContent;
private boolean mEventWasScroll;
private boolean mEventWasTap;
private WindowAndroid mWindowAndroid;
private Activity mActivity;
// --------------------------------------------------------------------------------------------
// OverlayPanelEventFilterWrapper
// --------------------------------------------------------------------------------------------
/** Wrapper around OverlayPanelEventFilter used by tests. */
public final class OverlayPanelEventFilterWrapper extends OverlayPanelEventFilter {
public OverlayPanelEventFilterWrapper(Context context, OverlayPanel panel) {
super(context, panel);
}
@Override
protected float getContentViewVerticalScroll() {
return mContentVerticalScroll;
}
@Override
protected void propagateEventToContent(MotionEvent e) {
mEventPropagatedToContent = MotionEvent.obtain(e);
super.propagateEventToContent(e);
mEventPropagatedToContent.recycle();
}
@Override
protected boolean handleSingleTapUp(MotionEvent e) {
boolean handled = super.handleSingleTapUp(e);
mEventWasTap = true;
return handled;
}
@Override
protected boolean handleScroll(MotionEvent e1, MotionEvent e2, float distanceY) {
boolean handled = super.handleScroll(e1, e2, distanceY);
mEventWasScroll = true;
return handled;
}
}
// --------------------------------------------------------------------------------------------
// MockOverlayPanel
// --------------------------------------------------------------------------------------------
/** Mocks an OverlayPanel, so it doesn't create WebContents or animations. */
private final class MockOverlayPanel extends OverlayPanel {
private boolean mWasTapDetectedOnPanel;
private boolean mWasScrollDetectedOnPanel;
public MockOverlayPanel(
Context context,
LayoutManagerImpl layoutManager,
OverlayPanelManager manager,
BrowserControlsStateProvider browserControlsStateProvider,
WindowAndroid windowAndroid,
Profile profile,
ViewGroup compositorViewHolder,
Tab tab) {
super(
context,
layoutManager,
manager,
browserControlsStateProvider,
windowAndroid,
profile,
compositorViewHolder,
MOCK_TOOLBAR_HEIGHT,
() -> tab);
}
@Override
public OverlayPanelContent createNewOverlayPanelContent() {
return new MockOverlayPanelContent();
}
/** Override creation and destruction of the WebContents as they rely on native methods. */
private class MockOverlayPanelContent extends OverlayPanelContent {
public MockOverlayPanelContent() {
super(
mOverlayPanelContentDelegate,
mOverlayPanelContentProgressObserver,
mActivity,
mProfile,
MOCK_TOOLBAR_HEIGHT,
mCompositorViewHolder,
mWindowAndroid,
() -> mTab);
}
@Override
public void removeLastHistoryEntry(String url, long timeInMs) {}
}
@Override
public ViewGroup getContainerView() {
return new ViewGroup(ApplicationProvider.getApplicationContext()) {
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
if (e.getActionMasked() != MotionEvent.ACTION_CANCEL) {
mWasScrollDetectedOnContent = mEventWasScroll;
mWasTapDetectedOnContent = mEventWasTap;
// Check that the event offset is correct.
if (!mShouldLockHorizontalMotionInContent) {
float propagatedEventY = mEventPropagatedToContent.getY();
float offsetY = mPanel.getContentY() * mDpToPx;
Assert.assertEquals(propagatedEventY - offsetY, e.getY(), EPSILON);
}
} else {
mWasScrollDetectedOnContent = false;
mWasTapDetectedOnContent = false;
}
return super.dispatchTouchEvent(e);
}
@Override
public void onLayout(boolean changed, int l, int t, int r, int b) {}
};
}
@Override
protected void resizePanelContentView() {}
@Override
protected void animatePanelTo(float height, long duration) {
// Do not create animations for tests.
}
public boolean getWasTapDetected() {
return mWasTapDetectedOnPanel;
}
public boolean getWasScrollDetected() {
return mWasScrollDetectedOnPanel;
}
// GestureHandler overrides.
@Override
public void onDown(float x, float y, boolean fromMouse, int buttons) {}
@Override
public void onUpOrCancel() {}
@Override
public void drag(float x, float y, float dx, float dy, float tx, float ty) {
mWasScrollDetectedOnPanel = true;
}
@Override
public void click(float x, float y, boolean fromMouse, int buttons) {
mWasTapDetectedOnPanel = true;
}
@Override
public void fling(float x, float y, float velocityX, float velocityY) {}
@Override
public void onLongPress(float x, float y) {}
@Override
public void onPinch(float x0, float y0, float x1, float y1, boolean firstEvent) {}
}
// --------------------------------------------------------------------------------------------
// Test Suite
// --------------------------------------------------------------------------------------------
@BeforeClass
public static void setupSuite() {
activityTestRule.launchActivity(null);
}
@Before
public void setupTest() {
Context context = ApplicationProvider.getApplicationContext();
mDpToPx = context.getResources().getDisplayMetrics().density;
mTouchSlopDp = ViewConfiguration.get(context).getScaledTouchSlop() / mDpToPx;
ThreadUtils.runOnUiThreadBlocking(
() -> {
mActivity = activityTestRule.getActivity();
mWindowAndroid =
new ActivityWindowAndroid(
mActivity,
/* listenToActivityState= */ true,
IntentRequestTracker.createFromActivity(mActivity));
mPanel =
new MockOverlayPanel(
mActivity,
mLayoutManager,
new OverlayPanelManager(),
mBrowserControlsStateProvider,
mWindowAndroid,
mProfile,
mCompositorViewHolder,
mTab);
mEventFilter = new OverlayPanelEventFilterWrapper(mActivity, mPanel);
mPanel.setSearchBarHeightForTesting(BAR_HEIGHT_DP);
mPanel.setHeightForTesting(LAYOUT_HEIGHT_DP);
mPanel.setIsFullWidthSizePanelForTesting(true);
// NOTE(pedrosimonetti): This should be called after calling the method
// setIsFullWidthSizePanelForTesting(), otherwise it will crash the test.
mPanel.onSizeChanged(LAYOUT_WIDTH_DP, LAYOUT_HEIGHT_DP, 0, 0);
});
setContentViewVerticalScroll(0);
mAlmostMaximizedContentOffsetYDp = PANEL_ALMOST_MAXIMIZED_OFFSET_Y_DP + BAR_HEIGHT_DP;
mMaximizedContentOffsetYDp = BAR_HEIGHT_DP;
mWasTapDetectedOnContent = false;
mWasScrollDetectedOnContent = false;
mShouldLockHorizontalMotionInContent = false;
}
@After
public void tearDown() {
ThreadUtils.runOnUiThreadBlocking(
() -> {
mWindowAndroid.destroy();
});
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testTapContentView() {
positionPanelInAlmostMaximizedState();
// Simulate tap.
simulateActionDownEvent(0.f, mAlmostMaximizedContentOffsetYDp + 1.f);
simulateActionUpEvent(0.f, mAlmostMaximizedContentOffsetYDp + 1.f);
Assert.assertFalse(mPanel.getWasScrollDetected());
Assert.assertFalse(mPanel.getWasTapDetected());
Assert.assertTrue(mWasTapDetectedOnContent);
Assert.assertFalse(mWasScrollDetectedOnContent);
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testScrollingContentViewDragsPanel() {
positionPanelInAlmostMaximizedState();
// Simulate swipe up sequence.
simulateActionDownEvent(0.f, mAlmostMaximizedContentOffsetYDp + 1.f);
simulateActionMoveEvent(0.f, mMaximizedContentOffsetYDp);
simulateActionUpEvent(0.f, mMaximizedContentOffsetYDp);
Assert.assertTrue(mPanel.getWasScrollDetected());
Assert.assertFalse(mPanel.getWasTapDetected());
Assert.assertFalse(mWasScrollDetectedOnContent);
Assert.assertFalse(mWasTapDetectedOnContent);
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testScrollUpContentView() {
positionPanelInMaximizedState();
// Simulate swipe up sequence.
simulateActionDownEvent(0.f, mAlmostMaximizedContentOffsetYDp + 1.f);
simulateActionMoveEvent(0.f, mMaximizedContentOffsetYDp);
simulateActionUpEvent(0.f, mMaximizedContentOffsetYDp);
Assert.assertFalse(mPanel.getWasScrollDetected());
Assert.assertFalse(mPanel.getWasTapDetected());
Assert.assertTrue(mWasScrollDetectedOnContent);
Assert.assertFalse(mWasTapDetectedOnContent);
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testScrollDownContentView() {
positionPanelInMaximizedState();
// When the Panel is maximized and the scroll position is greater than zero, a swipe down
// on the ContentView should trigger a scroll on it.
setContentViewVerticalScroll(100.f);
// Simulate swipe down sequence.
simulateActionDownEvent(0.f, mMaximizedContentOffsetYDp + 1.f);
simulateActionMoveEvent(0.f, mAlmostMaximizedContentOffsetYDp);
simulateActionUpEvent(0.f, mAlmostMaximizedContentOffsetYDp);
Assert.assertFalse(mPanel.getWasScrollDetected());
Assert.assertFalse(mPanel.getWasTapDetected());
Assert.assertTrue(mWasScrollDetectedOnContent);
Assert.assertFalse(mWasTapDetectedOnContent);
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testDragByOverscrollingContentView() {
positionPanelInMaximizedState();
// When the Panel is maximized and the scroll position is zero, a swipe down on the
// ContentView should trigger a swipe on the Panel.
setContentViewVerticalScroll(0.f);
// Simulate swipe down sequence.
simulateActionDownEvent(0.f, mMaximizedContentOffsetYDp + 1.f);
simulateActionMoveEvent(0.f, mAlmostMaximizedContentOffsetYDp);
simulateActionUpEvent(0.f, mAlmostMaximizedContentOffsetYDp);
Assert.assertTrue(mPanel.getWasScrollDetected());
Assert.assertFalse(mPanel.getWasTapDetected());
Assert.assertFalse(mWasScrollDetectedOnContent);
Assert.assertFalse(mWasTapDetectedOnContent);
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testUnwantedScrollDoesNotHappenInContentView() {
positionPanelInAlmostMaximizedState();
float contentViewOffsetYStart = mAlmostMaximizedContentOffsetYDp + 1.f;
float contentViewOffsetYEnd = mMaximizedContentOffsetYDp - 1.f;
// Simulate swipe up to maximized position.
simulateActionDownEvent(0.f, contentViewOffsetYStart);
simulateActionMoveEvent(0.f, mMaximizedContentOffsetYDp);
positionPanelInMaximizedState();
// Confirm that the Panel got a scroll event.
Assert.assertTrue(mPanel.getWasScrollDetected());
// Continue the swipe up for one more dp. From now on, the events might be forwarded
// to the ContentView.
simulateActionMoveEvent(0.f, contentViewOffsetYEnd);
simulateActionUpEvent(0.f, contentViewOffsetYEnd);
// But 1 dp is not enough to trigger a scroll in the ContentView, and in this
// particular case, it should also not trigger a tap because the total displacement
// of the touch gesture is greater than the touch slop.
float contentViewOffsetDelta = contentViewOffsetYStart - contentViewOffsetYEnd;
Assert.assertTrue(Math.abs(contentViewOffsetDelta) > mTouchSlopDp);
Assert.assertFalse(mPanel.getWasTapDetected());
Assert.assertFalse(mWasScrollDetectedOnContent);
Assert.assertFalse(mWasTapDetectedOnContent);
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testDragPanelThenContinuouslyScrollContentView() {
positionPanelInAlmostMaximizedState();
// Simulate swipe up to maximized position.
simulateActionDownEvent(0.f, mAlmostMaximizedContentOffsetYDp + 1.f);
simulateActionMoveEvent(0.f, mMaximizedContentOffsetYDp);
positionPanelInMaximizedState();
// Confirm that the Panel got a scroll event.
Assert.assertTrue(mPanel.getWasScrollDetected());
// Continue the swipe up for one more dp. From now on, the events might be forwarded
// to the ContentView.
simulateActionMoveEvent(0.f, mMaximizedContentOffsetYDp - 1.f);
// Now keep swiping up an amount greater than the touch slop. In this case a scroll
// should be triggered in the ContentView.
simulateActionMoveEvent(0.f, mMaximizedContentOffsetYDp - 2 * mTouchSlopDp);
simulateActionUpEvent(0.f, mMaximizedContentOffsetYDp - 2 * mTouchSlopDp);
Assert.assertFalse(mPanel.getWasTapDetected());
Assert.assertTrue(mWasScrollDetectedOnContent);
Assert.assertFalse(mWasTapDetectedOnContent);
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testTapPanel() {
positionPanelInAlmostMaximizedState();
// Simulate tap.
simulateActionDownEvent(0.f, mAlmostMaximizedContentOffsetYDp - 1.f);
simulateActionUpEvent(0.f, mAlmostMaximizedContentOffsetYDp - 1.f);
Assert.assertFalse(mPanel.getWasScrollDetected());
Assert.assertTrue(mPanel.getWasTapDetected());
Assert.assertFalse(mWasScrollDetectedOnContent);
Assert.assertFalse(mWasTapDetectedOnContent);
}
@Test
@MediumTest
@Feature({"OverlayPanel"})
@UiThreadTest
public void testScrollPanel() {
positionPanelInAlmostMaximizedState();
// Simulate swipe up sequence.
simulateActionDownEvent(0.f, mAlmostMaximizedContentOffsetYDp - 1.f);
simulateActionMoveEvent(0.f, mMaximizedContentOffsetYDp);
simulateActionUpEvent(0.f, mMaximizedContentOffsetYDp);
Assert.assertTrue(mPanel.getWasScrollDetected());
Assert.assertFalse(mPanel.getWasTapDetected());
Assert.assertFalse(mWasScrollDetectedOnContent);
Assert.assertFalse(mWasTapDetectedOnContent);
}
// --------------------------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------------------------
/** Positions the Panel in the almost maximized state. */
private void positionPanelInAlmostMaximizedState() {
mPanel.setSearchBarHeightForTesting(BAR_HEIGHT_DP);
mPanel.setMaximizedForTesting(false);
mPanel.setOffsetYForTesting(PANEL_ALMOST_MAXIMIZED_OFFSET_Y_DP);
}
/** Positions the Panel in the maximized state. */
private void positionPanelInMaximizedState() {
mPanel.setSearchBarHeightForTesting(BAR_HEIGHT_DP);
mPanel.setMaximizedForTesting(true);
mPanel.setOffsetYForTesting(0);
}
/**
* Sets the vertical scroll position of the ContentView.
*
* @param contentViewVerticalScroll The vertical scroll position.
*/
private void setContentViewVerticalScroll(float contentViewVerticalScroll) {
mContentVerticalScroll = contentViewVerticalScroll;
}
/**
* Simulates a MotionEvent in the OverlayPanelEventFilter.
*
* @param action The event's action.
* @param x The event's x coordinate in dps.
* @param y The event's y coordinate in dps.
*/
private void simulateEvent(int action, float x, float y) {
MotionEvent motionEvent = MotionEvent.obtain(0, 0, action, x * mDpToPx, y * mDpToPx, 0);
mEventFilter.onTouchEventInternal(motionEvent);
}
/**
* Simulates a MotionEvent.ACTION_DOWN in the OverlayPanelEventFilter.
*
* @param x The event's x coordinate in dps.
* @param y The event's y coordinate in dps.
*/
private void simulateActionDownEvent(float x, float y) {
simulateEvent(MotionEvent.ACTION_DOWN, x, y);
}
/**
* Simulates a MotionEvent.ACTION_MOVE in the OverlayPanelEventFilter.
*
* @param x The event's x coordinate in dps.
* @param y The event's y coordinate in dps.
*/
private void simulateActionMoveEvent(float x, float y) {
simulateEvent(MotionEvent.ACTION_MOVE, x, y);
}
/**
* Simulates a MotionEvent.ACTION_UP in the OverlayPanelEventFilter.
*
* @param x The event's x coordinate in dps.
* @param y The event's y coordinate in dps.
*/
private void simulateActionUpEvent(float x, float y) {
simulateEvent(MotionEvent.ACTION_UP, x, y);
}
}