0

Disable page refresh on overscroll when precision pointer is attached.

When mouse or touchpad is available, do not allow page refresh on
overscroll.

tools/autotest.py -C out/android_x64 DeviceInputTest

Bug: 352167190
Test: tools/autotest.py -C out/android_x64 SwipeRefreshHandlerTest
Change-Id: Ied3360f2ecf13a865a2814dd293ff1a89710976b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6475171
Auto-Submit: Eric Lok <lokeric@google.com>
Reviewed-by: Theresa Sullivan <twellington@chromium.org>
Commit-Queue: Eric Lok <lokeric@google.com>
Reviewed-by: Sirisha Kavuluru <skavuluru@google.com>
Cr-Commit-Position: refs/heads/main@{#1454256}
This commit is contained in:
Eric Lok
2025-04-30 17:34:46 -07:00
committed by Chromium LUCI CQ
parent b13518c296
commit f4cd5091b9
3 changed files with 82 additions and 30 deletions
chrome/android
java
src
org
chromium
javatests
src
org
chromium
ui/android/java/src/org/chromium/ui/base

@ -37,6 +37,7 @@ import org.chromium.third_party.android.swiperefresh.SwipeRefreshLayout;
import org.chromium.ui.OverscrollAction;
import org.chromium.ui.OverscrollRefreshHandler;
import org.chromium.ui.base.BackGestureEventSwipeEdge;
import org.chromium.ui.base.DeviceInput;
import org.chromium.ui.base.WindowAndroid;
import java.lang.annotation.Retention;
@ -281,23 +282,26 @@ public class SwipeRefreshHandler extends TabWebContentsUserData
public boolean start(
@OverscrollAction int type, @BackGestureEventSwipeEdge int initiatingEdge) {
mSwipeType = type;
if (type == OverscrollAction.PULL_TO_REFRESH) {
if (mSwipeRefreshLayout == null) initSwipeRefreshLayout(mTab.getContext());
attachSwipeRefreshLayoutIfNecessary();
return mSwipeRefreshLayout.start();
} else if (type == OverscrollAction.HISTORY_NAVIGATION) {
if (mNavigationCoordinator != null) {
mNavigationCoordinator.startGesture();
// Note: triggerUi returns true as long as the handler is in a valid state, i.e.
// even if the navigation direction doesn't have further history entries.
boolean navigable = mNavigationCoordinator.triggerUi(initiatingEdge);
return navigable;
}
} else if (type == OverscrollAction.PULL_FROM_BOTTOM_EDGE) {
if (mBrowserControls != null) {
recordEdgeToEdgeOverscrollFromBottom(mBrowserControls);
if (isRefreshOnOverscrollSupported()) {
if (type == OverscrollAction.PULL_TO_REFRESH) {
if (mSwipeRefreshLayout == null) initSwipeRefreshLayout(mTab.getContext());
attachSwipeRefreshLayoutIfNecessary();
return mSwipeRefreshLayout.start();
} else if (type == OverscrollAction.HISTORY_NAVIGATION) {
if (mNavigationCoordinator != null) {
mNavigationCoordinator.startGesture();
// Note: triggerUi returns true as long as the handler is in a valid state, i.e.
// even if the navigation direction doesn't have further history entries.
boolean navigable = mNavigationCoordinator.triggerUi(initiatingEdge);
return navigable;
}
} else if (type == OverscrollAction.PULL_FROM_BOTTOM_EDGE) {
if (mBrowserControls != null) {
recordEdgeToEdgeOverscrollFromBottom(mBrowserControls);
}
}
}
mSwipeType = OverscrollAction.NONE;
return false;
}
@ -419,4 +423,22 @@ public class SwipeRefreshHandler extends TabWebContentsUserData
sample,
BottomControlsStatus.NUM_TOTAL);
}
/**
* Checks to see if page refresh on overscroll is supported Wrapped so we can stub behavior in
* tests.
*
* <p>Currently, overscroll to refresh is disabled if a precision pointer device is attached.
* For example, this will disable it for touch screen when a mouse is attaached. However
* long-term, the plan is to selectively enable for things such as touchscreen only.
*
* <p>TODO(crbug.com/412465463): enable overscroll refresh for touch even when precision pointer
* is attached
*
* @return true if page refresh on overscroll is supported.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
boolean isRefreshOnOverscrollSupported() {
return !DeviceInput.supportsPrecisionPointer();
}
}

@ -5,10 +5,13 @@
package org.chromium.chrome.browser;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
@ -63,6 +66,9 @@ public class SwipeRefreshHandlerTest {
private OnRefreshListener mOnRefreshListener;
private OnResetListener mOnResetListener;
private SwipeRefreshLayout mSwipeRefreshLayout;
private SwipeRefreshHandler mHandler;
private final SwipeRefreshHandler.SwipeRefreshLayoutCreator mSwipeRefreshLayoutCreator =
context -> {
mSwipeRefreshLayout = mock();
@ -87,14 +93,18 @@ public class SwipeRefreshHandlerTest {
when(mTab.getContext()).thenReturn(activityTestRule.getActivity());
when(mTab.getUserDataHost()).thenReturn(new UserDataHost());
when(mTab.getContentView()).thenReturn(mock());
// Limited use of spy() so we can test actual object, while changing some behaviors
// dynamically (such as whether mouse is attached or not)
mHandler = spy(SwipeRefreshHandler.from(mTab, mSwipeRefreshLayoutCreator));
mHandler.initWebContents(mock()); // Needed to enable the overscroll refresh handler.
doReturn(true).when(mHandler).isRefreshOnOverscrollSupported(); // Default no mouse/touchpad
}
@Test
@SmallTest
public void testAccessibilityAnnouncement() {
var handler = SwipeRefreshHandler.from(mTab, mSwipeRefreshLayoutCreator);
handler.initWebContents(mock()); // Needed to enable the overscroll refresh handler.
triggerRefresh(handler);
triggerRefresh(mHandler);
InOrder orderVerifier = inOrder(mSwipeRefreshLayout);
orderVerifier
@ -104,17 +114,25 @@ public class SwipeRefreshHandlerTest {
.verify(mSwipeRefreshLayout, times(1))
.setContentDescription(sAccessibilitySwipeRefreshString);
reset(handler);
reset(mHandler);
orderVerifier.verify(mSwipeRefreshLayout, times(1)).setContentDescription(null);
}
/** Ensures that we do not trigger refresh if precision pointing device is attached */
@Test
@SmallTest
public void testOverscrollButNoRefresh() {
doReturn(false).when(mHandler).isRefreshOnOverscrollSupported(); // pointer device attached
triggerRefresh(mHandler);
// When refresh is NOT triggered, then refresh layout is NOT created
assertNull(mSwipeRefreshLayout);
}
@Test
@SmallTest
public void testAccessibilityAnnouncement_swipingASecondTime() {
var handler = SwipeRefreshHandler.from(mTab, mSwipeRefreshLayoutCreator);
handler.initWebContents(mock()); // Needed to enable the overscroll refresh handler.
triggerRefresh(handler);
triggerRefresh(mHandler);
var firstSwipeRefreshLayout = mSwipeRefreshLayout;
@ -126,11 +144,11 @@ public class SwipeRefreshHandlerTest {
.verify(firstSwipeRefreshLayout, times(1))
.setContentDescription(sAccessibilitySwipeRefreshString);
reset(handler);
reset(mHandler);
orderVerifier.verify(mSwipeRefreshLayout, times(1)).setContentDescription(null);
triggerRefresh(handler);
triggerRefresh(mHandler);
var secondSwipeRefreshLayout = mSwipeRefreshLayout;

@ -52,7 +52,9 @@ public class DeviceInput implements InputDeviceListener {
for (int i = 0; i < deviceIds.length; i++) {
int deviceId = deviceIds[i];
InputDevice device = InputDevice.getDevice(deviceId);
if (device != null) mDeviceSnapshotsById.put(deviceId, DeviceSnapshot.from(device));
if (device != null) {
mDeviceSnapshotsById.put(deviceId, DeviceSnapshot.from(device));
}
}
// Register listener to perform cache updates.
@ -89,7 +91,9 @@ public class DeviceInput implements InputDeviceListener {
return sSupportsAlphabeticKeyboardForTesting;
}
for (int i = 0; i < mDeviceSnapshotsById.size(); i++) {
if (mDeviceSnapshotsById.valueAt(i).supportsAlphabeticKeyboard) return true;
if (mDeviceSnapshotsById.valueAt(i).supportsAlphabeticKeyboard) {
return true;
}
}
return false;
}
@ -117,7 +121,9 @@ public class DeviceInput implements InputDeviceListener {
return sSupportsPrecisionPointerForTesting;
}
for (int i = 0; i < mDeviceSnapshotsById.size(); i++) {
if (mDeviceSnapshotsById.valueAt(i).supportsPrecisionPointer) return true;
if (mDeviceSnapshotsById.valueAt(i).supportsPrecisionPointer) {
return true;
}
}
return false;
}
@ -126,15 +132,20 @@ public class DeviceInput implements InputDeviceListener {
public void onInputDeviceAdded(int deviceId) {
ThreadUtils.assertOnUiThread();
InputDevice device = InputDevice.getDevice(deviceId);
if (device != null) mDeviceSnapshotsById.put(deviceId, DeviceSnapshot.from(device));
if (device != null) {
mDeviceSnapshotsById.put(deviceId, DeviceSnapshot.from(device));
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
ThreadUtils.assertOnUiThread();
InputDevice device = InputDevice.getDevice(deviceId);
if (device != null) mDeviceSnapshotsById.put(deviceId, DeviceSnapshot.from(device));
else mDeviceSnapshotsById.remove(deviceId);
if (device != null) {
mDeviceSnapshotsById.put(deviceId, DeviceSnapshot.from(device));
} else {
mDeviceSnapshotsById.remove(deviceId);
}
}
@Override
@ -171,6 +182,7 @@ public class DeviceInput implements InputDeviceListener {
return new DeviceSnapshot(
/* supportsAlphabeticKeyboard= */ isPhysical
&& device.getKeyboardType() == KEYBOARD_TYPE_ALPHABETIC,
// SOURCE_MOUSE applies to pointer devices, including mouse and touchpad
/* supportsPrecisionPointer= */ isPhysical
&& device.supportsSource(SOURCE_MOUSE));
}