Bug 1416310 - 2. Use per-GeckoView event to handle scroll-to-focused-input; r?rbarker draft
authorJim Chen <nchen@mozilla.com>
Mon, 20 Nov 2017 17:17:01 -0500
changeset 700793 cb0f792a60b0ceb016362c209ab7e7370db34088
parent 700792 830a7e066f73ca66b21ce7ff39b69b9833541729
child 700794 2c949bf456f32e798470a59730602eaaecc8bf99
push id89975
push userbmo:nchen@mozilla.com
push dateMon, 20 Nov 2017 22:17:18 +0000
reviewersrbarker
bugs1416310
milestone59.0a1
Bug 1416310 - 2. Use per-GeckoView event to handle scroll-to-focused-input; r?rbarker Instead of sending an event through the global EventDispatcher in GeckoLayerClient, switch to using the per-GeckoView EventDispatcher in GeckoInputConnection, to handle scroll-to-focused-input-on-resize. This lets us implement the same functionality for standalone GeckoView. The patch also fixes some small bugs including unregistering not-registered events, not scrolling when switching input focus, and inadvertent scrolling when not showing the keyboard. MozReview-Commit-ID: 20OZP9dMXtI
mobile/android/chrome/content/browser.js
mobile/android/chrome/geckoview/GeckoViewContent.js
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
mobile/android/geckoview_example/src/main/AndroidManifest.xml
mobile/android/modules/geckoview/GeckoViewContent.jsm
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -359,16 +359,17 @@ var BrowserApp = {
 
     this.deck = document.getElementById("browsers");
 
     BrowserEventHandler.init();
 
     Services.androidBridge.browserApp = this;
 
     WindowEventDispatcher.registerListener(this, [
+      "GeckoView:ZoomToInput",
       "Session:Restore",
       "Tab:Load",
       "Tab:Selected",
       "Tab:Closed",
       "Tab:Move",
       "Tab:OpenUri",
     ]);
 
@@ -378,17 +379,16 @@ var BrowserApp = {
       "Fonts:Reload",
       "FormHistory:Init",
       "FullScreen:Exit",
       "Locale:OS",
       "Locale:Changed",
       "Passwords:Init",
       "Sanitize:ClearData",
       "SaveAs:PDF",
-      "ScrollTo:FocusedInput",
       "Session:Back",
       "Session:Forward",
       "Session:GetHistory",
       "Session:Navigate",
       "Session:Reload",
       "Session:Stop",
       "Telemetry:CustomTabsPing",
     ]);
@@ -1605,59 +1605,59 @@ var BrowserApp = {
         // results in a better user experience
         focused = focused.ownerGlobal.frameElement;
       }
       return focused;
     }
     return null;
   },
 
