Bug 791400 - AccessFu refactor for separating chrome and content. r=davidb
authorEitan Isaacson <eitan@monotonous.org>
Mon, 01 Oct 2012 13:33:26 -0700
changeset 115078 ec776b922b2364734bce287cace8db8fa0c5cc2b
parent 115077 e3bec3b18597af256d2ca3f938897c08b6d4045e
child 115079 4ae30378c203315bcf647c37f378c207b18fa5c0
push id1708
push userakeybl@mozilla.com
push dateMon, 19 Nov 2012 21:10:21 +0000
treeherdermozilla-beta@27b14fe50103 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdavidb
bugs791400
milestone18.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 791400 - AccessFu refactor for separating chrome and content. r=davidb
accessible/src/jsat/AccessFu.jsm
accessible/src/jsat/EventManager.jsm
accessible/src/jsat/Presenters.jsm
accessible/src/jsat/TraversalRules.jsm
accessible/src/jsat/Utils.jsm
accessible/src/jsat/VirtualCursorController.jsm
accessible/src/jsat/content-script.js
accessible/src/jsat/jar.mn
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -7,486 +7,477 @@
 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/Geometry.jsm');
 
 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
-Cu.import('resource://gre/modules/accessibility/Presenters.jsm');
-Cu.import('resource://gre/modules/accessibility/VirtualCursorController.jsm');
 Cu.import('resource://gre/modules/accessibility/TouchAdapter.jsm');
 
 const ACCESSFU_DISABLE = 0;
 const ACCESSFU_ENABLE = 1;
 const ACCESSFU_AUTO = 2;
 
 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.
