Bug 1416316 - 1. Move overscroll to LayerSession; r?rbarker draft
authorJim Chen <nchen@mozilla.com>
Thu, 30 Nov 2017 13:25:50 -0500
changeset 705810 dc203200e659482ea14de71cd5fa67728132c5fd
parent 704807 cb9092a90f6ef501e6de8eb5fc6ce19e2717193f
child 705811 0e3da7bfd2f6b3e64c5cc1f0ba659cadc7ba5aa7
push id91595
push userbmo:nchen@mozilla.com
push dateThu, 30 Nov 2017 18:26:05 +0000
reviewersrbarker
bugs1416316
milestone59.0a1
Bug 1416316 - 1. Move overscroll to LayerSession; r?rbarker There is some overscroll handling code in NativePanZoomController that should be moved, along with other overscroll code in LayerView, to LayerSession. LayerSession now provides a getter for OverscrollEdgeEffect, which is cleaned up to have a public API with documentation. MozReview-Commit-ID: LkKHFS8OkR7
mobile/android/base/moz.build
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.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
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
widget/android/nsWindow.cpp
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -399,17 +399,16 @@ gvjar.sources += [geckoview_source_dir +
     'gfx/FullScreenState.java',
     'gfx/GeckoDisplay.java',
     'gfx/GeckoSurface.java',
     'gfx/GeckoSurfaceTexture.java',
     'gfx/IntSize.java',
     'gfx/LayerSession.java',
     'gfx/LayerView.java',
     'gfx/NativePanZoomController.java',
-    'gfx/Overscroll.java',
     'gfx/OverscrollEdgeEffect.java',
     'gfx/PanningPerfAPI.java',
     'gfx/PanZoomController.java',
     'gfx/PointUtils.java',
     'gfx/RenderTask.java',
     'gfx/StackScroller.java',
     'gfx/SurfaceAllocator.java',
     'gfx/SurfaceAllocatorService.java',
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
@@ -6,19 +6,21 @@
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.GeckoDisplay;
 import org.mozilla.gecko.gfx.LayerView;
 
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.ViewGroup;
@@ -185,16 +187,29 @@ public class GeckoView extends LayerView
 
         if (mSession != null) {
             mSession.removeDisplay(mDisplay);
         }
         if (session != null) {
             session.addDisplay(mDisplay);
         }
 
+        final Context context = getContext();
+        session.getOverscrollEdgeEffect().setTheme(context);
+        session.getOverscrollEdgeEffect().setInvalidationCallback(new Runnable() {
+            @Override
+            public void run() {
+                if (Build.VERSION.SDK_INT >= 16) {
+                    GeckoView.this.postInvalidateOnAnimation();
+                } else {
+                    GeckoView.this.postInvalidateDelayed(10);
+                }
+            }
+        });
+
         mSession = session;
     }
 
     public GeckoSession getSession() {
         return mSession;
     }
 
     public EventDispatcher getEventDispatcher() {
@@ -382,9 +397,18 @@ public class GeckoView extends LayerView
                 mInputConnectionListener.onKeyMultiple(keyCode, repeatCount, event);
     }
 
     @Override
     public boolean isIMEEnabled() {
         return mInputConnectionListener != null &&
                 mInputConnectionListener.isIMEEnabled();
     }
+
+    @Override
+    public void dispatchDraw(final Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (mSession != null) {
+            mSession.getOverscrollEdgeEffect().draw(canvas);
+        }
+    }
 }
--- 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
@@ -139,22 +139,40 @@ public class LayerSession {
         // 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")
         private void updateRootFrameMetrics(float scrollX, float scrollY, float zoom) {
             LayerSession.this.onMetricsChanged(scrollX, scrollY, zoom);
         }
+
+        @WrapForJNI(calledFrom = "ui")
+        private void updateOverscrollVelocity(final float x, final float y) {
+            LayerSession.this.updateOverscrollVelocity(x, y);
+        }
+
+        @WrapForJNI(calledFrom = "ui")
+        private void updateOverscrollOffset(final float x, final float y) {
+            LayerSession.this.updateOverscrollOffset(x, y);
+        }
+
+        @WrapForJNI(calledFrom = "ui")
+        private void onSelectionCaretDrag(final boolean dragging) {
+            // Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned to
+            // avoid unwanted scroll interactions.
+            LayerSession.this.onSelectionCaretDrag(dragging);
+        }
     }
 
     protected final Compositor mCompositor = new Compositor();
 
     // All fields are accessed on UI thread only.
     private GeckoDisplay mDisplay;
