Bug 1398409 - 1. Move FormAssistant out of browser.js; r=sebastian draft
authorJim Chen <nchen@mozilla.com>
Fri, 15 Sep 2017 14:44:50 -0400
changeset 665603 77cc5f2d0455e9d0e0b78aa6e08ad8bef8298b85
parent 665602 b3f9bb6f01a4b2f5fc29ba9f61d04fcb281cc891
child 665604 c68b6e5b8711ba021067a6bd7cbeac0d10e2f556
push id80117
push userbmo:nchen@mozilla.com
push dateFri, 15 Sep 2017 18:48:05 +0000
reviewerssebastian
bugs1398409
milestone57.0a1
Bug 1398409 - 1. Move FormAssistant out of browser.js; r=sebastian To support FormAssistPopup in custom tabs, we need to move the FormAssitant object out of browser.js and into its own separate file. BrowserCLH.h in turn loads FormAssistant.js when necessary. MozReview-Commit-ID: 7CFQ9R16P4J
mobile/android/chrome/content/FormAssistant.js
mobile/android/chrome/content/browser.js
mobile/android/chrome/jar.mn
mobile/android/components/BrowserCLH.js
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/FormAssistant.js
@@ -0,0 +1,388 @@
+/* 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/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", "resource://gre/modules/FormHistory.jsm");
+
+var FormAssistant = {
+  // Weak-ref used to keep track of the currently focused element.
+  _currentFocusedElement: null,
+
+  // Used to keep track of the element that corresponds to the current
+  // autocomplete suggestions.
+  _currentInputElement: null,
+
+  // The value of the currently focused input.
+  _currentInputValue: null,
+
+  // Whether we're in the middle of an autocomplete.
+  _doingAutocomplete: false,
+
+  init: function() {
+    Services.obs.addObserver(this, "PanZoom:StateChange");
+  },
+
+  register: function(aWindow) {
+    GeckoViewUtils.getDispatcherForWindow(aWindow).registerListener(this, [
+      "FormAssist:AutoComplete",
+      "FormAssist:Hidden",
+      "FormAssist:Remove",
+    ]);
+  },
+
+  onEvent: function(event, message, callback) {
+    switch (event) {
+      case "FormAssist:AutoComplete": {
+        if (!this._currentInputElement) {
+          break;
+        }
+        let editableElement = this._currentInputElement.QueryInterface(
+            Ci.nsIDOMNSEditableElement);
+        this._doingAutocomplete = true;
+
+        // If we have an active composition string, commit it before sending
+        // the autocomplete event with the text that will replace it.
+        try {
+          if (editableElement.editor.composing) {
+            editableElement.editor.forceCompositionEnd();
+          }
+        } catch (e) {}
+
+        editableElement.setUserInput(message.value);
+        this._currentInputValue = message.value;
+
+        let event = this._currentInputElement.ownerDocument.createEvent("Events");
+        event.initEvent("DOMAutoComplete", true, true);
+        this._currentInputElement.dispatchEvent(event);
+
+        this._doingAutocomplete = false;
+        break;
+      }
+
+      case "FormAssist:Hidden": {
+        this._currentInputElement = null;
+        break;
+      }
+
+      case "FormAssist:Remove": {
+        if (!this._currentInputElement) {
+          break;
+        }
+
+        FormHistory.update({
+          op: "remove",
+          fieldname: this._currentInputElement.name,
+          value: message.value,
+        });
+        break;
+      }
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "PanZoom:StateChange":
+        // If the user is just touching the screen and we haven't entered a pan
+        // or zoom state yet do nothing.
+        if (aData == "TOUCHING" || aData == "WAITING_LISTENERS") {
+          break;
+        }
+        let focused = this._currentFocusedElement && this._currentFocusedElement.get();
+        if (aData == "NOTHING") {
+          if (!focused || this._showValidationMessage(focused)) {
+            break;
+          }
+          this._showAutoCompleteSuggestions(focused, hasResults => {
+            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);
+        }
+        break;
+    }
+  },
+
+  notifyInvalidSubmit: function(aFormElement, aInvalidElements) {
+    if (!aInvalidElements.length) {
+        return;
+    }
+
+    // Ignore this notificaiton if the current tab doesn't contain the invalid element
+    let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
+    let focused = this._currentFocusedElement && this._currentFocusedElement.get();
+    if (focused && focused.ownerGlobal.top !== currentElement.ownerGlobal.top) {
+        return;
+    }
+
+    // Our focus listener will show the element's validation message
+    currentElement.focus();
+  },
+
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "focus": {
+        let currentElement = aEvent.target;
+        // Only show a validation message on focus.
+        if (this._showValidationMessage(currentElement) ||
+            this._isAutoComplete(currentElement)) {
+          this._currentFocusedElement = Cu.getWeakReference(currentElement);
+        }
+        break;
+      }
+
+      case "blur": {
+        this._currentInputValue = null;
+        this._currentFocusedElement = null;
+        break;
+      }
+
+      case "click": {
+        let currentElement = aEvent.target;
+
+        // Prioritize a form validation message over autocomplete suggestions
+        // when the element is first focused (a form validation message will
+        // only be available if an invalid form was submitted)
+        if (this._showValidationMessage(currentElement)) {
+          break;
+        }
+
+        let checkResultsClick = hasResults => {
+          if (!hasResults) {
+            this._hideFormAssistPopup(currentElement);
+          }
+        };
+
+        this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
+        break;
+      }
+
+      case "input": {
+        let currentElement = aEvent.target;
+        let focused = this._currentFocusedElement && this._currentFocusedElement.get();
+
+        // If this element isn't focused, we're already in middle of an
+        // autocomplete, or its value hasn't changed, don't show the
+        // autocomplete popup.
+        if (currentElement !== focused ||
+            this._doingAutocomplete ||
+            currentElement.value === this._currentInputValue) {
+          break;
+        }
+
+        this._currentInputValue = currentElement.value;
+
+        // Since we can only show one popup at a time, prioritze autocomplete
+        // suggestions over a form validation message
+        let checkResultsInput = hasResults => {
+          if (hasResults || this._showValidationMessage(currentElement)) {
+            return;
+          }
+          // If we're not showing autocomplete suggestions, hide the form assist popup
+          this._hideFormAssistPopup(currentElement);
+        };
+
+        this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
+        break;
+      }
+    }
+  },
+
+  // We only want to show autocomplete suggestions for certain elements
+  _isAutoComplete: function(aElement) {
+    return (aElement instanceof Ci.nsIDOMHTMLInputElement) &&
+           !aElement.readOnly &&
+           !this._isDisabledElement(aElement) &&
+           (aElement.type !== "password") &&
+           (aElement.autocomplete !== "off");
+  },
+
+  // Retrieves autocomplete suggestions for an element from the form autocomplete service.
+  // aCallback(array_of_suggestions) is called when results are available.
+  _getAutoCompleteSuggestions: function(aSearchString, aElement, aCallback) {
+    // Cache the form autocomplete service for future use
+    if (!this._formAutoCompleteService) {
+      this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"]
+          .getService(Ci.nsIFormAutoComplete);
+    }
+
+    let resultsAvailable = function(results) {
+      let suggestions = [];
+      for (let i = 0; i < results.matchCount; i++) {
+        let value = results.getValueAt(i);
+
+        // Do not show the value if it is the current one in the input field
+        if (value == aSearchString)
+          continue;
+
+        // Supply a label and value, since they can differ for datalist suggestions
+        suggestions.push({ label: value, value: value });
+      }
+      aCallback(suggestions);
+    };
+
+    this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
+                                                          aSearchString, aElement, null,
+                                                          null, resultsAvailable);
+  },
+
+  /**
+   * (Copied from mobile/xul/chrome/content/forms.js)
+   * This function is similar to getListSuggestions from
+   * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
+   * used by the autocomplete.xml binding which is not in used in fennec
+   */
+  _getListSuggestions: function(aElement) {
+    if (!(aElement instanceof Ci.nsIDOMHTMLInputElement) || !aElement.list) {
+      return [];
+    }
+
+    let suggestions = [];
+    let filter = !aElement.hasAttribute("mozNoFilter");
+    let lowerFieldValue = aElement.value.toLowerCase();
+
+    let options = aElement.list.options;
+    let length = options.length;
+    for (let i = 0; i < length; i++) {
+      let item = options.item(i);
+
+      let label = item.value;
+      if (item.label) {
+        label = item.label;
+      } else if (item.text) {
+        label = item.text;
+      }
+
+      if (filter && !(label.toLowerCase().includes(lowerFieldValue))) {
+        continue;
+      }
+      suggestions.push({ label: label, value: item.value });
+    }
+
+    return suggestions;
+  },
+
+  // Retrieves autocomplete suggestions for an element from the form autocomplete service
+  // and sends the suggestions to the Java UI, along with element position data. As
+  // autocomplete queries are asynchronous, calls aCallback when done with a true
+  // argument if results were found and false if no results were found.
+  _showAutoCompleteSuggestions: function(aElement, aCallback) {
+    if (!this._isAutoComplete(aElement)) {
+      aCallback(false);
+      return;
+    }
+
+    let isEmpty = (aElement.value.length === 0);
+
+    let resultsAvailable = autoCompleteSuggestions => {
+      // On desktop, we show datalist suggestions below autocomplete suggestions,
+      // without duplicates removed.
+      let listSuggestions = this._getListSuggestions(aElement);
+      let suggestions = autoCompleteSuggestions.concat(listSuggestions);
+
+      // Return false if there are no suggestions to show
+      if (!suggestions.length) {
+        aCallback(false);
+        return;
+      }
+
+      GeckoViewUtils.getDispatcherForWindow(aElement.ownerGlobal).sendRequest({
+        type: "FormAssist:AutoCompleteResult",
+        suggestions: suggestions,
+        rect: this._getBoundingContentRect(aElement),
+        isEmpty: isEmpty,
+      });
+
+      // Keep track of input element so we can fill it in if the user
+      // selects an autocomplete suggestion
+      this._currentInputElement = aElement;
+      aCallback(true);
+    };
+
+    this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
+  },
+
+  // Only show a validation message if the user submitted an invalid form,
+  // there's a non-empty message string, and the element is the correct type
+  _isValidateable: function(aElement) {
+    return (aElement instanceof Ci.nsIDOMHTMLInputElement ||
+            aElement instanceof Ci.nsIDOMHTMLTextAreaElement ||
+            aElement instanceof Ci.nsIDOMHTMLSelectElement ||
+            aElement instanceof Ci.nsIDOMHTMLButtonElement) &&
+           aElement.validationMessage;
+  },
+
+  // Sends a validation message and position data for an element to the Java UI.
+  // Returns true if there's a validation message to show, false otherwise.
+  _showValidationMessage: function(aElement) {
+    if (!this._isValidateable(aElement)) {
+      return false;
+    }
+
+    GeckoViewUtils.getDispatcherForWindow(aElement.ownerGlobal).sendRequest({
+      type: "FormAssist:ValidationMessage",
+      validationMessage: aElement.validationMessage,
+      rect: this._getBoundingContentRect(aElement),
+    });
+    return true;
+  },
+
+  _hideFormAssistPopup: function(aElement) {
+    GeckoViewUtils.getDispatcherForWindow(aElement.ownerGlobal).sendRequest({
+      type: "FormAssist:Hide",
+    });
+  },
+
+  _isDisabledElement: function(aElement) {
+    let currentElement = aElement;
+    while (currentElement) {
+      if (currentElement.disabled) {
+        return true;
+      }
+      currentElement = currentElement.parentElement;
+    }
+    return false;
+  },
+
+  _getBoundingContentRect: function(aElement) {
+    if (!aElement) {
+      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 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);
+    }
+
+    return {
+      x: r.left + scrollX.value,
+      y: r.top + scrollY.value,
+      w: r.width,
+      h: r.height,
+    };
+  },
+};
+
+FormAssistant.init();
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -431,17 +431,16 @@ var BrowserApp = {
         // Tab selection has changed during a fullscreen transition, handle it now.
         let tab = this.fullscreenTransitionTab;
         this.fullscreenTransitionTab = null;
         this.selectTab(tab);
       }
     });
 
     NativeWindow.init();
