Bug 1416319 - 8. Switch to using LayerSession coordinates APIs; r?rbarker draft
authorJim Chen <nchen@mozilla.com>
Wed, 22 Nov 2017 14:12:23 -0500
changeset 702131 d2ec431b2f3bf776fc215fd223099c609940a49e
parent 702130 5d94c5608a7c6cd5e96799ade2a29b1bcc6b998d
child 741374 a69cc271d1ffcb074d9ecbc8f64f8798f229ff8c
push id90386
push userbmo:nchen@mozilla.com
push dateWed, 22 Nov 2017 19:12:37 +0000
reviewersrbarker
bugs1416319
milestone59.0a1
Bug 1416319 - 8. Switch to using LayerSession coordinates APIs; r?rbarker Use the LayerSession coordinates APIs instead of manually calculating coordinates using viewport metrics and toolbar height, which is prone to error. MozReview-Commit-ID: 4ebI3BHEOXR
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.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/modules/FormAssistant.jsm
widget/android/GeckoEditableSupport.cpp
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -91,17 +91,16 @@ import org.mozilla.gecko.delegates.Offli
 import org.mozilla.gecko.delegates.ScreenshotDelegate;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.extensions.ExtensionPermissionsHelper;
 import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.home.BrowserSearch;
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.home.HomeConfigPrefsBackend;
 import org.mozilla.gecko.home.HomeFragment;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
@@ -4353,17 +4352,17 @@ public class BrowserApp extends GeckoApp
     public void startActionMode(final ActionModeCompat.Callback callback) {
         // If actionMode is null, we're not currently showing one. Flip to the action mode view
         if (mActionMode == null) {
             mActionBarFlipper.showNext();
             DynamicToolbarAnimator toolbar = mLayerView.getDynamicToolbarAnimator();
 
             // If the toolbar is dynamic and not currently showing, just show the real toolbar
             // and keep the animated snapshot hidden
-            if (mDynamicToolbar.isEnabled() && toolbar.getCurrentToolbarHeight() == 0) {
+            if (mDynamicToolbar.isEnabled() && !isToolbarChromeVisible()) {
                 toggleToolbarChrome(true);
                 mShowingToolbarChromeForActionBar = true;
             }
             mDynamicToolbar.setPinned(true, PinReason.ACTION_MODE);
 
         } else {
             // Otherwise, we're already showing an action mode. Just finish it and show the new one
             mActionMode.finish();
--- a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
@@ -1,29 +1,29 @@
 /* -*- 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;
 
 import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.gfx.FloatSize;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener;
 import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener.OnDismissCallback;
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.PointF;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
@@ -249,20 +249,16 @@ public class FormAssistPopup extends Rel
         mW = rect.getDouble("w");
         mH = rect.getDouble("h");
         mPopupType = (isAutoComplete ?
                       PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
         return true;
     }
 
     private void positionAndShowPopup() {
-        positionAndShowPopup(mGeckoView.getSession().getViewportMetrics());
-    }
-
-    private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
         ThreadUtils.assertOnUiThread();
 
         // Don't show the form assist popup when using fullscreen VKB
         InputMethodManager imm = (InputMethodManager)
                 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
         if (imm.isFullscreenMode()) {
             return;
         }
@@ -277,84 +273,75 @@ public class FormAssistPopup extends Rel
 
         if (sAutoCompleteMinWidth == 0) {
             Resources res = getContext().getResources();
             sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width));
             sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height));
             sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height));
         }
 
-        float zoom = aMetrics.zoomFactor;
-
         // These values correspond to the input box for which we want to
         // display the FormAssistPopup.
-        int left = (int) (mX * zoom - aMetrics.viewportRectLeft);
-        int top = (int) (mY * zoom - aMetrics.viewportRectTop + mGeckoView.getTop() +
-                         mGeckoView.getDynamicToolbarAnimator().getCurrentToolbarHeight());
-        int width = (int) (mW * zoom);
-        int height = (int) (mH * zoom);
+        final Matrix matrix = new Matrix();
+        final RectF input = new RectF((float) mX, (float) mY,
+                                      (float) (mX + mW), (float) (mY + mH));
+        mGeckoView.getSession().getClientToSurfaceMatrix(matrix);
+        matrix.mapRect(input);
+
+        final Rect page = new Rect();
+        mGeckoView.getSession().getSurfaceBounds(page);
 
         int popupWidth = LayoutParams.MATCH_PARENT;
-        int popupLeft = left < 0 ? 0 : left;
-
-        FloatSize viewport = aMetrics.getSize();
+        int popupLeft = Math.max((int) input.left, page.left);
 
         // For autocomplete suggestions, if the input is smaller than the screen-width,
         // shrink the popup's width. Otherwise, keep it as MATCH_PARENT.
-        if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) {
-            popupWidth = left < 0 ? left + width : width;
-
+        if ((mPopupType == PopupType.AUTOCOMPLETE) && (int) input.right < page.right) {
             // Ensure the popup has a minimum width.
-            if (popupWidth < sAutoCompleteMinWidth) {
-                popupWidth = sAutoCompleteMinWidth;
+            final int visibleWidth = (int) input.right - popupLeft;
+            popupWidth = Math.max(visibleWidth, sAutoCompleteMinWidth);
 
-                // Move the popup to the left if there isn't enough room for it.
-                if ((popupLeft + popupWidth) > viewport.width) {
-                    popupLeft = (int) (viewport.width - popupWidth);
-                }
+            // Move the popup to the left if there isn't enough room for it.
+            if ((popupLeft + popupWidth) > page.right) {
+                popupLeft = Math.max(page.right - popupWidth, page.left);
             }
         }
 
         int popupHeight;
         if (mPopupType == PopupType.AUTOCOMPLETE) {
             // Limit the amount of visible rows.
             int rows = mAutoCompleteList.getAdapter().getCount();
             if (rows > MAX_VISIBLE_ROWS) {
                 rows = MAX_VISIBLE_ROWS;
             }
 
             popupHeight = sAutoCompleteRowHeight * rows;
         } else {
             popupHeight = sValidationMessageHeight;
         }
 
-        int popupTop = top + height;
+        int popupTop = (int) input.bottom;
 
         if (mPopupType == PopupType.VALIDATIONMESSAGE) {
             mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
             mValidationMessageArrow.setVisibility(VISIBLE);
             mValidationMessageArrowInverted.setVisibility(GONE);
         }
 
         // If the popup doesn't fit below the input box, shrink its height, or
         // see if we can place it above the input instead.
-        if ((popupTop + popupHeight) > (mGeckoView.getTop() + viewport.height)) {
+        if ((popupTop + popupHeight) > page.bottom) {
             // Find where the maximum space is, and put the popup there.
-            if ((viewport.height - popupTop) > top) {
+            if ((page.bottom - (int) input.bottom) > ((int) input.top - page.top)) {
                 // Shrink the height to fit it below the input box.
-                popupHeight = (int) (viewport.height - popupTop);
+                popupHeight = page.bottom - (int) input.bottom;
             } else {
-                if (popupHeight < top) {
-                    // No shrinking needed to fit on top.
-                    popupTop = (top - popupHeight);
-                } else {
-                    // Shrink to available space on top.
-                    popupTop = 0;
-                    popupHeight = top;
-                }
+                // Shrink to available space on top if needed.
+                popupTop = Math.max((int) input.top - popupHeight, page.top);
+                popupHeight = (int) input.top - popupTop;
 
                 if (mPopupType == PopupType.VALIDATIONMESSAGE) {
                     mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted);
                     mValidationMessageArrow.setVisibility(GONE);
                     mValidationMessageArrowInverted.setVisibility(VISIBLE);
                 }
            }
         }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.annotation.TargetApi;
 import android.content.Context;
+import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -169,51 +170,47 @@ public class GeckoAccessibility {
             }
             sVirtualCursorNode.setContentDescription(message.getString("description", ""));
 
             final GeckoBundle bounds = message.getBundle("bounds");
             if (bounds != null) {
                 Rect relativeBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
                                                bounds.getInt("right"), bounds.getInt("bottom"));
                 sVirtualCursorNode.setBoundsInParent(relativeBounds);
-                int[] locationOnScreen = new int[2];
-                view.getLocationOnScreen(locationOnScreen);
-                locationOnScreen[1] += view.getDynamicToolbarAnimator().getCurrentToolbarHeight();
-                Rect screenBounds = new Rect(relativeBounds);
-                screenBounds.offset(locationOnScreen[0], locationOnScreen[1]);
-                sVirtualCursorNode.setBoundsInScreen(screenBounds);
+
+                final Matrix matrix = new Matrix();
+                final float[] origin = new float[2];
+                view.getSession().getClientToScreenMatrix(matrix);
+                matrix.mapPoints(origin);
+
+                relativeBounds.offset((int) origin[0], (int) origin[1]);
+                sVirtualCursorNode.setBoundsInScreen(relativeBounds);
             }
 
             final GeckoBundle braille = message.getBundle("brailleOutput");
             if (braille != null) {
                 sendBrailleText(view, braille.getString("text", ""),
                                 braille.getInt("selectionStart"), braille.getInt("selectionEnd"));
             }
 
             if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
                 sHoverEnter = message;
             }
 
-            ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
-                        event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
-                        event.setClassName(GeckoAccessibility.class.getName());
-                        if (eventType == AccessibilityEvent.TYPE_ANNOUNCEMENT ||
-                            eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
-                            event.setSource(view, View.NO_ID);
-                        } else {
-                            event.setSource(view, VIRTUAL_CURSOR_POSITION);
-                        }
-                        populateEventFromJSON(event, message);
-                        ((ViewParent) view).requestSendAccessibilityEvent(view, event);
-                    }
-                });
-
+            final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+            event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+            event.setClassName(GeckoAccessibility.class.getName());
+            if (eventType == AccessibilityEvent.TYPE_ANNOUNCEMENT ||
+                eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+                event.setSource(view, View.NO_ID);
+            } else {
+                event.setSource(view, VIRTUAL_CURSOR_POSITION);
+            }
+            populateEventFromJSON(event, message);
+            ((ViewParent) view).requestSendAccessibilityEvent(view, event);
         }
     }
 
     private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
         AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
         WriteData data = WriteData.forInfo(info);
         data.setText(text);
         // Set either the focus blink or the current caret position/selection
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1045,21 +1045,21 @@ public abstract class GeckoApp extends G
 
         session.getSettings().setString(GeckoSessionSettings.CHROME_URI,
                                         "chrome://browser/content/browser.xul");
         session.setContentListener(this);
 
         GeckoAccessibility.setDelegate(mLayerView);
 
         getAppEventDispatcher().registerGeckoThreadListener(this,
-            "Accessibility:Event",
             "Locale:Set",
             null);
 
         getAppEventDispatcher().registerUiThreadListener(this,
+            "Accessibility:Event",
             "Contact:Add",
             "DevToolsAuth:Scan",
             "DOMFullScreen:Start",
             "DOMFullScreen:Stop",
             "Mma:reader_available",
             "Mma:web_save_image",
             "Mma:web_save_media",
             "Permissions:Data",
@@ -2044,21 +2044,21 @@ public abstract class GeckoApp extends G
 
         EventDispatcher.getInstance().unregisterUiThreadListener(this,
             "Update:Check",
             "Update:Download",
             "Update:Install",
             null);
 
         getAppEventDispatcher().unregisterGeckoThreadListener(this,
-            "Accessibility:Event",
             "Locale:Set",
             null);
 
         getAppEventDispatcher().unregisterUiThreadListener(this,
+            "Accessibility:Event",
             "Contact:Add",
             "DevToolsAuth:Scan",
             "DOMFullScreen:Start",
             "DOMFullScreen:Stop",
             "Mma:reader_available",
             "Mma:web_save_image",
             "Mma:web_save_media",
             "Permissions:Data",
--- a/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
@@ -1,17 +1,19 @@
 /* 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.text;
 
 import android.annotation.TargetApi;
 import android.app.Activity;
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Build;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.ActionMode;
 
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoView;
 import org.mozilla.gecko.Telemetry;
@@ -163,25 +165,28 @@ public class FloatingToolbarTextSelectio
     private boolean isRectVisible() {
         // There's another case of an empty rect where just left == right but not top == bottom.
         // That's the rect for a collapsed selection. While technically this rect isn't visible too
         // we are not interested in this case because we do not want to hide the toolbar.
         return contentRect.left != contentRect.right || contentRect.top != contentRect.bottom;
     }
 
     private void updateRect(final GeckoBundle message) {
-        final double x = message.getDouble("x");
-        final double y = (int) message.getDouble("y");
-        final double width = (int) message.getDouble("width");
-        final double height = (int) message.getDouble("height");
+        final float x = (float) message.getDouble("x");
+        final float y = (float) message.getDouble("y");
+        final float width = (float) message.getDouble("width");
+        final float height = (float) message.getDouble("height");
 
-        final float toolbarOffset = geckoView.getDynamicToolbarAnimator().getCurrentToolbarHeight();
-        final float zoomFactor = geckoView.getSession().getViewportMetrics().zoomFactor;
-        geckoView.getLocationInWindow(locationInWindow);
+        final Matrix matrix = new Matrix();
+        final RectF rect = new RectF(x, y, x + width, y + height);
+        geckoView.getSession().getClientToScreenMatrix(matrix);
+        matrix.mapRect(rect);
 
-        contentRect = new Rect(
-                (int) (x * zoomFactor + locationInWindow[0]),
-                (int) (y * zoomFactor + locationInWindow[1] + toolbarOffset),
-                (int) ((x + width) * zoomFactor + locationInWindow[0]),
-                (int) ((y + height) * zoomFactor + locationInWindow[1] +
-                       (height > 0 ? handlesOffset : 0)));
+        if ((int) height > 0) {
+            rect.bottom += handlesOffset;
+        }
+
+        if (contentRect == null) {
+            contentRect = new Rect();
+        }
+        rect.round(contentRect);
     }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -347,80 +347,81 @@ class GeckoInputConnection
             return;
         }
         imm.updateSelection(v, start, end, getComposingSpanStart(editable),
                             getComposingSpanEnd(editable));
     }
 
     @TargetApi(21)
     @Override
-    public void updateCompositionRects(final RectF[] aRects) {
+    public void updateCompositionRects(final RectF[] rects) {
         if (!(Build.VERSION.SDK_INT >= 21)) {
             return;
         }
 
+        final GeckoView view = getView();
+        if (view == null) {
+            return;
+        }
+
+        final Editable content = getEditable();
+        if (content == null) {
+            return;
+        }
+
+        final int composingStart = getComposingSpanStart(content);
+        final int composingEnd = getComposingSpanEnd(content);
+        if (composingStart < 0 || composingEnd < 0) {
+            if (DEBUG) {
+                Log.d(LOGTAG, "No composition for updates");
+            }
+            return;
+        }
+
+        final CharSequence composition = content.subSequence(composingStart, composingEnd);
+
+        view.post(new Runnable() {
+            @Override
+            public void run() {
+                updateCompositionRectsOnUi(view, rects, composition);
+            }
+        });
+    }
+
+    @TargetApi(21)
+    /* package */ void updateCompositionRectsOnUi(final GeckoView view,
+                                                  final RectF[] rects,
+                                                  final CharSequence composition) {
+        if (view.getSession() == null) {
+            return;
+        }
+
         if (mCursorAnchorInfoBuilder == null) {
             mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
         }
         mCursorAnchorInfoBuilder.reset();
 
-        // Calculate Gecko logical coords to screen coords
-        final GeckoView view = getView();
-        if (view == null || view.getSession() == null) {
-            return;
+        final Matrix matrix = new Matrix();
+        view.getSession().getClientToScreenMatrix(matrix);
+        mCursorAnchorInfoBuilder.setMatrix(matrix);
+
+        for (int i = 0; i < rects.length; i++) {
+            mCursorAnchorInfoBuilder.addCharacterBounds(
+                    i, rects[i].left, rects[i].top, rects[i].right, rects[i].bottom,
+                    CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
         }
 
-        // First aRects element is the widget bounds in device units.
-        final float zoom = view.getSession().getViewportMetrics().zoomFactor;
-        final Matrix matrix = new Matrix();
-        matrix.postScale(zoom, zoom);
-        matrix.postTranslate(aRects[0].left, aRects[0].top);
-        mCursorAnchorInfoBuilder.setMatrix(matrix);
+        mCursorAnchorInfoBuilder.setComposingText(0, composition);
 
-        final Editable content = getEditable();
-        if (content == null) {
-            return;
-        }
-        int composingStart = getComposingSpanStart(content);
-        int composingEnd = getComposingSpanEnd(content);
-        if (composingStart < 0 || composingEnd < 0) {
-            if (DEBUG) {
-                Log.d(LOGTAG, "No composition for updates");
-            }
+        final InputMethodManager imm = getInputMethodManager();
+        if (imm == null) {
             return;
         }
 
-        // Subsequent aRects elements are character bounds in CSS units.
-        for (int i = 1; i < aRects.length; i++) {
-            mCursorAnchorInfoBuilder.addCharacterBounds(i - 1,
-                                                        aRects[i].left,
-                                                        aRects[i].top,
-                                                        aRects[i].right,
-                                                        aRects[i].bottom,
-                                                        CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
-        }
-
-        mCursorAnchorInfoBuilder.setComposingText(0, content.subSequence(composingStart, composingEnd));
-
-        updateCursor();
-    }
-
-    @TargetApi(21)
-    private void updateCursor() {
-        if (mCursorAnchorInfoBuilder == null) {
-            return;
-        }
-
-        final InputMethodManager imm = getInputMethodManager();
-        final View v = getView();
-        if (imm == null || v == null) {
-            return;
-        }
-
-        imm.updateCursorAnchorInfo(v, mCursorAnchorInfoBuilder.build());
+        imm.updateCursorAnchorInfo(view, mCursorAnchorInfoBuilder.build());
     }
 
     @Override
     public boolean requestCursorUpdates(int cursorUpdateMode) {
 
         if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
             mEditableClient.requestCursorUpdates(GeckoEditableClient.ONE_SHOT);
         }
--- 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
@@ -128,17 +128,17 @@ public class LayerView extends FrameLayo
     }
 
     @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, mSession.getViewportMetrics());
+            mOverscroll.draw(canvas);
         }
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
             requestFocus();
         }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
@@ -9,13 +9,13 @@ import android.graphics.Canvas;
 
 public interface Overscroll {
     // The axis to show overscroll on.
     public enum Axis {
         X,
         Y,
     };
 
-    public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics);
+    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
@@ -3,19 +3,19 @@
  * 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.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Paint;
-import android.graphics.PointF;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
 import android.os.Build;
 import android.widget.EdgeEffect;
 
 import java.lang.reflect.Field;
 
 public class OverscrollEdgeEffect implements Overscroll {
     // Used to index particular edges in the edges array
     private static final int TOP = 0;
@@ -113,42 +113,40 @@ public class OverscrollEdgeEffect implem
         }
 
         final EdgeEffect edge = getEdgeForAxisAndSide(axis, (int)distance);
         edge.onPull(distance / (axis == Axis.X ? mView.getWidth() : mView.getHeight()));
         invalidate();
     }
 
     @Override
-    public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics) {
-        if (metrics == null || mView.mSession == null) {
+    public void draw(final Canvas canvas) {
+        if (mView.mSession == null) {
             return;
         }
 
-        final float width = mView.getWidth();
-        final float height = mView.getHeight();
-        final float toolbarEnd = mView.mSession.getDynamicToolbarAnimator()
-                                               .getCurrentToolbarHeight();
+        final Rect pageRect = new Rect();
+        mView.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, 0, toolbarEnd, 0);
+            invalidate |= draw(mEdges[TOP], canvas, pageRect.left, pageRect.top, 0);
         }
 
         if (!mEdges[BOTTOM].isFinished()) {
-            invalidate |= draw(mEdges[BOTTOM], canvas, width, height, 180);
+            invalidate |= draw(mEdges[BOTTOM], canvas, pageRect.right, pageRect.bottom, 180);
         }
 
         if (!mEdges[LEFT].isFinished()) {
-            invalidate |= draw(mEdges[LEFT], canvas, 0, height, 270);
+            invalidate |= draw(mEdges[LEFT], canvas, pageRect.left, pageRect.bottom, 270);
         }
 
         if (!mEdges[RIGHT].isFinished()) {
-            invalidate |= draw(mEdges[RIGHT], canvas, width, 0, 90);
+            invalidate |= draw(mEdges[RIGHT], canvas, pageRect.right, pageRect.top, 90);
         }
 
         // If the edge effect is animating off screen, invalidate.
         if (invalidate) {
             invalidate();
         }
     }
 
--- a/mobile/android/modules/FormAssistant.jsm
+++ b/mobile/android/modules/FormAssistant.jsm
@@ -17,16 +17,19 @@ XPCOMUtils.defineLazyModuleGetters(this,
 
 var FormAssistant = {
   // Weak-ref used to keep track of the currently focused element.
   _currentFocusedElement: null,
 
   // Whether we're in the middle of an autocomplete.
   _doingAutocomplete: false,
 
+  // Last state received in "PanZoom:StateChange" observer.
+  _lastPanZoomState: "NOTHING",
+
   init: function() {
     Services.obs.addObserver(this, "PanZoom:StateChange");
   },
 
   _onPopupResponse: function(currentElement, message) {
     switch (message.action) {
       case "autocomplete": {
         let editableElement = currentElement.QueryInterface(Ci.nsIDOMNSEditableElement);
@@ -78,16 +81,17 @@ var FormAssistant = {
             if (!hasResults) {
               this._hideFormAssistPopup(focused);
             }
           });
         } else if (focused) {
           // temporarily hide the form assist popup while we're panning or zooming the page
           this._hideFormAssistPopup(focused);
         }
+        this._lastPanZoomState = aData;
         break;
     }
   },
 
   notifyInvalidSubmit: function(aFormElement, aInvalidElements) {
     if (!aInvalidElements.length) {
         return;
     }
@@ -178,18 +182,18 @@ var FormAssistant = {
 
         this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
         break;
       }
 
       case "resize": {
         let focused = this.focusedElement;
         if (focused && focused.ownerGlobal == aEvent.target) {
-          // Reposition the popup as if we just stopped pannning.
-          this.observe(null, "PanZoom:StateChange", "NOTHING");
+          // Reposition the popup as in the case of pan/zoom.
+          this.observe(null, "PanZoom:StateChange", this._lastPanZoomState);
           // Continue to listen to resizes.
           focused.ownerGlobal.addEventListener(
               "resize", this, {capture: true, mozSystemGroup: true, once: true});
         }
         break;
       }
     }
   },
@@ -359,36 +363,32 @@ var FormAssistant = {
       return {x: 0, y: 0, w: 0, h: 0};
     }
 
     let document = aElement.ownerDocument;
     while (document.defaultView.frameElement) {
       document = document.defaultView.frameElement.ownerDocument;
     }
 
-    let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
-                                  .getInterface(Ci.nsIDOMWindowUtils);
-    let scrollX = {}, scrollY = {};
-    cwu.getScrollXY(false, scrollX, scrollY);
-
+    let scrollX = 0, scrollY = 0;
     let r = aElement.getBoundingClientRect();
 
     // step out of iframes and frames, offsetting scroll values
     for (let frame = aElement.ownerGlobal; frame.frameElement && frame != content;
          frame = frame.parent) {
       // adjust client coordinates' origin to be top left of iframe viewport
       let rect = frame.frameElement.getBoundingClientRect();
       let left = frame.getComputedStyle(frame.frameElement).borderLeftWidth;
       let top = frame.getComputedStyle(frame.frameElement).borderTopWidth;
-      scrollX.value += rect.left + parseInt(left);
-      scrollY.value += rect.top + parseInt(top);
+      scrollX += rect.left + parseInt(left);
+      scrollY += rect.top + parseInt(top);
     }
 
     return {
-      x: r.left + scrollX.value,
-      y: r.top + scrollY.value,
+      x: r.left + scrollX,
+      y: r.top + scrollY,
       w: r.width,
       h: r.height,
     };
   },
 };
 
 FormAssistant.init();
--- a/widget/android/GeckoEditableSupport.cpp
+++ b/widget/android/GeckoEditableSupport.cpp
@@ -531,36 +531,29 @@ ConvertAndroidColor(uint32_t aArgb)
     return NS_RGBA((aArgb & 0x00ff0000) >> 16,
                    (aArgb & 0x0000ff00) >> 8,
                    (aArgb & 0x000000ff),
                    (aArgb & 0xff000000) >> 24);
 }
 
 static jni::ObjectArray::LocalRef
 ConvertRectArrayToJavaRectFArray(const nsTArray<LayoutDeviceIntRect>& aRects,
-                                 const LayoutDeviceIntRect& aWidgetBounds,
                                  const CSSToLayoutDeviceScale aScale)
 {
     const size_t length = aRects.Length();
-    auto rects = jni::ObjectArray::New<sdk::RectF>(length + 1);
-
-    // First element is the widget bounds in device units.
-    auto widgetRect = sdk::RectF::New(aWidgetBounds.x, aWidgetBounds.y,
-                                      aWidgetBounds.x + aWidgetBounds.width,
-                                      aWidgetBounds.y + aWidgetBounds.height);
-    rects->SetElement(0, widgetRect);
+    auto rects = jni::ObjectArray::New<sdk::RectF>(length);
 
     for (size_t i = 0; i < length; i++) {
-        LayoutDeviceIntRect tmp = aRects[i];
+        const LayoutDeviceIntRect& tmp = aRects[i];
 
-        // Subsequent elements are character bounds in CSS units.
+        // Character bounds in CSS units.
         auto rect = sdk::RectF::New(tmp.x / aScale.scale, tmp.y / aScale.scale,
                                     (tmp.x + tmp.width) / aScale.scale,
                                     (tmp.y + tmp.height) / aScale.scale);
-        rects->SetElement(i + 1, rect);
+        rects->SetElement(i, rect);
     }
     return rects;
 }
 
 namespace mozilla {
 namespace widget {
 
 NS_IMPL_ISUPPORTS(GeckoEditableSupport,
@@ -917,17 +910,16 @@ GeckoEditableSupport::UpdateCompositionR
     nsEventStatus status = nsEventStatus_eIgnore;
     uint32_t offset = composition->NativeOffsetOfStartComposition();
     WidgetQueryContentEvent textRects(true, eQueryTextRectArray, widget);
     textRects.InitForQueryTextRectArray(offset, composition->String().Length());
     widget->DispatchEvent(&textRects, status);
 
     auto rects = ConvertRectArrayToJavaRectFArray(
             textRects.mReply.mRectArray,
-            widget->GetScreenBounds(),
             widget->GetDefaultScale());
 
     mEditable->UpdateCompositionRects(rects);
 }
 
 void
 GeckoEditableSupport::OnImeSynchronize()
 {