Bug 1633322 - Consider content handling for `onTouchEventForResult` r=geckoview-reviewers,botond,agi,esawin

Differential Revision: https://phabricator.services.mozilla.com/D86384
This commit is contained in:
James Willcox 2020-08-18 18:39:17 +00:00
Родитель 260f84556d
Коммит c3a58d3e76
8 изменённых файлов: 234 добавлений и 74 удалений

Просмотреть файл

@ -1244,8 +1244,7 @@ package org.mozilla.geckoview {
method public boolean getAutofillEnabled();
method @NonNull public PanZoomController getPanZoomController();
method @AnyThread @Nullable public GeckoSession getSession();
method public int onGenericMotionEventForResult(@NonNull MotionEvent);
method public int onTouchEventForResult(@NonNull MotionEvent);
method @NonNull public GeckoResult<Integer> onTouchEventForResult(@NonNull MotionEvent);
method @UiThread @Nullable public GeckoSession releaseSession();
method public void setAutofillEnabled(boolean);
method public void setDynamicToolbarMaxHeight(int);
@ -1419,9 +1418,10 @@ package org.mozilla.geckoview {
@UiThread public class PanZoomController {
ctor protected PanZoomController(GeckoSession);
method public float getScrollFactor();
method public int onMotionEvent(@NonNull MotionEvent);
method public int onMouseEvent(@NonNull MotionEvent);
method public int onTouchEvent(@NonNull MotionEvent);
method public void onMotionEvent(@NonNull MotionEvent);
method public void onMouseEvent(@NonNull MotionEvent);
method public void onTouchEvent(@NonNull MotionEvent);
method @NonNull public GeckoResult<Integer> onTouchEventForResult(@NonNull MotionEvent);
method @UiThread public void scrollBy(@NonNull ScreenLength, @NonNull ScreenLength);
method @UiThread public void scrollBy(@NonNull ScreenLength, @NonNull ScreenLength, int);
method @UiThread public void scrollTo(@NonNull ScreenLength, @NonNull ScreenLength);

Просмотреть файл

@ -0,0 +1,45 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="height=device-height,width=device-width,initial-scale=1.0">
<style type="text/css">
body {
background-color: white;
margin: 0;
}
#one {
background-color: red;
width: 100%;
height: 33vh;
}
#two {
background-color: green;
width: 100%;
height: 33vh;
}
#three {
background-color: blue;
width: 100%;
height: 33vh;
}
</style>
</head>
<body>
<div id="one"></div>
<div id="two"></div>
<div id="three"></div>
<script>
document.getElementById('two').addEventListener('touchstart', e => {
console.log('preventing default');
e.preventDefault();
});
document.getElementById('three').addEventListener('touchstart', e => {
console.log('not preventing default');
});
</script>
</body>
</html>

Просмотреть файл

@ -74,6 +74,8 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) {
const val IFRAME_UNKNOWN_PROTOCOL = "/assets/www/iframe_unknown_protocol.html"
const val MEDIA_SESSION_DOM1_PATH = "/assets/www/media_session_dom1.html"
const val MEDIA_SESSION_DEFAULT1_PATH = "/assets/www/media_session_default1.html"
const val TOUCH_HTML_PATH = "/assets/www/touch.html"
const val TEST_ENDPOINT = GeckoSessionTestRule.TEST_ENDPOINT
}

Просмотреть файл