-  scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
+  scrollToFocusedInput: function(aBrowser) {
     let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
-    if (formHelperMode == kFormHelperModeDisabled)
+    if (formHelperMode == kFormHelperModeDisabled) {
       return;
-
-    let dwu = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+    }
+
+    let dwu = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                                    .getInterface(Ci.nsIDOMWindowUtils);
     if (!dwu) {
       return;
     }
 
-    let apzFlushDone = function() {
-      Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
-      dwu.zoomToFocusedInput();
+    let zoomToFocusedInput = function() {
+      if (!dwu.flushApzRepaints()) {
+        dwu.zoomToFocusedInput();
+        return;
+      }
+      Services.obs.addObserver(function apzFlushDone() {
+        Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
+        dwu.zoomToFocusedInput();
+      }, "apz-repaints-flushed");
     };
 
-    let paintDone = function() {
-      window.removeEventListener("MozAfterPaint", paintDone);
-      if (dwu.flushApzRepaints()) {
-        Services.obs.addObserver(apzFlushDone, "apz-repaints-flushed");
+    let gotResize = false;
+    let onResize = function() {
+      gotResize = true;
+      if (dwu.isMozAfterPaintPending) {
+        addEventListener("MozAfterPaint", zoomToFocusedInput, {once: true});
       } else {
-        apzFlushDone();
+        zoomToFocusedInput();
       }
     };
 
-    let gotResizeWindow = false;
-    let resizeWindow = function(e) {
-      gotResizeWindow = true;
-      aBrowser.contentWindow.removeEventListener("resize", resizeWindow);
-      if (dwu.isMozAfterPaintPending) {
-        window.addEventListener("MozAfterPaint", paintDone);
-      } else {
-        paintDone();
-      }
-    }
-
-    aBrowser.contentWindow.addEventListener("resize", resizeWindow);
-
-    // The "resize" event sometimes fails to fire, so set a timer to catch that case
-    // and unregister the event listener. See Bug 1253469
+    aBrowser.contentWindow.addEventListener("resize", onResize);
+
+    // When the keyboard is displayed, we can get one resize event, multiple
+    // resize events, or none at all. Try to handle all these cases by allowing
+    // resizing within a set interval, and still zoom to input if there is no
+    // resize event at the end of the interval.
     setTimeout(function(e) {
-    if (!gotResizeWindow) {
-        aBrowser.contentWindow.removeEventListener("resize", resizeWindow);
-        dwu.zoomToFocusedInput();
+      aBrowser.contentWindow.removeEventListener("resize", onResize);
+      if (!gotResize) {
+        onResize();
       }
     }, 500);
   },
 
   getUALocalePref: function () {
     return Services.locale.getRequestedLocale() || undefined;
   },
 
@@ -1764,20 +1764,20 @@ var BrowserApp = {
       case "Sanitize:ClearData":
         this.sanitize(data);
         break;
 
       case "SaveAs:PDF":
         this.saveAsPDF(browser);
         break;
 
-      case "ScrollTo:FocusedInput": {
+      case "GeckoView:ZoomToInput": {
         // these messages come from a change in the viewable area and not user interaction
         // we allow scrolling to the selected input, but not zooming the page
-        this.scrollToFocusedInput(browser, false);
+        this.scrollToFocusedInput(browser);
         break;
       }
 
       case "Telemetry:CustomTabsPing": {
         TelemetryController.submitExternalPing("anonymous", { client: data.client }, { addClientId: false });
         break;
       }
 
--- a/mobile/android/chrome/geckoview/GeckoViewContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContent.js
@@ -3,16 +3,20 @@
  * 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/. */
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/GeckoViewContentModule.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+});
+
 XPCOMUtils.defineLazyGetter(this, "dump", () =>
     Cu.import("resource://gre/modules/AndroidLog.jsm",
               {}).AndroidLog.d.bind(null, "ViewContent"));
 
 function debug(aMsg) {
   // dump(aMsg);
 }
 
@@ -20,58 +24,106 @@ class GeckoViewContent extends GeckoView
   register() {
     debug("register");
 
     addEventListener("DOMTitleChanged", this, false);
     addEventListener("MozDOMFullscreen:Entered", this, false);
     addEventListener("MozDOMFullscreen:Exit", this, false);
     addEventListener("MozDOMFullscreen:Exited", this, false);
     addEventListener("MozDOMFullscreen:Request", this, false);
-    addEventListener("contextmenu", this, { capture: true, passive: false });
+    addEventListener("contextmenu", this, { capture: true });
 
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenEntered",
                                            this);
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenExited",
                                            this);
+    this.messageManager.addMessageListener("GeckoView:ZoomToInput",
+                                           this);
   }
 
   unregister() {
     debug("unregister");
 
     removeEventListener("DOMTitleChanged", this);
     removeEventListener("MozDOMFullscreen:Entered", this);
     removeEventListener("MozDOMFullscreen:Exit", this);
     removeEventListener("MozDOMFullscreen:Exited", this);
     removeEventListener("MozDOMFullscreen:Request", this);
-    removeEventListener("contextmenu", this);
+    removeEventListener("contextmenu", this, { capture: true });
 
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenEntered",
                                               this);
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenExited",
                                               this);
+    this.messageManager.removeMessageListener("GeckoView:ZoomToInput",
+                                              this);
   }
 
   receiveMessage(aMsg) {
     debug("receiveMessage " + aMsg.name);
 
     switch (aMsg.name) {
       case "GeckoView:DOMFullscreenEntered":
         if (content) {
           content.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIDOMWindowUtils)
                  .handleFullscreenRequests();
         }
         break;
+
       case "GeckoView:DOMFullscreenExited":
         if (content) {
           content.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIDOMWindowUtils)
                  .exitFullscreen();
         }
         break;
+
+      case "GeckoView:ZoomToInput": {
+        let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils);
+
+        let zoomToFocusedInput = function() {
+          if (!dwu.flushApzRepaints()) {
+            dwu.zoomToFocusedInput();
+            return;
+          }
+          Services.obs.addObserver(function apzFlushDone() {
+            Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
+            dwu.zoomToFocusedInput();
+          }, "apz-repaints-flushed");
+        };
+
+        let gotResize = false;
+        let onResize = function() {
+          gotResize = true;
+          if (dwu.isMozAfterPaintPending) {
+            addEventListener("MozAfterPaint", function paintDone() {
+              removeEventListener("MozAfterPaint", paintDone, {capture: true});
+              zoomToFocusedInput();
+            }, {capture: true});
+          } else {
+            zoomToFocusedInput();
+          }
+        };
+
+        addEventListener("resize", onResize, { capture: true });
+
+        // When the keyboard is displayed, we can get one resize event,
+        // multiple resize events, or none at all. Try to handle all these
+        // cases by allowing resizing within a set interval, and still zoom to
+        // input if there is no resize event at the end of the interval.
+        content.setTimeout(() => {
+          removeEventListener("resize", onResize, { capture: true });
+          if (!gotResize) {
+            onResize();
+          }
+        }, 500);
+      }
+      break;
     }
   }
 
   handleEvent(aEvent) {
     debug("handleEvent " + aEvent.type);
 
     switch (aEvent.type) {
       case "contextmenu":
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -230,18 +230,21 @@ class GeckoInputConnection
             public void run() {
                 if (v.hasFocus() && !imm.isActive(v)) {
                     // Marshmallow workaround: The view has focus but it is not the active
                     // view for the input method. (Bug 1211848)
                     v.clearFocus();
                     v.requestFocus();
                 }
                 final GeckoView view = getView();
-                if (view != null && showToolbar) {
-                    view.getDynamicToolbarAnimator().showToolbar(/*immediately*/ true);
+                if (view != null) {
+                    if (showToolbar) {
+                        view.getDynamicToolbarAnimator().showToolbar(/*immediately*/ true);
+                    }
+                    view.getEventDispatcher().dispatch("GeckoView:ZoomToInput", null);
                 }
                 mSoftInputReentrancyGuard = true;
                 imm.showSoftInput(v, 0);
                 mSoftInputReentrancyGuard = false;
             }
         });
     }
 
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -2,17 +2,16 @@
  * 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.annotation.RobocopTarget;
 import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.util.GeckoBundle;
 
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.RectF;
 import android.os.SystemClock;
 import android.util.Log;