+    private OverscrollEdgeEffect mOverscroll;
     private DynamicToolbarAnimator mToolbar;
 
     private boolean mAttachedCompositor;
     private boolean mCalledCreateCompositor;
     private boolean mCompositorReady;
     private Surface mSurface;
 
     // All fields of coordinates are in screen units.
@@ -171,16 +189,30 @@ public class LayerSession {
     /* package */ GeckoDisplay getDisplay() {
         if (DEBUG) {
             ThreadUtils.assertOnUiThread();
         }
         return mDisplay;
     }
 
     /**
+     * Get the OverscrollEdgeEffect instance for this session.
+     *
+     * @return OverscrollEdgeEffect instance.
+     */
+    public OverscrollEdgeEffect getOverscrollEdgeEffect() {
+        ThreadUtils.assertOnUiThread();
+
+        if (mOverscroll == null) {
+            mOverscroll = new OverscrollEdgeEffect(this);
+        }
+        return mOverscroll;
+    }
+
+    /**
      * Get the DynamicToolbarAnimator instance for this session.
      *
      * @return DynamicToolbarAnimator instance.
      */
     public @NonNull DynamicToolbarAnimator getDynamicToolbarAnimator() {
         ThreadUtils.assertOnUiThread();
 
         if (mToolbar == null) {
@@ -383,16 +415,53 @@ public class LayerSession {
             mSurface = null;
         }
 
         if (mToolbar != null) {
             mToolbar.onCompositorReady();
         }
     }
 
+    /* package */ void updateOverscrollVelocity(final float x, final float y) {
+        if (DEBUG) {
+            ThreadUtils.assertOnUiThread();
+        }
+
+        if (mOverscroll == null) {
+            return;
+        }
+
+        // Multiply the velocity by 1000 to match what was done in JPZ.
+        mOverscroll.setVelocity(x * 1000.0f, OverscrollEdgeEffect.AXIS_X);
+        mOverscroll.setVelocity(y * 1000.0f, OverscrollEdgeEffect.AXIS_Y);
+    }
+
+    /* package */ void updateOverscrollOffset(final float x, final float y) {
+        if (DEBUG) {
+            ThreadUtils.assertOnUiThread();
+        }
+
+        if (mOverscroll == null) {
+            return;
+        }
+
+        mOverscroll.setDistance(x, OverscrollEdgeEffect.AXIS_X);
+        mOverscroll.setDistance(y, OverscrollEdgeEffect.AXIS_Y);
+    }
+
+    /* package */ void onSelectionCaretDrag(final boolean dragging) {
+        if (DEBUG) {
+            ThreadUtils.assertOnUiThread();
+        }
+
+        if (mToolbar != null) {
+            mToolbar.setPinned(dragging, DynamicToolbarAnimator.PinReason.CARET_DRAG);
+        }
+    }
+
     /* package */ void onMetricsChanged(final float scrollX, final float scrollY,
                                         final float zoom) {
         if (DEBUG) {
             ThreadUtils.assertOnUiThread();
         }
 
         mViewportLeft = scrollX;
         mViewportTop = scrollY;
@@ -413,18 +482,18 @@ public class LayerSession {
 
         mClientTop = mTop + toolbarHeight;
         mClientHeight = mHeight - toolbarHeight;
 
         if (mAttachedCompositor) {
             mCompositor.onBoundsChanged(mLeft, mClientTop, mWidth, mClientHeight);
         }
 
-        if (mCompositor.layerView != null) {
-            mCompositor.layerView.onSizeChanged(mWidth, mHeight);
+        if (mOverscroll != null) {
+            mOverscroll.setSize(mWidth, mClientHeight);
         }
     }
 
     /* package */ void onSurfaceChanged(final Surface surface, final int width,
                                         final int height) {
         ThreadUtils.assertOnUiThread();
 
         mWidth = width;
--- 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
@@ -47,19 +47,16 @@ import java.util.List;
 public class LayerView extends FrameLayout {
     private static final String LOGTAG = "GeckoLayerView";
 
     private static AccessibilityManager sAccessibilityManager;
 
     private PanZoomController mPanZoomController;
     private FullScreenState mFullScreenState;
 
-    /* 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;
 
     private void postCompositorMessage(final int message) {
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
@@ -81,29 +78,25 @@ public class LayerView extends FrameLayo
     /* protected */ LayerSession mSession;
     private LayerSession.Compositor mCompositor;
 
     public LayerView(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         mFullScreenState = FullScreenState.NONE;
 
-        mOverscroll = new OverscrollEdgeEffect(this);
         mDrawListeners = new ArrayList<DrawListener>();
     }
 
     public LayerView(Context context) {
         this(context, null);
     }
 
     public void initializeView() {
         mPanZoomController = PanZoomController.Factory.create(this);
-        if (mOverscroll != null) {
-            mPanZoomController.setOverscrollHandler(mOverscroll);
-        }
     }
 
     /**
      * 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.
      */
@@ -123,26 +116,16 @@ public class LayerView extends FrameLayo
 
     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 (mOverscroll != null) {
-            mOverscroll.draw(canvas);
-        }
-    }
-
-    @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
             requestFocus();
         }
 
         if (!isCompositorReady()) {
             // If gecko isn't loaded yet, don't try sending events to the
             // native code because it's just going to crash
@@ -251,22 +234,16 @@ public class LayerView extends FrameLayo
         }
     }
 
     @WrapForJNI(calledFrom = "ui")
     private Object getCompositor() {
         return isCompositorReady() ? mCompositor : null;
     }
 
-    /* package */ void onSizeChanged(int width, int height) {
-        if (mOverscroll != null) {
-            mOverscroll.setSize(width, height);
-        }
-    }
-
     @RobocopTarget
     public void addDrawListener(final DrawListener listener) {
         if (!ThreadUtils.isOnUiThread()) {
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
                     addDrawListener(listener);
                 }
--- 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
@@ -25,17 +25,16 @@ 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,
@@ -202,85 +201,26 @@ class NativePanZoomController extends JN
         }
         mDestroyed = true;
         disposeNative();
     }
 
     @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko_priority") @Override // JNIObject
     protected native void disposeNative();
 
-    @Override
-    public void setOverscrollHandler(final Overscroll handler) {
-        mOverscroll = handler;
-    }
-
     @WrapForJNI(stubName = "SetIsLongpressEnabled") // Called from test thread.
     private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
 
     @Override // PanZoomController
     public void setIsLongpressEnabled(boolean isLongpressEnabled) {
         if (!mDestroyed) {
             nativeSetIsLongpressEnabled(isLongpressEnabled);
         }
     }
 
-    @WrapForJNI
-    private void updateOverscrollVelocity(final float x, final float y) {
-        if (mOverscroll != null) {
-            if (ThreadUtils.isOnUiThread() == true) {
-                mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
-                mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
-            } else {
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        // Multiply the velocity by 1000 to match what was done in JPZ.
-                        mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
-                        mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
-                    }
-                });
-            }
-        }
-    }
-
-    @WrapForJNI
-    private void updateOverscrollOffset(final float x, final float y) {
-        if (mOverscroll != null) {
-            if (ThreadUtils.isOnUiThread() == true) {
-                mOverscroll.setDistance(x, Overscroll.Axis.X);
-                mOverscroll.setDistance(y, Overscroll.Axis.Y);
-            } else {
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        mOverscroll.setDistance(x, Overscroll.Axis.X);
-                        mOverscroll.setDistance(y, Overscroll.Axis.Y);
-                    }
-                });
-            }
-        }
-    }
-
-    /**
-     * Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
-     * to avoid unwanted scroll interactions.
-     */
-    @WrapForJNI(calledFrom = "gecko")
-    private void onSelectionDragState(final boolean state) {
-        final LayerView view = mView;
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                if (view.mSession != null) {
-                    view.mSession.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;
 
deleted file mode 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/* -*- 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.gecko.gfx;
-
-import android.graphics.Canvas;
-
-public interface Overscroll {
-    // The axis to show overscroll on.
-    public enum Axis {
-        X,
-        Y,
-    };
-
-    public void draw(final Canvas canvas);
-    public void setSize(final int width, final int height);
-    public void setVelocity(final float velocity, final Axis axis);
-    public void setDistance(final float distance, final Axis axis);
-}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
@@ -1,135 +1,180 @@
 /* -*- 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.gecko.gfx;
 
+import org.mozilla.gecko.util.ThreadUtils;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.os.Build;
+import android.view.View;
 import android.widget.EdgeEffect;
 
 import java.lang.reflect.Field;
 
-public class OverscrollEdgeEffect implements Overscroll {
+public class OverscrollEdgeEffect {
     // Used to index particular edges in the edges array
     private static final int TOP = 0;
     private static final int BOTTOM = 1;
     private static final int LEFT = 2;
     private static final int RIGHT = 3;
 
+    /* package */ static final int AXIS_X = 0;
+    /* package */ static final int AXIS_Y = 1;
+
     // All four edges of the screen
     private final EdgeEffect[] mEdges = new EdgeEffect[4];
 
-    // The view we're showing this overscroll on.
-    private final LayerView mView;
+    private final LayerSession mSession;
+    private Runnable mInvalidationCallback;
+    private int mWidth;
+    private int mHeight;
+
+    /* package */ OverscrollEdgeEffect(final LayerSession session) {
+        mSession = session;
+    }
 
-    public OverscrollEdgeEffect(final LayerView v) {
+    /**
+     * Set the theme to use for overscroll from a given Context.
+     *
+     * @param context Context to use for the overscroll theme.
+     */
+    public void setTheme(final Context context) {
+        ThreadUtils.assertOnUiThread();
+
+        final PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
         Field paintField = null;
+
         if (Build.VERSION.SDK_INT >= 21) {
             try {
                 paintField = EdgeEffect.class.getDeclaredField("mPaint");
                 paintField.setAccessible(true);
             } catch (NoSuchFieldException e) {
             }
         }
 
-        mView = v;
-        Context context = v.getContext();
-        for (int i = 0; i < 4; i++) {
+        for (int i = 0; i < mEdges.length; i++) {
             mEdges[i] = new EdgeEffect(context);
 
+            if (paintField == null) {
+                continue;
+            }
+
             try {
-                if (paintField != null) {
-                    final Paint p = (Paint) paintField.get(mEdges[i]);
+                final Paint p = (Paint) paintField.get(mEdges[i]);
 
-                    // The Android EdgeEffect class uses a mode of SRC_ATOP here, which means it will only
-                    // draw the effect where there are non-transparent pixels in the destination. Since the LayerView
-                    // itself is fully transparent, it doesn't display at all. We need to use SRC instead.
-                    p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
-                }
+                // The Android EdgeEffect class uses a mode of SRC_ATOP here, which means
+                // it will only draw the effect where there are non-transparent pixels in
+                // the destination. Since the LayerView itself is fully transparent, it
+                // doesn't display at all. We need to use SRC instead.
+                p.setXfermode(mode);
             } catch (IllegalAccessException e) {
             }
         }
     }
 
-    @Override
-    public void setSize(final int width, final int height) {
+    /**
+     * Set a Runnable that acts as a callback to invalidate the overscroll effect (for
+     * example, as a response to user fling for example). The Runnbale should schedule a
+     * future call to {@link #draw(Canvas)} as a result of the invalidation.
+     *
+     * @param runnable Invalidation Runnable.
+     * @see #getInvalidationCallback()
+     */
+    public void setInvalidationCallback(final Runnable runnable) {
+        ThreadUtils.assertOnUiThread();
+        mInvalidationCallback = runnable;
+    }
+
+    /**
+     * Get the current invalidatation Runnable.
+     *
+     * @return Invalidation Runnable.
+     * @see #setInvalidationCallback(Runnable)
+     */
+    public Runnable getInvalidationCallback() {
+        ThreadUtils.assertOnUiThread();
+        return mInvalidationCallback;
+    }
+
+    /* package */ void setSize(final int width, final int height) {
         mEdges[LEFT].setSize(height, width);
         mEdges[RIGHT].setSize(height, width);
         mEdges[TOP].setSize(width, height);
         mEdges[BOTTOM].setSize(width, height);
+
+        mWidth = width;
+        mHeight = height;
     }
 
-    private EdgeEffect getEdgeForAxisAndSide(final Axis axis, final float side) {
-        if (axis == Axis.Y) {
+    private EdgeEffect getEdgeForAxisAndSide(final int axis, final float side) {
+        if (axis == AXIS_Y) {
             if (side < 0) {
                 return mEdges[TOP];
             } else {
                 return mEdges[BOTTOM];
             }
         } else {
             if (side < 0) {
                 return mEdges[LEFT];
             } else {
                 return mEdges[RIGHT];
             }
         }
     }
 
-    private void invalidate() {
-        if (Build.VERSION.SDK_INT >= 16) {
-            mView.postInvalidateOnAnimation();
-        } else {
-            mView.postInvalidateDelayed(10);
-        }
-    }
-
-    @Override
-    public void setVelocity(final float velocity, final Axis axis) {
+    /* package */ void setVelocity(final float velocity, final int axis) {
         final EdgeEffect edge = getEdgeForAxisAndSide(axis, velocity);
 
         // If we're showing overscroll already, start fading it out.
         if (!edge.isFinished()) {
             edge.onRelease();
         } else {
             // Otherwise, show an absorb effect
             edge.onAbsorb((int)velocity);
         }
 
-        invalidate();
+        if (mInvalidationCallback != null) {
+            mInvalidationCallback.run();
+        }
     }
 
-    @Override
-    public void setDistance(final float distance, final Axis axis) {
+    /* package */ void setDistance(final float distance, final int axis) {
         // The first overscroll event often has zero distance. Throw it out
         if (distance == 0.0f) {
             return;
         }
 
         final EdgeEffect edge = getEdgeForAxisAndSide(axis, (int)distance);
-        edge.onPull(distance / (axis == Axis.X ? mView.getWidth() : mView.getHeight()));
-        invalidate();
+        edge.onPull(distance / (axis == AXIS_X ? mWidth : mHeight));
+
+        if (mInvalidationCallback != null) {
+            mInvalidationCallback.run();
+        }
     }
 
-    @Override
+    /**
+     * Draw the overscroll effect on a Canvas.
+     *
+     * @param canvas Canvas to draw on.
+     */
     public void draw(final Canvas canvas) {
-        if (mView.mSession == null) {
-            return;
-        }
+        ThreadUtils.assertOnUiThread();
 
         final Rect pageRect = new Rect();
-        mView.mSession.getSurfaceBounds(pageRect);
+        mSession.getSurfaceBounds(pageRect);
 
         // If we're pulling an edge, or fading it out, draw!
         boolean invalidate = false;
         if (!mEdges[TOP].isFinished()) {
             invalidate |= draw(mEdges[TOP], canvas, pageRect.left, pageRect.top, 0);
         }
 
         if (!mEdges[BOTTOM].isFinished()) {
@@ -140,18 +185,18 @@ public class OverscrollEdgeEffect implem
             invalidate |= draw(mEdges[LEFT], canvas, pageRect.left, pageRect.bottom, 270);
         }
 
         if (!mEdges[RIGHT].isFinished()) {
             invalidate |= draw(mEdges[RIGHT], canvas, pageRect.right, pageRect.top, 90);
         }
 
         // If the edge effect is animating off screen, invalidate.
-        if (invalidate) {
-            invalidate();
+        if (invalidate && mInvalidationCallback != null) {
+            mInvalidationCallback.run();
         }
     }
 
     private static boolean draw(final EdgeEffect edge, final Canvas canvas, final float translateX, final float translateY, final float rotation) {
         final int state = canvas.save();
         canvas.translate(translateX, translateY);
         canvas.rotate(rotation);
         boolean invalidate = edge.draw(canvas);
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -17,12 +17,10 @@ public interface PanZoomController {
         }
     }
 
     public void destroy();
 
     public boolean onTouchEvent(MotionEvent event);
     public boolean onMotionEvent(MotionEvent event);
 
-    public void setOverscrollHandler(final Overscroll controller);
-
     public void setIsLongpressEnabled(boolean isLongpressEnabled);
 }
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -175,16 +175,25 @@ namespace {
 
     template<class Instance, class Impl> typename EnableIf<
         jni::detail::NativePtrPicker<Impl>::value ==
         jni::detail::OWNING, void>::Type
     CallAttachNative(Instance aInstance, Impl* aImpl)
     {
         Impl::AttachNative(aInstance, UniquePtr<Impl>(aImpl));
     }
+
+    template<class Lambda> bool
+    DispatchToUiThread(const char* aName, Lambda&& aLambda) {
+        if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+            uiThread->Dispatch(NS_NewRunnableFunction(aName, Move(aLambda)));
+            return true;
+        }
+        return false;
+    }
 } // namespace
 
 template<class Impl>
 template<class Instance, typename... Args> void
 nsWindow::NativePtr<Impl>::Attach(Instance aInstance, nsWindow* aWindow,
                                   Args&&... aArgs)
 {
     MOZ_ASSERT(NS_IsMainThread());
@@ -733,31 +742,16 @@ public:
         PostInputEvent([input, guid, blockId, status] (nsWindow* window) {
             WidgetTouchEvent touchEvent = input.ToWidgetTouchEvent(window);
             window->ProcessUntransformedAPZEvent(&touchEvent, guid,
                                                  blockId, status);
             window->DispatchHitTest(touchEvent);
         });
         return true;
     }
-
-    void UpdateOverscrollVelocity(const float x, const float y)
-    {
-        mNPZC->UpdateOverscrollVelocity(x, y);
-    }
-
-    void UpdateOverscrollOffset(const float x, const float y)
-    {
-        mNPZC->UpdateOverscrollOffset(x, y);
-    }
-
-    void SetSelectionDragState(const bool aState)
-    {
-        mNPZC->OnSelectionDragState(aState);
-    }
 };
 
 template<> const char
 nsWindow::NativePtr<nsWindow::NPZCSupport>::sName[] = "NPZCSupport";
 
 bool nsWindow::NPZCSupport::sNegateWheelScroll;
 
 NS_IMPL_ISUPPORTS(nsWindow::AndroidView,
@@ -1997,35 +1991,67 @@ nsWindow::InitEvent(WidgetGUIEvent& even
     }
 
     event.mTime = PR_Now() / 1000;
 }
 
 void
 nsWindow::UpdateOverscrollVelocity(const float aX, const float aY)
 {
-    if (NativePtr<NPZCSupport>::Locked npzcs{mNPZCSupport}) {
-        npzcs->UpdateOverscrollVelocity(aX, aY);
+    if (NativePtr<LayerViewSupport>::Locked lvs{mLayerViewSupport}) {
+        const auto& compositor = lvs->GetJavaCompositor();
+        if (AndroidBridge::IsJavaUiThread()) {
+            compositor->UpdateOverscrollVelocity(aX, aY);
+            return;
+        }
+
+        DispatchToUiThread(
+                "nsWindow::UpdateOverscrollVelocity",
+                [compositor = LayerSession::Compositor::GlobalRef(compositor),
+                 aX, aY] {
+                    compositor->UpdateOverscrollVelocity(aX, aY);
+                });
     }
 }
 
 void
 nsWindow::UpdateOverscrollOffset(const float aX, const float aY)
 {
-    if (NativePtr<NPZCSupport>::Locked npzcs{mNPZCSupport}) {
-        npzcs->UpdateOverscrollOffset(aX, aY);
+    if (NativePtr<LayerViewSupport>::Locked lvs{mLayerViewSupport}) {
+        const auto& compositor = lvs->GetJavaCompositor();
+        if (AndroidBridge::IsJavaUiThread()) {
+            compositor->UpdateOverscrollOffset(aX, aY);
+            return;
+        }
+
+        DispatchToUiThread(
+                "nsWindow::UpdateOverscrollOffset",
+                [compositor = LayerSession::Compositor::GlobalRef(compositor),
+                 aX, aY] {
+                    compositor->UpdateOverscrollOffset(aX, aY);
+                });
     }
 }
 
 void
 nsWindow::SetSelectionDragState(bool aState)
 {
-    if (NativePtr<NPZCSupport>::Locked npzcs{mNPZCSupport}) {
-        npzcs->SetSelectionDragState(aState);
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!mLayerViewSupport) {
+        return;
     }
+
+    const auto& compositor = mLayerViewSupport->GetJavaCompositor();
+    DispatchToUiThread(
+            "nsWindow::SetSelectionDragState",
+            [compositor = LayerSession::Compositor::GlobalRef(compositor),
+             aState] {
+                compositor->OnSelectionCaretDrag(aState);
+            });
 }
 
 void *
 nsWindow::GetNativeData(uint32_t aDataType)
 {
     switch (aDataType) {
         // used by GLContextProviderEGL, nullptr is EGL_DEFAULT_DISPLAY
         case NS_NATIVE_DISPLAY: