Bug 1577005 - [3.1] Ensure node value updates are propagated through the API. r=snorp,geckoview-reviewers
authorEugen Sawin <esawin@me73.com>
Fri, 15 Nov 2019 15:49:02 +0000
changeset 502191 a69b9ff498cd1774ac76af6071e2022a3ea742e5
parent 502190 56bf24c7a31a61e9c339661963d0d7c8eab098bc
child 502192 f1815ce6163ae0d87e38b0690ef659f6d6a10767
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, geckoview-reviewers
bugs1577005
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1577005 - [3.1] Ensure node value updates are propagated through the API. r=snorp,geckoview-reviewers Differential Revision: https://phabricator.services.mozilla.com/D51922
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autofill.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
mobile/android/modules/geckoview/GeckoViewAutofill.jsm
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autofill.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autofill.java
@@ -21,16 +21,17 @@ import android.support.annotation.NonNul
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.support.v4.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.View;
 import android.view.ViewStructure;
 import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
 
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 
 public class Autofill {
     private static final boolean DEBUG = true;
 
@@ -638,16 +639,17 @@ public class Autofill {
                 final int flags) {
             Log.d(LOGTAG, "fillViewStructure");
 
             final Node root = getRoot();
 
             if (Build.VERSION.SDK_INT >= 26) {
                 structure.setAutofillId(view.getAutofillId(), getId());
                 structure.setWebDomain(getDomain());
+                structure.setAutofillValue(AutofillValue.forText(getValue()));
             }
 
             structure.setId(getId(), null, null, null);
             structure.setDimens(0, 0, 0, 0,
                     getDimensions().width(),
                     getDimensions().height());
 
             if (Build.VERSION.SDK_INT >= 26) {
@@ -673,17 +675,20 @@ public class Autofill {
 
             switch (getTag()) {
                 case "input":
                 case "textarea":
                     structure.setClassName("android.widget.EditText");
                     structure.setEnabled(getEnabled());
                     structure.setFocusable(getFocusable());
                     structure.setFocused(getFocused());
-                    structure.setVisibility(View.VISIBLE);
+                    structure.setVisibility(
+                            getVisible()
+                            ? View.VISIBLE
+                            : View.INVISIBLE);
 
                     if (Build.VERSION.SDK_INT >= 26) {
                         structure.setAutofillType(View.AUTOFILL_TYPE_TEXT);
                     }
                     break;
                 default:
                     if (childCount > 0) {
                         structure.setClassName("android.view.ViewGroup");
@@ -967,23 +972,26 @@ public class Autofill {
                         if ("GeckoView:AddAutofill".equals(event)) {
                             addNode(message, callback);
                         } else if ("GeckoView:ClearAutofill".equals(event)) {
                             clear();
                         } else if ("GeckoView:OnAutofillFocus".equals(event)) {
                             onFocusChanged(message);
                         } else if ("GeckoView:CommitAutofill".equals(event)) {
                             commit(message);
+                        } else if ("GeckoView:UpdateAutofill".equals(event)) {
+                            update(message);
                         }
                     }
                 },
                 "GeckoView:AddAutofill",
                 "GeckoView:ClearAutofill",
                 "GeckoView:CommitAutofill",
                 "GeckoView:OnAutofillFocus",
+                "GeckoView:UpdateAutofill",
                 null);
 
             mAutofillSession = new Session(geckoSession);
         }
 
         /**
          * Perform auto-fill using the specified values.
          *
@@ -1093,16 +1101,44 @@ public class Autofill {
                 Log.d(LOGTAG, "commit(" + id + ")");
             }
 
             maybeDispatch(
                     Notify.SESSION_COMMITTED,
                     getAutofillSession().getNode(id));
         }
 
+        /* package */ void update(@Nullable final GeckoBundle message) {
+            if (getAutofillSession().isEmpty()) {
+                return;
+            }
+
+            final int id = message.getInt("id");
+
+            if (DEBUG) {
+                Log.d(LOGTAG, "update(" + id + ")");
+            }
+
+            final Node node = getAutofillSession().getNode(id);
+            final String value = message.getString("value");
+
+            if (node == null) {
+                Log.d(LOGTAG, "could not find node " + id);
+                return;
+            }
+
+            if (DEBUG) {
+                Log.d(LOGTAG, "updating node " + id + " value from " +
+                      node.getValue() + " to " + value);
+            }
+
+            node.setValue(value);
+            maybeDispatch(Notify.NODE_UPDATED, node);
+        }
+
         /* package */ void clear() {
             if (getAutofillSession().isEmpty()) {
                 return;
             }
 
             if (DEBUG) {
                 Log.d(LOGTAG, "clear()");
             }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java
@@ -866,12 +866,18 @@ public class GeckoView extends FrameLayo
                 case Autofill.Notify.NODE_FOCUSED:
                     manager.notifyViewEntered(
                         GeckoView.this, node.getId(),
                         displayRectForId(session, node));
                     break;
                 case Autofill.Notify.NODE_BLURRED:
                     manager.notifyViewExited(GeckoView.this, node.getId());
                     break;
+                case Autofill.Notify.NODE_UPDATED:
+                    manager.notifyValueChanged(
+                            GeckoView.this,
+                            node.getId(),
+                            AutofillValue.forText(node.getValue()));
+                    break;
             }
         }
     }
 }
--- a/mobile/android/modules/geckoview/GeckoViewAutofill.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewAutofill.jsm
@@ -39,16 +39,97 @@ class GeckoViewAutofill {
    * there is no difference in how we process them.
    *
    * @param aFormLike A FormLike object produced by FormLikeFactory.
    */
   addElement(aFormLike) {
     this._addElement(aFormLike, /* fromDeferredTask */ false);
   }
 
+  _getInfo(aElement, aParent, aRoot, aUsernameField) {
+    let info = this._autoFillInfos.get(aElement);
+    if (info) {
+      return info;
+    }
+
+    const window = aElement.ownerGlobal;
+    const bounds = aElement.getBoundingClientRect();
+
+    info = {
+      id: ++this._autoFillId,
+      parent: aParent,
+      root: aRoot,
+      tag: aElement.tagName,
+      type: aElement instanceof window.HTMLInputElement ? aElement.type : null,
+      value: aElement.value,
+      editable:
+        aElement instanceof window.HTMLInputElement &&
+        [
+          "color",
+          "date",
+          "datetime-local",
+          "email",
+          "month",
+          "number",
+          "password",
+          "range",
+          "search",
+          "tel",
+          "text",
+          "time",
+          "url",
+          "week",
+        ].includes(aElement.type),
+      disabled:
+        aElement instanceof window.HTMLInputElement ? aElement.disabled : null,
+      attributes: Object.assign(
+        {},
+        ...Array.from(aElement.attributes)
+          .filter(attr => attr.localName !== "value")
+          .map(attr => ({ [attr.localName]: attr.value }))
+      ),
+      origin: aElement.ownerDocument.location.origin,
+      autofillhint: "",
+      bounds: {
+        left: bounds.left,
+        top: bounds.top,
+        right: bounds.right,
+        bottom: bounds.bottom,
+      },
+    };
+
+    if (aElement === aUsernameField) {
+      info.autofillhint = "username"; // AUTOFILL.HINT.USERNAME
+    }
+
+    this._autoFillInfos.set(aElement, info);
+    this._autoFillElements.set(info.id, Cu.getWeakReference(aElement));
+    return info;
+  }
+
+  _updateInfoValues(aElements) {
+    if (!this._autoFillInfos) {
+      return;
+    }
+
+    const updated = [];
+    for (const element of aElements) {
+      const info = this._autoFillInfos.get(element);
+      if (!info || info.value === element.value) {
+        continue;
+      }
+      debug`Updating value ${info.value} to ${element.value}`;
+
+      info.value = element.value;
+      this._autoFillInfos.set(element, info);
+      updated.push(info);
+    }
+    return updated;
+  }
+
   _addElement(aFormLike, aFromDeferredTask) {
     let task =
       this._autoFillTasks && this._autoFillTasks.get(aFormLike.rootElement);
     if (task && !aFromDeferredTask) {
       // We already have a pending task; cancel that and start a new one.
       debug`Canceling previous auto-fill task`;
       task.disarm();
       task = null;
@@ -75,103 +156,53 @@ class GeckoViewAutofill {
 
     this._autoFillTasks.delete(aFormLike.rootElement);
 
     if (!this._autoFillInfos) {
       this._autoFillInfos = new WeakMap();
       this._autoFillElements = new Map();
     }
 
-    let sendFocusEvent = false;
     const window = aFormLike.rootElement.ownerGlobal;
-    const getInfo = (element, parent, root, usernameField) => {
-      let info = this._autoFillInfos.get(element);
-      if (info) {
-        return info;
-      }
-      const bounds = element.getBoundingClientRect();
-      info = {
-        id: ++this._autoFillId,
-        parent,
-        root,
-        tag: element.tagName,
-        type: element instanceof window.HTMLInputElement ? element.type : null,
-        value: element.value,
-        editable:
-          element instanceof window.HTMLInputElement &&
-          [
-            "color",
-            "date",
-            "datetime-local",
-            "email",
-            "month",
-            "number",
-            "password",
-            "range",
-            "search",
-            "tel",
-            "text",
-            "time",
-            "url",
-            "week",
-          ].includes(element.type),
-        disabled:
-          element instanceof window.HTMLInputElement ? element.disabled : null,
-        attributes: Object.assign(
-          {},
-          ...Array.from(element.attributes)
-            .filter(attr => attr.localName !== "value")
-            .map(attr => ({ [attr.localName]: attr.value }))
-        ),
-        origin: element.ownerDocument.location.origin,
-        autofillhint: "",
-        bounds: {
-          left: bounds.left,
-          top: bounds.top,
-          right: bounds.right,
-          bottom: bounds.bottom,
-        },
-      };
-
-      if (element === usernameField) {
-        info.autofillhint = "username"; // AUTOFILL_HINT_USERNAME
-      }
-
-      this._autoFillInfos.set(element, info);
-      this._autoFillElements.set(info.id, Cu.getWeakReference(element));
-      sendFocusEvent |= element === element.ownerDocument.activeElement;
-      return info;
-    };
-
     // Get password field to get better form data via LoginManagerChild.
     let passwordField;
     for (const field of aFormLike.elements) {
       if (
         ChromeUtils.getClassName(field) === "HTMLInputElement" &&
         field.type == "password"
       ) {
         passwordField = field;
         break;
       }
     }
 
     const [usernameField] = LoginManagerChild.forWindow(
       window
     ).getUserNameAndPasswordFields(passwordField || aFormLike.elements[0]);
 
-    const rootInfo = getInfo(aFormLike.rootElement, null, undefined, null);
+    const focusedElement = aFormLike.rootElement.ownerDocument.activeElement;
+    let sendFocusEvent = aFormLike.rootElement === focusedElement;
+
+    const rootInfo = this._getInfo(
+      aFormLike.rootElement,
+      null,
+      undefined,
+      null
+    );
+
     rootInfo.root = rootInfo.id;
     rootInfo.children = aFormLike.elements
       .filter(
         element =>
           !usernameField || element.type != "text" || element == usernameField
       )
-      .map(element =>
-        getInfo(element, rootInfo.id, rootInfo.id, usernameField)
-      );
+      .map(element => {
+        sendFocusEvent |= element === focusedElement;
+        return this._getInfo(element, rootInfo.id, rootInfo.id, usernameField);
+      });
 
     this._eventDispatcher.dispatch("GeckoView:AddAutofill", rootInfo, {
       onSuccess: responses => {
         // `responses` is an object with IDs as keys.
         debug`Performing auto-fill ${Object.keys(responses)}`;
 
         const AUTOFILL_STATE = "-moz-autofill";
         const winUtils = window.windowUtils;
@@ -230,23 +261,32 @@ class GeckoViewAutofill {
       this._eventDispatcher.dispatch("GeckoView:OnAutofillFocus", info);
     }
   }
 
   commitAutofill(aFormLike) {
     if (!aFormLike) {
       throw new Error("null-form on autofill commit");
     }
+
     debug`Committing auto-fill for ${aFormLike.rootElement.tagName}`;
 
-    const info =
-      this._autoFillInfos && this._autoFillInfos.get(aFormLike.rootElement);
+    const updatedNodeInfos = this._updateInfoValues([
+      aFormLike.rootElement,
+      ...aFormLike.elements,
+    ]);
 
+    for (const updatedInfo of updatedNodeInfos) {
+      debug`Updating node ${updatedInfo}`;
+      this._eventDispatcher.dispatch("GeckoView:UpdateAutofill", updatedInfo);
+    }
+
+    const info = this._getInfo(aFormLike.rootElement);
     if (info) {
-      debug`Committing info ${info}`;
+      debug`Committing node ${info}`;
       this._eventDispatcher.dispatch("GeckoView:CommitAutofill", info);
     }
   }
 
   /**
    * Clear all tracked auto-fill forms and notify Java.
    */
   clearElements() {