diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/PanZoomController.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/geckoview/PanZoomController.java | 949 |
1 files changed, 949 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/PanZoomController.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/PanZoomController.java new file mode 100644 index 0000000000..0731e4e095 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/PanZoomController.java @@ -0,0 +1,949 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview; + +import android.app.UiModeManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.SystemClock; +import android.util.Log; +import android.util.Pair; +import android.view.InputDevice; +import android.view.MotionEvent; +import androidx.annotation.AnyThread; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.mozglue.JNIObject; +import org.mozilla.gecko.util.GeckoBundle; +import org.mozilla.gecko.util.ThreadUtils; + +@UiThread +public class PanZoomController { + private static final String LOGTAG = "GeckoNPZC"; + private static final int EVENT_SOURCE_SCROLL = 0; + private static final int EVENT_SOURCE_MOTION = 1; + private static final int EVENT_SOURCE_MOUSE = 2; + private static Boolean sTreatMouseAsTouch = null; + + private final GeckoSession mSession; + private final Rect mTempRect = new Rect(); + private boolean mAttached; + private float mPointerScrollFactor = 64.0f; + private long mLastDownTime; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({SCROLL_BEHAVIOR_SMOOTH, SCROLL_BEHAVIOR_AUTO}) + public @interface ScrollBehaviorType {} + + /** Specifies smooth scrolling which animates content to the desired scroll position. */ + public static final int SCROLL_BEHAVIOR_SMOOTH = 0; + + /** Specifies auto scrolling which jumps content to the desired scroll position. */ + public static final int SCROLL_BEHAVIOR_AUTO = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + INPUT_RESULT_UNHANDLED, + INPUT_RESULT_HANDLED, + INPUT_RESULT_HANDLED_CONTENT, + INPUT_RESULT_IGNORED + }) + public @interface InputResult {} + + /** + * Specifies that an input event was not handled by the PanZoomController for a panning or zooming + * operation. The event may have been handled by Web content or internally (e.g. text selection). + */ + @WrapForJNI public static final int INPUT_RESULT_UNHANDLED = 0; + + /** + * Specifies that an input event was handled by the PanZoomController for a panning or zooming + * operation, but likely not by any touch event listeners in Web content. + */ + @WrapForJNI public static final int INPUT_RESULT_HANDLED = 1; + + /** + * Specifies that an input event was handled by the PanZoomController and passed on to touch event + * listeners in Web content. + */ + @WrapForJNI public static final int INPUT_RESULT_HANDLED_CONTENT = 2; + + /** + * Specifies that an input event was consumed by a PanZoomController internally and browsers + * should do nothing in response to the event. + */ + @WrapForJNI public static final int INPUT_RESULT_IGNORED = 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + SCROLLABLE_FLAG_NONE, + SCROLLABLE_FLAG_TOP, + SCROLLABLE_FLAG_RIGHT, + SCROLLABLE_FLAG_BOTTOM, + SCROLLABLE_FLAG_LEFT + }) + public @interface ScrollableDirections {} + + /** + * Represents which directions can be scrolled in the scroll container where an input event was + * handled. This value is only useful in the case of {@link + * PanZoomController#INPUT_RESULT_HANDLED}. + */ + /* The container cannot be scrolled. */ + @WrapForJNI public static final int SCROLLABLE_FLAG_NONE = 0; + + /* The container cannot be scrolled to top */ + @WrapForJNI public static final int SCROLLABLE_FLAG_TOP = 1 << 0; + /* The container cannot be scrolled to right */ + @WrapForJNI public static final int SCROLLABLE_FLAG_RIGHT = 1 << 1; + /* The container cannot be scrolled to bottom */ + @WrapForJNI public static final int SCROLLABLE_FLAG_BOTTOM = 1 << 2; + /* The container cannot be scrolled to left */ + @WrapForJNI public static final int SCROLLABLE_FLAG_LEFT = 1 << 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {OVERSCROLL_FLAG_NONE, OVERSCROLL_FLAG_HORIZONTAL, OVERSCROLL_FLAG_VERTICAL}) + public @interface OverscrollDirections {} + + /** + * Represents which directions can be over-scrolled in the scroll container where an input event + * was handled. This value is only useful in the case of {@link + * PanZoomController#INPUT_RESULT_HANDLED}. + */ + /* the container cannot be over-scrolled. */ + @WrapForJNI public static final int OVERSCROLL_FLAG_NONE = 0; + + /* the container can be over-scrolled horizontally. */ + @WrapForJNI public static final int OVERSCROLL_FLAG_HORIZONTAL = 1 << 0; + /* the container can be over-scrolled vertically. */ + @WrapForJNI public static final int OVERSCROLL_FLAG_VERTICAL = 1 << 1; + + /** + * Represents how a {@link MotionEvent} was handled in Gecko. This value can be used by browser + * apps to implement features like pull-to-refresh. Failing to account this value might break some + * websites expectations about touch events. + * + * <p>For example, a {@link PanZoomController.InputResultDetail#handledResult} value of {@link + * PanZoomController#INPUT_RESULT_HANDLED} and {@link + * PanZoomController.InputResultDetail#overscrollDirections} of {@link + * PanZoomController#OVERSCROLL_FLAG_NONE} indicates that the event was consumed for a panning or + * zooming operation and that the website does not expect the browser to react to the touch event + * (say, by triggering the pull-to-refresh feature) even though the scroll container reached to + * the edge. + */ + @WrapForJNI + public static class InputResultDetail { + protected InputResultDetail( + final @InputResult int handledResult, + final @ScrollableDirections int scrollableDirections, + final @OverscrollDirections int overscrollDirections) { + mHandledResult = handledResult; + mScrollableDirections = scrollableDirections; + mOverscrollDirections = overscrollDirections; + } + + /** + * @return One of the {@link #INPUT_RESULT_UNHANDLED INPUT_RESULT_*} indicating how the event + * was handled. + */ + @AnyThread + public @InputResult int handledResult() { + return mHandledResult; + } + + /** + * @return an OR-ed value of {@link #SCROLLABLE_FLAG_NONE SCROLLABLE_FLAG_*} indicating which + * directions can be scrollable. + */ + @AnyThread + public @ScrollableDirections int scrollableDirections() { + return mScrollableDirections; + } + + /** + * @return an OR-ed value of {@link #OVERSCROLL_FLAG_NONE OVERSCROLL_FLAG_*} indicating which + * directions can be over-scrollable. + */ + @AnyThread + public @OverscrollDirections int overscrollDirections() { + return mOverscrollDirections; + } + + private final @InputResult int mHandledResult; + private final @ScrollableDirections int mScrollableDirections; + private final @OverscrollDirections int mOverscrollDirections; + } + + private SynthesizedEventState mPointerState; + + private ArrayList<Pair<Integer, MotionEvent>> mQueuedEvents; + + private boolean mSynthesizedEvent = false; + + @WrapForJNI + private static class MotionEventData { + public final int action; + public final int actionIndex; + public final long time; + public final int metaState; + public final int pointerId[]; + public final int historySize; + public final long historicalTime[]; + public final float historicalX[]; + public final float historicalY[]; + public final float historicalOrientation[]; + public final float historicalPressure[]; + public final float historicalToolMajor[]; + public final float historicalToolMinor[]; + public final float x[]; + public final float y[]; + public final float orientation[]; + public final float pressure[]; + public final float toolMajor[]; + public final float toolMinor[]; + + public MotionEventData(final MotionEvent event) { + final int count = event.getPointerCount(); + action = event.getActionMasked(); + actionIndex = event.getActionIndex(); + time = event.getEventTime(); + metaState = event.getMetaState(); + historySize = event.getHistorySize(); + historicalTime = new long[historySize]; + historicalX = new float[historySize * count]; + historicalY = new float[historySize * count]; + historicalOrientation = new float[historySize * count]; + historicalPressure = new float[historySize * count]; + historicalToolMajor = new float[historySize * count]; + historicalToolMinor = new float[historySize * count]; + pointerId = new int[count]; + x = new float[count]; + y = new float[count]; + orientation = new float[count]; + pressure = new float[count]; + toolMajor = new float[count]; + toolMinor = new float[count]; + + for (int historyIndex = 0; historyIndex < historySize; historyIndex++) { + historicalTime[historyIndex] = event.getHistoricalEventTime(historyIndex); + } + + final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + for (int i = 0; i < count; i++) { + pointerId[i] = event.getPointerId(i); + + for (int historyIndex = 0; historyIndex < historySize; historyIndex++) { + event.getHistoricalPointerCoords(i, historyIndex, coords); + + final int historicalI = historyIndex * count + i; + historicalX[historicalI] = coords.x; + historicalY[historicalI] = coords.y; + + historicalOrientation[historicalI] = coords.orientation; + historicalPressure[historicalI] = coords.pressure; + + // If we are converting to CSS pixels, we should adjust the radii as well. + historicalToolMajor[historicalI] = coords.toolMajor; + historicalToolMinor[historicalI] = coords.toolMinor; + } + + event.getPointerCoords(i, coords); + + x[i] = coords.x; + y[i] = coords.y; + + orientation[i] = coords.orientation; + pressure[i] = coords.pressure; + + // If we are converting to CSS pixels, we should adjust the radii as well. + toolMajor[i] = coords.toolMajor; + toolMinor[i] = coords.toolMinor; + } + } + } + + /* package */ final class NativeProvider extends JNIObject { + @Override // JNIObject + protected void disposeNative() { + // Disposal happens in native code. + throw new UnsupportedOperationException(); + } + + @WrapForJNI(calledFrom = "ui") + private native void handleMotionEvent( + MotionEventData eventData, + float screenX, + float screenY, + GeckoResult<InputResultDetail> result); + + @WrapForJNI(calledFrom = "ui") + private native @InputResult int handleScrollEvent( + long time, int metaState, float x, float y, float hScroll, float vScroll); + + @WrapForJNI(calledFrom = "ui") + private native @InputResult int handleMouseEvent( + int action, long time, int metaState, float x, float y, int buttons); + + @WrapForJNI(stubName = "SetIsLongpressEnabled") // Called from test thread. + private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled); + + @WrapForJNI(calledFrom = "ui") + private void synthesizeNativeTouchPoint( + final int pointerId, + final int eventType, + final int clientX, + final int clientY, + final double pressure, + final int orientation) { + if (pointerId == PointerInfo.RESERVED_MOUSE_POINTER_ID) { + throw new IllegalArgumentException("Pointer ID reserved for mouse"); + } + synthesizeNativePointer( + InputDevice.SOURCE_TOUCHSCREEN, + pointerId, + eventType, + clientX, + clientY, + pressure, + orientation, + 0); + } + + @WrapForJNI(calledFrom = "ui") + private void synthesizeNativeMouseEvent( + final int eventType, final int clientX, final int clientY, final int button) { + synthesizeNativePointer( + InputDevice.SOURCE_MOUSE, + PointerInfo.RESERVED_MOUSE_POINTER_ID, + eventType, + clientX, + clientY, + 0, + 0, + button); + } + + @WrapForJNI(calledFrom = "ui") + private void setAttached(final boolean attached) { + if (attached) { + mAttached = true; + flushEventQueue(); + } else if (mAttached) { + mAttached = false; + enableEventQueue(); + } + } + } + + /* package */ final NativeProvider mNative = new NativeProvider(); + + private void handleMotionEvent(final MotionEvent event) { + handleMotionEvent(event, null); + } + + private void handleMotionEvent( + final MotionEvent event, final GeckoResult<InputResultDetail> result) { + if (!mAttached) { + mQueuedEvents.add(new Pair<>(EVENT_SOURCE_MOTION, event)); + if (result != null) { + result.complete( + new InputResultDetail( + INPUT_RESULT_HANDLED, SCROLLABLE_FLAG_NONE, OVERSCROLL_FLAG_NONE)); + } + return; + } + + final int action = event.getActionMasked(); + + if (action == MotionEvent.ACTION_DOWN) { + mLastDownTime = event.getDownTime(); + } else if (mLastDownTime != event.getDownTime()) { + if (result != null) { + result.complete( + new InputResultDetail( + INPUT_RESULT_UNHANDLED, SCROLLABLE_FLAG_NONE, OVERSCROLL_FLAG_NONE)); + } + return; + } + + final float screenX = event.getRawX() - event.getX(); + final float screenY = event.getRawY() - event.getY(); + + // Take this opportunity to update screen origin of session. This gets + // dispatched to the gecko thread, so we also pass the new screen x/y directly to apz. + // If this is a synthesized touch, the screen offset is bogus so ignore it. + if (!mSynthesizedEvent) { + mSession.onScreenOriginChanged((int) screenX, (int) screenY); + } + + final MotionEventData data = new MotionEventData(event); + mNative.handleMotionEvent(data, screenX, screenY, result); + } + + private @InputResult int handleScrollEvent(final MotionEvent event) { + if (!mAttached) { + mQueuedEvents.add(new Pair<>(EVENT_SOURCE_SCROLL, event)); + return INPUT_RESULT_HANDLED; + } + + final int count = event.getPointerCount(); + + if (count <= 0) { + return INPUT_RESULT_UNHANDLED; + } + + final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + event.getPointerCoords(0, coords); + + // Translate surface origin to client origin for scroll events. + mSession.getSurfaceBounds(mTempRect); + final float x = coords.x - mTempRect.left; + final float y = coords.y - mTempRect.top; + + final float hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) * mPointerScrollFactor; + final float vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * mPointerScrollFactor; + + return mNative.handleScrollEvent( + event.getEventTime(), event.getMetaState(), x, y, hScroll, vScroll); + } + + private @InputResult int handleMouseEvent(final MotionEvent event) { + if (!mAttached) { + mQueuedEvents.add(new Pair<>(EVENT_SOURCE_MOUSE, event)); + return INPUT_RESULT_UNHANDLED; + } + + final int count = event.getPointerCount(); + + if (count <= 0) { + return INPUT_RESULT_UNHANDLED; + } + + final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + event.getPointerCoords(0, coords); + + // Translate surface origin to client origin for mouse events. + mSession.getSurfaceBounds(mTempRect); + final float x = coords.x - mTempRect.left; + final float y = coords.y - mTempRect.top; + + return mNative.handleMouseEvent( + event.getActionMasked(), + event.getEventTime(), + event.getMetaState(), + x, + y, + event.getButtonState()); + } + + protected PanZoomController(final GeckoSession session) { + mSession = session; + enableEventQueue(); + } + + private boolean treatMouseAsTouch() { + if (sTreatMouseAsTouch == null) { + final Context c = GeckoAppShell.getApplicationContext(); + if (c == null) { + // This might happen if the GeckoRuntime has not been initialized yet. + return false; + } + final UiModeManager m = (UiModeManager) c.getSystemService(Context.UI_MODE_SERVICE); + // on TV devices, treat mouse as touch. everywhere else, don't + sTreatMouseAsTouch = (m.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION); + } + + return sTreatMouseAsTouch; + } + + /** + * Set the current scroll factor. The scroll factor is the maximum scroll amount that one scroll + * event may generate, in device pixels. + * + * @param factor Scroll factor. + */ + public void setScrollFactor(final float factor) { + ThreadUtils.assertOnUiThread(); + mPointerScrollFactor = factor; + } + + /** + * Get the current scroll factor. + * + * @return Scroll factor. + */ + public float getScrollFactor() { + ThreadUtils.assertOnUiThread(); + return mPointerScrollFactor; + } + + /** + * This is a workaround for touch pad on Android app by Chrome OS. Android app on Chrome OS fires + * weird motion event by two finger scroll. See https://crbug.com/704051 + */ + private boolean mayTouchpadScroll(final @NonNull MotionEvent event) { + final int action = event.getActionMasked(); + return event.getButtonState() == 0 + && (action == MotionEvent.ACTION_DOWN + || (mLastDownTime == event.getDownTime() + && (action == MotionEvent.ACTION_MOVE + || action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_CANCEL))); + } + + /** + * 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. + * + * @param event MotionEvent to process. + */ + public void onTouchEvent(final @NonNull MotionEvent event) { + ThreadUtils.assertOnUiThread(); + + if (!treatMouseAsTouch() + && event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE + && !mayTouchpadScroll(event)) { + handleMouseEvent(event); + return; + } + 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. + * + * <p>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 {@link PanZoomController.InputResultDetail}). + */ + public @NonNull GeckoResult<InputResultDetail> onTouchEventForDetailResult( + final @NonNull MotionEvent event) { + ThreadUtils.assertOnUiThread(); + + if (!treatMouseAsTouch() + && event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE + && !mayTouchpadScroll(event)) { + return GeckoResult.fromValue( + new InputResultDetail( + handleMouseEvent(event), SCROLLABLE_FLAG_NONE, OVERSCROLL_FLAG_NONE)); + } + + final GeckoResult<InputResultDetail> result = new GeckoResult<>(); + handleMotionEvent(event, result); + return result; + } + + /** + * Process a touch event through the pan-zoom controller. Treat any mouse events as "mouse" rather + * than as "touch". Pointer coordinates should be relative to the display surface. + * + * @param event MotionEvent to process. + */ + public void onMouseEvent(final @NonNull MotionEvent event) { + ThreadUtils.assertOnUiThread(); + + if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { + return; + } + handleMotionEvent(event); + } + + @Override + protected void finalize() throws Throwable { + mNative.setAttached(false); + } + + /** + * Process a non-touch motion event through the pan-zoom controller. Currently, hover and scroll + * events are supported. Pointer coordinates should be relative to the display surface. + * + * @param event MotionEvent to process. + */ + public void onMotionEvent(final @NonNull MotionEvent event) { + ThreadUtils.assertOnUiThread(); + + final int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_SCROLL) { + if (event.getDownTime() >= mLastDownTime) { + mLastDownTime = event.getDownTime(); + } else if ((InputDevice.getDevice(event.getDeviceId()) != null) + && (InputDevice.getDevice(event.getDeviceId()).getSources() & InputDevice.SOURCE_TOUCHPAD) + == InputDevice.SOURCE_TOUCHPAD) { + return; + } + handleScrollEvent(event); + } else if ((action == MotionEvent.ACTION_HOVER_MOVE) + || (action == MotionEvent.ACTION_HOVER_ENTER) + || (action == MotionEvent.ACTION_HOVER_EXIT)) { + handleMouseEvent(event); + } + } + + private void enableEventQueue() { + if (mQueuedEvents != null) { + throw new IllegalStateException("Already have an event queue"); + } + mQueuedEvents = new ArrayList<>(); + } + + private void flushEventQueue() { + if (mQueuedEvents == null) { + return; + } + + final ArrayList<Pair<Integer, MotionEvent>> events = mQueuedEvents; + mQueuedEvents = null; + for (final Pair<Integer, MotionEvent> pair : events) { + switch (pair.first) { + case EVENT_SOURCE_MOTION: + handleMotionEvent(pair.second); + break; + case EVENT_SOURCE_SCROLL: + handleScrollEvent(pair.second); + break; + case EVENT_SOURCE_MOUSE: + handleMouseEvent(pair.second); + break; + } + } + } + + /** + * Set whether Gecko should generate long-press events. + * + * @param isLongpressEnabled True if Gecko should generate long-press events. + */ + public void setIsLongpressEnabled(final boolean isLongpressEnabled) { + ThreadUtils.assertOnUiThread(); + + if (mAttached) { + mNative.nativeSetIsLongpressEnabled(isLongpressEnabled); + } + } + + private static class PointerInfo { + // We reserve one pointer ID for the mouse, so that tests don't have + // to worry about tracking pointer IDs if they just want to test mouse + // event synthesization. If somebody tries to use this ID for a + // synthesized touch event we'll throw an exception. + public static final int RESERVED_MOUSE_POINTER_ID = 100000; + + public int pointerId; + public int source; + public int surfaceX; + public int surfaceY; + public double pressure; + public int orientation; + public int buttonState; + + public MotionEvent.PointerCoords getCoords() { + final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + coords.orientation = orientation; + coords.pressure = (float) pressure; + coords.x = surfaceX; + coords.y = surfaceY; + return coords; + } + } + + private static class SynthesizedEventState { + public final ArrayList<PointerInfo> pointers; + public long downTime; + + SynthesizedEventState() { + pointers = new ArrayList<PointerInfo>(); + } + + int getPointerIndex(final int pointerId) { + for (int i = 0; i < pointers.size(); i++) { + if (pointers.get(i).pointerId == pointerId) { + return i; + } + } + return -1; + } + + int addPointer(final int pointerId, final int source) { + final PointerInfo info = new PointerInfo(); + info.pointerId = pointerId; + info.source = source; + pointers.add(info); + return pointers.size() - 1; + } + + int getPointerCount(final int source) { + int count = 0; + for (int i = 0; i < pointers.size(); i++) { + if (pointers.get(i).source == source) { + count++; + } + } + return count; + } + + int getPointerButtonState(final int source) { + for (int i = 0; i < pointers.size(); i++) { + if (pointers.get(i).source == source) { + return pointers.get(i).buttonState; + } + } + return 0; + } + + MotionEvent.PointerProperties[] getPointerProperties(final int source) { + final MotionEvent.PointerProperties[] props = + new MotionEvent.PointerProperties[getPointerCount(source)]; + int index = 0; + for (int i = 0; i < pointers.size(); i++) { + if (pointers.get(i).source == source) { + final MotionEvent.PointerProperties p = new MotionEvent.PointerProperties(); + p.id = pointers.get(i).pointerId; + switch (source) { + case InputDevice.SOURCE_TOUCHSCREEN: + p.toolType = MotionEvent.TOOL_TYPE_FINGER; + break; + case InputDevice.SOURCE_MOUSE: + p.toolType = MotionEvent.TOOL_TYPE_MOUSE; + break; + } + props[index++] = p; + } + } + return props; + } + + MotionEvent.PointerCoords[] getPointerCoords(final int source) { + final MotionEvent.PointerCoords[] coords = + new MotionEvent.PointerCoords[getPointerCount(source)]; + int index = 0; + for (int i = 0; i < pointers.size(); i++) { + if (pointers.get(i).source == source) { + coords[index++] = pointers.get(i).getCoords(); + } + } + return coords; + } + } + + private void synthesizeNativePointer( + final int source, + final int pointerId, + final int originalEventType, + final int clientX, + final int clientY, + final double pressure, + final int orientation, + final int button) { + if (mPointerState == null) { + mPointerState = new SynthesizedEventState(); + } + + // Find the pointer if it already exists + int pointerIndex = mPointerState.getPointerIndex(pointerId); + + // Event-specific handling + int eventType = originalEventType; + switch (originalEventType) { + case MotionEvent.ACTION_POINTER_UP: + if (pointerIndex < 0) { + Log.w(LOGTAG, "Pointer-up for invalid pointer"); + return; + } + if (mPointerState.pointers.size() == 1) { + // Last pointer is going up + eventType = MotionEvent.ACTION_UP; + } + break; + case MotionEvent.ACTION_CANCEL: + if (pointerIndex < 0) { + Log.w(LOGTAG, "Pointer-cancel for invalid pointer"); + return; + } + break; + case MotionEvent.ACTION_POINTER_DOWN: + if (pointerIndex < 0) { + // Adding a new pointer + pointerIndex = mPointerState.addPointer(pointerId, source); + if (pointerIndex == 0) { + // first pointer + eventType = MotionEvent.ACTION_DOWN; + mPointerState.downTime = SystemClock.uptimeMillis(); + } + } else { + // We're moving an existing pointer + eventType = MotionEvent.ACTION_MOVE; + } + break; + case MotionEvent.ACTION_HOVER_MOVE: + if (pointerIndex < 0) { + // Mouse-move a pointer without it going "down". However + // in order to send the right MotionEvent without a lot of + // duplicated code, we add the pointer to mPointerState, + // and then remove it at the bottom of this function. + pointerIndex = mPointerState.addPointer(pointerId, source); + } else { + // We're moving an existing mouse pointer that went down. + eventType = MotionEvent.ACTION_MOVE; + } + break; + } + + // Translate client origin to surface origin. + mSession.getSurfaceBounds(mTempRect); + final int surfaceX = clientX + mTempRect.left; + final int surfaceY = clientY + mTempRect.top; + + // Update the pointer with the new info + final PointerInfo info = mPointerState.pointers.get(pointerIndex); + info.surfaceX = surfaceX; + info.surfaceY = surfaceY; + info.pressure = pressure; + info.orientation = orientation; + if (source == InputDevice.SOURCE_MOUSE) { + if (eventType == MotionEvent.ACTION_DOWN || eventType == MotionEvent.ACTION_MOVE) { + info.buttonState |= button; + } else if (eventType == MotionEvent.ACTION_UP) { + info.buttonState &= button; + } + } + + // Dispatch the event + int action = 0; + if (eventType == MotionEvent.ACTION_POINTER_DOWN + || eventType == MotionEvent.ACTION_POINTER_UP) { + // for pointer-down and pointer-up events we need to add the + // index of the relevant pointer. + action = (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + action &= MotionEvent.ACTION_POINTER_INDEX_MASK; + } + action |= (eventType & MotionEvent.ACTION_MASK); + final MotionEvent event = + MotionEvent.obtain( + /*downTime*/ mPointerState.downTime, + /*eventTime*/ SystemClock.uptimeMillis(), + /*action*/ action, + /*pointerCount*/ mPointerState.getPointerCount(source), + /*pointerProperties*/ mPointerState.getPointerProperties(source), + /*pointerCoords*/ mPointerState.getPointerCoords(source), + /*metaState*/ 0, + /*buttonState*/ mPointerState.getPointerButtonState(source), + /*xPrecision*/ 0, + /*yPrecision*/ 0, + /*deviceId*/ 0, + /*edgeFlags*/ 0, + /*source*/ source, + /*flags*/ 0); + + mSynthesizedEvent = true; + onTouchEvent(event); + mSynthesizedEvent = false; + + // Forget about removed pointers + if (eventType == MotionEvent.ACTION_POINTER_UP + || eventType == MotionEvent.ACTION_UP + || eventType == MotionEvent.ACTION_CANCEL + || eventType == MotionEvent.ACTION_HOVER_MOVE) { + mPointerState.pointers.remove(pointerIndex); + } + } + + /** + * Scroll the document body by an offset from the current scroll position. Uses {@link + * #SCROLL_BEHAVIOR_SMOOTH}. + * + * @param width {@link ScreenLength} offset to scroll along X axis. + * @param height {@link ScreenLength} offset to scroll along Y axis. + */ + @UiThread + public void scrollBy(final @NonNull ScreenLength width, final @NonNull ScreenLength height) { + scrollBy(width, height, SCROLL_BEHAVIOR_SMOOTH); + } + + /** + * Scroll the document body by an offset from the current scroll position. + * + * @param width {@link ScreenLength} offset to scroll along X axis. + * @param height {@link ScreenLength} offset to scroll along Y axis. + * @param behavior ScrollBehaviorType One of {@link #SCROLL_BEHAVIOR_SMOOTH}, {@link + * #SCROLL_BEHAVIOR_AUTO}, that specifies how to scroll the content. + */ + @UiThread + public void scrollBy( + final @NonNull ScreenLength width, + final @NonNull ScreenLength height, + final @ScrollBehaviorType int behavior) { + final GeckoBundle msg = buildScrollMessage(width, height, behavior); + mSession.getEventDispatcher().dispatch("GeckoView:ScrollBy", msg); + } + + /** + * Scroll the document body to an absolute position. Uses {@link #SCROLL_BEHAVIOR_SMOOTH}. + * + * @param width {@link ScreenLength} position to scroll along X axis. + * @param height {@link ScreenLength} position to scroll along Y axis. + */ + @UiThread + public void scrollTo(final @NonNull ScreenLength width, final @NonNull ScreenLength height) { + scrollTo(width, height, SCROLL_BEHAVIOR_SMOOTH); + } + + /** + * Scroll the document body to an absolute position. + * + * @param width {@link ScreenLength} position to scroll along X axis. + * @param height {@link ScreenLength} position to scroll along Y axis. + * @param behavior ScrollBehaviorType One of {@link #SCROLL_BEHAVIOR_SMOOTH}, {@link + * #SCROLL_BEHAVIOR_AUTO}, that specifies how to scroll the content. + */ + @UiThread + public void scrollTo( + final @NonNull ScreenLength width, + final @NonNull ScreenLength height, + final @ScrollBehaviorType int behavior) { + final GeckoBundle msg = buildScrollMessage(width, height, behavior); + mSession.getEventDispatcher().dispatch("GeckoView:ScrollTo", msg); + } + + /** Scroll to the top left corner of the screen. Uses {@link #SCROLL_BEHAVIOR_SMOOTH}. */ + @UiThread + public void scrollToTop() { + scrollTo(ScreenLength.zero(), ScreenLength.top(), SCROLL_BEHAVIOR_SMOOTH); + } + + /** Scroll to the bottom left corner of the screen. Uses {@link #SCROLL_BEHAVIOR_SMOOTH}. */ + @UiThread + public void scrollToBottom() { + scrollTo(ScreenLength.zero(), ScreenLength.bottom(), SCROLL_BEHAVIOR_SMOOTH); + } + + private GeckoBundle buildScrollMessage( + final @NonNull ScreenLength width, + final @NonNull ScreenLength height, + final @ScrollBehaviorType int behavior) { + final GeckoBundle msg = new GeckoBundle(); + msg.putDouble("widthValue", width.getValue()); + msg.putInt("widthType", width.getType()); + msg.putDouble("heightValue", height.getValue()); + msg.putInt("heightType", height.getType()); + msg.putInt("behavior", behavior); + return msg; + } +} |