Bug 1182222 - Make Layerview support accessibility HTML navigation. r=yzen r=mfinkle
authorEitan Isaacson <eitan@monotonous.org>
Mon, 14 Sep 2015 10:52:43 -0700
changeset 295000 2ed46aca6ae071b59fb287f3aaf325129d9e4327
parent 294999 a2e610b46e85597e865a601c5d3f6c381702fbba
child 295001 013a4f0f60e3d353a4dbf16cf4c12033fc94306c
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen, mfinkle
bugs1182222
milestone43.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 1182222 - Make Layerview support accessibility HTML navigation. r=yzen r=mfinkle
accessible/jsat/AccessFu.jsm
mobile/android/base/GeckoAccessibility.java
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -303,21 +303,25 @@ this.AccessFu = { // jshint ignore:line
 
   observe: function observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case 'Accessibility:Settings':
         this._systemPref = JSON.parse(aData).enabled;
         this._enableOrDisable();
         break;
       case 'Accessibility:NextObject':
-        this.Input.moveCursor('moveNext', 'Simple', 'gesture');
+      case 'Accessibility:PreviousObject':
+      {
+        let rule = aData ?
+          aData.substr(0, 1).toUpperCase() + aData.substr(1).toLowerCase() :
+          'Simple';
+        let method = aTopic.replace(/Accessibility:(\w+)Object/, 'move$1');
+        this.Input.moveCursor(method, rule, 'gesture');
         break;
-      case 'Accessibility:PreviousObject':
-        this.Input.moveCursor('movePrevious', 'Simple', 'gesture');
-        break;
+      }
       case 'Accessibility:ActivateObject':
         this.Input.activateCurrent(JSON.parse(aData));
         break;
       case 'Accessibility:LongPress':
         this.Input.sendContextMenuMessage();
         break;
       case 'Accessibility:ScrollForward':
         this.Input.androidScroll('forward');
--- a/mobile/android/base/GeckoAccessibility.java
+++ b/mobile/android/base/GeckoAccessibility.java
@@ -30,24 +30,22 @@ import android.view.accessibility.Access
 import android.view.accessibility.AccessibilityNodeProvider;
 
 import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
 import com.googlecode.eyesfree.braille.selfbraille.WriteData;
 
 public class GeckoAccessibility {
     private static final String LOGTAG = "GeckoAccessibility";
     private static final int VIRTUAL_ENTRY_POINT_BEFORE = 1;
-    private static final int VIRTUAL_CURSOR_PREVIOUS = 2;
-    private static final int VIRTUAL_CURSOR_POSITION = 3;
-    private static final int VIRTUAL_CURSOR_NEXT = 4;
-    private static final int VIRTUAL_ENTRY_POINT_AFTER = 5;
+    private static final int VIRTUAL_CURSOR_POSITION = 2;
+    private static final int VIRTUAL_ENTRY_POINT_AFTER = 3;
 
     private static boolean sEnabled;
     // Used to store the JSON message and populate the event later in the code path.
-    private static JSONObject sEventMessage;
+    private static JSONObject sHoverEnter;
     private static AccessibilityNodeInfo sVirtualCursorNode;
     private static int sCurrentNode;
 
     // This is the number Brailleback uses to start indexing routing keys.
     private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
     private static SelfBrailleClient sSelfBrailleClient;
 
     private static final HashSet<String> sServiceWhitelist =
@@ -153,31 +151,38 @@ public class GeckoAccessibility {
     public static boolean isEnabled() {
         return sEnabled;
     }
 
     public static void sendAccessibilityEvent (final JSONObject message) {
         if (!sEnabled)
             return;
 
+        final int eventType = message.optInt("eventType", -1);
+        if (eventType < 0) {
+            Log.e(LOGTAG, "No accessibility event type provided");
+            return;
+        }
+
+        sendAccessibilityEvent(message, eventType);
+    }
+
+    public static void sendAccessibilityEvent (final JSONObject message, final int eventType) {
+        if (!sEnabled)
+            return;
+
         final String exitView = message.optString("exitView");
         if (exitView.equals("moveNext")) {
             sCurrentNode = VIRTUAL_ENTRY_POINT_AFTER;
         } else if (exitView.equals("movePrevious")) {
             sCurrentNode = VIRTUAL_ENTRY_POINT_BEFORE;
         } else {
             sCurrentNode = VIRTUAL_CURSOR_POSITION;
         }
 
-        final int eventType = message.optInt("eventType", -1);
-        if (eventType < 0) {
-            Log.e(LOGTAG, "No accessibility event type provided");
-            return;
-        }
-
         if (Versions.preJB) {
             // Before Jelly Bean we send events directly from here while spoofing the source by setting
             // the package and class name manually.
             ThreadUtils.postToBackgroundThread(new Runnable() {
                     @Override
                     public void run() {
                         sendDirectAccessibilityEvent(eventType, message);
                 }
@@ -221,39 +226,34 @@ public class GeckoAccessibility {
             }
 
             final JSONObject braille = message.optJSONObject("brailleOutput");
             if (braille != null) {
                 sendBrailleText(view, braille.optString("text"),
                                 braille.optInt("selectionStart"), braille.optInt("selectionEnd"));
             }
 
+            if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
+                sHoverEnter = message;
+            }
+
             ThreadUtils.postToUiThread(new Runnable() {
                     @Override
                     public void run() {
-                        // If this is an accessibility focus, a lot of internal voodoo happens so we perform an
-                        // accessibility focus action on the view, and it in turn sends the right events.
-                        switch (eventType) {
-                        case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
-                            sEventMessage = message;
-                            view.performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
-                            break;
-                        case AccessibilityEvent.TYPE_ANNOUNCEMENT:
-                        case AccessibilityEvent.TYPE_VIEW_SCROLLED:
-                            sEventMessage = null;
-                            final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
-                            view.onInitializeAccessibilityEvent(accEvent);
-                            populateEventFromJSON(accEvent, message);
-                            view.getParent().requestSendAccessibilityEvent(view, accEvent);
-                            break;
-                        default:
-                            sEventMessage = message;
-                            view.sendAccessibilityEvent(eventType);
-                            break;
+                        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+                        event.setPackageName(GeckoAppShell.getContext().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);
+                        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);
@@ -292,52 +292,35 @@ public class GeckoAccessibility {
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:Focus",
                                                                            gainFocus ? "true" : "false"));
     }
 
     public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
         AccessibilityNodeProvider mAccessibilityNodeProvider;
 
         @Override
-        public void onPopulateAccessibilityEvent (View host, AccessibilityEvent event) {
-            super.onPopulateAccessibilityEvent(host, event);
-            if (sEventMessage != null) {
-                populateEventFromJSON(event, sEventMessage);
-                event.setSource(host, sCurrentNode);
-            }
-            // We save the hover enter event so that we could reuse it for a subsequent accessibility focus event.
-            if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
-                sEventMessage = null;
-        }
-
-        @Override
         public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
             if (mAccessibilityNodeProvider == null)
-                // The accessibility node structure for web content consists of 5 LayerView child nodes:
+                // The accessibility node structure for web content consists of 3 LayerView child nodes:
                 // 1. VIRTUAL_ENTRY_POINT_BEFORE: Represents the entry point before the LayerView.
-                // 2. VIRTUAL_CURSOR_PREVIOUS: Represents the virtual cursor position that is previous to the
-                // current one.
-                // 3. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
-                // 4. VIRTUAL_CURSOR_NEXT: Represents the next virtual cursor position.
-                // 5. VIRTUAL_ENTRY_POINT_AFTER: Represents the entry point after the LayerView.
+                // 2. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
+                // 3. VIRTUAL_ENTRY_POINT_AFTER: Represents the entry point after the LayerView.
                 mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
                         @Override
                         public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
                             AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CURSOR_POSITION && sVirtualCursorNode != null) ?
                                 AccessibilityNodeInfo.obtain(sVirtualCursorNode) :
                                 AccessibilityNodeInfo.obtain(host, virtualDescendantId);
 
                             switch (virtualDescendantId) {
                             case View.NO_ID:
                                 // This is the parent LayerView node, populate it with children.
                                 onInitializeAccessibilityNodeInfo(host, info);
                                 info.addChild(host, VIRTUAL_ENTRY_POINT_BEFORE);
-                                info.addChild(host, VIRTUAL_CURSOR_PREVIOUS);
                                 info.addChild(host, VIRTUAL_CURSOR_POSITION);
-                                info.addChild(host, VIRTUAL_CURSOR_NEXT);
                                 info.addChild(host, VIRTUAL_ENTRY_POINT_AFTER);
                                 break;
                             default:
                                 info.setParent(host);
                                 info.setSource(host, virtualDescendantId);
                                 info.setVisibleToUser(host.isShown());
                                 info.setPackageName(GeckoAppShell.getContext().getPackageName());
                                 info.setClassName(host.getClass().getName());
@@ -345,70 +328,76 @@ public class GeckoAccessibility {
                                 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
                                 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
                                 info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
                                 info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
                                 info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
                                 info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
                                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
                                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+                                info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
+                                info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
                                 info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
                                                               AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
                                                               AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
                                 break;
                             }
                             return info;
                         }
 
                         @Override
                         public boolean performAction (int virtualViewId, int action, Bundle arguments) {
                             if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
                                 // The accessibility focus is permanently on the middle node, VIRTUAL_CURSOR_POSITION.
-                                // When accessibility focus is requested on one of its siblings we move the virtual cursor
-                                // either forward or backward depending on which sibling was selected.
                                 // When we enter the view forward or backward we just ask Gecko to get focus, keeping the current position.
-
-                                switch (virtualViewId) {
-                                case VIRTUAL_CURSOR_PREVIOUS:
+                                if (virtualViewId == VIRTUAL_CURSOR_POSITION && sHoverEnter != null) {
+                                    GeckoAccessibility.sendAccessibilityEvent(sHoverEnter, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+                                } else {
                                     GeckoAppShell.
-                                        sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:PreviousObject", null));
-                                    return true;
-                                case VIRTUAL_CURSOR_NEXT:
-                                    GeckoAppShell.
-                                        sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:NextObject", null));
-                                    return true;
-                                case VIRTUAL_ENTRY_POINT_BEFORE:
-                                case VIRTUAL_ENTRY_POINT_AFTER:
-                                    GeckoAppShell.
-                                        sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:Focus", "true"));
-                                default:
-                                    break;
+                                      sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:Focus", "true"));
                                 }
+                                return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 GeckoAppShell.
                                     sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ActivateObject", null));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 GeckoAppShell.
                                     sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:LongPress", null));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 GeckoAppShell.
                                     sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ScrollForward", null));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 GeckoAppShell.
                                     sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ScrollBackward", null));
                                 return true;
+                            } else if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+                                String traversalRule = "";
+                                if (arguments != null) {
+                                    traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+                                }
+                                GeckoAppShell.
+                                    sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:NextObject", traversalRule));
+                                return true;
+                            } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+                                String traversalRule = "";
+                                if (arguments != null) {
+                                    traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+                                }
+                                GeckoAppShell.
+                                    sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:PreviousObject", traversalRule));
+                                return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY &&
                                        virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 // XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
                                 // the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit
                                 int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
-                                if (granularity < 0) {
+                                if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
                                     int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
                                     JSONObject activationData = new JSONObject();
                                     try {
                                         activationData.put("keyIndex", keyIndex);
                                     } catch (JSONException e) {
                                         return true;
                                     }
                                     GeckoAppShell.