Bug 1492704 - 2. Keep track of the auto-fill focus root node; r=droeh
authorJim Chen <nchen@mozilla.com>
Thu, 04 Oct 2018 20:41:40 +0000
changeset 439716 538da6bb518b6a794a81ea76584c190dd66e17e9
parent 439715 7b49a74b30e60b6e030bb3e23b444f8eac0ca715
child 439717 ed764e9bab674041bc7512baab708558e649d542
push id34783
push userncsoregi@mozilla.com
push dateFri, 05 Oct 2018 04:46:05 +0000
treeherdermozilla-central@6e67424a39d7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdroeh
bugs1492704
milestone64.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 1492704 - 2. Keep track of the auto-fill focus root node; r=droeh The crash is happening because we may not have a full node tree when looking at a particular node; in that case we may not be able to find the root node. This patch makes us keep track of the root node from the beginning, so we always know what the root node is. Differential Revision: https://phabricator.services.mozilla.com/D6758
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
mobile/android/modules/geckoview/GeckoViewAutoFill.jsm
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
@@ -712,20 +712,20 @@ class AccessibilityTest : BaseSessionTes
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled
             override fun onFocused(event: AccessibilityEvent) {
             }
         })
         assertThat("Should have one focused field",
                    countAutoFillNodes({ it.isFocused }), equalTo(1))
         // The focused field, its siblings, and its parent should be visible.
-        assertThat("Should have at least six visible fields",
+        assertThat("Should have six visible nodes",
                    countAutoFillNodes({ node -> node.isVisibleToUser &&
                            !(Rect().also({ node.getBoundsInScreen(it) }).isEmpty) }),
-                   greaterThanOrEqualTo(6))
+                   equalTo(6))
 
         mainSession.evaluateJS("$('#pass1').blur()")
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled
             override fun onFocused(event: AccessibilityEvent) {
             }
         })
         assertThat("Should not have focused field",
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -419,20 +419,20 @@ class ContentDelegateTest : BaseSessionT
                 assertThat("Should be entering auto-fill view",
                            notification,
                            equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
                 assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
             }
         })
         assertThat("Should have one focused field",
                    countAutoFillNodes({ it.isFocused }), equalTo(1))
