Bug 739498 - Added Javascript modules for mobile accessibility. r=surkov
authorEitan Isaacson <eitan@monotonous.org>
Fri, 13 Apr 2012 16:18:57 -0700
changeset 93461 071ec7e1766004cf7faaf4f457f5edaa2c50a292
parent 93460 362fd0d7c8c36402d46f31d8cd4cb45eb2ada9b0
child 93462 1b3d61ce3604df8cbc8b3656cfea8d2d701ba53b
push idunknown
push userunknown
push dateunknown
reviewerssurkov
bugs739498
milestone14.0a1
Bug 739498 - Added Javascript modules for mobile accessibility. r=surkov
accessible/src/Makefile.in
accessible/src/base/Role.h
accessible/src/jsat/AccessFu.css
accessible/src/jsat/AccessFu.jsm
accessible/src/jsat/Makefile.in
accessible/src/jsat/Presenters.jsm
accessible/src/jsat/UtteranceGenerator.jsm
accessible/src/jsat/VirtualCursorController.jsm
accessible/src/jsat/jar.mn
dom/locales/en-US/chrome/accessibility/AccessFu.properties
dom/locales/jar.mn
--- a/accessible/src/Makefile.in
+++ b/accessible/src/Makefile.in
@@ -58,16 +58,17 @@ endif
 endif
 
 DIRS += $(PLATFORM_DIR)
 
 DIRS += \
   base \
   generic \
   html \
+  jsat \
   xpcom \
   xforms \
   $(null)
 
 ifdef MOZ_XUL
 DIRS +=   xul
 endif
 
--- a/accessible/src/base/Role.h
+++ b/accessible/src/base/Role.h
@@ -35,16 +35,20 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef _role_h_
 #define _role_h_
 