-    FormAssistant.init();
     IndexedDB.init();
     XPInstallObserver.init();
     CharacterEncoding.init();
     ActivityObserver.init();
     RemoteDebugger.init();
     DesktopUserAgent.init();
     Distribution.init();
     Tabs.init();
@@ -4800,48 +4799,16 @@ var BrowserEventHandler = {
   _cancelTapHighlight: function _cancelTapHighlight() {
     if (!this._highlightElement)
       return;
 
     this._highlightElement = null;
   }
 };
 
-const ElementTouchHelper = {
-  getBoundingContentRect: function(aElement) {
-    if (!aElement)
-      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 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);
-    }
-
-    return {x: r.left + scrollX.value,
-            y: r.top + scrollY.value,
-            w: r.width,
-            h: r.height };
-  }
-};
-
 var ErrorPageEventHandler = {
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "click": {
         // Don't trust synthetic events
         if (!aEvent.isTrusted)
           return;
 
@@ -4930,384 +4897,16 @@ var ErrorPageEventHandler = {
           }
         }
         break;
       }
     }
   }
 };
 
-var FormAssistant = {
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
-
-  // Used to keep track of the element that corresponds to the current
-  // autocomplete suggestions
-  _currentInputElement: null,
-
-  // The value of the currently focused input
-  _currentInputValue: null,
-
-  // Whether we're in the middle of an autocomplete
-  _doingAutocomplete: false,
-
-  // Keep track of whether or not an invalid form has been submitted
-  _invalidSubmit: false,
-
-  init: function() {
-    WindowEventDispatcher.registerListener(this, [
-      "FormAssist:AutoComplete",
-      "FormAssist:Hidden",
-      "FormAssist:Remove",
-    ]);
-
-    Services.obs.addObserver(this, "invalidformsubmit");
-    Services.obs.addObserver(this, "PanZoom:StateChange");
-
-    // We need to use a capturing listener for focus events
-    BrowserApp.deck.addEventListener("focus", this, true);
-    BrowserApp.deck.addEventListener("blur", this, true);
-    BrowserApp.deck.addEventListener("click", this, true);
-    BrowserApp.deck.addEventListener("input", this);
-    BrowserApp.deck.addEventListener("pageshow", this);
-  },
-
-  onEvent: function(event, message, callback) {
-    switch (event) {
-      case "FormAssist:AutoComplete":
-        if (!this._currentInputElement)
-          break;
-
-        let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
-
-        this._doingAutocomplete = true;
-
-        // If we have an active composition string, commit it before sending
-        // the autocomplete event with the text that will replace it.
-        try {
-          if (editableElement.editor.composing)
-            editableElement.editor.forceCompositionEnd();
-        } catch (e) {}
-
-        editableElement.setUserInput(message.value);
-        this._currentInputValue = message.value;
-
-        let event = this._currentInputElement.ownerDocument.createEvent("Events");
-        event.initEvent("DOMAutoComplete", true, true);
-        this._currentInputElement.dispatchEvent(event);
-
-        this._doingAutocomplete = false;
-
-        break;
-
-      case "FormAssist:Hidden":
-        this._currentInputElement = null;
-        break;
-
-      case "FormAssist:Remove":
-        if (!this._currentInputElement) {
-          break;
-        }
-
-        FormHistory.update({
-          op: "remove",
-          fieldname: this._currentInputElement.name,
-          value: message.value,
-        });
-        break;
-    }
-  },
-
-  observe: function(aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case "PanZoom:StateChange":
-        // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
-        if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
-          break;
-        if (aData == "NOTHING") {
-          // only look for input elements, not contentEditable or multiline text areas
-          let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
-          if (!focused)
-            break;
-
-          if (this._showValidationMessage(focused))
-            break;
-          let checkResultsClick = hasResults => {
-            if (!hasResults) {
-              this._hideFormAssistPopup();
-            }
-          };
-          this._showAutoCompleteSuggestions(focused, checkResultsClick);
-        } else {
-          // temporarily hide the form assist popup while we're panning or zooming the page
-          this._hideFormAssistPopup();
-        }
-        break;
-    }
-  },
-
-  notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
-    if (!aInvalidElements.length)
-      return;
-
-    // Ignore this notificaiton if the current tab doesn't contain the invalid element
-    let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
-    if (BrowserApp.selectedBrowser.contentDocument !=
-        currentElement.ownerGlobal.top.document)
-      return;
-
-    this._invalidSubmit = true;
-
-    // Our focus listener will show the element's validation message
-    currentElement.focus();
-  },
-
-  handleEvent: function(aEvent) {
-    switch (aEvent.type) {
-      case "focus": {
-        let currentElement = aEvent.target;
-
-        // Only show a validation message on focus.
-        this._showValidationMessage(currentElement);
-        break;
-      }
-
-      case "blur": {
-        this._currentInputValue = null;
-        break;
-      }
-
-      case "click": {
-        let currentElement = aEvent.target;
-
-        // Prioritize a form validation message over autocomplete suggestions
-        // when the element is first focused (a form validation message will
-        // only be available if an invalid form was submitted)
-        if (this._showValidationMessage(currentElement))
-          break;
-
-        let checkResultsClick = hasResults => {
-          if (!hasResults) {
-            this._hideFormAssistPopup();
-          }
-        };
-
-        this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
-        break;
-      }
-
-      case "input": {
-        let currentElement = aEvent.target;
-
-        // If this element isn't focused, we're already in middle of an
-        // autocomplete, or its value hasn't changed, don't show the
-        // autocomplete popup.
-        if (currentElement !== BrowserApp.getFocusedInput(BrowserApp.selectedBrowser) ||
-            this._doingAutocomplete ||
-            currentElement.value === this._currentInputValue) {
-          break;
-        }
-
-        this._currentInputValue = currentElement.value;
-
-        // Since we can only show one popup at a time, prioritze autocomplete
-        // suggestions over a form validation message
-        let checkResultsInput = hasResults => {
-          if (hasResults)
-            return;
-
-          if (this._showValidationMessage(currentElement))
-            return;
-
-          // If we're not showing autocomplete suggestions, hide the form assist popup
-          this._hideFormAssistPopup();
-        };
-
-        this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
-        break;
-      }
-
-      // Reset invalid submit state on each pageshow
-      case "pageshow": {
-        if (!this._invalidSubmit)
-          return;
-
-        let selectedBrowser = BrowserApp.selectedBrowser;
-        if (selectedBrowser) {
-          let selectedDocument = selectedBrowser.contentDocument;
-          let target = aEvent.originalTarget;
-          if (target == selectedDocument || target.ownerDocument == selectedDocument)
-            this._invalidSubmit = false;
-        }
-        break;
-      }
-    }
-  },
-
-  // We only want to show autocomplete suggestions for certain elements
-  _isAutoComplete: function _isAutoComplete(aElement) {
-    if (!(aElement instanceof HTMLInputElement) || aElement.readOnly || aElement.disabled ||
-        (aElement.getAttribute("type") == "password") ||
-        (aElement.hasAttribute("autocomplete") &&
-         aElement.getAttribute("autocomplete").toLowerCase() == "off"))
-      return false;
-
-    return true;
-  },
-
-  // Retrieves autocomplete suggestions for an element from the form autocomplete service.
-  // aCallback(array_of_suggestions) is called when results are available.
-  _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
-    // Cache the form autocomplete service for future use
-    if (!this._formAutoCompleteService) {
-      this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"]
-          .getService(Ci.nsIFormAutoComplete);
-    }
-
-    let resultsAvailable = function (results) {
-      let suggestions = [];
-      for (let i = 0; i < results.matchCount; i++) {
-        let value = results.getValueAt(i);
-
-        // Do not show the value if it is the current one in the input field
-        if (value == aSearchString)
-          continue;
-
-        // Supply a label and value, since they can differ for datalist suggestions
-        suggestions.push({ label: value, value: value });
-      }
-      aCallback(suggestions);
-    };
-
-    this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
-                                                          aSearchString, aElement, null,
-                                                          null, resultsAvailable);
-  },
-
-  /**
-   * (Copied from mobile/xul/chrome/content/forms.js)
-   * This function is similar to getListSuggestions from
-   * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
-   * used by the autocomplete.xml binding which is not in used in fennec
-   */
-  _getListSuggestions: function _getListSuggestions(aElement) {
-    if (!(aElement instanceof HTMLInputElement) || !aElement.list)
-      return [];
-
-    let suggestions = [];
-    let filter = !aElement.hasAttribute("mozNoFilter");
-    let lowerFieldValue = aElement.value.toLowerCase();
-
-    let options = aElement.list.options;
-    let length = options.length;
-    for (let i = 0; i < length; i++) {
-      let item = options.item(i);
-
-      let label = item.value;
-      if (item.label)
-        label = item.label;
-      else if (item.text)
-        label = item.text;
-
-      if (filter && !(label.toLowerCase().includes(lowerFieldValue)) )
-        continue;
-      suggestions.push({ label: label, value: item.value });
-    }
-
-    return suggestions;
-  },
-
-  // Retrieves autocomplete suggestions for an element from the form autocomplete service
-  // and sends the suggestions to the Java UI, along with element position data. As
-  // autocomplete queries are asynchronous, calls aCallback when done with a true
-  // argument if results were found and false if no results were found.
-  _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
-    if (!this._isAutoComplete(aElement)) {
-      aCallback(false);
-      return;
-    }
-    if (this._isDisabledElement(aElement)) {
-      aCallback(false);
-      return;
-    }
-
-    let isEmpty = (aElement.value.length === 0);
-
-    let resultsAvailable = autoCompleteSuggestions => {
-      // On desktop, we show datalist suggestions below autocomplete suggestions,
-      // without duplicates removed.
-      let listSuggestions = this._getListSuggestions(aElement);
-      let suggestions = autoCompleteSuggestions.concat(listSuggestions);
-
-      // Return false if there are no suggestions to show
-      if (!suggestions.length) {
-        aCallback(false);
-        return;
-      }
-
-      WindowEventDispatcher.sendRequest({
-        type:  "FormAssist:AutoCompleteResult",
-        suggestions: suggestions,
-        rect: ElementTouchHelper.getBoundingContentRect(aElement),
-        isEmpty: isEmpty,
-      });
-
-      // Keep track of input element so we can fill it in if the user
-      // selects an autocomplete suggestion
-      this._currentInputElement = aElement;
-      aCallback(true);
-    };
-
-    this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
-  },
-
-  // Only show a validation message if the user submitted an invalid form,
-  // there's a non-empty message string, and the element is the correct type
-  _isValidateable: function _isValidateable(aElement) {
-    if (!this._invalidSubmit ||
-        !aElement.validationMessage ||
-        !(aElement instanceof HTMLInputElement ||
-          aElement instanceof HTMLTextAreaElement ||
-          aElement instanceof HTMLSelectElement ||
-          aElement instanceof HTMLButtonElement))
-      return false;
-
-    return true;
-  },
-
-  // Sends a validation message and position data for an element to the Java UI.
-  // Returns true if there's a validation message to show, false otherwise.
-  _showValidationMessage: function _sendValidationMessage(aElement) {
-    if (!this._isValidateable(aElement))
-      return false;
-
-    WindowEventDispatcher.sendRequest({
-      type: "FormAssist:ValidationMessage",
-      validationMessage: aElement.validationMessage,
-      rect: ElementTouchHelper.getBoundingContentRect(aElement)
-    });
-
-    return true;
-  },
-
-  _hideFormAssistPopup: function _hideFormAssistPopup() {
-    WindowEventDispatcher.sendRequest({ type: "FormAssist:Hide" });
-  },
-
-  _isDisabledElement : function(aElement) {
-    let currentElement = aElement;
-    while (currentElement) {
-      if(currentElement.disabled)
-	return true;
-
-      currentElement = currentElement.parentElement;
-    }
-    return false;
-  }
-};
-
 var XPInstallObserver = {
   init: function() {
     Services.obs.addObserver(this, "addon-install-origin-blocked");
     Services.obs.addObserver(this, "addon-install-disabled");
     Services.obs.addObserver(this, "addon-install-blocked");
     Services.obs.addObserver(this, "addon-install-started");
     Services.obs.addObserver(this, "xpi-signature-changed");
     Services.obs.addObserver(this, "browser-delayed-startup-finished");
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -38,16 +38,17 @@ chrome.jar:
   content/WebrtcUI.js                  (content/WebrtcUI.js)
   content/MemoryObserver.js            (content/MemoryObserver.js)
   content/ConsoleAPI.js                (content/ConsoleAPI.js)
   content/PluginHelper.js              (content/PluginHelper.js)
   content/PrintHelper.js               (content/PrintHelper.js)
   content/OfflineApps.js               (content/OfflineApps.js)
   content/MasterPassword.js            (content/MasterPassword.js)
   content/FindHelper.js                (content/FindHelper.js)
+  content/FormAssistant.js             (content/FormAssistant.js)
   content/PermissionsHelper.js         (content/PermissionsHelper.js)
   content/FeedHandler.js               (content/FeedHandler.js)
   content/Feedback.js                  (content/Feedback.js)
   content/Linkify.js                   (content/Linkify.js)
   content/CastingApps.js               (content/CastingApps.js)
   content/RemoteDebugger.js            (content/RemoteDebugger.js)
 #ifdef MOZ_SERVICES_HEALTHREPORT
   content/aboutHealthReport.xhtml      (content/aboutHealthReport.xhtml)
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -71,16 +71,28 @@ BrowserCLH.prototype = {
 
         GeckoViewUtils.addLazyGetter(this, "SelectHelper", {
           script: "chrome://browser/content/SelectHelper.js",
         });
         GeckoViewUtils.addLazyGetter(this, "InputWidgetHelper", {
           script: "chrome://browser/content/InputWidgetHelper.js",
         });
 
+        GeckoViewUtils.addLazyGetter(this, "FormAssistant", {
+          script: "chrome://browser/content/FormAssistant.js",
+        });
+        Services.obs.addObserver({
+          QueryInterface: XPCOMUtils.generateQI([
+            Ci.nsIObserver, Ci.nsIFormSubmitObserver,
+          ]),
+          notifyInvalidSubmit: (form, element) => {
+            this.FormAssistant.notifyInvalidSubmit(form, element);
+          },
+        }, "invalidformsubmit");
+
         GeckoViewUtils.addLazyGetter(this, "LoginManagerParent", {
           module: "resource://gre/modules/LoginManagerParent.jsm",
           mm: [
             // PLEASE KEEP THIS LIST IN SYNC WITH THE DESKTOP LIST IN nsBrowserGlue.js
             "RemoteLogins:findLogins",
             "RemoteLogins:findRecipes",
             "RemoteLogins:onFormSubmit",
             "RemoteLogins:autoCompleteLogins",
@@ -116,16 +128,36 @@ BrowserCLH.prototype = {
         GeckoViewUtils.addLazyEventListener(win, "click", {
           handler: _ => [this.SelectHelper, this.InputWidgetHelper],
           options: {
             capture: true,
             mozSystemGroup: true,
           },
         });
 
+        GeckoViewUtils.addLazyEventListener(win, [
+          "focus", "blur", "click", "input",
+        ], {
+          handler: event => {
+            if (event.target instanceof Ci.nsIDOMHTMLInputElement ||
+                event.target instanceof Ci.nsIDOMHTMLTextAreaElement ||
+                event.target instanceof Ci.nsIDOMHTMLSelectElement ||
+                event.target instanceof Ci.nsIDOMHTMLButtonElement) {
+              // Only load FormAssistant when the event target is what we care about.
+              this.FormAssistant.register(win);
+              return this.FormAssistant;
+            }
+            return null;
+          },
+          options: {
+            capture: true,
+            mozSystemGroup: true,
+          },
+        });
+
         this._initLoginManagerEvents(win);
         break;
       }
     }
   },
 
   _initLoginManagerEvents: function(aWindow) {
     if (Services.prefs.getBoolPref("reftest.remote", false)) {