-        // The focused field, its siblings, and its parent should be visible.
-        assertThat("Should have at least six visible fields",
+        // The focused field, its siblings, its parent, and the root node should be visible.
+        assertThat("Should have seven visible nodes",
                    countAutoFillNodes({ node -> node.width > 0 && node.height > 0 }),
-                   greaterThanOrEqualTo(6))
+                   equalTo(7))
 
         mainSession.evaluateJS("$('#pass1').blur()")
         sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
             @AssertCalled(count = 1)
             override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
                 assertThat("Should be exiting auto-fill view",
                            notification,
                            equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -239,16 +239,17 @@ public class SessionAccessibility {
     // Have we reached the last item in content?
     private boolean mLastItem;
     // Used to store the JSON message and populate the event later in the code path.
     private AccessibilityNodeInfo mVirtualContentNode;
     // Auto-fill nodes.
     private SparseArray<GeckoBundle> mAutoFillNodes;
     private SparseArray<EventCallback> mAutoFillRoots;
     private int mAutoFillFocusedId = View.NO_ID;
+    private int mAutoFillFocusedRoot = View.NO_ID;
 
     private boolean mAttached = false;
 
     /* package */ SessionAccessibility(final GeckoSession session) {
         mSession = session;
 
         Settings.updateAccessibilitySettings();
 
@@ -561,25 +562,16 @@ public class SessionAccessibility {
         }
 
         final GeckoBundle data = new GeckoBundle(2);
         data.putDoubleArray("coordinates", new double[] {event.getRawX(), event.getRawY()});
         mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityExploreByTouch", data);
         return true;
     }
 
-    private int getAutoFillRootId(final int id) {
-        int root = View.NO_ID;
-        for (int newId = id; newId != View.NO_ID;) {
-            root = newId;
-            newId = mAutoFillNodes.get(newId).getInt("parent", View.NO_ID);
-        }
-        return root;
-    }
-
     /* package */ AccessibilityNodeInfo getAutoFillNode(final int id) {
         if (mView == null || mAutoFillRoots == null) {
             return null;
         }
 
         final GeckoBundle bundle = mAutoFillNodes.get(id);
         if (bundle == null) {
             return null;
@@ -589,17 +581,18 @@ public class SessionAccessibility {
             Log.d(LOGTAG, "getAutoFillNode(" + id + ')');
         }
 
         final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView, id);
         node.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
         node.setParent(mView, bundle.getInt("parent", View.NO_ID));
         node.setEnabled(true);
 
-        if (getAutoFillRootId(mAutoFillFocusedId) == getAutoFillRootId(id)) {
+        if (mAutoFillFocusedRoot != View.NO_ID &&
+                mAutoFillFocusedRoot == bundle.getInt("root", View.NO_ID)) {
             // Some auto-fill clients require a dummy rect for the focused View.
             final Rect rect = new Rect();
             mSession.getSurfaceBounds(rect);
             node.setVisibleToUser(!rect.isEmpty());
             node.setBoundsInParent(rect);
 
             final int[] offset = new int[2];
             mView.getLocationOnScreen(offset);
@@ -760,40 +753,43 @@ public class SessionAccessibility {
     /* package */ void clearAutoFill() {
         if (mAutoFillRoots != null) {
             if (DEBUG) {
                 Log.d(LOGTAG, "clearAutoFill()");
             }
             mAutoFillRoots = null;
             mAutoFillNodes = null;
             mAutoFillFocusedId = View.NO_ID;
+            mAutoFillFocusedRoot = View.NO_ID;
             fireWindowChangedEvent(View.NO_ID);
         }
     }
 
     /* package */ void onAutoFillFocus(final GeckoBundle message) {
         if (!Settings.isEnabled() || !(mView instanceof ViewParent) || mAutoFillNodes == null) {
             return;
         }
 
         final int id;
+        final int root;
         if (message != null) {
             id = message.getInt("id");
-            mAutoFillNodes.put(id, message);
+            root = message.getInt("root");
         } else {
-            id = View.NO_ID;
+            id = root = View.NO_ID;
         }
 
         if (DEBUG) {
             Log.d(LOGTAG, "onAutoFillFocus(" + id + ')');
         }
         if (mAutoFillFocusedId == id) {
             return;
         }
         mAutoFillFocusedId = id;
+        mAutoFillFocusedRoot = root;
 
         // We already send "TYPE_VIEW_FOCUSED" in touch exploration mode,
         // so in that case don't send it here.
         if (!Settings.isTouchExplorationEnabled()) {
             AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, id);
             ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
         }
     }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
@@ -275,16 +275,17 @@ public final class SessionTextInput {
     private final GeckoEditable mEditable;
     private final GeckoEditableChild mEditableChild;
     private InputConnectionClient mInputConnection;
     private GeckoSession.TextInputDelegate mDelegate;
     // Auto-fill nodes.
     private SparseArray<GeckoBundle> mAutoFillNodes;
     private SparseArray<EventCallback> mAutoFillRoots;
     private int mAutoFillFocusedId = View.NO_ID;
+    private int mAutoFillFocusedRoot = View.NO_ID;
 
     /* package */ SessionTextInput(final @NonNull GeckoSession session,
                                    final @NonNull NativeQueue queue) {
         mSession = session;
         mQueue = queue;
         mEditable = new GeckoEditable(session);
         mEditableChild = new GeckoEditableChild(mEditable);
         mEditable.setDefaultEditableChild(mEditableChild);
@@ -505,27 +506,20 @@ public final class SessionTextInput {
         if (mAutoFillRoots == null) {
             structure.setChildCount(0);
             return;
         }
 
         final int size = mAutoFillRoots.size();
         structure.setChildCount(size);
 
-        int focusedRoot = View.NO_ID;
-        for (int newId = mAutoFillFocusedId; newId != View.NO_ID;) {
-            focusedRoot = newId;
-            newId = mAutoFillNodes.get(newId).getInt("parent", View.NO_ID);
-        }
-
         for (int i = 0; i < size; i++) {
             final int id = mAutoFillRoots.keyAt(i);
             final GeckoBundle root = mAutoFillNodes.get(id);
-            fillAutoFillStructure(view, id, root, structure.newChild(i),
-                                  (focusedRoot == id) ? rect : null);
+            fillAutoFillStructure(view, id, root, structure.newChild(i), rect);
         }
     }
 
     /**
      * Perform auto-fill using the specified values.
      *
      * @param values Map of auto-fill IDs to values.
      */
@@ -570,34 +564,35 @@ public final class SessionTextInput {
             callback.sendSuccess(response);
         }
     }
 
     @TargetApi(23)
     private void fillAutoFillStructure(@Nullable final View view, final int id,
                                        @NonNull final GeckoBundle bundle,
                                        @NonNull final ViewStructure structure,
-                                       @Nullable final Rect rect) {
+                                       @NonNull final Rect rect) {
         if (mAutoFillRoots == null) {
             return;
         }
 
         if (DEBUG) {
             Log.d(LOGTAG, "fillAutoFillStructure(" + id + ')');
         }
 
         if (Build.VERSION.SDK_INT >= 26) {
             if (view != null) {
                 structure.setAutofillId(view.getAutofillId(), id);
             }
             structure.setWebDomain(bundle.getString("origin"));
         }
         structure.setId(id, null, null, null);
 
-        if (rect != null) {
+        if (mAutoFillFocusedRoot != View.NO_ID &&
+                mAutoFillFocusedRoot == bundle.getInt("root", View.NO_ID)) {
             structure.setDimens(0, 0, 0, 0, rect.width(), rect.height());
         }
 
         final GeckoBundle[] children = bundle.getBundleArray("children");
         if (children != null) {
             structure.setChildCount(children.length);
             for (int i = 0; i < children.length; i++) {
                 final GeckoBundle childBundle = children[i];
@@ -716,47 +711,50 @@ public final class SessionTextInput {
             return;
         }
         if (DEBUG) {
             Log.d(LOGTAG, "clearAutoFill()");
         }
         mAutoFillRoots = null;
         mAutoFillNodes = null;
         mAutoFillFocusedId = View.NO_ID;
+        mAutoFillFocusedRoot = View.NO_ID;
 
         getDelegate().notifyAutoFill(
                 mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_CANCELED, View.NO_ID);
     }
 
     /* package */ void onAutoFillFocus(@Nullable final GeckoBundle message) {
         if (mAutoFillRoots == null) {
             return;
         }
 
         final int id;
+        final int root;
         if (message != null) {
             id = message.getInt("id");
-            mAutoFillNodes.put(id, message);
+            root = message.getInt("root");
         } else {
-            id = View.NO_ID;
+            id = root = View.NO_ID;
         }
 
         if (DEBUG) {
             Log.d(LOGTAG, "onAutoFillFocus(" + id + ')');
         }
         if (mAutoFillFocusedId == id) {
             return;
         }
         if (mAutoFillFocusedId != View.NO_ID) {
             getDelegate().notifyAutoFill(
                     mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED,
                     mAutoFillFocusedId);
         }
 
         mAutoFillFocusedId = id;
+        mAutoFillFocusedRoot = root;
         if (id != View.NO_ID) {
             getDelegate().notifyAutoFill(
                     mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED, id);
         }
     }
 
     /* package */ static Rect getDummyAutoFillRect(@NonNull final GeckoSession session,
                                                    final boolean screen,
--- a/mobile/android/modules/geckoview/GeckoViewAutoFill.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewAutoFill.jsm
@@ -73,24 +73,25 @@ class GeckoViewAutoFill {
 
     if (!this._autoFillInfos) {
       this._autoFillInfos = new WeakMap();
       this._autoFillElements = new Map();
     }
 
     let sendFocusEvent = false;
     const window = aFormLike.rootElement.ownerGlobal;
-    const getInfo = (element, parent) => {
+    const getInfo = (element, parent, root) => {
       let info = this._autoFillInfos.get(element);
       if (info) {
         return info;
       }
       info = {
         id: ++this._autoFillId,
         parent,
+        root,
         tag: element.tagName,
         type: element instanceof window.HTMLInputElement ? element.type : null,
         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,
@@ -100,19 +101,20 @@ class GeckoViewAutoFill {
         origin: element.ownerDocument.location.origin,
       };
       this._autoFillInfos.set(element, info);
       this._autoFillElements.set(info.id, Cu.getWeakReference(element));
       sendFocusEvent |= (element === element.ownerDocument.activeElement);
       return info;
     };
 
-    const rootInfo = getInfo(aFormLike.rootElement, null);
+    const rootInfo = getInfo(aFormLike.rootElement, null, undefined);
+    rootInfo.root = rootInfo.id;
     rootInfo.children = aFormLike.elements.map(
-        element => getInfo(element, rootInfo.id));
+        element => getInfo(element, rootInfo.id, rootInfo.id));
 
     this._eventDispatcher.dispatch("GeckoView:AddAutoFill", rootInfo, {
       onSuccess: responses => {
         // `responses` is an object with IDs as keys.
         debug `Performing auto-fill ${responses}`;
 
         const AUTOFILL_STATE = "-moz-autofill";
         const winUtils = window.windowUtils;