+   * Initialize chrome-layer accessibility functionality.
+   * If accessibility is enabled on the platform, then a special accessibility
+   * mode is started.
    */
   attach: function attach(aWindow) {
     if (this.chromeWin)
       // XXX: only supports attaching to one window now.
       throw new Error('Only one window could be attached to AccessFu');
 
     Logger.info('attach');
     this.chromeWin = aWindow;
-    this.presenters = [];
 
     this.prefsBranch = Cc['@mozilla.org/preferences-service;1']
       .getService(Ci.nsIPrefService).getBranch('accessibility.accessfu.');
     this.prefsBranch.addObserver('activate', this, false);
-    this.prefsBranch.addObserver('explorebytouch', this, false);
 
     this.touchAdapter = TouchAdapter;
 
-    switch(Utils.MozBuildApp) {
+    switch (Utils.MozBuildApp) {
       case 'mobile/android':
         Services.obs.addObserver(this, 'Accessibility:Settings', false);
-        Services.obs.addObserver(this, 'Accessibility:NextObject', false);
-        Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
-        Services.obs.addObserver(this, 'Accessibility:CurrentObject', false);
+        Cc['@mozilla.org/android/bridge;1'].
+          getService(Ci.nsIAndroidBridge).handleGeckoMessage(
+            JSON.stringify({ gecko: { type: 'Accessibility:Ready' } }));
         this.touchAdapter = AndroidTouchAdapter;
         break;
       case 'b2g':
         aWindow.addEventListener(
           'ContentStart',
           (function(event) {
              let content = aWindow.shell.contentBrowser.contentWindow;
              content.addEventListener('mozContentEvent', this, false, true);
            }).bind(this), false);
         break;
       default:
         break;
     }
 
-    this._processPreferences();
+    try {
+      this._activatePref = this.prefsBranch.getIntPref('activate');
+    } catch (x) {
+      this._activatePref = ACCESSFU_DISABLE;
+    }
+
+    this._enableOrDisable();
   },
 
   /**
    * Start AccessFu mode, this primarily means controlling the virtual cursor
    * with arrow keys.
    */
   _enable: function _enable() {
     if (this._enabled)
       return;
     this._enabled = true;
 
     Logger.info('enable');
 
+    for each (let mm in Utils.getAllMessageManagers(this.chromeWin))
+      this._loadFrameScript(mm);
+
     // Add stylesheet
     let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
     this.stylesheet = this.chromeWin.document.createProcessingInstruction(
       'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
-    this.chromeWin.document.insertBefore(this.stylesheet, this.chromeWin.document.firstChild);
-
-    this.addPresenter(new VisualPresenter());
+    this.chromeWin.document.insertBefore(this.stylesheet,
+                                         this.chromeWin.document.firstChild);
 
-    // Implicitly add the Android presenter on Android.
-    if (Utils.MozBuildApp == 'mobile/android') {
-      this._androidPresenter = new AndroidPresenter();
-      this.addPresenter(this._androidPresenter);
-    } else if (Utils.MozBuildApp == 'b2g') {
-      this.addPresenter(new SpeechPresenter());
-    }
+    Input.attach(this.chromeWin);
+    Output.attach(this.chromeWin);
+    this.touchAdapter.attach(this.chromeWin);
 
-    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('focus', this, true);
+    Services.obs.addObserver(this, 'remote-browser-frame-shown', false);
+    Services.obs.addObserver(this, 'Accessibility:NextObject', false);
+    Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
+    Services.obs.addObserver(this, 'Accessibility:CurrentObject', false);
   },
 
   /**
    * Disable AccessFu and return to default interaction mode.
    */
   _disable: function _disable() {
     if (!this._enabled)
       return;
+
     this._enabled = false;
 
     Logger.info('disable');
 
     this.chromeWin.document.removeChild(this.stylesheet);
+    for each (let mm in Utils.getAllMessageManagers(this.chromeWin))
+      mm.sendAsyncMessage('AccessFu:Stop');
 
-    this.presenters.forEach(function(p) { p.detach(); });
-    this.presenters = [];
-
-    VirtualCursorController.detach();
+    Input.detach();
 
-    Services.obs.removeObserver(this, 'accessible-event');
-    this.chromeWin.removeEventListener('DOMActivate', this, true);
-    this.chromeWin.removeEventListener('resize', this, true);
-    this.chromeWin.removeEventListener('scroll', this, true);
-    this.chromeWin.removeEventListener('TabOpen', this, true);
-    this.chromeWin.removeEventListener('focus', this, true);
+    Services.obs.removeObserver(this, 'remote-browser-frame-shown');
+    Services.obs.removeObserver(this, 'Accessibility:NextObject');
+    Services.obs.removeObserver(this, 'Accessibility:PreviousObject');
+    Services.obs.removeObserver(this, 'Accessibility:CurrentObject');
   },
 
-  _processPreferences: function _processPreferences(aEnabled, aTouchEnabled) {
-    let accessPref = ACCESSFU_DISABLE;
+  _enableOrDisable: function _enableOrDisable() {
     try {
-      accessPref = (aEnabled == undefined) ?
-        this.prefsBranch.getIntPref('activate') : aEnabled;
+      if (this._activatePref == ACCESSFU_ENABLE ||
+          this._systemPref && this._activatePref == ACCESSFU_AUTO)
+        this._enable();
+      else
+        this._disable();
     } catch (x) {
-    }
-
-    let ebtPref = ACCESSFU_DISABLE;
-    try {
-      ebtPref = (aTouchEnabled == undefined) ?
-        this.prefsBranch.getIntPref('explorebytouch') : aTouchEnabled;
-    } catch (x) {
+      Logger.error(x);
     }
-
-    if (Utils.MozBuildApp == 'mobile/android') {
-      if (accessPref == ACCESSFU_AUTO) {
-        Cc['@mozilla.org/android/bridge;1'].
-          getService(Ci.nsIAndroidBridge).handleGeckoMessage(
-            JSON.stringify({ gecko: { type: 'Accessibility:Ready' } }));
-        return;
-      }
-    }
-
-    if (accessPref == ACCESSFU_ENABLE)
-      this._enable();
-    else
-      this._disable();
-
-    if (ebtPref == ACCESSFU_ENABLE)
-      this.touchAdapter.attach(this.chromeWin);
-    else
-      this.touchAdapter.detach(this.chromeWin);
-  },
-
-  addPresenter: function addPresenter(presenter) {
-    this.presenters.push(presenter);
-    presenter.attach(this.chromeWin);
   },
 
-  handleEvent: function handleEvent(aEvent) {
-    switch (aEvent.type) {
-      case 'focus':
-      {
-        if (aEvent.target instanceof Ci.nsIDOMWindow) {
-          let docAcc = getAccessible(aEvent.target.document);
-          let docContext = new PresenterContext(docAcc, null);
-          let cursorable = docAcc.QueryInterface(Ci.nsIAccessibleCursorable);
-          let vcContext = new PresenterContext(
-            (cursorable) ? cursorable.virtualCursor.position : null, null);
-          this.presenters.forEach(
-            function(p) { p.tabSelected(docContext, vcContext); });
+  receiveMessage: function receiveMessage(aMessage) {
+    if (Logger.logLevel >= Logger.DEBUG)
+      Logger.debug('Recieved', aMessage.name, JSON.stringify(aMessage.json));
+
+    switch (aMessage.name) {
+      case 'AccessFu:Ready':
+      let mm = Utils.getMessageManager(aMessage.target);
+      mm.sendAsyncMessage('AccessFu:Start',
+                          {method: 'start', buildApp: Utils.MozBuildApp});
+      break;
+      case 'AccessFu:Present':
+      try {
+        for each (let presenter in aMessage.json) {
+          Output[presenter.type](presenter.details, aMessage.target);
+        }
+      } catch (x) {
+        Logger.error(x);
+      }
+      break;
+      case 'AccessFu:Input':
+      Input.setEditState(aMessage.json);
+      break;
+    }
+  },
+
+  _loadFrameScript: function _loadFrameScript(aMessageManager) {
+    aMessageManager.addMessageListener('AccessFu:Present', this);
+    aMessageManager.addMessageListener('AccessFu:Input', this);
+    aMessageManager.addMessageListener('AccessFu:Ready', this);
+    aMessageManager.
+      loadFrameScript(
+        'chrome://global/content/accessibility/content-script.js', true);
+  },
+
+  observe: function observe(aSubject, aTopic, aData) {
+    Logger.debug('observe', aTopic);
+    switch (aTopic) {
+      case 'Accessibility:Settings':
+        this._systemPref = JSON.parse(aData).enabled;
+        this._enableOrDisable();
+        break;
+      case 'Accessibility:NextObject':
+        Input.moveCursor('moveNext', 'Simple', 'gesture');
+        break;
+      case 'Accessibility:PreviousObject':
+        Input.moveCursor('movePrevious', 'Simple', 'gesture');
+        break;
+      case 'Accessibility:CurrentObject':
+        let mm = Utils.getCurrentBrowser(this.chromeWin).
+          frameLoader.messageManager;
+        mm.sendAsyncMessage('AccessFu:VirtualCursor',
+                            {action: 'presentLastPivot'});
+        break;
+      case 'nsPref:changed':
+        if (aData == 'activate') {
+          this._activatePref = this.prefsBranch.getIntPref('activate');
+          this._enableOrDisable();
         }
         break;
-      }
-      case 'TabOpen':
-      {
-        let browser = aEvent.target.linkedBrowser || aEvent.target;
-        // Store the new browser node. We will need to check later when a new
-        // content document is attached if it has been attached to this new tab.
-        // If it has, than we will need to send a 'loading' message along with
-        // the usual 'newdoc' to presenters.
-        this._pendingDocuments[browser] = true;
-        this.presenters.forEach(
-          function(p) {
-            p.tabStateChanged(null, 'newtab');
-          }
-        );
-        break;
-      }
-      case 'DOMActivate':
+      case 'remote-browser-frame-shown':
       {
-        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;
-      }
-      case 'mozContentEvent':
-      {
-        if (aEvent.detail.type == 'accessibility-screenreader') {
-          let pref = aEvent.detail.enabled + 0;
-          this._processPreferences(pref, pref);
-        }
+        this._loadFrameScript(
+          aSubject.QueryInterface(Ci.nsIFrameLoader).messageManager);
         break;
       }
     }
   },
 
-  observe: function observe(aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case 'Accessibility:Settings':
-        this._processPreferences(JSON.parse(aData).enabled + 0,
-                                 JSON.parse(aData).exploreByTouch + 0);
-        break;
-      case 'Accessibility:NextObject':
-        VirtualCursorController.
-          moveForward(Utils.getCurrentContentDoc(this.chromeWin));
-        break;
-      case 'Accessibility:PreviousObject':
-        VirtualCursorController.
-          moveBackward(Utils.getCurrentContentDoc(this.chromeWin));
-        break;
-      case 'Accessibility:CurrentObject':
-        this._androidPresenter.accessibilityFocus();
-        break;
-      case 'nsPref:changed':
-        this._processPreferences(this.prefsBranch.getIntPref('activate'),
-                                 this.prefsBranch.getIntPref('explorebytouch'));
-        break;
-      case 'accessible-event':
-        let event;
-        try {
-          event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
-          this._handleAccEvent(event);
-        } catch (ex) {
-          Logger.error(ex);
-          return;
-        }
+  handleEvent: function handleEvent(aEvent) {
+    if (aEvent.type == 'mozContentEvent' &&
+        aEvent.detail.type == 'accessibility-screenreader') {
+      this._systemPref = aEvent.detail.enabled;
+      this._enableOrDisable();
     }
   },
 
-  _handleAccEvent: function _handleAccEvent(aEvent) {
-    if (Logger.logLevel <= Logger.DEBUG)
-      Logger.debug(Logger.eventToString(aEvent),
-                   Logger.accessibleToString(aEvent.accessible));
-
-    switch (aEvent.eventType) {
-      case Ci.nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED:
-        {
-          let pivot = aEvent.accessible.
-            QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor;
-          let event = aEvent.
-            QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
-          let position = pivot.position;
-          let doc = aEvent.DOMNode;
-
-          let presenterContext =
-            new PresenterContext(position, event.oldAccessible);
-          let reason = event.reason;
-          this.presenters.forEach(
-            function(p) { p.pivotChanged(presenterContext, reason); });
-
-          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');
-              }
-            );
-          }
-          else if (event.state == Ci.nsIAccessibleStates.STATE_BUSY &&
-                   !(event.isExtraState()) && event.isEnabled()) {
-            let role = event.accessible.role;
-            if ((role == Ci.nsIAccessibleRole.ROLE_DOCUMENT ||
-                 role == Ci.nsIAccessibleRole.ROLE_APPLICATION)) {
-              // An existing document has changed to state "busy", this means
-              // something is loading. Send a 'loading' message to presenters.
-              this.presenters.forEach(
-                function(p) {
-                  p.tabStateChanged(event.accessible, 'loading');
-                }
-              );
-            }
-          }
-          break;
-        }
-      case Ci.nsIAccessibleEvent.EVENT_REORDER:
-        {
-          let acc = aEvent.accessible;
-          if (acc.childCount) {
-            let docAcc = acc.getChildAt(0);
-            if (this._pendingDocuments[aEvent.DOMNode]) {
-              // This is a document in a new tab. Check if it is
-              // in a BUSY state (i.e. loading), and inform presenters.
-              // We need to do this because a state change event will not be
-              // fired when an object is created with the BUSY state.
-              // If this is not a new tab, don't bother because we sent
-              // 'loading' when the previous doc changed its state to BUSY.
-              let state = {};
-              docAcc.getState(state, {});
-              if (state.value & Ci.nsIAccessibleStates.STATE_BUSY &&
-                  this._isNotChromeDoc(docAcc))
-                this.presenters.forEach(
-                  function(p) { p.tabStateChanged(docAcc, 'loading'); }
-                );
-              delete this._pendingDocuments[aEvent.DOMNode];
-            }
-            if (this._isBrowserDoc(docAcc))
-              // A new top-level content document has been attached
-              this.presenters.forEach(
-                function(p) { p.tabStateChanged(docAcc, 'newdoc'); }
-              );
-          }
-          break;
-        }
-      case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE:
-        {
-          if (this._isNotChromeDoc(aEvent.accessible)) {
-            this.presenters.forEach(
-              function(p) {
-                p.tabStateChanged(aEvent.accessible, 'loaded');
-              }
-            );
-          }
-          break;
-        }
-      case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED:
-        {
-          this.presenters.forEach(
-            function(p) {
-              p.tabStateChanged(aEvent.accessible, 'loadstopped');
-            }
-          );
-          break;
-        }
-      case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD:
-        {
-          this.presenters.forEach(
-            function(p) {
-              p.tabStateChanged(aEvent.accessible, 'reload');
-            }
-          );
-          break;
-        }
-      case Ci.nsIAccessibleEvent.EVENT_TEXT_INSERTED:
-      case Ci.nsIAccessibleEvent.EVENT_TEXT_REMOVED:
-      {
-        if (aEvent.isFromUserInput) {
-          // XXX support live regions as well.
-          let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
-          let isInserted = event.isInserted();
-          let txtIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
-
-          let text = '';
-          try {
-            text = txtIface.
-              getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
-          } catch (x) {
-            // XXX we might have gotten an exception with of a
-            // zero-length text. If we did, ignore it (bug #749810).
-            if (txtIface.characterCount)
-              throw x;
-          }
-
-          this.presenters.forEach(
-            function(p) {
-              p.textChanged(isInserted, event.start, event.length,
-                            text, event.modifiedText);
-            }
-          );
-        }
-        break;
-      }
-      case Ci.nsIAccessibleEvent.EVENT_SCROLLING_START:
-      {
-        VirtualCursorController.moveCursorToObject(
-          Utils.getVirtualCursor(aEvent.accessibleDocument), aEvent.accessible);
-        break;
-      }
-      case Ci.nsIAccessibleEvent.EVENT_FOCUS:
-      {
-        let acc = aEvent.accessible;
-        let doc = aEvent.accessibleDocument;
-        if (acc.role != Ci.nsIAccessibleRole.ROLE_DOCUMENT &&
-            doc.role != Ci.nsIAccessibleRole.ROLE_CHROME_WINDOW)
-          VirtualCursorController.moveCursorToObject(
-            Utils.getVirtualCursor(doc), acc);
-
-        let [,extState] = Utils.getStates(acc);
-        let editableState = extState &
-          (Ci.nsIAccessibleStates.EXT_STATE_EDITABLE |
-           Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE);
-
-        if (editableState != VirtualCursorController.editableState) {
-          if (!VirtualCursorController.editableState)
-            this.presenters.forEach(
-              function(p) {
-                p.editingModeChanged(true);
-              }
-            );
-        }
-        VirtualCursorController.editableState = editableState;
-        break;
-      }
-      default:
-        break;
-    }
-  },
-
-  /**
-   * Check if accessible is a top-level content document (i.e. a child of a XUL
-   * browser node).
-   * @param {nsIAccessible} aDocAcc the accessible to check.
-   * @return {boolean} true if this is a top-level content document.
-   */
-  _isBrowserDoc: function _isBrowserDoc(aDocAcc) {
-    let parent = aDocAcc.parent;
-    if (!parent)
-      return false;
-
-    let domNode = parent.DOMNode;
-    if (!domNode)
-      return false;
-
-    const ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
-    return (domNode.localName == 'browser' && domNode.namespaceURI == ns);
-  },
-
-  /**
-   * Check if document is not a local "chrome" document, like about:home.
-   * @param {nsIDOMDocument} aDocument the document to check.
-   * @return {boolean} true if this is not a chrome document.
-   */
-  _isNotChromeDoc: function _isNotChromeDoc(aDocument) {
-    let location = aDocument.DOMNode.location;
-    if (!location)
-      return false;
-
-    return location.protocol != 'about:';
-  },
-
-  // A hash of documents that don't yet have an accessible tree.
-  _pendingDocuments: {},
-
   // So we don't enable/disable twice
   _enabled: false
 };
 
-function getAccessible(aNode) {
-  try {
-    return Cc['@mozilla.org/accessibleRetrieval;1'].
-      getService(Ci.nsIAccessibleRetrieval).getAccessibleFor(aNode);
-  } catch (e) {
-    return null;
+var Output = {
+  attach: function attach(aWindow) {
+    this.chromeWin = aWindow;
+  },
+
+  Speech: function Speech(aDetails, aBrowser) {
+    for each (let action in aDetails.actions)
+      Logger.info('tts.' + action.method, '"' + action.data + '"', JSON.stringify(action.options));
+  },
+
+  Visual: function Visual(aDetails, aBrowser) {
+    if (!this.highlightBox) {
+      // 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);
+    }
+
+    if (aDetails.method == 'show') {
+      let padding = aDetails.padding;
+      let r = this._adjustBounds(aDetails.bounds, aBrowser);
+
+      // First hide it to avoid flickering when changing the style.
+      this.highlightBox.style.display = 'none';
+      this.highlightBox.style.top = (r.top - padding) + 'px';
+      this.highlightBox.style.left = (r.left - padding) + 'px';
+      this.highlightBox.style.width = (r.width + padding*2) + 'px';
+      this.highlightBox.style.height = (r.height + padding*2) + 'px';
+      this.highlightBox.style.display = 'block';
+    } else if (aDetails.method == 'hide') {
+      this.highlightBox.style.display = 'none';
+    }
+  },
+
+  Android: function Android(aDetails, aBrowser) {
+    if (!this._bridge)
+      this._bridge = Cc['@mozilla.org/android/bridge;1'].getService(Ci.nsIAndroidBridge);
+
+    for each (let androidEvent in aDetails) {
+      androidEvent.type = 'Accessibility:Event';
+      if (androidEvent.bounds)
+        androidEvent.bounds = this._adjustBounds(androidEvent.bounds, aBrowser);
+      this._bridge.handleGeckoMessage(JSON.stringify({gecko: androidEvent}));
+    }
+  },
+
+  _adjustBounds: function(aJsonBounds, aBrowser) {
+    let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
+                          aJsonBounds.right - aJsonBounds.left,
+                          aJsonBounds.bottom - aJsonBounds.top);
+    let vp = Utils.getViewport(this.chromeWin) || { zoom: 1.0, offsetY: 0 };
+    let browserOffset = aBrowser.getBoundingClientRect();
+
+    return bounds.translate(browserOffset.left, browserOffset.top).
+      scale(vp.zoom, vp.zoom).expandToIntegers();
   }
-}
+};
+
+var Input = {
+  editState: {},
+
+  attach: function attach(aWindow) {
+    this.chromeWin = aWindow;
+    this.chromeWin.document.addEventListener('keypress', this, true);
+    this.chromeWin.addEventListener('mozAccessFuGesture', this, true);
+  },
+
+  detach: function detach() {
+    this.chromeWin.document.removeEventListener('keypress', this, true);
+    this.chromeWin.removeEventListener('mozAccessFuGesture', this, true);
+  },
+
+  handleEvent: function Input_handleEvent(aEvent) {
+    try {
+      switch (aEvent.type) {
+      case 'keypress':
+        this._handleKeypress(aEvent);
+        break;
+      case 'mozAccessFuGesture':
+        this._handleGesture(aEvent);
+        break;
+      }
+    } catch (x) {
+      Logger.error(x);
+    }
+  },
+
+  _handleGesture: function _handleGesture(aEvent) {
+    let detail = aEvent.detail;
+    Logger.info('Gesture', detail.type,
+                '(fingers: ' + detail.touches.length + ')');
+
+    if (detail.touches.length == 1) {
+      switch (detail.type) {
+        case 'swiperight':
+          this.moveCursor('moveNext', 'Simple', 'gestures');
+          break;
+        case 'swipeleft':
+          this.moveCursor('movePrevious', 'Simple', 'gesture');
+          break;
+        case 'doubletap':
+          this.activateCurrent();
+          break;
+        case 'explore':
+          this.moveCursor('moveToPoint', 'Simple', 'gesture',
+                          detail.x, detail.y);
+          break;
+      }
+    }
+
+    if (detail.touches.length == 3) {
+      switch (detail.type) {
+        case 'swiperight':
+          this.scroll(-1, true);
+          break;
+        case 'swipedown':
+          this.scroll(-1);
+          break;
+        case 'swipeleft':
+          this.scroll(1, true);
+          break;
+        case 'swipeup':
+          this.scroll(1);
+          break;
+      }
+    }
+  },
+
+  _handleKeypress: function _handleKeypress(aEvent) {
+    let target = aEvent.target;
+
+    // Ignore keys with modifiers so the content could take advantage of them.
+    if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)
+      return;
+
+    switch (aEvent.keyCode) {
+      case 0:
+        // an alphanumeric key was pressed, handle it separately.
+        // If it was pressed with either alt or ctrl, just pass through.
+        // If it was pressed with meta, pass the key on without the meta.
+        if (this.editState.editing)
+          return;
+
+        let key = String.fromCharCode(aEvent.charCode);
+        try {
+          let [methodName, rule] = this.keyMap[key];
+          this.moveCursor(methodName, rule, 'keyboard');
+        } catch (x) {
+          return;
+        }
+        break;
+      case aEvent.DOM_VK_RIGHT:
+        if (this.editState.editing) {
+          if (!this.editState.atEnd)
+            // Don't move forward if caret is not at end of entry.
+            // XXX: Fix for rtl
+            return;
+          else
+            target.blur();
+        }
+        this.moveCursor(aEvent.shiftKey ? 'moveLast' : 'moveNext', 'Simple', 'keyboard');
+        break;
+      case aEvent.DOM_VK_LEFT:
+        if (this.editState.editing) {
+          if (!this.editState.atStart)
+            // Don't move backward if caret is not at start of entry.
+            // XXX: Fix for rtl
+            return;
+          else
+            target.blur();
+        }
+        this.moveCursor(aEvent.shiftKey ? 'moveFirst' : 'movePrevious', 'Simple', 'keyboard');
+        break;
+      case aEvent.DOM_VK_UP:
+        if (this.editState.multiline) {
+          if (!this.editState.atStart)
+            // Don't blur content if caret is not at start of text area.
+            return;
+          else
+            target.blur();
+        }
+
+        if (Utils.MozBuildApp == 'mobile/android')
+          // Return focus to native Android browser chrome.
+          Cc['@mozilla.org/android/bridge;1'].
+            getService(Ci.nsIAndroidBridge).handleGeckoMessage(
+              JSON.stringify({ gecko: { type: 'ToggleChrome:Focus' } }));
+        break;
+      case aEvent.DOM_VK_RETURN:
+      case aEvent.DOM_VK_ENTER:
+        if (this.editState.editing)
+          return;
+        this.activateCurrent();
+        break;
+    default:
+      return;
+    }
+
+    aEvent.preventDefault();
+    aEvent.stopPropagation();
+  },
+
+  moveCursor: function moveCursor(aAction, aRule, aInputType, aX, aY) {
+    let mm = Utils.getMessageManager(Utils.getCurrentBrowser(this.chromeWin));
+    mm.sendAsyncMessage('AccessFu:VirtualCursor',
+                        {action: aAction, rule: aRule,
+                         x: aX, y: aY, origin: 'top',
+                         inputType: aInputType});
+  },
+
+  activateCurrent: function activateCurrent() {
+    let mm = Utils.getMessageManager(Utils.getCurrentBrowser(this.chromeWin));
+    mm.sendAsyncMessage('AccessFu:Activate', {});
+  },
+
+  setEditState: function setEditState(aEditState) {
+    this.editState = aEditState;
+  },
+
+  scroll: function scroll(aPage, aHorizontal) {
+    let mm = Utils.getMessageManager(Utils.getCurrentBrowser(this.chromeWin));
+    mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'});
+  },
+
+  keyMap: {
+    a: ['moveNext', 'Anchor'],
+    A: ['movePrevious', 'Anchor'],
+    b: ['moveNext', 'Button'],
+    B: ['movePrevious', 'Button'],
+    c: ['moveNext', 'Combobox'],
+    C: ['movePrevious', 'Combobox'],
+    e: ['moveNext', 'Entry'],
+    E: ['movePrevious', 'Entry'],
+    f: ['moveNext', 'FormElement'],
+    F: ['movePrevious', 'FormElement'],
+    g: ['moveNext', 'Graphic'],
+    G: ['movePrevious', 'Graphic'],
+    h: ['moveNext', 'Heading'],
+    H: ['movePrevious', 'Heading'],
+    i: ['moveNext', 'ListItem'],
+    I: ['movePrevious', 'ListItem'],
+    k: ['moveNext', 'Link'],
+    K: ['movePrevious', 'Link'],
+    l: ['moveNext', 'List'],
+    L: ['movePrevious', 'List'],
+    p: ['moveNext', 'PageTab'],
+    P: ['movePrevious', 'PageTab'],
+    r: ['moveNext', 'RadioButton'],
+    R: ['movePrevious', 'RadioButton'],
+    s: ['moveNext', 'Separator'],
+    S: ['movePrevious', 'Separator'],
+    t: ['moveNext', 'Table'],
+    T: ['movePrevious', 'Table'],
+    x: ['moveNext', 'Checkbox'],
+    X: ['movePrevious', 'Checkbox']
+  }
+};
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/EventManager.jsm
@@ -0,0 +1,299 @@
+/* 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/. */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+Cu.import('resource://gre/modules/accessibility/Presenters.jsm');
+Cu.import('resource://gre/modules/accessibility/TraversalRules.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+var EXPORTED_SYMBOLS = ['EventManager'];
+
+var EventManager = {
+  editState: {},
+
+  start: function start(aSendMsgFunc) {
+    try {
+      if (!this._started) {
+        this.sendMsgFunc = aSendMsgFunc || function() {};
+        this.presenters = [new VisualPresenter()];
+
+        if (Utils.MozBuildApp == 'b2g') {
+          this.presenters.push(new SpeechPresenter());
+        } else if (Utils.MozBuildApp == 'mobile/android') {
+          this.presenters.push(new AndroidPresenter());
+        }
+
+        Logger.info('EventManager.start', Utils.MozBuildApp, [p.type for each(p in this.presenters)].join(', '));
+
+        this._started = true;
+        Services.obs.addObserver(this, 'accessible-event', false);
+      }
+
+      this.present(
+        function(p) {
+          return p.tabStateChanged(null, 'newtab');
+        }
+      );
+    } catch (x) {
+      Logger.error('Failed to start EventManager:', x);
+    }
+  },
+
+  stop: function stop() {
+    Services.obs.removeObserver(this, 'accessible-event');
+    this.presenters = [];
+    this._started = false;
+  },
+
+  handleEvent: function handleEvent(aEvent) {
+    try {
+      switch (aEvent.type) {
+      case 'DOMActivate':
+      {
+        let activatedAcc =
+          Utils.AccRetrieval.getAccessibleFor(aEvent.originalTarget);
+        let [state, extState] = Utils.getStates(activatedAcc);
+
+        // 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 & Ci.nsIAccessibleStates.STATE_CHECKABLE)
+          return;
+
+        this.present(
+          function(p) {
+            return p.actionInvoked(activatedAcc, 'click');
+          }
+        );
+        break;
+      }
+      case 'scroll':
+      case 'resize':
+      {
+        this.present(
+          function(p) {
+            return p.viewportChanged();;
+          }
+        );
+        break;
+      }
+      }
+    } catch (x) {
+      Logger.error('Error handling DOM event:', x);
+    }
+  },
+
+  observe: function observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case 'accessible-event':
+        var event;
+        try {
+          event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
+          this.handleAccEvent(event);
+        } catch (x) {
+          Logger.error('Error handing accessible event:', x);
+          return;
+        }
+    }
+  },
+
+  presentLastPivot: function presentLastPivot() {
+    this.present(
+      function(p) {
+        return p.presentLastPivot();
+      }
+    );
+  },
+
+  handleAccEvent: function handleAccEvent(aEvent) {
+    if (Logger.logLevel >= Logger.DEBUG)
+      Logger.debug('A11yEvent', Logger.eventToString(aEvent),
+                   Logger.accessibleToString(aEvent.accessible));
+
+    switch (aEvent.eventType) {
+      case Ci.nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED:
+      {
+        let pivot = aEvent.accessible.
+          QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor;
+        let position = pivot.position;
+        if (position.role == Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME)
+          break;
+        let event = aEvent.
+          QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
+        let presenterContext =
+          new PresenterContext(position, event.oldAccessible);
+        let reason = event.reason;
+
+        if (this.editState.editing)
+          aEvent.accessibleDocument.takeFocus();
+
+        this.present(
+          function(p) {
+            return p.pivotChanged(presenterContext, reason);
+          }
+        );
+        break;
+      }
+      case Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE:
+      {
+        let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
+        if (event.state == Ci.nsIAccessibleStates.STATE_CHECKED &&
+            !(event.isExtraState())) {
+          this.present(
+            function(p) {
+              return p.actionInvoked(aEvent.accessible,
+                                     event.isEnabled() ? 'check' : 'uncheck');
+            }
+          );
+        }
+        break;
+      }
+      case Ci.nsIAccessibleEvent.EVENT_SCROLLING_START:
+      {
+        let vc = Utils.getVirtualCursor(aEvent.accessibleDocument);
+        vc.moveNext(TraversalRules.Simple, aEvent.accessible, true);
+        break;
+      }
+      case Ci.nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED:
+      {
+        let acc = aEvent.accessible;
+        let characterCount = acc.
+          QueryInterface(Ci.nsIAccessibleText).characterCount;
+        let caretOffset = aEvent.
+          QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
+
+        // Update editing state, both for presenter and other things
+        let [,extState] = Utils.getStates(acc);
+        let editState = {
+          editing: !!(extState & Ci.nsIAccessibleStates.EXT_STATE_EDITABLE),
+          multiline: !!(extState & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE),
+          atStart: caretOffset == 0,
+          atEnd: caretOffset == characterCount
+        };
+
+        // Not interesting
+        if (!editState.editing && editState.editing == this.editState.editing)
+          break;
+
+        if (editState.editing != this.editState.editing)
+          this.present(
+            function(p) {
+              return p.editingModeChanged(editState.editing);
+            }
+          );
+
+        if (editState.editing != this.editState.editing ||
+            editState.multiline != this.editState.multiline ||
+            editState.atEnd != this.editState.atEnd ||
+            editState.atStart != this.editState.atStart)
+          this.sendMsgFunc("AccessFu:Input", editState);
+
+        this.editState = editState;
+        break;
+      }
+      case Ci.nsIAccessibleEvent.EVENT_TEXT_INSERTED:
+      case Ci.nsIAccessibleEvent.EVENT_TEXT_REMOVED:
+      {
+        if (aEvent.isFromUserInput) {
+          // XXX support live regions as well.
+          let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
+          let isInserted = event.isInserted();
+          let txtIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
+
+          let text = '';
+          try {
+            text = txtIface.
+              getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
+          } catch (x) {
+            // XXX we might have gotten an exception with of a
+            // zero-length text. If we did, ignore it (bug #749810).
+            if (txtIface.characterCount)
+              throw x;
+          }
+          this.present(
+            function(p) {
+              return p.textChanged(isInserted, event.start, event.length,
+                                   text, event.modifiedText);
+            }
+          );
+        }
+        break;
+      }
+      case Ci.nsIAccessibleEvent.EVENT_FOCUS:
+      {
+        // Put vc where the focus is at
+        let acc = aEvent.accessible;
+        let doc = aEvent.accessibleDocument;
+        if (acc.role != Ci.nsIAccessibleRole.ROLE_DOCUMENT &&
+            doc.role != Ci.nsIAccessibleRole.ROLE_CHROME_WINDOW) {
+          let vc = Utils.getVirtualCursor(doc);
+          vc.moveNext(TraversalRules.Simple, acc, true);
+        }
+        break;
+      }
+    }
+  },
+
+  present: function present(aPresenterFunc) {
+    try {
+      this.sendMsgFunc(
+        "AccessFu:Present",
+        [aPresenterFunc(p) for each (p in this.presenters)].
+          filter(function(d) {return !!d;}));
+    } catch (x) {
+      Logger.error(x);
+    }
+  },
+
+  onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+    let tabstate = '';
+
+    let loadingState = Ci.nsIWebProgressListener.STATE_TRANSFERRING |
+      Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+    let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
+      Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+
+    if ((aStateFlags & loadingState) == loadingState) {
+      tabstate = 'loading';
+    } else if ((aStateFlags & loadedState) == loadedState &&
+               !aWebProgress.isLoadingDocument) {
+      tabstate = 'loaded';
+    }
+
+    if (tabstate) {
+      let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document);
+      this.present(
+        function(p) {
+          return p.tabStateChanged(docAcc, tabstate);
+        }
+      );
+    }
+  },
+
+  onProgressChange: function onProgressChange() {},
+
+  onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+    let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document);
+    this.present(
+      function(p) {
+        return p.tabStateChanged(docAcc, 'newdoc');
+      }
+    );
+  },
+
+  onStatusChange: function onStatusChange() {},
+
+  onSecurityChange: function onSecurityChange() {},
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                         Ci.nsISupportsWeakReference,
+                                         Ci.nsISupports,
+                                         Ci.nsIObserver])
+};
--- a/accessible/src/jsat/Presenters.jsm
+++ b/accessible/src/jsat/Presenters.jsm
@@ -22,16 +22,21 @@ var EXPORTED_SYMBOLS = ['VisualPresenter
 /**
  * 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 type of presenter. Used for matching it with the appropriate output method.
+   */
+  type: 'Base',
+
+  /**
    * Attach function for presenter.
    * @param {ChromeWindow} aWindow Chrome window the presenter could use.
    */
   attach: function attach(aWindow) {},
 
   /**
    * Detach function.
    */
@@ -87,318 +92,141 @@ Presenter.prototype = {
    * @param {PresenterContext} aVCContext context object for tab's current
    *   virtual cursor position.
    */
   tabSelected: function tabSelected(aDocContext, aVCContext) {},
 
   /**
    * The viewport has changed, either a scroll, pan, zoom, or
    *    landscape/portrait toggle.
+   * @param {Window} aWindow window of viewport that changed.
    */
-  viewportChanged: function viewportChanged() {},
+  viewportChanged: function viewportChanged(aWindow) {},
 
   /**
    * We have entered or left text editing mode.
    */
-  editingModeChanged: function editingModeChanged(aIsEditing) {}
+  editingModeChanged: function editingModeChanged(aIsEditing) {},
+
+  /**
+   * Re-present the last pivot change.
+   */
+  presentLastPivot: function AndroidPresenter_presentLastPivot() {}
 };
 
 /**
  * Visual presenter. Draws a box around the virtual cursor's position.
  */
 
 function VisualPresenter() {}
 
 VisualPresenter.prototype = {
   __proto__: Presenter.prototype,
 
+  type: 'Visual',
+
   /**
    * The padding in pixels between the object and the highlight border.
    */
   BORDER_PADDING: 2,
 
-  attach: function VisualPresenter_attach(aWindow) {
-    this.chromeWin = aWindow;
-
-    // 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';
+  viewportChanged: function VisualPresenter_viewportChanged(aWindow) {
+    if (this._currentContext)
+      return {
+        type: this.type,
+        details: {
+          method: 'show',
+          bounds: this._currentContext.bounds,
+          padding: this.BORDER_PADDING
+        }
+      };
 
-    // 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);
-  },
-
-  detach: function VisualPresenter_detach() {
-    this.highlightBox.parentNode.removeChild(this.highlightBox);
-    this.highlightBox = this.stylesheet = null;
-  },
-
-  viewportChanged: function VisualPresenter_viewportChanged() {
-    if (this._currentContext)
-      this._highlight(this._currentContext);
+    return null;
   },
 
   pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) {
     this._currentContext = aContext;
 
-    if (!aContext.accessible) {
-      this._hide();
-      return;
-    }
+    if (!aContext.accessible)
+      return {type: this.type, details: {method: 'hide'}};
 
     try {
       aContext.accessible.scrollTo(
         Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
-      this._highlight(aContext);
+      return {
+        type: this.type,
+        details: {
+          method: 'show',
+          bounds: aContext.bounds,
+          padding: this.BORDER_PADDING
+        }
+      };
     } catch (e) {
       Logger.error('Failed to get bounds: ' + e);
-      return;
+      return null;
     }
   },
 
   tabSelected: function VisualPresenter_tabSelected(aDocContext, aVCContext) {
-    this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
+    return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
   },
 
   tabStateChanged: function VisualPresenter_tabStateChanged(aDocObj,
                                                             aPageState) {
     if (aPageState == 'newdoc')
-      this._hide();
-  },
-
-  // Internals
-
-  _hide: function _hide() {
-    this.highlightBox.style.display = 'none';
-  },
+      return {type: this.type, details: {method: 'hide'}};
 
-  _highlight: function _highlight(aContext) {
-    let vp = Utils.getViewport(this.chromeWin) || { zoom: 1.0, offsetY: 0 };
-    let r = aContext.bounds.scale(vp.zoom, vp.zoom).expandToIntegers();
-
-    // First hide it to avoid flickering when changing the style.
-    this.highlightBox.style.display = 'none';
-    this.highlightBox.style.top = (r.top - this.BORDER_PADDING) + 'px';
-    this.highlightBox.style.left = (r.left - this.BORDER_PADDING) + 'px';
-    this.highlightBox.style.width = (r.width + this.BORDER_PADDING*2) + 'px';
-    this.highlightBox.style.height = (r.height + this.BORDER_PADDING*2) + 'px';
-    this.highlightBox.style.display = 'block';
+    return null;
   }
 };
 
 /**
  * Android presenter. Fires Android a11y events.
  */
 
 function AndroidPresenter() {}
 
 AndroidPresenter.prototype = {
   __proto__: Presenter.prototype,
 
+  type: 'Android',
+
   // Android AccessibilityEvent type constants.
   ANDROID_VIEW_CLICKED: 0x01,
   ANDROID_VIEW_LONG_CLICKED: 0x02,
   ANDROID_VIEW_SELECTED: 0x04,
   ANDROID_VIEW_FOCUSED: 0x08,
   ANDROID_VIEW_TEXT_CHANGED: 0x10,
   ANDROID_WINDOW_STATE_CHANGED: 0x20,
   ANDROID_VIEW_HOVER_ENTER: 0x80,
   ANDROID_VIEW_HOVER_EXIT: 0x100,
   ANDROID_VIEW_SCROLLED: 0x1000,
   ANDROID_ANNOUNCEMENT: 0x4000,
   ANDROID_VIEW_ACCESSIBILITY_FOCUSED: 0x8000,
 
-  attach: function AndroidPresenter_attach(aWindow) {
-    this.chromeWin = aWindow;
-  },
-
   pivotChanged: function AndroidPresenter_pivotChanged(aContext, aReason) {
     if (!aContext.accessible)
-      return;
+      return null;
 
     this._currentContext = aContext;
 
+    let androidEvents = [];
+
     let isExploreByTouch = (aReason == Ci.nsIAccessiblePivot.REASON_POINT &&
                             Utils.AndroidSdkVersion >= 14);
     let focusEventType = (Utils.AndroidSdkVersion >= 16) ?
       this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED :
       this.ANDROID_VIEW_FOCUSED;
 
     if (isExploreByTouch) {
       // This isn't really used by TalkBack so this is a half-hearted attempt
       // for now.
-      this.sendMessageToJava({
-         gecko: {
-           type: 'Accessibility:Event',
-           eventType: this.ANDROID_VIEW_HOVER_EXIT,
-           text: []
-         }
-      });
-    }
-
-    let vp = Utils.getViewport(this.chromeWin) || { zoom: 1.0, offsetY: 0 };
-    let bounds = aContext.bounds.scale(vp.zoom, vp.zoom).expandToIntegers();
-    let output = [];
-
-    aContext.newAncestry.forEach(
-      function(acc) {
-        output.push.apply(output, UtteranceGenerator.genForObject(acc));
-      }
-    );
-
-    output.push.apply(output,
-                      UtteranceGenerator.genForObject(aContext.accessible));
-
-    aContext.subtreePreorder.forEach(
-      function(acc) {
-        output.push.apply(output, UtteranceGenerator.genForObject(acc));
-      }
-    );
-
-    this.sendMessageToJava({
-      gecko: {
-        type: 'Accessibility:Event',
-        eventType: (isExploreByTouch) ? this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
-        text: output,
-        bounds: bounds
-      }
-    });
-  },
-
-  actionInvoked: function AndroidPresenter_actionInvoked(aObject, aActionName) {
-    this.sendMessageToJava({
-      gecko: {
-        type: 'Accessibility:Event',
-        eventType: this.ANDROID_VIEW_CLICKED,
-        text: UtteranceGenerator.genForAction(aObject, aActionName)
-      }
-    });
-  },
-
-  tabSelected: function AndroidPresenter_tabSelected(aDocContext, aVCContext) {
-    // Send a pivot change message with the full context utterance for this doc.
-    this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
-  },
-
-  tabStateChanged: function AndroidPresenter_tabStateChanged(aDocObj,
-                                                             aPageState) {
-    this._appAnnounce(
-      UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
-  },
-
-  textChanged: function AndroidPresenter_textChanged(aIsInserted, aStart,
-                                                     aLength, aText,
-                                                     aModifiedText) {
-    let androidEvent = {
-      type: 'Accessibility:Event',
-      eventType: this.ANDROID_VIEW_TEXT_CHANGED,
-      text: [aText],
-      fromIndex: aStart,
-      removedCount: 0,
-      addedCount: 0
-    };
-
-    if (aIsInserted) {
-      androidEvent.addedCount = aLength;
-      androidEvent.beforeText =
-        aText.substring(0, aStart) + aText.substring(aStart + aLength);
-    } else {
-      androidEvent.removedCount = aLength;
-      androidEvent.beforeText =
-        aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
+      androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
     }
 
-    this.sendMessageToJava({gecko: androidEvent});
-  },
-
-  viewportChanged: function AndroidPresenter_viewportChanged() {
-    if (Utils.AndroidSdkVersion < 14)
-      return;
-
-    let win = Utils.getBrowserApp(this.chromeWin).selectedBrowser.contentWindow;
-    this.sendMessageToJava({
-      gecko: {
-        type: 'Accessibility:Event',
-        eventType: this.ANDROID_VIEW_SCROLLED,
-        text: [],
-        scrollX: win.scrollX,
-        scrollY: win.scrollY,
-        maxScrollX: win.scrollMaxX,
-        maxScrollY: win.scrollMaxY
-      }
-    });
-  },
-
-  editingModeChanged: function AndroidPresenter_editingModeChanged(aIsEditing) {
-    this._appAnnounce(UtteranceGenerator.genForEditingMode(aIsEditing));
-  },
-
-  _appAnnounce: function _appAnnounce(aUtterance) {
-    if (!aUtterance.length)
-      return;
-
-    this.sendMessageToJava({
-      gecko: {
-        type: 'Accessibility:Event',
-        eventType: (Utils.AndroidSdkVersion >= 16) ?
-          this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
-        text: aUtterance,
-        addedCount: aUtterance.join(' ').length,
-        removedCount: 0,
-        fromIndex: 0
-      }
-    });
-  },
-
-  accessibilityFocus: function AndroidPresenter_accessibilityFocus() {
-    if (this._currentContext)
-      this.pivotChanged(this._currentContext);
-  },
-
-  sendMessageToJava: function AndroidPresenter_sendMessageTojava(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 = {
-  __proto__: AndroidPresenter.prototype,
-
-  sendMessageToJava: function DummyAndroidPresenter_sendMessageToJava(aMsg) {
-    Logger.debug('Android event:\n' + JSON.stringify(aMsg, null, 2));
-  }
-};
-
-/**
- * A speech presenter for direct TTS output
- */
-
-function SpeechPresenter() {}
-
-SpeechPresenter.prototype = {
-  __proto__: Presenter.prototype,
-
-
-  pivotChanged: function SpeechPresenter_pivotChanged(aContext, aReason) {
-    if (!aContext.accessible)
-      return;
-
     let output = [];
 
     aContext.newAncestry.forEach(
       function(acc) {
         output.push.apply(output, UtteranceGenerator.genForObject(acc));
       }
     );
 
@@ -406,19 +234,163 @@ SpeechPresenter.prototype = {
                       UtteranceGenerator.genForObject(aContext.accessible));
 
     aContext.subtreePreorder.forEach(
       function(acc) {
         output.push.apply(output, UtteranceGenerator.genForObject(acc));
       }
     );
 
-    Logger.info('SPEAK', '"' + output.join(' ') + '"');
+    androidEvents.push({eventType: (isExploreByTouch) ?
+                          this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
+                        text: output,
+                        bounds: aContext.bounds});
+    return {
+      type: this.type,
+      details: androidEvents
+    };
+  },
+
+  actionInvoked: function AndroidPresenter_actionInvoked(aObject, aActionName) {
+    return {
+      type: this.type,
+      details: [{
+        eventType: this.ANDROID_VIEW_CLICKED,
+        text: UtteranceGenerator.genForAction(aObject, aActionName)
+      }]
+    };
+  },
+
+  tabSelected: function AndroidPresenter_tabSelected(aDocContext, aVCContext) {
+    // Send a pivot change message with the full context utterance for this doc.
+    return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
+  },
+
+  tabStateChanged: function AndroidPresenter_tabStateChanged(aDocObj,
+                                                             aPageState) {
+    return this._appAnnounce(
+      UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
+  },
+
+  textChanged: function AndroidPresenter_textChanged(aIsInserted, aStart,
+                                                     aLength, aText,
+                                                     aModifiedText) {
+    let androidEvent = {
+      type: this.type,
+      details: [{
+        eventType: this.ANDROID_VIEW_TEXT_CHANGED,
+        text: [aText],
+        fromIndex: aStart,
+        removedCount: 0,
+        addedCount: 0
+      }]
+    };
+
+    if (aIsInserted) {
+      androidEvent.addedCount = aLength;
+      androidEvent.beforeText =
+        aText.substring(0, aStart) + aText.substring(aStart + aLength);
+    } else {
+      androidEvent.removedCount = aLength;
+      androidEvent.beforeText =
+        aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
+    }
+
+    return androidEvent;
+  },
+
+  viewportChanged: function AndroidPresenter_viewportChanged(aWindow) {
+    if (Utils.AndroidSdkVersion < 14)
+      return null;
+
+    return {
+      type: this.type,
+      details: [{
+        eventType: this.ANDROID_VIEW_SCROLLED,
+        text: [],
+        scrollX: aWindow.scrollX,
+        scrollY: aWindow.scrollY,
+        maxScrollX: aWindow.scrollMaxX,
+        maxScrollY: aWindow.scrollMaxY
+      }]
+    };
+  },
+
+  editingModeChanged: function AndroidPresenter_editingModeChanged(aIsEditing) {
+    return this._appAnnounce(UtteranceGenerator.genForEditingMode(aIsEditing));
+  },
+
+  _appAnnounce: function _appAnnounce(aUtterance) {
+    if (!aUtterance.length)
+      return null;
+
+    return {
+      type: this.type,
+      details: [{
+        eventType: (Utils.AndroidSdkVersion >= 16) ?
+          this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
+        text: aUtterance,
+        addedCount: aUtterance.join(' ').length,
+        removedCount: 0,
+        fromIndex: 0
+      }]
+    };
+  },
+
+  presentLastPivot: function AndroidPresenter_presentLastPivot() {
+    if (this._currentContext)
+      return this.pivotChanged(this._currentContext);
+    else
+      return null;
   }
-}
+};
+
+/**
+ * A speech presenter for direct TTS output
+ */
+
+function SpeechPresenter() {}
+
+SpeechPresenter.prototype = {
+  __proto__: Presenter.prototype,
+
+  type: 'Speech',
+
+  pivotChanged: function SpeechPresenter_pivotChanged(aContext, aReason) {
+    if (!aContext.accessible)
+      return null;
+
+    let output = [];
+
+    aContext.newAncestry.forEach(
+      function(acc) {
+        output.push.apply(output, UtteranceGenerator.genForObject(acc));
+      }
+    );
+
+    output.push.apply(output,
+                      UtteranceGenerator.genForObject(aContext.accessible));
+
+    aContext.subtreePreorder.forEach(
+      function(acc) {
+        output.push.apply(output, UtteranceGenerator.genForObject(acc));
+      }
+    );
+
+    return {
+      type: this.type,
+      details: {
+        actions: [
+          {method: 'playEarcon', data: 'tick', options: {}},
+          {method: 'speak', data: output.join(' '), options: {enqueue: true}}
+        ]
+      }
+    };
+  }
+};
 
 /**
  * PresenterContext: An object that generates and caches context information
  * for a given accessible and its relationship with another accessible.
  */
 function PresenterContext(aAccessible, aOldAccessible) {
   this._accessible = aAccessible;
   this._oldAccessible =
@@ -499,17 +471,18 @@ PresenterContext.prototype = {
   },
 
   get bounds() {
     if (!this._bounds) {
       let objX = {}, objY = {}, objW = {}, objH = {};
 
       this._accessible.getBounds(objX, objY, objW, objH);
 
-      // Can't specify relative coords in nsIAccessible.getBounds, so we do it.
+      // XXX: OOP content provides a screen offset of 0, while in-process provides a real
+      // offset. Removing the offset and using content-relative coords normalizes this.
       let docX = {}, docY = {};
       let docRoot = this._accessible.rootDocument.
         QueryInterface(Ci.nsIAccessible);
       docRoot.getBounds(docX, docY, {}, {});
 
       this._bounds = new Rect(objX.value, objY.value, objW.value, objH.value).
         translate(-docX.value, -docY.value);
     }
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/TraversalRules.jsm
@@ -0,0 +1,207 @@
+/* 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 = ['TraversalRules'];
+
+Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+function BaseTraversalRule(aRoles, aMatchFunc) {
+  this._matchRoles = aRoles;
+  this._matchFunc = aMatchFunc;
+}
+
+BaseTraversalRule.prototype = {
+    getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) {
+      aRules.value = this._matchRoles;
+      return aRules.value.length;
+    },
+
+    preFilter: Ci.nsIAccessibleTraversalRule.PREFILTER_DEFUNCT |
+    Ci.nsIAccessibleTraversalRule.PREFILTER_INVISIBLE,
+
+    match: function BaseTraversalRule_match(aAccessible)
+    {
+      if (aAccessible.role == Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME) {
+        return (aAccessible.childCount) ?
+          Ci.nsIAccessibleTraversalRule.FILTER_IGNORE :
+          Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+      }
+
+      if (this._matchFunc)
+        return this._matchFunc(aAccessible);
+
+      return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule])
+};
+
+var gSimpleTraversalRoles =
+  [Ci.nsIAccessibleRole.ROLE_MENUITEM,
+   Ci.nsIAccessibleRole.ROLE_LINK,
+   Ci.nsIAccessibleRole.ROLE_PAGETAB,
+   Ci.nsIAccessibleRole.ROLE_GRAPHIC,
+   // XXX: Find a better solution for ROLE_STATICTEXT.
+   // It allows to filter list bullets but at the same time it
+   // filters CSS generated content too as an unwanted side effect.
+   // Ci.nsIAccessibleRole.ROLE_STATICTEXT,
+   Ci.nsIAccessibleRole.ROLE_TEXT_LEAF,
+   Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
+   Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
+   Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
+   Ci.nsIAccessibleRole.ROLE_COMBOBOX,
+   Ci.nsIAccessibleRole.ROLE_PROGRESSBAR,
+   Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN,
+   Ci.nsIAccessibleRole.ROLE_BUTTONMENU,
+   Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
+   Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
+   Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
+   Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
+   Ci.nsIAccessibleRole.ROLE_ENTRY,
+   // Used for traversing in to child OOP frames.
+   Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME];
+
+var TraversalRules = {
+  Simple: new BaseTraversalRule(
+    gSimpleTraversalRoles,
+    function Simple_match(aAccessible) {
+      switch (aAccessible.role) {
+      case Ci.nsIAccessibleRole.ROLE_COMBOBOX:
+        // We don't want to ignore the subtree because this is often
+        // where the list box hangs out.
+        return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+      case Ci.nsIAccessibleRole.ROLE_TEXT_LEAF:
+        {
+          // Nameless text leaves are boring, skip them.
+          let name = aAccessible.name;
+          if (name && name.trim())
+            return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+          else
+            return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
+        }
+      case Ci.nsIAccessibleRole.ROLE_LINK:
+        // If the link has children we should land on them instead.
+        // Image map links don't have children so we need to match those.
+        if (aAccessible.childCount == 0)
+          return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+        else
+          return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
+      default:
+        // Ignore the subtree, if there is one. So that we don't land on
+        // the same content that was already presented by its parent.
+        return Ci.nsIAccessibleTraversalRule.FILTER_MATCH |
+          Ci.nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE;
+      }
+    }
+  ),
+
+  SimpleTouch: new BaseTraversalRule(
+    gSimpleTraversalRoles,
+    function Simple_match(aAccessible) {
+      return Ci.nsIAccessibleTraversalRule.FILTER_MATCH |
+        Ci.nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE;
+    }
+  ),
+
+  Anchor: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_LINK],
+    function Anchor_match(aAccessible)
+    {
+      // We want to ignore links, only focus named anchors.
+      let state = {};
+      let extraState = {};
+      aAccessible.getState(state, extraState);
+      if (state.value & Ci.nsIAccessibleStates.STATE_LINKED) {
+        return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
+      } else {
+        return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+      }
+    }),
+
+  Button: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
+     Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
+     Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
+     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN,
+     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID]),
+
+  Combobox: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_COMBOBOX,
+     Ci.nsIAccessibleRole.ROLE_LISTBOX]),
+
+  Entry: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_ENTRY,
+     Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT]),
+
+  FormElement: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
+     Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
+     Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
+     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN,
+     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID,
+     Ci.nsIAccessibleRole.ROLE_COMBOBOX,
+     Ci.nsIAccessibleRole.ROLE_LISTBOX,
+     Ci.nsIAccessibleRole.ROLE_ENTRY,
+     Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
+     Ci.nsIAccessibleRole.ROLE_PAGETAB,
+     Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
+     Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
+     Ci.nsIAccessibleRole.ROLE_SLIDER,
+     Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
+     Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]),
+
+  Graphic: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_GRAPHIC]),
+
+  Heading: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_HEADING]),
+
+  ListItem: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_LISTITEM,
+     Ci.nsIAccessibleRole.ROLE_TERM]),
+
+  Link: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_LINK],
+    function Link_match(aAccessible)
+    {
+      // We want to ignore anchors, only focus real links.
+      let state = {};
+      let extraState = {};
+      aAccessible.getState(state, extraState);
+      if (state.value & Ci.nsIAccessibleStates.STATE_LINKED) {
+        return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+      } else {
+        return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
+      }
+    }),
+
+  List: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_LIST,
+     Ci.nsIAccessibleRole.ROLE_DEFINITION_LIST]),
+
+  PageTab: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_PAGETAB]),
+
+  RadioButton: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
+     Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM]),
+
+  Separator: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_SEPARATOR]),
+
+  Table: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_TABLE]),
+
+  Checkbox: new BaseTraversalRule(
+    [Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
+     Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM])
+};
--- a/accessible/src/jsat/Utils.jsm
+++ b/accessible/src/jsat/Utils.jsm
@@ -24,28 +24,39 @@ var Utils = {
     if (!this._AccRetrieval) {
       this._AccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1'].
         getService(Ci.nsIAccessibleRetrieval);
     }
 
     return this._AccRetrieval;
   },
 
+  set MozBuildApp(value) {
+    this._buildApp = value;
+  },
+
   get MozBuildApp() {
     if (!this._buildApp)
       this._buildApp = this._buildAppMap[Services.appinfo.ID];
     return this._buildApp;
   },
 
   get OS() {
     if (!this._OS)
       this._OS = Services.appinfo.OS;
     return this._OS;
   },
 
+  get ScriptName() {
+    if (!this._ScriptName)
+      this._ScriptName =
+        (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu';
+    return this._ScriptName;
+  },
+
   get AndroidSdkVersion() {
     if (!this._AndroidSdkVersion) {
       let shellVersion = Services.sysinfo.get('shellVersion') || '';
       let matches = shellVersion.match(/\((\d+)\)$/);
       if (matches)
         this._AndroidSdkVersion = parseInt(matches[1]);
       else
         this._AndroidSdkVersion = 15; // Most useful in desktop debugging.
@@ -66,34 +77,51 @@ var Utils = {
         return aWindow.gBrowser;
       case 'b2g':
         return aWindow.shell;
       default:
         return null;
     }
   },
 
+  getCurrentBrowser: function getCurrentBrowser(aWindow) {
+    if (this.MozBuildApp == 'b2g')
+      return this.getBrowserApp(aWindow).contentBrowser;
+    return this.getBrowserApp(aWindow).selectedBrowser;
+  },
+
   getCurrentContentDoc: function getCurrentContentDoc(aWindow) {
-    if (this.MozBuildApp == "b2g")
-      return this.getBrowserApp(aWindow).contentBrowser.contentDocument;
-    return this.getBrowserApp(aWindow).selectedBrowser.contentDocument;
+    return this.getCurrentBrowser(aWindow).contentDocument;
   },
 
-  getAllDocuments: function getAllDocuments(aWindow) {
-    let doc = this.AccRetrieval.
-      getAccessibleFor(this.getCurrentContentDoc(aWindow)).
-      QueryInterface(Ci.nsIAccessibleDocument);
-    let docs = [];
-    function getAllDocuments(aDocument) {
-      docs.push(aDocument.DOMDocument);
-      for (let i = 0; i < aDocument.childDocumentCount; i++)
-        getAllDocuments(aDocument.getChildDocumentAt(i));
+  getMessageManager: function getMessageManager(aBrowser) {
+    try {
+      return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner).
+         frameLoader.messageManager;
+    } catch (x) {
+      Logger.error(x);
+      return null;
     }
-    getAllDocuments(doc);
-    return docs;
+  },
+
+  getAllMessageManagers: function getAllMessageManagers(aWindow) {
+    let messageManagers = [];
+
+    for (let i = 0; i < aWindow.messageManager.childCount; i++)
+      messageManagers.push(aWindow.messageManager.getChildAt(i));
+
+    let remoteframes = this.getCurrentContentDoc(aWindow).
+      querySelectorAll('iframe[remote=true]');
+
+    for (let i = 0; i < remoteframes.length; ++i)
+      messageManagers.push(this.getMessageManager(remoteframes[i]));
+
+    Logger.info(messageManagers.length);
+
+    return messageManagers;
   },
 
   getViewport: function getViewport(aWindow) {
     switch (this.MozBuildApp) {
       case 'mobile/android':
         return aWindow.BrowserApp.selectedTab.getViewport();
       default:
         return null;
@@ -118,93 +146,16 @@ var Utils = {
       try {
         return doc.QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor;
       } catch (x) {
         doc = doc.parentDocument;
       }
     }
 
     return null;
-  },
-
-  scroll: function scroll(aWindow, aPage, aHorizontal) {
-    for each (let doc in this.getAllDocuments(aWindow)) {
-      // First see if we could scroll a window.
-      let win = doc.defaultView;
-      if (!aHorizontal && win.scrollMaxY &&
-          ((aPage > 0 && win.scrollY < win.scrollMaxY) ||
-           (aPage < 0 && win.scrollY > 0))) {
-        win.scroll(0, win.innerHeight);
-        return true;
-      } else if (aHorizontal && win.scrollMaxX &&
-                 ((aPage > 0 && win.scrollX < win.scrollMaxX) ||
-                  (aPage < 0 && win.scrollX > 0))) {
-        win.scroll(win.innerWidth, 0);
-        return true;
-      }
-
-      // Second, try to scroll main section or current target if there is no
-      // main section.
-      let main = doc.querySelector('[role=main]') ||
-        doc.querySelector(':target');
-
-      if (main) {
-        if ((!aHorizontal && main.clientHeight < main.scrollHeight) ||
-          (aHorizontal && main.clientWidth < main.scrollWidth)) {
-          let s = win.getComputedStyle(main);
-          if (!aHorizontal) {
-            if (s.overflowY == 'scroll' || s.overflowY == 'auto') {
-              main.scrollTop += aPage * main.clientHeight;
-              return true;
-            }
-          } else {
-            if (s.overflowX == 'scroll' || s.overflowX == 'auto') {
-              main.scrollLeft += aPage * main.clientWidth;
-              return true;
-            }
-          }
-        }
-      }
-    }
-
-    return false;
-  },
-
-  changePage: function changePage(aWindow, aPage) {
-    for each (let doc in this.getAllDocuments(aWindow)) {
-      // Get current main section or active target.
-      let main = doc.querySelector('[role=main]') ||
-        doc.querySelector(':target');
-      if (!main)
-        continue;
-
-      let mainAcc = this.AccRetrieval.getAccessibleFor(main);
-      if (!mainAcc)
-        continue;
-
-      let controllers = mainAcc.
-        getRelationByType(Ci.nsIAccessibleRelation.RELATION_CONTROLLED_BY);
-
-      for (var i=0; controllers.targetsCount > i; i++) {
-        let controller = controllers.getTarget(i);
-        // If the section has a controlling slider, it should be considered
-        // the page-turner.
-        if (controller.role == Ci.nsIAccessibleRole.ROLE_SLIDER) {
-          // Sliders are controlled with ctrl+right/left. I just decided :)
-          let evt = doc.createEvent("KeyboardEvent");
-          evt.initKeyEvent('keypress', true, true, null,
-                           true, false, false, false,
-                           (aPage > 0) ? evt.DOM_VK_RIGHT : evt.DOM_VK_LEFT, 0);
-          controller.DOMNode.dispatchEvent(evt);
-          return true;
-        }
-      }
-    }
-
-    return false;
   }
 };
 
 var Logger = {
   DEBUG: 0,
   INFO: 1,
   WARNING: 2,
   ERROR: 3,
@@ -212,17 +163,18 @@ var Logger = {
 
   logLevel: 1, // INFO;
 
   log: function log(aLogLevel) {
     if (aLogLevel < this.logLevel)
       return;
 
     let message = Array.prototype.slice.call(arguments, 1).join(' ');
-    dump('[AccessFu] ' + this._LEVEL_NAMES[aLogLevel] + ' ' + message + '\n');
+    dump('[' + Utils.ScriptName + '] ' +
+         this._LEVEL_NAMES[aLogLevel] +' ' + message + '\n');
   },
 
   info: function info() {
     this.log.apply(
       this, [this.INFO].concat(Array.prototype.slice.call(arguments)));
   },
 
   debug: function debug() {
deleted file mode 100644
--- a/accessible/src/jsat/VirtualCursorController.jsm
+++ /dev/null
@@ -1,434 +0,0 @@
-/* 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/accessibility/Utils.jsm');
-Cu.import('resource://gre/modules/XPCOMUtils.jsm');
-
-function BaseTraversalRule(aRoles, aMatchFunc) {
-  this._matchRoles = aRoles;
-  this._matchFunc = aMatchFunc;
-}
-
-BaseTraversalRule.prototype = {
-    getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) {
-      aRules.value = this._matchRoles;
-      return aRules.value.length;
-    },
-
-    preFilter: Ci.nsIAccessibleTraversalRule.PREFILTER_DEFUNCT |
-    Ci.nsIAccessibleTraversalRule.PREFILTER_INVISIBLE,
-
-    match: function BaseTraversalRule_match(aAccessible)
-    {
-      if (this._matchFunc)
-        return this._matchFunc(aAccessible);
-
-      return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
-    },
-
-    QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule])
-};
-
-var TraversalRules = {
-  Simple: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_MENUITEM,
-     Ci.nsIAccessibleRole.ROLE_LINK,
-     Ci.nsIAccessibleRole.ROLE_PAGETAB,
-     Ci.nsIAccessibleRole.ROLE_GRAPHIC,
-     // XXX: Find a better solution for ROLE_STATICTEXT.
-     // It allows to filter list bullets but at the same time it
-     // filters CSS generated content too as an unwanted side effect.
-     // Ci.nsIAccessibleRole.ROLE_STATICTEXT,
-     Ci.nsIAccessibleRole.ROLE_TEXT_LEAF,
-     Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
-     Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
-     Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
-     Ci.nsIAccessibleRole.ROLE_COMBOBOX,
-     Ci.nsIAccessibleRole.ROLE_PROGRESSBAR,
-     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN,
-     Ci.nsIAccessibleRole.ROLE_BUTTONMENU,
-     Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
-     Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
-     Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
-     Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
-     Ci.nsIAccessibleRole.ROLE_ENTRY],
-    function Simple_match(aAccessible) {
-      switch (aAccessible.role) {
-      case Ci.nsIAccessibleRole.ROLE_COMBOBOX:
-        // We don't want to ignore the subtree because this is often
-        // where the list box hangs out.
-        return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
-      case Ci.nsIAccessibleRole.ROLE_TEXT_LEAF:
-        {
-          // Nameless text leaves are boring, skip them.
-          let name = aAccessible.name;
-          if (name && name.trim())
-            return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
-          else
-            return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
-        }
-      case Ci.nsIAccessibleRole.ROLE_LINK:
-        // If the link has children we should land on them instead.
-        // Image map links don't have children so we need to match those.
-        if (aAccessible.childCount == 0)
-          return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
-        else
-          return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
-      default:
-        // Ignore the subtree, if there is one. So that we don't land on
-        // the same content that was already presented by its parent.
-        return Ci.nsIAccessibleTraversalRule.FILTER_MATCH |
-          Ci.nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE;
-      }
-    }
-  ),
-
-  Anchor: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_LINK],
-    function Anchor_match(aAccessible)
-    {
-      // We want to ignore links, only focus named anchors.
-      let state = {};
-      let extraState = {};
-      aAccessible.getState(state, extraState);
-      if (state.value & Ci.nsIAccessibleStates.STATE_LINKED) {
-        return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
-      } else {
-        return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
-      }
-    }),
-
-  Button: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
-     Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
-     Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
-     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN,
-     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID]),
-
-  Combobox: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_COMBOBOX,
-     Ci.nsIAccessibleRole.ROLE_LISTBOX]),
-
-  Entry: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_ENTRY,
-     Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT]),
-
-  FormElement: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
-     Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
-     Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
-     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN,
-     Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID,
-     Ci.nsIAccessibleRole.ROLE_COMBOBOX,
-     Ci.nsIAccessibleRole.ROLE_LISTBOX,
-     Ci.nsIAccessibleRole.ROLE_ENTRY,
-     Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
-     Ci.nsIAccessibleRole.ROLE_PAGETAB,
-     Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
-     Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
-     Ci.nsIAccessibleRole.ROLE_SLIDER,
-     Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
-     Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]),
-
-  Graphic: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_GRAPHIC]),
-
-  Heading: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_HEADING]),
-
-  ListItem: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_LISTITEM,
-     Ci.nsIAccessibleRole.ROLE_TERM]),
-
-  Link: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_LINK],
-    function Link_match(aAccessible)
-    {
-      // We want to ignore anchors, only focus real links.
-      let state = {};
-      let extraState = {};
-      aAccessible.getState(state, extraState);
-      if (state.value & Ci.nsIAccessibleStates.STATE_LINKED) {
-        return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
-      } else {
-        return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
-      }
-    }),
-
-  List: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_LIST,
-     Ci.nsIAccessibleRole.ROLE_DEFINITION_LIST]),
-
-  PageTab: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_PAGETAB]),
-
-  RadioButton: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
-     Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM]),
-
-  Separator: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_SEPARATOR]),
-
-  Table: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_TABLE]),
-
-  Checkbox: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
-     Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM])
-};
-
-var VirtualCursorController = {
-  exploreByTouch: false,
-  editableState: 0,
-
-  attach: function attach(aWindow) {
-    this.chromeWin = aWindow;
-    this.chromeWin.document.addEventListener('keypress', this, true);
-    this.chromeWin.addEventListener('mozAccessFuGesture', this, true);
-  },
-
-  detach: function detach() {
-    this.chromeWin.document.removeEventListener('keypress', this, true);
-    this.chromeWin.removeEventListener('mozAccessFuGesture', this, true);
-  },
-
-  handleEvent: function VirtualCursorController_handleEvent(aEvent) {
-    switch (aEvent.type) {
-      case 'keypress':
-        this._handleKeypress(aEvent);
-        break;
-      case 'mozAccessFuGesture':
-        this._handleGesture(aEvent);
-        break;
-    }
-  },
-
-  _handleGesture: function _handleGesture(aEvent) {
-    let document = Utils.getCurrentContentDoc(this.chromeWin);
-    let detail = aEvent.detail;
-    Logger.info('Gesture', detail.type,
-                '(fingers: ' + detail.touches.length + ')');
-
-    if (detail.touches.length == 1) {
-      switch (detail.type) {
-        case 'swiperight':
-          this.moveForward(document, aEvent.shiftKey);
-          break;
-        case 'swipeleft':
-          this.moveBackward(document, aEvent.shiftKey);
-          break;
-        case 'doubletap':
-          this.activateCurrent(document);
-          break;
-        case 'explore':
-          this.moveToPoint(document, detail.x, detail.y);
-          break;
-      }
-    }
-
-    if (detail.touches.length == 3) {
-      switch (detail.type) {
-        case 'swiperight':
-          if (!Utils.scroll(this.chromeWin, -1, true))
-            Utils.changePage(this.chromeWin, -1);
-          break;
-        case 'swipedown':
-          Utils.scroll(this.chromeWin, -1);
-          break;
-        case 'swipeleft':
-          if (!Utils.scroll(this.chromeWin, 1, true))
-            Utils.changePage(this.chromeWin, 1);
-        case 'swipeup':
-          Utils.scroll(this.chromeWin, 1);
-          break;
-      }
-    }
-  },
-
-  _handleKeypress: function _handleKeypress(aEvent) {
-    let document = Utils.getCurrentContentDoc(this.chromeWin);
-    let target = aEvent.target;
-
-    // Ignore keys with modifiers so the content could take advantage of them.
-    if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)
-      return;
-
-    switch (aEvent.keyCode) {
-      case 0:
-        // an alphanumeric key was pressed, handle it separately.
-        // If it was pressed with either alt or ctrl, just pass through.
-        // If it was pressed with meta, pass the key on without the meta.
-        if (this.editableState)
-          return;
-
-        let key = String.fromCharCode(aEvent.charCode);
-        let methodName = '', rule = {};
-        try {
-          [methodName, rule] = this.keyMap[key];
-        } catch (x) {
-          return;
-        }
-        this[methodName](document, false, rule);
-        break;
-      case aEvent.DOM_VK_RIGHT:
-        if (this.editableState) {
-          if (target.selectionEnd != target.textLength)
-            // Don't move forward if caret is not at end of entry.
-            // XXX: Fix for rtl
-            return;
-          else
-            target.blur();
-        }
-        this.moveForward(document, aEvent.shiftKey);
-        break;
-      case aEvent.DOM_VK_LEFT:
-        if (this.editableState) {
-          if (target.selectionEnd != 0)
-            // Don't move backward if caret is not at start of entry.
-            // XXX: Fix for rtl
-            return;
-          else
-            target.blur();
-        }
-        this.moveBackward(document, aEvent.shiftKey);
-        break;
-      case aEvent.DOM_VK_UP:
-        if (this.editableState & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE) {
-          if (target.selectionEnd != 0)
-            // Don't blur content if caret is not at start of text area.
-            return;
-          else
-            target.blur();
-        }
-
-        if (Utils.MozBuildApp == 'mobile/android')
-          // Return focus to native Android browser chrome.
-          Cc['@mozilla.org/android/bridge;1'].
-            getService(Ci.nsIAndroidBridge).handleGeckoMessage(
-              JSON.stringify({ gecko: { type: 'ToggleChrome:Focus' } }));
-        break;
-      case aEvent.DOM_VK_RETURN:
-      case aEvent.DOM_VK_ENTER:
-        if (this.editableState)
-          return;
-        this.activateCurrent(document);
-        break;
-      default:
-        return;
-    }
-
-    aEvent.preventDefault();
-    aEvent.stopPropagation();
-  },
-
-  moveToPoint: function moveToPoint(aDocument, aX, aY) {
-    Utils.getVirtualCursor(aDocument).moveToPoint(TraversalRules.Simple,
-                                                  aX, aY, true);
-  },
-
-  moveForward: function moveForward(aDocument, aLast, aRule) {
-    let virtualCursor = Utils.getVirtualCursor(aDocument);
-    if (aLast) {
-      virtualCursor.moveLast(TraversalRules.Simple);
-    } else {
-      try {
-        virtualCursor.moveNext(aRule || TraversalRules.Simple);
-      } catch (x) {
-        this.moveCursorToObject(
-          virtualCursor,
-          Utils.AccRetrieval.getAccessibleFor(aDocument.activeElement), aRule);
-      }
-    }
-  },
-
-  moveBackward: function moveBackward(aDocument, aFirst, aRule) {
-    let virtualCursor = Utils.getVirtualCursor(aDocument);
-    if (aFirst) {
-      virtualCursor.moveFirst(TraversalRules.Simple);
-    } else {
-      try {
-        virtualCursor.movePrevious(aRule || TraversalRules.Simple);
-      } catch (x) {
-        this.moveCursorToObject(
-          virtualCursor,
-          Utils.AccRetrieval.getAccessibleFor(aDocument.activeElement), aRule);
-      }
-    }
-  },
-
-  activateCurrent: function activateCurrent(document) {
-    let virtualCursor = Utils.getVirtualCursor(document);
-    let acc = virtualCursor.position;
-
-    if (acc.actionCount > 0) {
-      acc.doAction(0);
-    } else {
-      // XXX Some mobile widget sets do not expose actions properly
-      // (via ARIA roles, etc.), so we need to generate a click.
-      // Could possibly be made simpler in the future. Maybe core
-      // engine could expose nsCoreUtiles::DispatchMouseEvent()?
-      let docAcc = Utils.AccRetrieval.getAccessibleFor(this.chromeWin.document);
-      let docX = {}, docY = {}, docW = {}, docH = {};
-      docAcc.getBounds(docX, docY, docW, docH);
-
-      let objX = {}, objY = {}, objW = {}, objH = {};
-      acc.getBounds(objX, objY, objW, objH);
-
-      let x = Math.round((objX.value - docX.value) + objW.value / 2);
-      let y = Math.round((objY.value - docY.value) + objH.value / 2);
-
-      let cwu = this.chromeWin.QueryInterface(Ci.nsIInterfaceRequestor).
-        getInterface(Ci.nsIDOMWindowUtils);
-      cwu.sendMouseEventToWindow('mousedown', x, y, 0, 1, 0, false);
-      cwu.sendMouseEventToWindow('mouseup', x, y, 0, 1, 0, false);
-    }
-  },
-
-  moveCursorToObject: function moveCursorToObject(aVirtualCursor,
-                                                  aAccessible, aRule) {
-    aVirtualCursor.moveNext(aRule || TraversalRules.Simple, aAccessible, true);
-  },
-
-  keyMap: {
-    a: ['moveForward', TraversalRules.Anchor],
-    A: ['moveBackward', TraversalRules.Anchor],
-    b: ['moveForward', TraversalRules.Button],
-    B: ['moveBackward', TraversalRules.Button],
-    c: ['moveForward', TraversalRules.Combobox],
-    C: ['moveBackward', TraversalRules.Combobox],
-    e: ['moveForward', TraversalRules.Entry],
-    E: ['moveBackward', TraversalRules.Entry],
-    f: ['moveForward', TraversalRules.FormElement],
-    F: ['moveBackward', TraversalRules.FormElement],
-    g: ['moveForward', TraversalRules.Graphic],
-    G: ['moveBackward', TraversalRules.Graphic],
-    h: ['moveForward', TraversalRules.Heading],
-    H: ['moveBackward', TraversalRules.Heading],
-    i: ['moveForward', TraversalRules.ListItem],
-    I: ['moveBackward', TraversalRules.ListItem],
-    k: ['moveForward', TraversalRules.Link],
-    K: ['moveBackward', TraversalRules.Link],
-    l: ['moveForward', TraversalRules.List],
-    L: ['moveBackward', TraversalRules.List],
-    p: ['moveForward', TraversalRules.PageTab],
-    P: ['moveBackward', TraversalRules.PageTab],
-    r: ['moveForward', TraversalRules.RadioButton],
-    R: ['moveBackward', TraversalRules.RadioButton],
-    s: ['moveForward', TraversalRules.Separator],
-    S: ['moveBackward', TraversalRules.Separator],
-    t: ['moveForward', TraversalRules.Table],
-    T: ['moveBackward', TraversalRules.Table],
-    x: ['moveForward', TraversalRules.Checkbox],
-    X: ['moveBackward', TraversalRules.Checkbox]
-  }
-};
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/content-script.js
@@ -0,0 +1,253 @@
+/* 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/. */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+Cu.import('resource://gre/modules/accessibility/EventManager.jsm');
+Cu.import('resource://gre/modules/accessibility/TraversalRules.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+Logger.debug('content-script.js');
+
+function virtualCursorControl(aMessage) {
+  if (Logger.logLevel >= Logger.DEBUG)
+    Logger.debug(aMessage.name, JSON.stringify(aMessage.json));
+
+  try {
+    let vc = Utils.getVirtualCursor(content.document);
+    let origin = aMessage.json.origin;
+    if (origin != 'child') {
+      if (forwardMessage(vc, aMessage))
+        return;
+    }
+
+    let details = aMessage.json;
+    let rule = TraversalRules[details.rule];
+    let moved = 0;
+    switch (details.action) {
+    case 'moveFirst':
+    case 'moveLast':
+      moved = vc[details.action](rule);
+      break;
+    case 'moveNext':
+    case 'movePrevious':
+      try {
+        if (origin == 'parent' && vc.position == null) {
+          if (details.action == 'moveNext')
+            moved = vc.moveFirst(rule);
+          else
+            moved = vc.moveLast(rule);
+        } else {
+          moved = vc[details.action](rule);
+        }
+      } catch (x) {
+        moved = vc.moveNext(rule, content.document.activeElement, true);
+      }
+      break;
+    case 'moveToPoint':
+      moved = vc.moveToPoint(rule, details.x, details.y, true);
+      break;
+    case 'presentLastPivot':
+      EventManager.presentLastPivot();
+      break;
+    default:
+      break;
+    }
+
+    if (moved == true) {
+      forwardMessage(vc, aMessage);
+    } else if (moved == false && details.action != 'moveToPoint') {
+      if (origin == 'parent') {
+        vc.position = null;
+      }
+      aMessage.json.origin = 'child';
+      sendAsyncMessage('AccessFu:VirtualCursor', aMessage.json);
+    }
+  } catch (x) {
+    Logger.error(x);
+  }
+}
+
+function forwardMessage(aVirtualCursor, aMessage) {
+  try {
+    let acc = aVirtualCursor.position;
+    if (acc && acc.role == Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME) {
+      let mm = Utils.getMessageManager(acc.DOMNode);
+      mm.addMessageListener(aMessage.name, virtualCursorControl);
+      aMessage.json.origin = 'parent';
+      // XXX: OOP content's screen offset is 0,
+      // so we remove the real screen offset here.
+      aMessage.json.x -= content.mozInnerScreenX;
+      aMessage.json.y -= content.mozInnerScreenY;
+      mm.sendAsyncMessage(aMessage.name, aMessage.json);
+      return true;
+    }
+  } catch (x) {
+    Logger.error(x);
+  }
+  return false;
+}
+
+function activateCurrent(aMessage) {
+  Logger.debug('activateCurrent');
+  function activateAccessible(aAccessible) {
+    if (aAccessible.actionCount > 0) {
+      aAccessible.doAction(0);
+    } else {
+      // XXX Some mobile widget sets do not expose actions properly
+      // (via ARIA roles, etc.), so we need to generate a click.
+      // Could possibly be made simpler in the future. Maybe core
+      // engine could expose nsCoreUtiles::DispatchMouseEvent()?
+      let docAcc = Utils.AccRetrieval.getAccessibleFor(content.document);
+      let docX = {}, docY = {}, docW = {}, docH = {};
+      docAcc.getBounds(docX, docY, docW, docH);
+
+      let objX = {}, objY = {}, objW = {}, objH = {};
+      aAccessible.getBounds(objX, objY, objW, objH);
+
+      let x = Math.round((objX.value - docX.value) + objW.value / 2);
+      let y = Math.round((objY.value - docY.value) + objH.value / 2);
+
+      let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor).
+        getInterface(Ci.nsIDOMWindowUtils);
+      cwu.sendMouseEventToWindow('mousedown', x, y, 0, 1, 0, false);
+      cwu.sendMouseEventToWindow('mouseup', x, y, 0, 1, 0, false);
+    }
+  }
+
+  let vc = Utils.getVirtualCursor(content.document);
+  if (!forwardMessage(vc, aMessage))
+    activateAccessible(vc.position);
+}
+
+function scroll(aMessage) {
+  let vc = Utils.getVirtualCursor(content.document);
+
+  function tryToScroll() {
+    let horiz = aMessage.json.horizontal;
+    let page = aMessage.json.page;
+
+    // Search up heirarchy for scrollable element.
+    let acc = vc.position;
+    while (acc) {
+      let elem = acc.DOMNode;
+
+      // We will do window scrolling next.
+      if (elem == content.document)
+        break;
+
+      if (!horiz && elem.clientHeight < elem.scrollHeight) {
+        let s = content.getComputedStyle(elem);
+        if (s.overflowY == 'scroll' || s.overflowY == 'auto') {
+          elem.scrollTop += page * elem.clientHeight;
+          return true;
+        }
+      }
+
+      if (horiz) {
+        if (elem.clientWidth < elem.scrollWidth) {
+          let s = content.getComputedStyle(elem);
+          if (s.overflowX == 'scroll' || s.overflowX == 'auto') {
+            elem.scrollLeft += page * elem.clientWidth;
+            return true;
+          }
+        }
+
+        let controllers = acc.
+          getRelationByType(
+            Ci.nsIAccessibleRelation.RELATION_CONTROLLED_BY);
+        for (let i = 0; controllers.targetsCount > i; i++) {
+          let controller = controllers.getTarget(i);
+          // If the section has a controlling slider, it should be considered
+          // the page-turner.
+          if (controller.role == Ci.nsIAccessibleRole.ROLE_SLIDER) {
+            // Sliders are controlled with ctrl+right/left. I just decided :)
+            let evt = content.document.createEvent('KeyboardEvent');
+            evt.initKeyEvent(
+              'keypress', true, true, null,
+              true, false, false, false,
+              (page > 0) ? evt.DOM_VK_RIGHT : evt.DOM_VK_LEFT, 0);
+            controller.DOMNode.dispatchEvent(evt);
+            return true;
+          }
+        }
+      }
+      acc = acc.parent;
+    }
+
+    // Scroll window.
+    if (!horiz && content.scrollMaxY &&
+        ((page > 0 && content.scrollY < content.scrollMaxY) ||
+         (page < 0 && content.scrollY > 0))) {
+      content.scroll(0, content.innerHeight);
+      return true;
+    } else if (horiz && content.scrollMaxX &&
+               ((page > 0 && content.scrollX < content.scrollMaxX) ||
+                (page < 0 && content.scrollX > 0))) {
+      content.scroll(content.innerWidth, 0);
+      return true;
+    }
+
+    return false;
+  }
+
+  if (aMessage.json.origin != 'child') {
+    if (forwardMessage(vc, aMessage))
+      return;
+  }
+
+  if (!tryToScroll()) {
+    // Failed to scroll anything in this document. Try in parent document.
+    aMessage.json.origin = 'child';
+    sendAsyncMessage('AccessFu:Scroll', aMessage.json);
+  }
+}
+
+addMessageListener('AccessFu:VirtualCursor', virtualCursorControl);
+addMessageListener('AccessFu:Activate', activateCurrent);
+addMessageListener('AccessFu:Scroll', scroll);
+
+addMessageListener(
+  'AccessFu:Start',
+  function(m) {
+    if (m.json.buildApp)
+      Utils.MozBuildApp = m.json.buildApp;
+
+    EventManager.start(
+      function sendMessage(aName, aDetails) {
+        sendAsyncMessage(aName, aDetails);
+      });
+
+    docShell.QueryInterface(Ci.nsIInterfaceRequestor).
+      getInterface(Ci.nsIWebProgress).
+      addProgressListener(EventManager,
+                          (Ci.nsIWebProgress.NOTIFY_STATE_ALL |
+                           Ci.nsIWebProgress.NOTIFY_LOCATION));
+    addEventListener('scroll', EventManager, true);
+    addEventListener('resize', EventManager, true);
+    // XXX: Ideally this would be an a11y event. Bug #742280.
+    addEventListener('DOMActivate', EventManager, true);
+  });
+
+addMessageListener(
+  'AccessFu:Stop',
+  function(m) {
+    Logger.debug('AccessFu:Stop');
+
+    EventManager.stop();
+
+    docShell.QueryInterface(Ci.nsIInterfaceRequestor).
+      getInterface(Ci.nsIWebProgress).
+      removeProgressListener(EventManager);
+    removeEventListener('scroll', EventManager, true);
+    removeEventListener('resize', EventManager, true);
+    // XXX: Ideally this would be an a11y event. Bug #742280.
+    removeEventListener('DOMActivate', EventManager, true);
+  });
+
+sendAsyncMessage('AccessFu:Ready');
--- a/accessible/src/jsat/jar.mn
+++ b/accessible/src/jsat/jar.mn
@@ -1,6 +1,7 @@
 # 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/.
 
 toolkit.jar:
     content/global/accessibility/AccessFu.css (AccessFu.css)
+    content/global/accessibility/content-script.js (content-script.js)