+/**
+ * Note: Make sure to update the localized role names when changing the list.
+ */
+
 namespace mozilla {
 namespace a11y {
 namespace roles {
   enum Role {
   /**
    * Used when accessible hans't strong defined role.
    */
   NOTHING = 0,
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/AccessFu.css
@@ -0,0 +1,22 @@
+/* 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/. */
+
+#virtual-cursor-box { 
+  position: fixed;
+  border: 1px solid orange;
+  pointer-events: none;
+  display: none;
+  border-radius: 2px;
+  box-shadow: 1px 1px 1px #444;
+}
+
+#virtual-cursor-inset { 
+  border-radius: 1px;
+  box-shadow: inset 1px 1px 1px #444;
+  display: block;
+  box-sizing: border-box;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -0,0 +1,258 @@
+/* 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';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+var EXPORTED_SYMBOLS = ['AccessFu'];
+
+Cu.import('resource://gre/modules/Services.jsm');
+
+Cu.import('resource://gre/modules/accessibility/Presenters.jsm');
+Cu.import('resource://gre/modules/accessibility/VirtualCursorController.jsm');
+
+var AccessFu = {
+  /**
+   * Attach chrome-layer accessibility functionality to the given chrome window.
+   * If accessibility is enabled on the platform (currently Android-only), then
+   * a special accessibility mode is started (see startup()).
+   * @param {ChromeWindow} aWindow Chrome window to attach to.
+   * @param {boolean} aForceEnabled Skip platform accessibility check and enable
+   *  AccessFu.
+   */
+  attach: function attach(aWindow) {
+    dump('AccessFu attach!! ' + Services.appinfo.OS + '\n');
+    this.chromeWin = aWindow;
+    this.presenters = [];
+
+    function checkA11y() {
+      if (Services.appinfo.OS == 'Android') {
+        let msg = Cc['@mozilla.org/android/bridge;1'].
+          getService(Ci.nsIAndroidBridge).handleGeckoMessage(
+            JSON.stringify(
+                { gecko: {
+                    type: 'Accessibility:IsEnabled',
+                    eventType: 1,
+                    text: []
+                  }
+                }));
+        return JSON.parse(msg).enabled;
+      }
+      return false;
+    }
+
+    if (checkA11y())
+      this.enable();
+  },
+
+  /**
+   * Start the special AccessFu mode, this primarily means controlling the virtual
+   * cursor with arrow keys. Currently, on platforms other than Android this needs
+   * to be called explicitly.
+   */
+  enable: function enable() {
+    dump('AccessFu enable');
+    this.addPresenter(new VisualPresenter());
+
+    // Implicitly add the Android presenter on Android.
+    if (Services.appinfo.OS == 'Android')
+      this.addPresenter(new AndroidPresenter());
+
+    VirtualCursorController.attach(this.chromeWin);
+
+    Services.obs.addObserver(this, 'accessible-event', false);
+    this.chromeWin.addEventListener('DOMActivate', this, true);
+    this.chromeWin.addEventListener('resize', this, true);
+    this.chromeWin.addEventListener('scroll', this, true);
+    this.chromeWin.addEventListener('TabOpen', this, true);
+    this.chromeWin.addEventListener('TabSelect', this, true);
+    this.chromeWin.addEventListener('TabClosed', this, true);
+  },
+
+  /**
+   * Disable AccessFu and return to default interaction mode.
+   */
+  disable: function disable() {
+    dump('AccessFu disable');
+
+    this.presenters.forEach(function(p) {p.detach();});
+    this.presenters = [];
+
+    VirtualCursorController.detach();
+
+    Services.obs.addObserver(this, 'accessible-event', false);
+    this.chromeWin.removeEventListener('DOMActivate', this);
+    this.chromeWin.removeEventListener('resize', this);
+    this.chromeWin.removeEventListener('scroll', this);
+    this.chromeWin.removeEventListener('TabOpen', this);
+    this.chromeWin.removeEventListener('TabSelect', this);
+    this.chromeWin.removeEventListener('TabClose', this);
+  },
+
+  addPresenter: function addPresenter(presenter) {
+    this.presenters.push(presenter);
+    presenter.attach(this.chromeWin);
+  },
+
+  handleEvent: function handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case 'TabSelect':
+        {
+          this.getDocAccessible(
+              function(docAcc) {
+                this.presenters.forEach(function(p) {p.tabSelected(docAcc);});
+              });
+          break;
+        }
+      case 'DOMActivate':
+      {
+        let activatedAcc = getAccessible(aEvent.originalTarget);
+        let state = {};
+        activatedAcc.getState(state, {});
+
+        // Checkable objects will have a state changed event that we will use
+        // instead of this hackish DOMActivate. We will also know the true
+        // action that was taken.
+        if (state.value & Ci.nsIAccessibleStates.STATE_CHECKABLE)
+          return;
+
+        this.presenters.forEach(function(p) {
+                                  p.actionInvoked(activatedAcc, 'click');
+                                });
+        break;
+      }
+      case 'scroll':
+      case 'resize':
+      {
+        this.presenters.forEach(function(p) {p.viewportChanged();});
+        break;
+      }
+    }
+  },
+
+  getDocAccessible: function getDocAccessible(aCallback) {
+    let browserApp = (Services.appinfo.OS == 'Android') ?
+      this.chromeWin.BrowserApp : this.chromeWin.gBrowser;
+
+    let docAcc = getAccessible(browserApp.selectedBrowser.contentDocument);
+    if (!docAcc) {
+      // Wait for a reorder event fired by the parent of the new doc.
+      this._pendingDocuments[browserApp.selectedBrowser] = aCallback;
+    } else {
+      aCallback.apply(this, [docAcc]);
+    }
+  },
+
+  observe: function observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case 'accessible-event':
+        let event;
+        try {
+          event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
+          this.handleAccEvent(event);
+        } catch (ex) {
+          dump(ex);
+          return;
+        }
+    }
+  },
+
+  handleAccEvent: function handleAccEvent(aEvent) {
+    switch (aEvent.eventType) {
+      case Ci.nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED:
+        {
+          let pivot = aEvent.accessible.
+            QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor;
+          let event = aEvent.
+            QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
+
+          let newContext = this.getNewContext(event.oldAccessible,
+                                              pivot.position);
+          this.presenters.forEach(
+            function(p) {
+              p.pivotChanged(pivot.position, newContext);
+            });
+          break;
+        }
+      case Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE:
+        {
+          let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
+          if (event.state == Ci.nsIAccessibleStates.STATE_CHECKED &&
+              !(event.isExtraState())) {
+            this.presenters.forEach(
+              function(p) {
+                p.actionInvoked(aEvent.accessible,
+                                event.isEnabled() ? 'check' : 'uncheck');
+              }
+            );
+          }
+          break;
+        }
+      case Ci.nsIAccessibleEvent.EVENT_REORDER:
+        {
+          let node = aEvent.accessible.DOMNode;
+          let callback = this._pendingDocuments[node];
+          if (callback && aEvent.accessible.childCount) {
+            // We have a callback associated with a document.
+            callback.apply(this, [aEvent.accessible.getChildAt(0)]);
+            delete this._pendingDocuments[node];
+          }
+          break;
+        }
+      default:
+        break;
+    }
+  },
+
+  getNewContext: function getNewContext(aOldObject, aNewObject) {
+    let newLineage = [];
+    let oldLineage = [];
+
+    let parent = aNewObject;
+    while ((parent = parent.parent))
+      newLineage.push(parent);
+
+    if (aOldObject) {
+      parent = aOldObject;
+      while ((parent = parent.parent))
+        oldLineage.push(parent);
+    }
+
+//    newLineage.reverse();
+//    oldLineage.reverse();
+
+    let i = 0;
+    let newContext = [];
+
+    while (true) {
+      let newAncestor = newLineage.pop();
+      let oldAncestor = oldLineage.pop();
+
+      if (newAncestor == undefined)
+        break;
+
+      if (newAncestor != oldAncestor)
+        newContext.push(newAncestor);
+      i++;
+    }
+
+    return newContext;
+  },
+
+  // A hash of documents that don't yet have an accessible tree.
+  _pendingDocuments: {}
+};
+
+function getAccessible(aNode) {
+  try {
+    return Cc['@mozilla.org/accessibleRetrieval;1'].
+      getService(Ci.nsIAccessibleRetrieval).getAccessibleFor(aNode);
+  } catch (e) {
+    return null;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/Makefile.in
@@ -0,0 +1,15 @@
+# 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/.
+
+DEPTH     = ../../..
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/accessibility
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/Presenters.jsm
@@ -0,0 +1,273 @@
+/* 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';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import('resource://gre/modules/accessibility/UtteranceGenerator.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+var EXPORTED_SYMBOLS = ['VisualPresenter',
+                        'AndroidPresenter',
+                        'DummyAndroidPresenter'];
+
+/**
+ * The interface for all presenter classes. A presenter could be, for example,
+ * a speech output module, or a visual cursor indicator.
+ */
+function Presenter() {}
+
+Presenter.prototype = {
+  /**
+   * The padding in pixels between the object and the highlight border.
+   */
+  BORDER_PADDING: 2,
+
+  /**
+   * Attach function for presenter.
+   * @param {ChromeWindow} aWindow Chrome window the presenter could use.
+   */
+  attach: function attach(aWindow) {},
+
+  /**
+   * Detach function.
+   */
+  detach: function detach() {},
+
+  /**
+   * The virtual cursor's position changed.
+   * @param {nsIAccessible} aObject the new position.
+   * @param {nsIAccessible[]} aNewContext the ancestry of the new position that
+   *    is different from the old virtual cursor position.
+   */
+  pivotChanged: function pivotChanged(aObject, aNewContext) {},
+
+  /**
+   * An object's action has been invoked.
+   * @param {nsIAccessible} aObject the object that has been invoked.
+   * @param {string} aActionName the name of the action.
+   */
+  actionInvoked: function actionInvoked(aObject, aActionName) {},
+
+  /**
+   * Text has changed, either by the user or by the system. TODO.
+   */
+  textChanged: function textChanged() {},
+
+  /**
+   * Text selection has changed. TODO.
+   */
+  textSelectionChanged: function textSelectionChanged() {},
+
+  /**
+   * Selection has changed. TODO.
+   * @param {nsIAccessible} aObject the object that has been selected.
+   */
+  selectionChanged: function selectionChanged(aObject) {},
+
+  /**
+   * The page state has changed, loading, stopped loading, etc. TODO.
+   */
+  pageStateChanged: function pageStateChanged() {},
+
+  /**
+   * The tab has changed.
+   * @param {nsIAccessible} aObject the document contained in the tab.
+   */
+  tabSelected: function tabSelected(aObject) {},
+
+  /**
+   * The viewport has changed, either a scroll, pan, zoom, or
+   *    landscape/portrait toggle.
+   */
+  viewportChanged: function viewportChanged() {}
+};
+
+/**
+ * Visual presenter. Draws a box around the virtual cursor's position.
+ */
+
+function VisualPresenter() {}
+
+VisualPresenter.prototype = new Presenter();
+
+VisualPresenter.prototype.attach = function(aWindow) {
+  this.chromeWin = aWindow;
+
+  // Add stylesheet
+  let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
+  this.stylesheet = aWindow.document.createProcessingInstruction(
+    'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
+  aWindow.document.insertBefore(this.stylesheet, aWindow.document.firstChild);
+
+  // Add highlight box
+  this.highlightBox = this.chromeWin.document.
+    createElementNS('http://www.w3.org/1999/xhtml', 'div');
+  this.chromeWin.document.documentElement.appendChild(this.highlightBox);
+  this.highlightBox.id = 'virtual-cursor-box';
+
+  // Add highlight inset for inner shadow
+  let inset = this.chromeWin.document.
+    createElementNS('http://www.w3.org/1999/xhtml', 'div');
+  inset.id = 'virtual-cursor-inset';
+
+  this.highlightBox.appendChild(inset);
+};
+
+VisualPresenter.prototype.detach = function() {
+  this.chromeWin.document.removeChild(this.stylesheet);
+  this.highlightBox.parentNode.removeChild(this.highlightBox);
+  this.highlightBox = this.stylesheet = null;
+};
+
+VisualPresenter.prototype.viewportChanged = function() {
+  if (this._currentObject)
+    this.highlight(this._currentObject);
+};
+
+VisualPresenter.prototype.pivotChanged = function(aObject, aNewContext) {
+  this._currentObject = aObject;
+
+  if (!aObject) {
+    this.hide();
+    return;
+  }
+
+  try {
+    aObject.scrollTo(Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
+    this.highlight(aObject);
+  } catch (e) {
+    dump('Error getting bounds: ' + e);
+    return;
+  }
+};
+
+VisualPresenter.prototype.tabSelected = function(aObject) {
+  let vcDoc = aObject.QueryInterface(Ci.nsIAccessibleCursorable);
+  this.pivotChanged(vcDoc.virtualCursor.position);
+};
+
+// Internals
+
+VisualPresenter.prototype.hide = function hide() {
+  this.highlightBox.style.display = 'none';
+};
+
+VisualPresenter.prototype.highlight = function(aObject) {
+  let vp = (Services.appinfo.OS == 'Android') ?
+    this.chromeWin.BrowserApp.selectedTab.getViewport() :
+    { zoom: 1.0, offsetY: 0 };
+
+  let bounds = this.getBounds(aObject, vp.zoom);
+
+  // First hide it to avoid flickering when changing the style.
+  this.highlightBox.style.display = 'none';
+  this.highlightBox.style.top = bounds.top + 'px';
+  this.highlightBox.style.left = bounds.left + 'px';
+  this.highlightBox.style.width = bounds.width + 'px';
+  this.highlightBox.style.height = bounds.height + 'px';
+  this.highlightBox.style.display = 'block';
+};
+
+VisualPresenter.prototype.getBounds = function(aObject, aZoom, aStart, aEnd) {
+  let objX = {}, objY = {}, objW = {}, objH = {};
+
+  if (aEnd >= 0 && aStart >= 0 && aEnd != aStart) {
+    // TODO: Get bounds for text ranges. Leaving this blank until we have
+    // proper text navigation in the virtual cursor.
+  }
+
+  aObject.getBounds(objX, objY, objW, objH);
+
+  // Can't specify relative coords in nsIAccessible.getBounds, so we do it.
+  let docX = {}, docY = {};
+  let docRoot = aObject.rootDocument.QueryInterface(Ci.nsIAccessible);
+  docRoot.getBounds(docX, docY, {}, {});
+
+  let rv = {
+    left: Math.round((objX.value - docX.value - this.BORDER_PADDING) * aZoom),
+    top: Math.round((objY.value - docY.value - this.BORDER_PADDING) * aZoom),
+    width: Math.round((objW.value + (this.BORDER_PADDING * 2)) * aZoom),
+    height: Math.round((objH.value + (this.BORDER_PADDING * 2)) * aZoom)
+  };
+
+  return rv;
+};
+
+/**
+ * Android presenter. Fires Android a11y events.
+ */
+
+const ANDROID_TYPE_VIEW_CLICKED = 0x01;
+const ANDROID_TYPE_VIEW_LONG_CLICKED = 0x02;
+const ANDROID_TYPE_VIEW_SELECTED = 0x04;
+const ANDROID_TYPE_VIEW_FOCUSED = 0x08;
+const ANDROID_TYPE_VIEW_TEXT_CHANGED = 0x10;
+const ANDROID_TYPE_WINDOW_STATE_CHANGED = 0x20;
+
+function AndroidPresenter() {}
+
+AndroidPresenter.prototype = new Presenter();
+
+AndroidPresenter.prototype.pivotChanged = function(aObject, aNewContext) {
+  let output = [];
+  for (let i in aNewContext)
+    output.push.apply(output,
+                      UtteranceGenerator.genForObject(aNewContext[i]));
+
+  output.push.apply(output,
+                    UtteranceGenerator.genForObject(aObject, true));
+
+  this.sendMessageToJava({
+    gecko: {
+      type: 'Accessibility:Event',
+      eventType: ANDROID_TYPE_VIEW_FOCUSED,
+      text: output
+    }
+  });
+};
+
+AndroidPresenter.prototype.actionInvoked = function(aObject, aActionName) {
+  this.sendMessageToJava({
+    gecko: {
+      type: 'Accessibility:Event',
+      eventType: ANDROID_TYPE_VIEW_CLICKED,
+      text: [UtteranceGenerator.genForAction(aObject, aActionName)]
+    }
+  });
+};
+
+AndroidPresenter.prototype.tabSelected = function(aObject) {
+  let vcDoc = aObject.QueryInterface(Ci.nsIAccessibleCursorable);
+  let context = [];
+
+  let parent = vcDoc.virtualCursor.position || aObject;
+  while ((parent = parent.parent))
+    context.push(parent);
+  context.reverse();
+
+  this.pivotChanged(vcDoc.virtualCursor.position, context);
+};
+
+AndroidPresenter.prototype.sendMessageToJava = function(aMessage) {
+  return Cc['@mozilla.org/android/bridge;1'].
+    getService(Ci.nsIAndroidBridge).
+    handleGeckoMessage(JSON.stringify(aMessage));
+};
+
+/**
+ * A dummy Android presenter for desktop testing
+ */
+
+function DummyAndroidPresenter() {}
+
+DummyAndroidPresenter.prototype = new AndroidPresenter();
+
+DummyAndroidPresenter.prototype.sendMessageToJava = function(aMessage) {
+  dump(JSON.stringify(aMessage, null, 2) + '\n');
+};
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/UtteranceGenerator.jsm
@@ -0,0 +1,162 @@
+/* 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';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+const INCLUDE_ROLE = 0x01;
+const INCLUDE_NAME = 0x02;
+const INCLUDE_CUSTOM = 0x04;
+
+var gStringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
+  getService(Ci.nsIStringBundleService).
+  createBundle('chrome://global/locale/AccessFu.properties');
+
+var gAccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1'].
+  getService(Ci.nsIAccessibleRetrieval);
+
+var EXPORTED_SYMBOLS = ['UtteranceGenerator'];
+
+var UtteranceGenerator = {
+  gActionMap: {
+    jump: 'jumpAction',
+    press: 'pressAction',
+    check: 'checkAction',
+    uncheck: 'uncheckAction',
+    select: 'selectAction',
+    open: 'openAction',
+    close: 'closeAction',
+    switch: 'switchAction',
+    click: 'clickAction',
+    collapse: 'collapseAction',
+    expand: 'expandAction',
+    activate: 'activateAction',
+    cycle: 'cycleAction'
+  },
+
+  genForObject: function(aAccessible, aForceName) {
+    let roleString = gAccRetrieval.getStringRole(aAccessible.role);
+
+    let func = this.objectUtteranceFunctions[roleString] ||
+      this.objectUtteranceFunctions.defaultFunc;
+
+    let flags = this.verbosityRoleMap[roleString] || 0;
+
+    if (aForceName)
+      flags |= INCLUDE_NAME;
+
+    return func(aAccessible, roleString, flags);
+  },
+
+  genForAction: function(aObject, aActionName) {
+    return gStringBundle.GetStringFromName(this.gActionMap[aActionName]);
+  },
+
+  verbosityRoleMap: {
+    'menubar': INCLUDE_ROLE,
+    'scrollbar': INCLUDE_ROLE,
+    'grip': INCLUDE_ROLE,
+    'alert': INCLUDE_ROLE,
+    'menupopup': INCLUDE_ROLE,
+    'menuitem': INCLUDE_ROLE,
+    'tooltip': INCLUDE_ROLE,
+    'application': INCLUDE_NAME,
+    'document': INCLUDE_NAME,
+    'toolbar': INCLUDE_ROLE,
+    'link': INCLUDE_ROLE,
+    'list': INCLUDE_ROLE,
+    'listitem': INCLUDE_ROLE,
+    'outline': INCLUDE_ROLE,
+    'outlineitem': INCLUDE_ROLE,
+    'pagetab': INCLUDE_ROLE,
+    'graphic': INCLUDE_ROLE | INCLUDE_NAME,
+    'statictext': INCLUDE_NAME,
+    'text leaf': INCLUDE_NAME,
+    'pushbutton': INCLUDE_ROLE,
+    'checkbutton': INCLUDE_ROLE | INCLUDE_NAME,
+    'radiobutton': INCLUDE_ROLE | INCLUDE_NAME,
+    'combobox': INCLUDE_ROLE,
+    'droplist': INCLUDE_ROLE,
+    'progressbar': INCLUDE_ROLE,
+    'slider': INCLUDE_ROLE,
+    'spinbutton': INCLUDE_ROLE,
+    'diagram': INCLUDE_ROLE,
+    'animation': INCLUDE_ROLE,
+    'equation': INCLUDE_ROLE,
+    'buttonmenu': INCLUDE_ROLE,
+    'pagetablist': INCLUDE_ROLE,
+    'canvas': INCLUDE_ROLE,
+    'check menu item': INCLUDE_ROLE,
+    'label': INCLUDE_ROLE,
+    'password text': INCLUDE_ROLE,
+    'popup menu': INCLUDE_ROLE,
+    'radio menu item': INCLUDE_ROLE,
+    'toggle button': INCLUDE_ROLE,
+    'header': INCLUDE_ROLE,
+    'footer': INCLUDE_ROLE,
+    'entry': INCLUDE_ROLE,
+    'caption': INCLUDE_ROLE,
+    'document frame': INCLUDE_ROLE,
+    'heading': INCLUDE_ROLE,
+    'calendar': INCLUDE_ROLE,
+    'combobox list': INCLUDE_ROLE,
+    'combobox option': INCLUDE_ROLE,
+    'image map': INCLUDE_ROLE,
+    'option': INCLUDE_ROLE,
+    'listbox': INCLUDE_ROLE},
+
+  objectUtteranceFunctions: {
+    defaultFunc: function defaultFunc(aAccessible, aRoleStr, aFlags) {
+      let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : '';
+      let desc = (aFlags & INCLUDE_ROLE) ?
+        gStringBundle.GetStringFromName(aRoleStr) : '';
+
+      if (!name && !desc)
+        return [];
+
+      let state = {};
+      let extState = {};
+      aAccessible.getState(state, extState);
+
+      if (state.value & Ci.nsIAccessibleStates.STATE_CHECKABLE) {
+        let stateStr = (state.value & Ci.nsIAccessibleStates.STATE_CHECKED) ?
+          'objChecked' : 'objNotChecked';
+        desc = gStringBundle.formatStringFromName(stateStr, [desc], 1);
+      }
+
+      if (extState.value & Ci.nsIAccessibleStates.EXT_STATE_EXPANDABLE) {
+        let stateStr = (state.value & Ci.nsIAccessibleStates.STATE_EXPANDED) ?
+          'objExpanded' : 'objCollapsed';
+        desc = gStringBundle.formatStringFromName(stateStr, [desc], 1);
+      }
+
+      return [desc, name];
+    },
+
+    heading: function(aAccessible, aRoleStr, aFlags) {
+      let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : '';
+      let level = {};
+      aAccessible.groupPosition(level, {}, {});
+      let desc = gStringBundle.formatStringFromName('headingLevel',
+                                                   [level.value], 1);
+      return [desc, name];
+    },
+
+    listitem: function(aAccessible, aRoleStr, aFlags) {
+      let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : '';
+      let localizedRole = gStringBundle.GetStringFromName(aRoleStr);
+      let itemno = {};
+      let itemof = {};
+      aAccessible.groupPosition({}, itemof, itemno);
+      let desc = gStringBundle.formatStringFromName(
+          'objItemOf', [localizedRole, itemno.value, itemof.value], 3);
+
+      return [desc, name];
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/VirtualCursorController.jsm
@@ -0,0 +1,143 @@
+/* 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';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+var EXPORTED_SYMBOLS = ['VirtualCursorController'];
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+var gAccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1'].
+  getService(Ci.nsIAccessibleRetrieval);
+
+var VirtualCursorController = {
+  attach: function attach(aWindow) {
+    this.chromeWin = aWindow;
+    this.chromeWin.document.addEventListener('keypress', this.onkeypress, true);
+  },
+
+  detach: function detach() {
+    this.chromeWin.document.removeEventListener('keypress', this.onkeypress);
+  },
+
+  getBrowserApp: function getBrowserApp() {
+    switch (Services.appinfo.OS) {
+      case 'Android':
+        return this.chromeWin.BrowserApp;
+      default:
+        return this.chromeWin.gBrowser;
+    }
+  },
+
+  onkeypress: function onkeypress(aEvent) {
+    let document = VirtualCursorController.getBrowserApp().
+      selectedBrowser.contentDocument;
+
+    dump('keypress ' + aEvent.keyCode + '\n');
+
+    switch (aEvent.keyCode) {
+      case aEvent.DOM_END:
+        VirtualCursorController.moveForward(document, true);
+        break;
+      case aEvent.DOM_HOME:
+        VirtualCursorController.moveBackward(document, true);
+        break;
+      case aEvent.DOM_VK_DOWN:
+        VirtualCursorController.moveForward(document, aEvent.shiftKey);
+        break;
+      case aEvent.DOM_VK_UP:
+        VirtualCursorController.moveBackward(document, aEvent.shiftKey);
+        break;
+      case aEvent.DOM_VK_RETURN:
+        //It is true that desktop does not map the kp enter key to ENTER.
+        //So for desktop we require a ctrl+return instead.
+        if (Services.appinfo.OS == 'Android' || !aEvent.ctrlKey)
+          return;
+      case aEvent.DOM_VK_ENTER:
+        VirtualCursorController.activateCurrent(document);
+        break;
+      default:
+        return;
+    }
+
+    aEvent.preventDefault();
+    aEvent.stopPropagation();
+  },
+
+  moveForward: function moveForward(document, last) {
+    let virtualCursor = this.getVirtualCursor(document);
+    if (last) {
+      virtualCursor.moveLast(this.SimpleTraversalRule);
+    } else {
+      virtualCursor.moveNext(this.SimpleTraversalRule);
+    }
+  },
+
+  moveBackward: function moveBackward(document, first) {
+    let virtualCursor = this.getVirtualCursor(document);
+
+    if (first) {
+      virtualCursor.moveFirst(this.SimpleTraversalRule);
+      return
+
+    }
+
+    if (!virtualCursor.movePrevious(this.SimpleTraversalRule) &&
+        Services.appinfo.OS == 'Android') {
+      // Return focus to browser chrome, which in Android is a native widget.
+      Cc['@mozilla.org/android/bridge;1'].
+        getService(Ci.nsIAndroidBridge).handleGeckoMessage(
+          JSON.stringify({ gecko: { type: 'ToggleChrome:Focus' } }));
+      virtualCursor.position = null;
+    }
+  },
+
+  activateCurrent: function activateCurrent(document) {
+    let virtualCursor = this.getVirtualCursor(document);
+    let acc = virtualCursor.position;
+
+    if (acc.numActions > 0)
+      acc.doAction(0);
+  },
+
+  getVirtualCursor: function getVirtualCursor(document) {
+    return gAccRetrieval.getAccessibleFor(document).
+      QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor;
+  },
+
+  SimpleTraversalRule: {
+    getMatchRoles: function(aRules) {
+      aRules.value = [];
+      return 0;
+    },
+
+    preFilter: Ci.nsIAccessibleTraversalRule.PREFILTER_DEFUNCT |
+      Ci.nsIAccessibleTraversalRule.PREFILTER_INVISIBLE,
+
+    match: function(aAccessible) {
+      let rv = Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
+      if (aAccessible.childCount == 0) {
+        // TODO: Find a better solution for ROLE_STATICTEXT.
+        // Right now it helps filter list bullets, but it is also used
+        // in CSS generated content.
+        let ignoreRoles = [Ci.nsIAccessibleRole.ROLE_WHITESPACE,
+                           Ci.nsIAccessibleRole.ROLE_STATICTEXT];
+        let state = {};
+        aAccessible.getState(state, {});
+        if ((state.value & Ci.nsIAccessibleStates.STATE_FOCUSABLE) ||
+          (aAccessible.name && ignoreRoles.indexOf(aAccessible.role) < 0))
+          rv = Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+        }
+      return rv;
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule])
+  }
+};
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/jar.mn
@@ -0,0 +1,2 @@
+toolkit.jar:
+    content/global/accessibility/AccessFu.css (AccessFu.css)
new file mode 100644
--- /dev/null
+++ b/dom/locales/en-US/chrome/accessibility/AccessFu.properties
@@ -0,0 +1,91 @@
+# 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/.
+
+menubar        =       menu bar
+scrollbar      =       scroll bar
+grip           =       grip
+alert          =       alert
+menupopup      =       menu popup
+document       =       document
+pane           =       pane
+dialog         =       dialog
+separator      =       separator
+toolbar        =       toolbar
+statusbar      =       status bar
+table          =       table
+columnheader   =       column header
+rowheader      =       row header
+column         =       column
+row            =       row
+cell           =       cell
+link           =       link
+list           =       list
+listitem       =       list item
+outline        =       outline
+outlineitem    =       outline item
+pagetab        =       page tab
+propertypage   =       property page
+graphic        =       graphic
+pushbutton     =       button
+checkbutton    =       check button
+radiobutton    =       radio button
+combobox       =       combo box
+progressbar    =       progress bar
+slider         =       slider
+spinbutton     =       spin button
+diagram        =       diagram
+animation      =       animation
+equation       =       equation
+buttonmenu     =       button menu
+whitespace     =       white space
+pagetablist    =       page tab list
+canvas         =       canvas
+checkmenuitem  =       check menu item
+label          =       label
+passwordtext   =       password text
+radiomenuitem  =       radio menu item
+textcontainer  =       text container
+togglebutton   =       toggle button
+treetable      =       tree table
+header         =       header
+footer         =       footer
+paragraph      =       paragraph
+entry          =       entry
+caption        =       caption
+heading        =       heading
+section        =       section
+form           =       form
+comboboxlist   =       combo box list
+comboboxoption =       combo box option
+imagemap       =       image map
+listboxoption  =       list box option
+listbox        =       list box
+flatequation   =       flat equation
+gridcell       =       gridcell
+note           =       note
+figure         =       figure
+
+# More sophisiticated object descriptions
+headingLevel   =       heading level %S
+# LOCALIZATION NOTE: %1$S is the item's role name (e.g. "List item" or "Page tab"), %2$S is the position of the item n the set. %3$S is the total number of such items in the set. An expanded example would read "List item 2 of 5".
+objItemOf      =       %1$S %2$S of %3$S
+objChecked     =       checked %S
+objNotChecked  =       not checked %S
+objExpanded    =       expanded %S
+objCollapsed   =       collapsed %S
+
+# Invoked actions
+jumpAction     =      jumped
+pressAction    =      pressed
+checkAction    =      checked
+uncheckAction  =      unchecked
+selectAction   =      selected
+openAction     =      opened
+closeAction    =      closed
+switchAction   =      switched
+clickAction    =      clicked
+collapseAction =      collapsed
+expandAction   =      expanded
+activateAction =      activated
+cycleAction    =      cycled
\ No newline at end of file
--- a/dom/locales/jar.mn
+++ b/dom/locales/jar.mn
@@ -21,8 +21,9 @@
   locale/@AB_CD@/global/layout/htmlparser.properties           (%chrome/layout/htmlparser.properties)
   locale/@AB_CD@/global/layout/xmlparser.properties            (%chrome/layout/xmlparser.properties)
   locale/@AB_CD@/global/layout/HtmlForm.properties             (%chrome/layout/HtmlForm.properties)
   locale/@AB_CD@/global/security/caps.properties               (%chrome/security/caps.properties)
   locale/@AB_CD@/global/xml/prettyprint.dtd                    (%chrome/xml/prettyprint.dtd)
   locale/@AB_CD@/global-platform/win/accessible.properties     (%chrome/accessibility/win/accessible.properties)
   locale/@AB_CD@/global-platform/mac/accessible.properties     (%chrome/accessibility/mac/accessible.properties)
   locale/@AB_CD@/global-platform/unix/accessible.properties    (%chrome/accessibility/unix/accessible.properties)
+  locale/@AB_CD@/global/AccessFu.properties                    (%chrome/accessibility/AccessFu.properties)