@@ -23,17 +22,16 @@ import java.util.ArrayList;
 
 class GeckoLayerClient implements LayerView.Listener
 {
     private static final String LOGTAG = "GeckoLayerClient";
 
     private IntSize mWindowSize;
 
     private boolean mForceRedraw;
-    private boolean mImeWasEnabledOnLastResize;
 
     /* The current viewport metrics.
      * This is volatile so that we can read and write to it from different threads.
      * We avoid synchronization to make getting the viewport metrics from
      * the compositor as cheap as possible. The viewport is immutable so
      * we don't need to worry about anyone mutating it while we're reading from it.
      * Specifically:
      * 1) reading mViewportMetrics from any thread is fine without synchronization
@@ -100,29 +98,16 @@ class GeckoLayerClient implements LayerV
      * result in an infinite loop.
      */
     boolean setViewportSize(int width, int height) {
         if (mViewportMetrics.viewportRectWidth == width &&
             mViewportMetrics.viewportRectHeight == height) {
             return false;
         }
         mViewportMetrics = mViewportMetrics.setViewportSize(width, height);
-
-        if (mView.isCompositorReady()) {
-            // the following call also sends gecko a message, which will be processed after the resize
-            // message above has updated the viewport. this message ensures that if we have just put
-            // focus in a text field, we scroll the content so that the text field is in view.
-            final boolean imeIsEnabled = mView.isIMEEnabled();
-            if (imeIsEnabled && !mImeWasEnabledOnLastResize) {
-                // The IME just came up after not being up, so let's scroll
-                // to the focused input.
-                EventDispatcher.getInstance().dispatch("ScrollTo:FocusedInput", null);
-            }
-            mImeWasEnabledOnLastResize = imeIsEnabled;
-        }
         return true;
     }
 
     PanZoomController getPanZoomController() {
         return mPanZoomController;
     }
 
     DynamicToolbarAnimator getDynamicToolbarAnimator() {
--- a/mobile/android/geckoview_example/src/main/AndroidManifest.xml
+++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
@@ -4,16 +4,17 @@
     <application android:allowBackup="true"
                  android:label="@string/app_name"
                  android:supportsRtl="true">
 
         <uses-library android:name="android.test.runner" />
 
         <activity android:name="org.mozilla.geckoview_example.GeckoViewActivity"
                   android:label="GeckoViewActivity"
+                  android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:launchMode="singleTop">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
                 <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
                 <category android:name="android.intent.category.APP_BROWSER" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -34,40 +34,50 @@ class GeckoViewContent extends GeckoView
       this.frameScriptLoaded = true;
     }
 
     this.window.addEventListener("MozDOMFullScreen:Entered", this,
                                  /* capture */ true, /* untrusted */ false);
     this.window.addEventListener("MozDOMFullScreen:Exited", this,
                                  /* capture */ true, /* untrusted */ false);
 
-    this.eventDispatcher.registerListener(this, "GeckoViewContent:ExitFullScreen");
+    this.eventDispatcher.registerListener(this, [
+        "GeckoViewContent:ExitFullScreen",
+        "GeckoView:ZoomToInput",
+    ]);
+
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenExit", this);
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenRequest", this);
   }
 
   // Bundle event handler.
   onEvent(aEvent, aData, aCallback) {
     debug("onEvent: " + aEvent);
     switch (aEvent) {
       case "GeckoViewContent:ExitFullScreen":
-        this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenExited");
+      case "GeckoView:ZoomToInput":
+        this.messageManager.sendAsyncMessage(aEvent);
         break;
       case "GeckoView:SetActive":
         this.browser.docShellIsActive = aData.active;
         break;
     }
   }
 
   unregister() {
     this.window.removeEventListener("MozDOMFullScreen:Entered", this,
                                     /* capture */ true);
     this.window.removeEventListener("MozDOMFullScreen:Exited", this,
                                     /* capture */ true);
-    this.eventDispatcher.unregisterListener(this, "GeckoViewContent:ExitFullScreen");
+
+    this.eventDispatcher.unregisterListener(this, [
+        "GeckoViewContent:ExitFullScreen",
+        "GeckoView:ZoomToInput",
+    ]);
+
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenExit", this);
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenRequest", this);
   }
 
   // DOM event handler
   handleEvent(aEvent) {
     debug("handleEvent: aEvent.type=" + aEvent.type);