Bug 1182208 - Add support for android scrolling and range accessibility actions. r=mfinkle r=yzen
authorEitan Isaacson <eitan@monotonous.org>
Fri, 21 Aug 2015 11:32:57 -0700
changeset 287168 581329d6602b9dc55cb16f0c089557e981214ed5
parent 287167 95c5738f4cec950cd584b79432257a75ecad307d
child 287169 4c14cdd926f9969fa875dd2d9d9d69274615fc64
push id4660
push usermartin.thomson@gmail.com
push dateFri, 21 Aug 2015 22:37:38 +0000
reviewersmfinkle, yzen
bugs1182208
milestone43.0a1
Bug 1182208 - Add support for android scrolling and range accessibility actions. r=mfinkle r=yzen
accessible/jsat/AccessFu.jsm
accessible/jsat/ContentControl.jsm
accessible/tests/mochitest/jsat/jsatcommon.js
accessible/tests/mochitest/jsat/test_content_integration.html
mobile/android/base/GeckoAccessibility.java
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -129,16 +129,18 @@ this.AccessFu = { // jshint ignore:line
 
     Services.obs.addObserver(this, 'remote-browser-shown', false);
     Services.obs.addObserver(this, 'inprocess-browser-shown', false);
     Services.obs.addObserver(this, 'Accessibility:NextObject', false);
     Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
     Services.obs.addObserver(this, 'Accessibility:Focus', false);
     Services.obs.addObserver(this, 'Accessibility:ActivateObject', false);
     Services.obs.addObserver(this, 'Accessibility:LongPress', false);
+    Services.obs.addObserver(this, 'Accessibility:ScrollForward', false);
+    Services.obs.addObserver(this, 'Accessibility:ScrollBackward', false);
     Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false);
     Utils.win.addEventListener('TabOpen', this);
     Utils.win.addEventListener('TabClose', this);
     Utils.win.addEventListener('TabSelect', this);
 
     if (this.readyCallback) {
       this.readyCallback();
       delete this.readyCallback;
@@ -182,16 +184,18 @@ this.AccessFu = { // jshint ignore:line
 
     Services.obs.removeObserver(this, 'remote-browser-shown');
     Services.obs.removeObserver(this, 'inprocess-browser-shown');
     Services.obs.removeObserver(this, 'Accessibility:NextObject');
     Services.obs.removeObserver(this, 'Accessibility:PreviousObject');
     Services.obs.removeObserver(this, 'Accessibility:Focus');
     Services.obs.removeObserver(this, 'Accessibility:ActivateObject');
     Services.obs.removeObserver(this, 'Accessibility:LongPress');
+    Services.obs.removeObserver(this, 'Accessibility:ScrollForward');
+    Services.obs.removeObserver(this, 'Accessibility:ScrollBackward');
     Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity');
 
     delete this._quicknavModesPref;
     delete this._notifyOutputPref;
 
     if (this.doneCallback) {
       this.doneCallback();
       delete this.doneCallback;
@@ -310,16 +314,22 @@ this.AccessFu = { // jshint ignore:line
         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');
+        break;
+      case 'Accessibility:ScrollBackward':
+        this.Input.androidScroll('backward');
+        break;
       case 'Accessibility:Focus':
         this._focused = JSON.parse(aData);
         if (this._focused) {
           this.autoMove({ forcePresent: true, noOpIfOnScreen: true });
         }
         break;
       case 'Accessibility:MoveByGranularity':
         this.Input.moveByGranularity(JSON.parse(aData));
@@ -832,16 +842,22 @@ var Input = {
   moveCursor: function moveCursor(aAction, aRule, aInputType, aAdjustRange) {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
     mm.sendAsyncMessage('AccessFu:MoveCursor',
                         { action: aAction, rule: aRule,
                           origin: 'top', inputType: aInputType,
                           adjustRange: aAdjustRange });
   },
 
+  androidScroll: function androidScroll(aDirection) {
+    let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+    mm.sendAsyncMessage('AccessFu:AndroidScroll',
+                        { direction: aDirection, origin: 'top' });
+  },
+
   moveByGranularity: function moveByGranularity(aDetails) {
     const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
 
     if (!this.editState.editing) {
       if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) {
         this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture');
         return;
       }
--- a/accessible/jsat/ContentControl.jsm
+++ b/accessible/jsat/ContentControl.jsm
@@ -33,17 +33,18 @@ this.ContentControl = function ContentCo
 
 this.ContentControl.prototype = {
   messagesOfInterest: ['AccessFu:MoveCursor',
                        'AccessFu:ClearCursor',
                        'AccessFu:MoveToPoint',
                        'AccessFu:AutoMove',
                        'AccessFu:Activate',
                        'AccessFu:MoveCaret',
-                       'AccessFu:MoveByGranularity'],
+                       'AccessFu:MoveByGranularity',
+                       'AccessFu:AndroidScroll'],
 
   start: function cc_start() {
     let cs = this._contentScope.get();
     for (let message of this.messagesOfInterest) {
       cs.addMessageListener(message, this);
     }
     cs.addEventListener('mousemove', this);
   },
@@ -86,16 +87,37 @@ this.ContentControl.prototype = {
         Logger.warning('ContentControl: Unhandled message:', aMessage.name);
       }
     } catch (x) {
       Logger.logException(
         x, 'Error handling message: ' + JSON.stringify(aMessage.json));
     }
   },
 
+  handleAndroidScroll: function cc_handleAndroidScroll(aMessage) {
+    let vc = this.vc;
+    let position = vc.position;
+
+    if (aMessage.json.origin != 'child' && this.sendToChild(vc, aMessage)) {
+      // Forwarded succesfully to child cursor.
+      return;
+    }
+
+    // Counter-intuitive, but scrolling backward (ie. up), actually should
+    // increase range values.
+    if (this.adjustRange(position, aMessage.json.direction === 'backward')) {
+      return;
+    }
+
+    this._contentScope.get().sendAsyncMessage('AccessFu:DoScroll',
+      { bounds: Utils.getBounds(position, true),
+        page: aMessage.json.direction === 'forward' ? 1 : -1,
+        horizontal: false });
+  },
+
   handleMoveCursor: function cc_handleMoveCursor(aMessage) {
     let origin = aMessage.json.origin;
     let action = aMessage.json.action;
     let adjustRange = aMessage.json.adjustRange;
     let vc = this.vc;
 
     if (origin != 'child' && this.sendToChild(vc, aMessage)) {
       // Forwarded succesfully to child cursor.
--- a/accessible/tests/mochitest/jsat/jsatcommon.js
+++ b/accessible/tests/mochitest/jsat/jsatcommon.js
@@ -386,16 +386,30 @@ var ContentMessages = {
         action: 'moveNext',
         inputType: 'gesture',
         rule: (aRule || 'Simple'),
         adjustRange: true
       }
     }
   },
 
+  androidScrollForward: function adjustUp() {
+    return {
+      name: 'AccessFu:AndroidScroll',
+      json: { origin: 'top', direction: 'forward' }
+    };
+  },
+
+  androidScrollBackward: function adjustDown() {
+    return {
+      name: 'AccessFu:AndroidScroll',
+      json: { origin: 'top', direction: 'backward' }
+    };
+  },
+
   focusSelector: function focusSelector(aSelector, aBlur) {
     return {
       name: 'AccessFuTest:Focus',
       json: {
         selector: aSelector,
         blur: aBlur
       }
     };
--- a/accessible/tests/mochitest/jsat/test_content_integration.html
+++ b/accessible/tests/mochitest/jsat/test_content_integration.html
@@ -76,16 +76,18 @@
            new ExpectedSwitchAction(false)],
           [ContentMessages.simpleMovePrevious,
            new ExpectedCursorChange(['apple', {'string': 'pushbutton'}])],
           [ContentMessages.simpleMovePrevious,
            new ExpectedCursorChange(['Home', {'string': 'pushbutton'}])],
           [ContentMessages.simpleMovePrevious,
            new ExpectedCursorChange(['much range', '6', {'string': 'slider'}, 'such app'])],
           [ContentMessages.moveOrAdjustDown(), new ExpectedValueChange('5')],
+          [ContentMessages.androidScrollForward(), new ExpectedValueChange('6')],
+          [ContentMessages.androidScrollBackward(), new ExpectedValueChange('5')],
           [ContentMessages.simpleMovePrevious,
            new ExpectedCursorChange(['much range', {'string': 'label'}])],
           [ContentMessages.simpleMovePrevious,
            new ExpectedCursorChange(['many option', {'string': 'stateChecked'},
             {'string': 'checkbutton'}, {'string': 'listStart'},
             {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}])],
           // uncheck checkbox
           [ContentMessages.activateCurrent(),
--- a/mobile/android/base/GeckoAccessibility.java
+++ b/mobile/android/base/GeckoAccessibility.java
@@ -343,16 +343,18 @@ public class GeckoAccessibility {
                                 info.setClassName(host.getClass().getName());
                                 info.setEnabled(true);
                                 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.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
                                                               AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
                                                               AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
                                 break;
                             }
                             return info;
                         }
 
@@ -383,16 +385,24 @@ public class GeckoAccessibility {
                             } 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_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) {
                                     int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
                                     JSONObject activationData = new JSONObject();