Bug 1416310 - 3. Merge GeckoLayerClient into other classes; r?rbarker draft
authorJim Chen <nchen@mozilla.com>
Mon, 20 Nov 2017 17:17:02 -0500
changeset 700794 2c949bf456f32e798470a59730602eaaecc8bf99
parent 700793 cb0f792a60b0ceb016362c209ab7e7370db34088
child 700795 054d517d208e04ebc3cdf392c28d9aac6e17795b
push id89975
push userbmo:nchen@mozilla.com
push dateMon, 20 Nov 2017 22:17:18 +0000
reviewersrbarker
bugs1416310
milestone59.0a1
Bug 1416310 - 3. Merge GeckoLayerClient into other classes; r?rbarker Move the GeckoLayerClient JNI callbacks to LayerSession.Compositor. Move the rest of the viewport code to LayerView. Finally, move the input event synthesis code to NativePanZoomController. MozReview-Commit-ID: 1FEAM43KcwL
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerSession.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
@@ -43,23 +43,23 @@ public class DynamicToolbarAnimator {
     public interface ToolbarChromeProxy {
         public Bitmap getBitmapOfToolbarChrome();
         public boolean isToolbarChromeVisible();
         public void toggleToolbarChrome(boolean aShow);
     }
 
     private final Set<PinReason> mPinFlags = Collections.synchronizedSet(EnumSet.noneOf(PinReason.class));
 
-    private final GeckoLayerClient mTarget;
+    private final LayerView mTarget;
     private LayerSession.Compositor mCompositor;
     private final List<MetricsListener> mListeners;
     private ToolbarChromeProxy mToolbarChromeProxy;
     private int mMaxToolbarHeight;
 
-    public DynamicToolbarAnimator(GeckoLayerClient aTarget) {
+    public DynamicToolbarAnimator(final LayerView aTarget) {
         mTarget = aTarget;
         mListeners = new ArrayList<MetricsListener>();
     }
 
     public void addMetricsListener(MetricsListener aListener) {
         mListeners.add(aListener);
     }
 
@@ -149,27 +149,26 @@ public class DynamicToolbarAnimator {
             mCompositor.sendToolbarAnimatorMessage(immediately ?
                 LayerView.REQUEST_HIDE_TOOLBAR_IMMEDIATELY : LayerView.REQUEST_HIDE_TOOLBAR_ANIMATED);
         }
     }
 
     /* package-private */ IntSize getViewportSize() {
         ThreadUtils.assertOnUiThread();
 
-        int viewWidth = mTarget.getView().getWidth();
-        int viewHeight = mTarget.getView().getHeight();
+        int viewWidth = mTarget.getWidth();
+        int viewHeight = mTarget.getHeight();
         if ((mToolbarChromeProxy != null) && mToolbarChromeProxy.isToolbarChromeVisible()) {
           viewHeight -= mMaxToolbarHeight;
         }
         return new IntSize(viewWidth, viewHeight);
     }
 
     public PointF getVisibleEndOfLayerView() {
-        return new PointF(mTarget.getView().getWidth(),
-            mTarget.getView().getHeight());
+        return new PointF(mTarget.getWidth(), mTarget.getHeight());
     }
 
     /* package-private */ void updateCompositor() {
         if (isCompositorReady()) {
             mCompositor.setMaxToolbarHeight(mMaxToolbarHeight);
             if ((mToolbarChromeProxy != null) && mToolbarChromeProxy.isToolbarChromeVisible()) {
                 mCompositor.sendToolbarAnimatorMessage(LayerView.REQUEST_SHOW_TOOLBAR_IMMEDIATELY);
             } else {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerSession.java
@@ -26,16 +26,18 @@ public class LayerSession {
     // Special message sent from UiCompositorControllerChild once it is open.
     /* package */ final static int COMPOSITOR_CONTROLLER_OPEN = 20;
     // Special message sent from controller to query if the compositor controller is open.
     /* package */ final static int IS_COMPOSITOR_CONTROLLER_OPEN = 21;
 
     protected class Compositor extends JNIObject {
         public LayerView layerView;
 
+        private volatile boolean mContentDocumentIsDisplayed;
+
         public boolean isReady() {
             return LayerSession.this.isCompositorReady();
         }
 
         @WrapForJNI(calledFrom = "ui")
         private void onCompositorAttached() {
             LayerSession.this.onCompositorAttached();
         }
@@ -50,18 +52,17 @@ public class LayerSession {
             LayerSession.this.onCompositorDetached();
             disposeNative();
         }
 
         @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
         @Override protected native void disposeNative();
 
         @WrapForJNI(calledFrom = "any", dispatchTo = "gecko")
-        public native void attachToJava(GeckoLayerClient layerClient,
-                                        NativePanZoomController npzc);
+        public native void attachToJava(NativePanZoomController npzc);
 
         @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
         public native void onBoundsChanged(int left, int top, int width, int height);
 
         // Gecko thread creates compositor; blocks UI thread.
         @WrapForJNI(calledFrom = "ui", dispatchTo = "proxy")
         public native void createCompositor(int width, int height, Object surface);
 
@@ -122,16 +123,38 @@ public class LayerSession {
         }
 
         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
         public native void enableLayerUpdateNotifications(boolean enable);
 
         @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
         public native void sendToolbarPixelsToCompositor(final int width, final int height,
                                                          final int[] pixels);
+
+        @WrapForJNI(calledFrom = "gecko")
+        private void contentDocumentChanged() {
+            mContentDocumentIsDisplayed = false;
+        }
+
+        @WrapForJNI(calledFrom = "gecko")
+        private boolean isContentDocumentDisplayed() {
+            return mContentDocumentIsDisplayed;
+        }
+
+        // The compositor invokes this function just before compositing a frame where the
+        // document is different from the document composited on the last frame. In these
+        // cases, the viewport information we have in Java is no longer valid and needs to
+        // be replaced with the new viewport information provided.
+        @WrapForJNI(calledFrom = "ui")
+        public void updateRootFrameMetrics(float scrollX, float scrollY, float zoom) {
+            mContentDocumentIsDisplayed = true;
+            if (layerView != null) {
+                layerView.onMetricsChanged(scrollX, scrollY, zoom);
+            }
+        }
     }
 
     protected final Compositor mCompositor = new Compositor();
 
     // Following fields are accessed on UI thread.
     private GeckoDisplay mDisplay;
     private boolean mAttachedCompositor;
     private boolean mCalledCreateCompositor;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -44,22 +44,22 @@ import java.util.List;
 /**
  * A view rendered by the layer compositor.
  */
 public class LayerView extends FrameLayout {
     private static final String LOGTAG = "GeckoLayerView";
 
     private static AccessibilityManager sAccessibilityManager;
 
-    private GeckoLayerClient mLayerClient;
     private PanZoomController mPanZoomController;
     private DynamicToolbarAnimator mToolbarAnimator;
     private FullScreenState mFullScreenState;
 
-    private Listener mListener;
+    // Accessed on UI thread.
+    private ImmutableViewportMetrics mViewportMetrics;
 
     /* This should only be modified on the Java UI thread. */
     private final Overscroll mOverscroll;
 
     private int mDefaultClearColor = Color.WHITE;
     /* package */ GetPixelsResult mGetPixelsResult;
     private final List<DrawListener> mDrawListeners;
 
@@ -112,23 +112,23 @@ public class LayerView extends FrameLayo
                     mCompositor.sendToolbarPixelsToCompositor(width, height, pixels);
                 } catch (Exception e) {
                     Log.e(LOGTAG, "Caught exception while getting toolbar pixels from Bitmap: " + e.toString());
                 }
                 break;
             case STATIC_TOOLBAR_READY:
                 // Hide toolbar and send TOOLBAR_HIDDEN message to compositor
                 mToolbarAnimator.onToggleChrome(false);
-                mListener.surfaceChanged();
+                adjustViewportSize();
                 postCompositorMessage(TOOLBAR_HIDDEN);
                 break;
             case TOOLBAR_SHOW:
                 // Show toolbar.
                 mToolbarAnimator.onToggleChrome(true);
-                mListener.surfaceChanged();
+                adjustViewportSize();
                 postCompositorMessage(TOOLBAR_VISIBLE);
                 break;
             case FIRST_PAINT:
                 setSurfaceBackgroundColor(Color.TRANSPARENT);
                 break;
             case LAYERS_UPDATED:
                 for (DrawListener listener : mDrawListeners) {
                     listener.drawFinished();
@@ -161,23 +161,24 @@ public class LayerView extends FrameLayo
         mDrawListeners = new ArrayList<DrawListener>();
     }
 
     public LayerView(Context context) {
         this(context, null);
     }
 
     public void initializeView() {
-        mLayerClient = new GeckoLayerClient(this);
+        mToolbarAnimator = new DynamicToolbarAnimator(this);
+        mPanZoomController = PanZoomController.Factory.create(this);
         if (mOverscroll != null) {
-            mLayerClient.setOverscrollHandler(mOverscroll);
+            mPanZoomController.setOverscrollHandler(mOverscroll);
         }
 
-        mPanZoomController = mLayerClient.getPanZoomController();
-        mToolbarAnimator = mLayerClient.getDynamicToolbarAnimator();
+        mViewportMetrics = new ImmutableViewportMetrics()
+                .setViewportSize(getWidth(), getHeight());
     }
 
     /**
      * MotionEventHelper dragAsync() robocop tests can instruct
      * PanZoomController not to generate longpress events.
      * This call comes in from a thread other than the UI thread.
      * So dispatch to UI thread first to prevent assert in nsWindow.
      */
@@ -190,28 +191,28 @@ public class LayerView extends FrameLayo
         });
     }
 
     private static Point getEventRadius(MotionEvent event) {
         return new Point((int)event.getToolMajor() / 2,
                          (int)event.getToolMinor() / 2);
     }
 
-    public void destroy() {
-        if (mLayerClient != null) {
-            mLayerClient.destroy();
+    protected void destroy() {
+        if (mPanZoomController != null) {
+            mPanZoomController.destroy();
         }
     }
 
     @Override
     public void dispatchDraw(final Canvas canvas) {
         super.dispatchDraw(canvas);
 
         // We must have a layer client to get valid viewport metrics
-        if (mLayerClient != null && mOverscroll != null) {
+        if (mOverscroll != null) {
             mOverscroll.draw(canvas, getViewportMetrics());
         }
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
             requestFocus();
@@ -268,24 +269,21 @@ public class LayerView extends FrameLayo
             return true;
         }
         if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
             return true;
         }
         return false;
     }
 
-    // Don't expose GeckoLayerClient to things outside this package; only expose it as an Object
-    GeckoLayerClient getLayerClient() { return mLayerClient; }
-
     public PanZoomController getPanZoomController() { return mPanZoomController; }
     public DynamicToolbarAnimator getDynamicToolbarAnimator() { return mToolbarAnimator; }
 
     public ImmutableViewportMetrics getViewportMetrics() {
-        return mLayerClient.getViewportMetrics();
+        return mViewportMetrics;
     }
 
     public void setSurfaceBackgroundColor(int newColor) {
     }
 
     public interface GetPixelsResult {
         public void onPixelsResult(int width, int height, IntBuffer pixels);
     }
@@ -312,59 +310,67 @@ public class LayerView extends FrameLayo
 
     /* package */ void recvScreenPixels(int width, int height, int[] pixels) {
         if (mGetPixelsResult != null) {
             mGetPixelsResult.onPixelsResult(width, height, IntBuffer.wrap(pixels));
             mGetPixelsResult = null;
         }
     }
 
-    public void setListener(Listener listener) {
-        mListener = listener;
-    }
-
-    Listener getListener() {
-        return mListener;
-    }
-
     protected void attachCompositor(final LayerSession session) {
         mCompositor = session.mCompositor;
         mCompositor.layerView = this;
 
+        // Reset the content-document-is-displayed flag.
+        mCompositor.updateRootFrameMetrics(/* scroolX */ 0, /* scrollY */ 0,
+                                           /* zoom */ 1.0f);
+
         mToolbarAnimator.notifyCompositorCreated(mCompositor);
 
         final NativePanZoomController npzc = (NativePanZoomController) mPanZoomController;
 
         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
-            mCompositor.attachToJava(mLayerClient, npzc);
+            mCompositor.attachToJava(npzc);
         } else {
             GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
                     mCompositor, "attachToJava",
-                    GeckoLayerClient.class, mLayerClient,
                     NativePanZoomController.class, npzc);
         }
     }
 
     @WrapForJNI(calledFrom = "ui")
     private Object getCompositor() {
         return isCompositorReady() ? mCompositor : null;
     }
 
+    private void adjustViewportSize() {
+        final IntSize viewportSize = mToolbarAnimator.getViewportSize();
+
+        if (mViewportMetrics.viewportRectWidth == viewportSize.width &&
+            mViewportMetrics.viewportRectHeight == viewportSize.height) {
+            return;
+        }
+
+        mViewportMetrics = mViewportMetrics.setViewportSize(viewportSize.width,
+                                                            viewportSize.height);
+    }
+
     /* package */ void onSizeChanged(int width, int height) {
-        if (mListener != null) {
-            mListener.surfaceChanged();
-        }
+        adjustViewportSize();
 
         if (mOverscroll != null) {
             mOverscroll.setSize(width, height);
         }
     }
 
-    public interface Listener {
-        void surfaceChanged();
+    /* package */ void onMetricsChanged(final float scrollX, final float scrollY,
+                                        final float zoom) {
+        mViewportMetrics = mViewportMetrics.setViewportOrigin(scrollX, scrollY)
+                                           .setZoomFactor(zoom);
+        mToolbarAnimator.onMetricsChanged(mViewportMetrics);
     }
 
     @RobocopTarget
     public void addDrawListener(final DrawListener listener) {
         if (!ThreadUtils.isOnUiThread()) {
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
@@ -405,17 +411,17 @@ public class LayerView extends FrameLayo
     }
 
     @RobocopTarget
     public static interface DrawListener {
         public void drawFinished();
     }
 
     public float getZoomFactor() {
-        return getLayerClient().getViewportMetrics().zoomFactor;
+        return getViewportMetrics().zoomFactor;
     }
 
     public void setFullScreenState(FullScreenState state) {
         mFullScreenState = state;
     }
 
     public boolean isFullScreen() {
         return mFullScreenState != FullScreenState.NONE;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
@@ -8,32 +8,39 @@ package org.mozilla.gecko.gfx;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.mozglue.JNIObject;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import org.json.JSONObject;
 
 import android.graphics.PointF;
+import android.os.SystemClock;
+import android.util.Log;
 import android.util.TypedValue;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.InputDevice;
 
+import java.util.ArrayList;
+
 class NativePanZoomController extends JNIObject implements PanZoomController {
+    private static final String LOGTAG = "GeckoNPZC";
     private final float MAX_SCROLL;
 
     private final LayerView mView;
 
     private boolean mDestroyed;
     private Overscroll mOverscroll;
     private float mPointerScrollFactor;
     private long mLastDownTime;
 
+    private SynthesizedEventState mPointerState;
+
     @WrapForJNI(calledFrom = "ui")
     private native boolean handleMotionEvent(
             int action, int actionIndex, long time, int metaState,
             int pointerId[], float x[], float y[], float orientation[], float pressure[],
             float toolMajor[], float toolMinor[]);
 
     @WrapForJNI(calledFrom = "ui")
     private native boolean handleScrollEvent(
@@ -251,9 +258,236 @@ class NativePanZoomController extends JN
     /**
      * Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
      * to avoid unwanted scroll interactions.
      */
     @WrapForJNI(calledFrom = "gecko")
     private void onSelectionDragState(boolean state) {
         mView.getDynamicToolbarAnimator().setPinned(state, PinReason.CARET_DRAG);
     }
+
+    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 screenX;
+        public int screenY;
+        public double pressure;
+        public int orientation;
+
+        public MotionEvent.PointerCoords getCoords() {
+            MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+            coords.orientation = orientation;
+            coords.pressure = (float)pressure;
+            coords.x = screenX;
+            coords.y = screenY;
+            return coords;
+        }
+    }
+
+    private static class SynthesizedEventState {
+        public final ArrayList<PointerInfo> pointers;
+        public long downTime;
+
+        SynthesizedEventState() {
+            pointers = new ArrayList<PointerInfo>();
+        }
+
+        int getPointerIndex(int pointerId) {
+            for (int i = 0; i < pointers.size(); i++) {
+                if (pointers.get(i).pointerId == pointerId) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        int addPointer(int pointerId, int source) {
+            PointerInfo info = new PointerInfo();
+            info.pointerId = pointerId;
+            info.source = source;
+            pointers.add(info);
+            return pointers.size() - 1;
+        }
+
+        int getPointerCount(int source) {
+            int count = 0;
+            for (int i = 0; i < pointers.size(); i++) {
+                if (pointers.get(i).source == source) {
+                    count++;
+                }
+            }
+            return count;
+        }
+
+        MotionEvent.PointerProperties[] getPointerProperties(int source) {
+            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) {
+                    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(int source) {
+            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(int source, int pointerId, int eventType,
+                                         int screenX, int screenY, double pressure,
+                                         int orientation)
+    {
+        final View view = mView;
+        final int[] origin = new int[2];
+        view.getLocationOnScreen(origin);
+        screenX -= origin[0];
+        screenY -= origin[1];
+
+        if (mPointerState == null) {
+            mPointerState = new SynthesizedEventState();
+        }
+
+        // Find the pointer if it already exists
+        int pointerIndex = mPointerState.getPointerIndex(pointerId);
+
+        // Event-specific handling
+        switch (eventType) {
+            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;
+        }
+
+        // Update the pointer with the new info
+        PointerInfo info = mPointerState.pointers.get(pointerIndex);
+        info.screenX = screenX;
+        info.screenY = screenY;
+        info.pressure = pressure;
+        info.orientation = orientation;
+
+        // 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);
+        boolean isButtonDown = (source == InputDevice.SOURCE_MOUSE) &&
+                               (eventType == MotionEvent.ACTION_DOWN ||
+                                eventType == MotionEvent.ACTION_MOVE);
+        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*/ (isButtonDown ? MotionEvent.BUTTON_PRIMARY : 0),
+            /*xPrecision*/ 0,
+            /*yPrecision*/ 0,
+            /*deviceId*/ 0,
+            /*edgeFlags*/ 0,
+            /*source*/ source,
+            /*flags*/ 0);
+        view.post(new Runnable() {
+            @Override
+            public void run() {
+                view.dispatchTouchEvent(event);
+            }
+        });
+
+        // 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);
+        }
+    }
+
+    @WrapForJNI(calledFrom = "gecko")
+    private void synthesizeNativeTouchPoint(int pointerId, int eventType, int screenX,
+            int screenY, double pressure, int orientation)
+    {
+        if (pointerId == PointerInfo.RESERVED_MOUSE_POINTER_ID) {
+            throw new IllegalArgumentException("Pointer ID reserved for mouse");
+        }
+        synthesizeNativePointer(InputDevice.SOURCE_TOUCHSCREEN, pointerId,
+                                eventType, screenX, screenY, pressure, orientation);
+    }
+
+    @WrapForJNI(calledFrom = "gecko")
+    private void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
+        synthesizeNativePointer(InputDevice.SOURCE_MOUSE,
+                                PointerInfo.RESERVED_MOUSE_POINTER_ID,
+                                eventType, screenX, screenY, 0, 0);
+    }
 }