@ -1,5 +1,7 @@
package org.mozilla.geckoview.test
import android.os.SystemClock
import android.view.MotionEvent
import org.mozilla.geckoview.ScreenLength
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
@ -8,6 +10,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.Matchers.*
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.PanZoomController
@RunWith(AndroidJUnit4::class)
@ -16,7 +19,7 @@ class PanZoomControllerTest : BaseSessionTest() {
private val errorEpsilon = 3.0
private val scrollWaitTimeout = 10000.0 // 10 seconds
private fun setup() {
private fun setupScroll() {
sessionRule.setPrefsUntilTestEnd(mapOf("dom.visualviewport.enabled" to true))
sessionRule.session.loadTestPath(SCROLL_TEST_PATH)
sessionRule.waitForPageStop()
@ -50,7 +53,7 @@ class PanZoomControllerTest : BaseSessionTest() {
private fun scrollByVertical(mode: Int) {
setup()
setupScroll()
val vh = mainSession.evaluateJS("window.visualViewport.height") as Double
assertThat("Visual viewport height is not zero", vh, greaterThan(0.0))
sessionRule.session.panZoomController.scrollBy(ScreenLength.zero(), ScreenLength.fromVisualViewportHeight(1.0), mode)
@ -61,7 +64,7 @@ class PanZoomControllerTest : BaseSessionTest() {
private fun scrollByHorizontal(mode: Int) {
setup()
setupScroll()
val vw = mainSession.evaluateJS("window.visualViewport.width") as Double
assertThat("Visual viewport width is not zero", vw, greaterThan(0.0))
sessionRule.session.panZoomController.scrollBy(ScreenLength.fromVisualViewportWidth(1.0), ScreenLength.zero(), mode)
@ -95,7 +98,7 @@ class PanZoomControllerTest : BaseSessionTest() {
}
private fun scrollByVerticalTwice(mode: Int) {
setup()
setupScroll()
val vh = mainSession.evaluateJS("window.visualViewport.height") as Double
assertThat("Visual viewport height is not zero", vh, greaterThan(0.0))
sessionRule.session.panZoomController.scrollBy(ScreenLength.zero(), ScreenLength.fromVisualViewportHeight(1.0), mode)
@ -119,7 +122,7 @@ class PanZoomControllerTest : BaseSessionTest() {
}
private fun scrollToVertical(mode: Int) {
setup()
setupScroll()
val vh = mainSession.evaluateJS("window.visualViewport.height") as Double
assertThat("Visual viewport height is not zero", vh, greaterThan(0.0))
sessionRule.session.panZoomController.scrollTo(ScreenLength.zero(), ScreenLength.fromVisualViewportHeight(1.0), mode)
@ -130,7 +133,7 @@ class PanZoomControllerTest : BaseSessionTest() {
private fun scrollToHorizontal(mode: Int) {
setup()
setupScroll()
val vw = mainSession.evaluateJS("window.visualViewport.width") as Double
assertThat("Visual viewport width is not zero", vw, greaterThan(0.0))
sessionRule.session.panZoomController.scrollTo(ScreenLength.fromVisualViewportWidth(1.0), ScreenLength.zero(), mode)
@ -164,7 +167,7 @@ class PanZoomControllerTest : BaseSessionTest() {
}
private fun scrollToVerticalOnZoomedContent(mode: Int) {
setup()
setupScroll()
val originalVH = mainSession.evaluateJS("window.visualViewport.height") as Double
assertThat("Visual viewport height is not zero", originalVH, greaterThan(0.0))
@ -204,7 +207,7 @@ class PanZoomControllerTest : BaseSessionTest() {
}
private fun scrollToVerticalTwice(mode: Int) {
setup()
setupScroll()
val vh = mainSession.evaluateJS("window.visualViewport.height") as Double
assertThat("Visual viewport height is not zero", vh, greaterThan(0.0))
sessionRule.session.panZoomController.scrollTo(ScreenLength.zero(), ScreenLength.fromVisualViewportHeight(1.0), mode)
@ -226,4 +229,47 @@ class PanZoomControllerTest : BaseSessionTest() {
fun scrollToVerticalTwiceAuto() {
scrollToVerticalTwice(PanZoomController.SCROLL_BEHAVIOR_AUTO)
}
private fun setupTouch() {
sessionRule.session.loadTestPath(TOUCH_HTML_PATH)
sessionRule.waitForPageStop()
}
private fun sendDownEvent(x: Float, y: Float): GeckoResult<Int> {
val downTime = SystemClock.uptimeMillis();
val down = MotionEvent.obtain(
downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0);
val result = mainSession.panZoomController.onTouchEventForResult(down)
val up = MotionEvent.obtain(
downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
mainSession.panZoomController.onTouchEvent(up)
return result
}
@WithDisplay(width = 100, height = 100)
@Test
fun touchEventForResult() {
setupTouch()
// No touch handlers, without scrolling
var value = sessionRule.waitForResult(sendDownEvent(50f, 15f))
assertThat("Value should match", value, equalTo(PanZoomController.INPUT_RESULT_UNHANDLED))
// Touch handler with preventDefault
value = sessionRule.waitForResult(sendDownEvent(50f, 45f))
assertThat("Value should match", value, equalTo(PanZoomController.INPUT_RESULT_HANDLED_CONTENT))
// Touch handler without preventDefault
value = sessionRule.waitForResult(sendDownEvent(50f, 75f))
assertThat("Value should match", value, equalTo(PanZoomController.INPUT_RESULT_HANDLED))
// No touch handlers, with scrolling
setupScroll()
value = sessionRule.waitForResult(sendDownEvent(50f, 50f))
assertThat("Value should match", value, equalTo(PanZoomController.INPUT_RESULT_HANDLED))
}
}

Просмотреть файл

@ -739,7 +739,7 @@ public class GeckoView extends FrameLayout {
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(final MotionEvent event) {
onTouchEventForResult(event);
mSession.getPanZoomController().onTouchEventForResult(event);
return true;
}
@ -748,51 +748,43 @@ public class GeckoView extends FrameLayout {
* {@link #onTouchEvent(MotionEvent)}, but instead returns a {@link PanZoomController.InputResult}
* indicating how the event was handled.
*
* NOTE: It is highly recommended to only call this with ACTION_DOWN or in otherwise
* limited capacity. Returning a GeckoResult for every touch event will generate
* a lot of allocations and unnecessary GC pressure.
*
* @param event A {@link MotionEvent}
* @return One of the {@link PanZoomController#INPUT_RESULT_UNHANDLED INPUT_RESULT_*}) indicating how the event was handled.
*/
public @PanZoomController.InputResult int onTouchEventForResult(final @NonNull MotionEvent event) {
public @NonNull GeckoResult<Integer> onTouchEventForResult(final @NonNull MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
requestFocus();
}
if (mSession == null) {
return PanZoomController.INPUT_RESULT_UNHANDLED;
return GeckoResult.fromValue(PanZoomController.INPUT_RESULT_UNHANDLED);
}
// NOTE: Treat mouse events as "touch" rather than as "mouse", so mouse can be
// used to pan/zoom. Call onMouseEvent() instead for behavior similar to desktop.
return mSession.getPanZoomController().onTouchEvent(event);
return mSession.getPanZoomController().onTouchEventForResult(event);
}
@Override
public boolean onGenericMotionEvent(final MotionEvent event) {
onGenericMotionEventForResult(event);
return true;
}
/**
* Dispatches a {@link MotionEvent} to the {@link PanZoomController}. This is the same as
* {@link #onGenericMotionEvent(MotionEvent)} (MotionEvent)}, but instead returns
* a {@link PanZoomController.InputResult} indicating how the event was handled.
*
* @param event A {@link MotionEvent}
* @return One of the {@link PanZoomController#INPUT_RESULT_UNHANDLED INPUT_RESULT_*}) indicating how the event was handled.
*/
public @PanZoomController.InputResult int onGenericMotionEventForResult(final @NonNull MotionEvent event) {
if (AndroidGamepadManager.handleMotionEvent(event)) {
return PanZoomController.INPUT_RESULT_HANDLED;
return true;
}
if (mSession == null) {
return PanZoomController.INPUT_RESULT_UNHANDLED;
return true;
}
if (mSession.getAccessibility().onMotionEvent(event)) {
return PanZoomController.INPUT_RESULT_HANDLED;
return true;
}
return mSession.getPanZoomController().onMotionEvent(event);
mSession.getPanZoomController().onMotionEvent(event);
return true;
}
@Override

Просмотреть файл

@ -98,10 +98,10 @@ public class PanZoomController {
}
@WrapForJNI(calledFrom = "ui")
private native @InputResult int handleMotionEvent(
private native void handleMotionEvent(
int action, int actionIndex, long time, int metaState, float screenX, float screenY,
int pointerId[], float x[], float y[], float orientation[], float pressure[],
float toolMajor[], float toolMinor[]);
float toolMajor[], float toolMinor[], GeckoResult<Integer> result);
@WrapForJNI(calledFrom = "ui")
private native @InputResult int handleScrollEvent(
@ -150,10 +150,15 @@ public class PanZoomController {
/* package */ final NativeProvider mNative = new NativeProvider();
private @InputResult int handleMotionEvent(final MotionEvent event) {
private void handleMotionEvent(final MotionEvent event) {
handleMotionEvent(event, null);
}
private void handleMotionEvent(final MotionEvent event, final GeckoResult<Integer> result) {
if (!mAttached) {
mQueuedEvents.add(new Pair<>(EVENT_SOURCE_MOTION, event));
return INPUT_RESULT_HANDLED;
result.complete(INPUT_RESULT_HANDLED);
return;
}
final int action = event.getActionMasked();
@ -162,7 +167,8 @@ public class PanZoomController {
if (action == MotionEvent.ACTION_DOWN) {
mLastDownTime = event.getDownTime();
} else if (mLastDownTime != event.getDownTime()) {
return INPUT_RESULT_UNHANDLED;
result.complete(INPUT_RESULT_UNHANDLED);
return;
}
final int[] pointerId = new int[count];
@ -200,9 +206,9 @@ public class PanZoomController {
mSession.onScreenOriginChanged((int)screenX, (int)screenY);
}
return mNative.handleMotionEvent(action, event.getActionIndex(), event.getEventTime(),
event.getMetaState(), screenX, screenY, pointerId, x, y,
orientation, pressure, toolMajor, toolMinor);
mNative.handleMotionEvent(action, event.getActionIndex(), event.getEventTime(),
event.getMetaState(), screenX, screenY, pointerId, x, y,
orientation, pressure, toolMajor, toolMinor, result);
}
private @InputResult int handleScrollEvent(final MotionEvent event) {
@ -314,15 +320,41 @@ public class PanZoomController {
* display surface.
*
* @param event MotionEvent to process.
* @return One of the {@link PanZoomController#INPUT_RESULT_UNHANDLED INPUT_RESULT_*}) constants indicating how the event was handled.
*/
public @InputResult int onTouchEvent(final @NonNull MotionEvent event) {
public void onTouchEvent(final @NonNull MotionEvent event) {
ThreadUtils.assertOnUiThread();
if (!sTreatMouseAsTouch && event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return handleMouseEvent(event);
return;
}
return handleMotionEvent(event);
handleMotionEvent(event);
}
/**
* Process a touch event through the pan-zoom controller. Treat any mouse events as
* "touch" rather than as "mouse". Pointer coordinates should be relative to the
* display surface.
*
* NOTE: It is highly recommended to only call this with ACTION_DOWN or in otherwise
* limited capacity. Returning a GeckoResult for every touch event will generate
* a lot of allocations and unnecessary GC pressure. Instead, prefer to call
* {@link #onTouchEvent(MotionEvent)}.
*
* @param event MotionEvent to process.
* @return A GeckoResult resolving to one of the
* {@link PanZoomController#INPUT_RESULT_UNHANDLED INPUT_RESULT_*}) constants indicating
* how the event was handled.
*/
public @NonNull GeckoResult<Integer> onTouchEventForResult(final @NonNull MotionEvent event) {
ThreadUtils.assertOnUiThread();
if (!sTreatMouseAsTouch && event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return GeckoResult.fromValue(handleMouseEvent(event));
}
final GeckoResult<Integer> result = new GeckoResult<>();
handleMotionEvent(event, result);
return result;
}
/**
@ -331,15 +363,14 @@ public class PanZoomController {
* display surface.
*
* @param event MotionEvent to process.
* @return One of the {@link PanZoomController#INPUT_RESULT_UNHANDLED INPUT_RESULT_*}) constants indicating how the event was handled.
*/
public @InputResult int onMouseEvent(final @NonNull MotionEvent event) {
public void onMouseEvent(final @NonNull MotionEvent event) {
ThreadUtils.assertOnUiThread();
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
return handleMouseEvent(event);
return;
}
return handleMotionEvent(event);
handleMotionEvent(event);
}
@Override
@ -353,9 +384,8 @@ public class PanZoomController {
* display surface.
*
* @param event MotionEvent to process.
* @return One of the {@link PanZoomController#INPUT_RESULT_UNHANDLED INPUT_RESULT_*}) indicating how the event was handled.
*/
public @InputResult int onMotionEvent(final @NonNull MotionEvent event) {
public void onMotionEvent(final @NonNull MotionEvent event) {
ThreadUtils.assertOnUiThread();
final int action = event.getActionMasked();
@ -365,15 +395,13 @@ public class PanZoomController {
} else if ((InputDevice.getDevice(event.getDeviceId()) != null) &&
(InputDevice.getDevice(event.getDeviceId()).getSources() &
InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
return INPUT_RESULT_UNHANDLED;
return;
}
return handleScrollEvent(event);
handleScrollEvent(event);
} else if ((action == MotionEvent.ACTION_HOVER_MOVE) ||
(action == MotionEvent.ACTION_HOVER_ENTER) ||
(action == MotionEvent.ACTION_HOVER_EXIT)) {
return handleMouseEvent(event);
} else {
return INPUT_RESULT_UNHANDLED;
handleMouseEvent(event);
}
}

Просмотреть файл

@ -16,9 +16,13 @@ exclude: true
## v81
- Added `cookiePurging` to [`ContentBlocking.Settings.Builder`][81.1] and `getCookiePurging` and `setCookiePurging`
to [`ContentBlocking.Settings`][81.2].
⚠️ - Changed [`GeckoView.onTouchEventForResult`][81.3] to return a `GeckoResult`, as it now
makes a round-trip to Gecko. The result will be more accurate now, since how content treats
the event is now considered.
[81.1]: {{javadoc_uri}}/ContentBlocking.Settings.Builder.html
[81.2]: {{javadoc_uri}}/ContentBlocking.Settings.html
[81.3]: {{javadoc_uri}}/GeckoView.html#onTouchEventForResult-android.view.MotionEvent-
## v80
- Removed `GeckoSession.hashCode` and `GeckoSession.equals` overrides in favor
@ -761,4 +765,4 @@ to allow adding gecko profiler markers.
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: a3245a37268efa5b1bbf787a3aca046670d36cdb
[api-version]: 9ee462cf1333c166efa9749ee87bbfd7675dfb81

Просмотреть файл

@ -34,12 +34,17 @@
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/DataSurfaceHelpers.h"
#include "mozilla/gfx/Types.h"
#include "mozilla/layers/RenderTrace.h"
#include <algorithm>
using mozilla::Unused;
using mozilla::dom::ContentChild;
using mozilla::dom::ContentParent;
using mozilla::gfx::DataSourceSurface;
using mozilla::gfx::IntSize;
using mozilla::gfx::Matrix;
using mozilla::gfx::SurfaceFormat;
#include "nsWindow.h"
@ -1025,15 +1030,17 @@ class nsWindow::NPZCSupport final
}
}
int32_t HandleMotionEvent(
void HandleMotionEvent(
const java::PanZoomController::NativeProvider::LocalRef& aInstance,
int32_t aAction, int32_t aActionIndex, int64_t aTime, int32_t aMetaState,
float aScreenX, float aScreenY, jni::IntArray::Param aPointerId,
jni::FloatArray::Param aX, jni::FloatArray::Param aY,
jni::FloatArray::Param aOrientation, jni::FloatArray::Param aPressure,
jni::FloatArray::Param aToolMajor, jni::FloatArray::Param aToolMinor) {
jni::FloatArray::Param aToolMajor, jni::FloatArray::Param aToolMinor,
jni::Object::Param aResult) {
MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
auto returnResult = java::GeckoResult::Ref::From(aResult);
RefPtr<IAPZCTreeManager> controller;
if (LockedWindowPtr window{mWindow}) {
@ -1041,7 +1048,11 @@ class nsWindow::NPZCSupport final
}
if (!controller) {
return INPUT_RESULT_UNHANDLED;
if (returnResult) {
returnResult->Complete(
java::sdk::Integer::ValueOf(INPUT_RESULT_UNHANDLED));
}
return;
}
nsTArray<int32_t> pointerId(aPointerId->GetElements());
@ -1070,7 +1081,11 @@ class nsWindow::NPZCSupport final
type = MultiTouchInput::MULTITOUCH_CANCEL;
break;
default:
return INPUT_RESULT_UNHANDLED;
if (returnResult) {
returnResult->Complete(
java::sdk::Integer::ValueOf(INPUT_RESULT_UNHANDLED));
}
return;
}
MultiTouchInput input(type, aTime, GetEventTimeStamp(aTime), 0);
@ -1127,12 +1142,15 @@ class nsWindow::NPZCSupport final
}
APZEventResult result = controller->InputBridge()->ReceiveInputEvent(input);
int32_t ret = (result.mHandledByRootApzc == Some(true))
? INPUT_RESULT_HANDLED
: INPUT_RESULT_HANDLED_CONTENT;
int32_t handled = (result.mHandledByRootApzc == Some(true))
? INPUT_RESULT_HANDLED
: INPUT_RESULT_HANDLED_CONTENT;
if (result.mStatus == nsEventStatus_eConsumeNoDefault) {
return ret;
if (returnResult) {
returnResult->Complete(java::sdk::Integer::ValueOf(handled));
}
return;
}
// Dispatch APZ input event on Gecko thread.
@ -1142,15 +1160,40 @@ class nsWindow::NPZCSupport final
window->DispatchHitTest(touchEvent);
});
switch (result.mStatus) {
case nsEventStatus_eIgnore:
return INPUT_RESULT_UNHANDLED;
case nsEventStatus_eConsumeDoDefault:
return ret;
default:
MOZ_ASSERT_UNREACHABLE("Unexpected nsEventStatus");
return INPUT_RESULT_UNHANDLED;
if (!returnResult) {
// We don't care how APZ handled the event so we're done here.
return;
}
if (result.mHandledByRootApzc != Nothing()) {
// We know conclusively that the root APZ handled this or not and
// don't need to do any more work.
switch (result.mStatus) {
case nsEventStatus_eIgnore:
returnResult->Complete(
java::sdk::Integer::ValueOf(INPUT_RESULT_UNHANDLED));
break;
case nsEventStatus_eConsumeDoDefault:
returnResult->Complete(java::sdk::Integer::ValueOf(handled));
break;
default:
MOZ_ASSERT_UNREACHABLE("Unexpected nsEventStatus");
returnResult->Complete(
java::sdk::Integer::ValueOf(INPUT_RESULT_UNHANDLED));
break;
}
return;
}
// Wait to see if APZ handled the event or not...
controller->AddInputBlockCallback(
result.mInputBlockId,
[returnResult = java::GeckoResult::GlobalRef(returnResult)](
uint64_t aInputBlockId, bool aHandledByRootApzc) {
returnResult->Complete(java::sdk::Integer::ValueOf(
aHandledByRootApzc ? INPUT_RESULT_HANDLED
: INPUT_RESULT_HANDLED_CONTENT));
});
}
};