Bug 780350 - Introduce TouchAdapter and add some gesture support. r=davidb
authorEitan Isaacson <eitan@monotonous.org>
Fri, 17 Aug 2012 15:49:34 -0700
changeset 102688 2e24233c9339e69ed785b95a71efe004f0a97d2a
parent 102687 3131d6765a0e611773a1657ffb9a9f765ab07a03
child 102689 0d9fbfd32c1055172df7f80ed47e410ee8315edd
push id23303
push userryanvm@gmail.com
push dateSat, 18 Aug 2012 11:22:19 +0000
treeherdermozilla-central@9c48df21d744 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdavidb
bugs780350
milestone17.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 780350 - Introduce TouchAdapter and add some gesture support. r=davidb
accessible/src/jsat/AccessFu.css
accessible/src/jsat/AccessFu.jsm
accessible/src/jsat/Presenters.jsm
accessible/src/jsat/TouchAdapter.jsm
accessible/src/jsat/VirtualCursorController.jsm
--- a/accessible/src/jsat/AccessFu.css
+++ b/accessible/src/jsat/AccessFu.css
@@ -1,22 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#virtual-cursor-box { 
+#virtual-cursor-box {
   position: fixed;
   border: 1px solid orange;
   pointer-events: none;
   display: none;
   border-radius: 2px;
   box-shadow: 1px 1px 1px #444;
 }
 
-#virtual-cursor-inset { 
+#virtual-cursor-inset {
   border-radius: 1px;
   box-shadow: inset 1px 1px 1px #444;
   display: block;
   box-sizing: border-box;
   width: 100%;
   height: 100%;
   pointer-events: none;
-}
\ No newline at end of file
+}
+
+#accessfu-glass {
+  width: 100%;
+  height: 100%;
+  position: fixed;
+  top: 0px;
+  left: 0px;
+}
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -11,16 +11,17 @@ const Cr = Components.results;
 
 var EXPORTED_SYMBOLS = ['AccessFu'];
 
 Cu.import('resource://gre/modules/Services.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.
@@ -39,40 +40,55 @@ var AccessFu = {
     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);
 
-    if (Utils.MozBuildApp == 'mobile/android')
-      Services.obs.addObserver(this, 'Accessibility:Settings', false);
+    this.touchAdapter = TouchAdapter;
 
-    if (Utils.MozBuildApp == 'b2g')
-      aWindow.addEventListener(
-        'ContentStart',
-        (function(event) {
-           let content = aWindow.shell.contentBrowser.contentWindow;
-           content.addEventListener('mozContentEvent', this, false, true);
-         }).bind(this), false);
+    switch(Utils.MozBuildApp) {
+      case 'mobile/android':
+        Services.obs.addObserver(this, 'Accessibility:Settings', false);
+        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();
   },
 
   /**
    * 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');
+
+    // 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());
 
     // Implicitly add the Android presenter on Android.
     if (Utils.MozBuildApp == 'mobile/android')
       this.addPresenter(new AndroidPresenter());
     else if (Utils.MozBuildApp == 'b2g')
       this.addPresenter(new SpeechPresenter());
 
@@ -91,16 +107,18 @@ var AccessFu = {
    */
   _disable: function _disable() {
     if (!this._enabled)
       return;
     this._enabled = false;
 
     Logger.info('disable');
 
+    this.chromeWin.document.removeChild(this.stylesheet);
+
     this.presenters.forEach(function(p) { p.detach(); });
     this.presenters = [];
 
     VirtualCursorController.detach();
 
     Services.obs.removeObserver(this, 'accessible-event');
     this.chromeWin.removeEventListener('DOMActivate', this, true);
     this.chromeWin.removeEventListener('resize', this, true);
@@ -133,18 +151,20 @@ var AccessFu = {
       }
     }
 
     if (accessPref == ACCESSFU_ENABLE)
       this._enable();
     else
       this._disable();
 
-    VirtualCursorController.exploreByTouch = ebtPref == ACCESSFU_ENABLE;
-    Logger.info('Explore by touch:', VirtualCursorController.exploreByTouch);
+    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) {
--- a/accessible/src/jsat/Presenters.jsm
+++ b/accessible/src/jsat/Presenters.jsm
@@ -112,38 +112,31 @@ VisualPresenter.prototype = {
   /**
    * The padding in pixels between the object and the highlight border.
    */
   BORDER_PADDING: 2,
 
   attach: function VisualPresenter_attach(aWindow) {
     this.chromeWin = aWindow;
 
-    // Add stylesheet
-    let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
-    this.stylesheet = aWindow.document.createProcessingInstruction(
-      'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
-    aWindow.document.insertBefore(this.stylesheet, aWindow.document.firstChild);
-
     // Add highlight box
     this.highlightBox = this.chromeWin.document.
       createElementNS('http://www.w3.org/1999/xhtml', 'div');
     this.chromeWin.document.documentElement.appendChild(this.highlightBox);
     this.highlightBox.id = 'virtual-cursor-box';
 
     // Add highlight inset for inner shadow
     let inset = this.chromeWin.document.
       createElementNS('http://www.w3.org/1999/xhtml', 'div');
     inset.id = 'virtual-cursor-inset';
 
     this.highlightBox.appendChild(inset);
   },
 
   detach: function VisualPresenter_detach() {
-    this.chromeWin.document.removeChild(this.stylesheet);
     this.highlightBox.parentNode.removeChild(this.highlightBox);
     this.highlightBox = this.stylesheet = null;
   },
 
   viewportChanged: function VisualPresenter_viewportChanged() {
     if (this._currentObject)
       this._highlight(this._currentObject);
   },
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/TouchAdapter.jsm
@@ -0,0 +1,402 @@
+/* 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 = ['TouchAdapter', 'AndroidTouchAdapter'];
+
+Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+
+// We should not be emitting explore events more than 10 times a second.
+// It is granular enough to feel natural, and it does not hammer the CPU.
+const EXPLORE_THROTTLE = 100;
+
+var TouchAdapter = {
+  // minimal swipe distance in inches
+  SWIPE_MIN_DISTANCE: 0.4,
+
+  // maximum duration of swipe
+  SWIPE_MAX_DURATION: 400,
+
+  // how straight does a swipe need to be
+  SWIPE_DIRECTNESS: 1.2,
+
+  // maximum consecutive
+  MAX_CONSECUTIVE_GESTURE_DELAY: 400,
+
+  // delay before tap turns into dwell
+  DWELL_THRESHOLD: 500,
+
+  // delay before distinct dwell events
+  DWELL_REPEAT_DELAY: 300,
+
+  // maximum distance the mouse could move during a tap in inches
+  TAP_MAX_RADIUS: 0.2,
+
+  attach: function TouchAdapter_attach(aWindow) {
+    if (this.chromeWin)
+      return;
+
+    Logger.info('TouchAdapter.attach');
+
+    this.chromeWin = aWindow;
+    this._touchPoints = {};
+    this._dwellTimeout = 0;
+    this._prevGestures = {};
+    this._lastExploreTime = 0;
+    this._dpi = this.chromeWin.QueryInterface(Ci.nsIInterfaceRequestor).
+      getInterface(Ci.nsIDOMWindowUtils).displayDPI;
+
+    this.glass = this.chromeWin.document.
+      createElementNS('http://www.w3.org/1999/xhtml', 'div');
+    this.glass.id = 'accessfu-glass';
+    this.chromeWin.document.documentElement.appendChild(this.glass);
+
+    this.glass.addEventListener('touchend', this, true, true);
+    this.glass.addEventListener('touchmove', this, true, true);
+    this.glass.addEventListener('touchstart', this, true, true);
+
+    if (Utils.OS != 'Android')
+      Mouse2Touch.attach(aWindow);
+  },
+
+  detach: function TouchAdapter_detach(aWindow) {
+    if (!this.chromeWin)
+      return;
+
+    Logger.info('TouchAdapter.detach');
+
+    this.glass.removeEventListener('touchend', this, true, true);
+    this.glass.removeEventListener('touchmove', this, true, true);
+    this.glass.removeEventListener('touchstart', this, true, true);
+    this.glass.parentNode.removeChild(this.glass);
+
+    if (Utils.OS != 'Android')
+      Mouse2Touch.detach(aWindow);
+
+    delete this.chromeWin;
+  },
+
+  handleEvent: function TouchAdapter_handleEvent(aEvent) {
+    let touches = aEvent.changedTouches;
+    switch (aEvent.type) {
+      case 'touchstart':
+        for (var i = 0; i < touches.length; i++) {
+          let touch = touches[i];
+          let touchPoint = new TouchPoint(touch, aEvent.timeStamp, this._dpi);
+          this._touchPoints[touch.identifier] = touchPoint;
+          this._lastExploreTime = aEvent.timeStamp + this.SWIPE_MAX_DURATION;
+        }
+        this._dwellTimeout = this.chromeWin.setTimeout(
+          (function () {
+             this.compileAndEmit(aEvent.timeStamp + this.DWELL_THRESHOLD);
+           }).bind(this), this.DWELL_THRESHOLD);
+        break;
+      case 'touchmove':
+        for (var i = 0; i < touches.length; i++) {
+          let touch = touches[i];
+          let touchPoint = this._touchPoints[touch.identifier];
+          touchPoint.update(touch, aEvent.timeStamp);
+        }
+        if (aEvent.timeStamp - this._lastExploreTime >= EXPLORE_THROTTLE) {
+          this.compileAndEmit(aEvent.timeStamp);
+          this._lastExploreTime = aEvent.timeStamp;
+        }
+        break;
+      case 'touchend':
+        for (var i = 0; i < touches.length; i++) {
+          let touch = touches[i];
+          let touchPoint = this._touchPoints[touch.identifier];
+          touchPoint.update(touch, aEvent.timeStamp);
+          touchPoint.finish();
+        }
+        this.compileAndEmit(aEvent.timeStamp);
+        break;
+    }
+  },
+
+  cleanupTouches: function cleanupTouches() {
+    for (var identifier in this._touchPoints) {
+      if (!this._touchPoints[identifier].done)
+        continue;
+
+      delete this._touchPoints[identifier];
+    }
+  },
+
+  compile: function TouchAdapter_compile(aTime) {
+    let multiDetails = {};
+
+    // Compound multiple simultaneous touch gestures.
+    for (let identifier in this._touchPoints) {
+      let touchPoint = this._touchPoints[identifier];
+      let details = touchPoint.compile(aTime);
+
+      if (!details)
+        continue;
+
+      details.touches = [identifier];
+
+      let otherTouches = multiDetails[details.type];
+      if (otherTouches) {
+        otherTouches.touches.push(identifier);
+        otherTouches.startTime =
+          Math.min(otherTouches.startTime, touchPoint.startTime);
+      } else {
+        details.startTime = touchPoint.startTime;
+        details.endTime = aTime;
+        multiDetails[details.type] = details;
+      }
+    }
+
+    // Compound multiple consecutive touch gestures.
+    for each (let details in multiDetails) {
+      let idhash = details.touches.slice().sort().toString();
+      let prevGesture = this._prevGestures[idhash];
+
+      if (prevGesture) {
+        // The time delta is calculated as the period between the end of the
+        // last gesture and the start of this one.
+        let timeDelta = details.startTime - prevGesture.endTime;
+        if (timeDelta > this.MAX_CONSECUTIVE_GESTURE_DELAY) {
+          delete this._prevGestures[idhash];
+        } else {
+          if (details.type == 'tap' && prevGesture.type == 'tap')
+            details.type = 'doubletap';
+          if (details.type == 'tap' && prevGesture.type == 'doubletap')
+            details.type = 'tripletap';
+          if (details.type == 'dwell' && prevGesture.type == 'tap')
+            details.type = 'taphold';
+        }
+      }
+
+      this._prevGestures[idhash] = details;
+    }
+
+    this.chromeWin.clearTimeout(this._dwellTimeout);
+    this.cleanupTouches();
+
+    return multiDetails;
+  },
+
+  emitGesture: function TouchAdapter_emitGesture(aDetails) {
+    let evt = this.chromeWin.document.createEvent('CustomEvent');
+    evt.initCustomEvent('mozAccessFuGesture', true, true, aDetails);
+    this.chromeWin.dispatchEvent(evt);
+  },
+
+  compileAndEmit: function TouchAdapter_compileAndEmit(aTime) {
+    for each (let details in this.compile(aTime)) {
+      this.emitGesture(details);
+    }
+  }
+};
+
+/***
+ * A TouchPoint represents a single touch from the moment of contact until it is
+ * lifted from the surface. It is capable of compiling gestures from the scope
+ * of one single touch.
+ */
+function TouchPoint(aTouch, aTime, aDPI) {
+  this.startX = aTouch.screenX;
+  this.startY = aTouch.screenY;
+  this.startTime = aTime;
+  this.distanceTraveled = 0;
+  this.dpi = aDPI;
+  this.done = false;
+}
+
+TouchPoint.prototype = {
+  update: function TouchPoint_update(aTouch, aTime) {
+    let lastX = this.x;
+    let lastY = this.y;
+    this.x = aTouch.screenX;
+    this.y = aTouch.screenY;
+    this.time = aTime;
+
+    if (lastX != undefined && lastY != undefined)
+      this.distanceTraveled += this.getDistanceToCoord(lastX, lastY);
+  },
+
+  getDistanceToCoord: function TouchPoint_getDistanceToCoord(aX, aY) {
+    return Math.sqrt(Math.pow(this.x - aX, 2) + Math.pow(this.y - aY, 2));
+  },
+
+  finish: function TouchPoint_finish() {
+    this.done = true;
+  },
+
+  /**
+   * Compile a gesture from an individual touch point. This is used by the
+   * TouchAdapter to compound multiple single gestures in to higher level
+   * gestures.
+   */
+  compile: function TouchPoint_compile(aTime) {
+    let directDistance = this.directDistanceTraveled;
+    let duration = aTime - this.startTime;
+
+    // To be considered a tap/dwell...
+    if ((this.distanceTraveled / this.dpi) < TouchAdapter.TAP_MAX_RADIUS) { // Didn't travel
+      if (duration < TouchAdapter.DWELL_THRESHOLD) {
+        // Mark it as done so we don't use this touch for another gesture.
+        this.finish();
+        return {type: 'tap', x: this.startX, y: this.startY};
+      } else if (!this.done && duration == TouchAdapter.DWELL_THRESHOLD) {
+        return {type: 'dwell', x: this.startX, y: this.startY};
+      }
+    }
+
+    // To be considered a swipe...
+    if (duration <= TouchAdapter.SWIPE_MAX_DURATION && // Quick enough
+        (directDistance / this.dpi) >= TouchAdapter.SWIPE_MIN_DISTANCE && // Traveled far
+        (directDistance * 1.2) >= this.distanceTraveled) { // Direct enough
+
+      let swipeGesture = {x1: this.startX, y1: this.startY,
+                          x2: this.x, y2: this.y};
+      let deltaX = this.x - this.startX;
+      let deltaY = this.y - this.startY;
+
+      if (Math.abs(deltaX) > Math.abs(deltaY)) {
+        // Horizontal swipe.
+        if (deltaX > 0)
+          swipeGesture.type = 'swiperight';
+        else
+          swipeGesture.type = 'swipeleft';
+      } else if (Math.abs(deltaX) < Math.abs(deltaY)) {
+        // Vertical swipe.
+        if (deltaY > 0)
+          swipeGesture.type = 'swipedown';
+        else
+          swipeGesture.type = 'swipeup';
+      } else {
+        // A perfect 45 degree swipe?? Not in our book.
+          return null;
+      }
+
+      this.finish();
+
+      return swipeGesture;
+    }
+
+    // To be considered an explore...
+    if (!this.done &&
+        duration > TouchAdapter.SWIPE_MAX_DURATION &&
+        (this.distanceTraveled / this.dpi) > TouchAdapter.TAP_MAX_RADIUS) {
+      return {type: 'explore', x: this.x, y: this.y};
+    }
+
+    return null;
+  },
+
+  get directDistanceTraveled() {
+    return this.getDistanceToCoord(this.startX, this.startY);
+  }
+};
+
+var Mouse2Touch = {
+  _MouseToTouchMap: {
+    mousedown: 'touchstart',
+    mouseup: 'touchend',
+    mousemove: 'touchmove'
+  },
+
+  attach: function Mouse2Touch_attach(aWindow) {
+    this.chromeWin = aWindow;
+    this.chromeWin.addEventListener('mousedown', this, true, true);
+    this.chromeWin.addEventListener('mouseup', this, true, true);
+    this.chromeWin.addEventListener('mousemove', this, true, true);
+  },
+
+  detach: function Mouse2Touch_detach(aWindow) {
+    this.chromeWin.removeEventListener('mousedown', this, true, true);
+    this.chromeWin.removeEventListener('mouseup', this, true, true);
+    this.chromeWin.removeEventListener('mousemove', this, true, true);
+  },
+
+  handleEvent: function Mouse2Touch_handleEvent(aEvent) {
+    if (aEvent.buttons == 0)
+      return;
+
+    let name = this._MouseToTouchMap[aEvent.type];
+    let evt = this.chromeWin.document.createEvent("touchevent");
+    let points = [this.chromeWin.document.createTouch(
+                    this.chromeWin, aEvent.target, 0,
+                    aEvent.pageX, aEvent.pageY, aEvent.screenX, aEvent.screenY,
+                    aEvent.clientX, aEvent.clientY, 1, 1, 0, 0)];
+
+    // Simulate another touch point at a 5px offset when ctrl is pressed.
+    if (aEvent.ctrlKey)
+      points.push(this.chromeWin.document.createTouch(
+                    this.chromeWin, aEvent.target, 1,
+                    aEvent.pageX + 5, aEvent.pageY + 5,
+                    aEvent.screenX + 5, aEvent.screenY + 5,
+                    aEvent.clientX + 5, aEvent.clientY + 5,
+                    1, 1, 0, 0));
+
+    // Simulate another touch point at a -5px offset when alt is pressed.
+    if (aEvent.altKey)
+      points.push(this.chromeWin.document.createTouch(
+                    this.chromeWin, aEvent.target, 2,
+                    aEvent.pageX - 5, aEvent.pageY - 5,
+                    aEvent.screenX - 5, aEvent.screenY - 5,
+                    aEvent.clientX - 5, aEvent.clientY - 5,
+                    1, 1, 0, 0));
+
+    let touches = this.chromeWin.document.createTouchList(points);
+    if (name == "touchend") {
+      let empty = this.chromeWin.document.createTouchList();
+      evt.initTouchEvent(name, true, true, this.chromeWin, 0,
+                         false, false, false, false, empty, empty, touches);
+    } else {
+      evt.initTouchEvent(name, true, true, this.chromeWin, 0,
+                         false, false, false, false, touches, touches, touches);
+    }
+    aEvent.target.dispatchEvent(evt);
+    aEvent.preventDefault();
+    aEvent.stopImmediatePropagation();
+  }
+};
+
+var AndroidTouchAdapter = {
+  attach: function AndroidTouchAdapter_attach(aWindow) {
+    if (this.chromeWin)
+      return;
+
+    Logger.info('AndroidTouchAdapter.attach');
+
+    this.chromeWin = aWindow;
+    this.chromeWin.addEventListener('mousemove', this, true, true);
+    this._lastExploreTime = 0;
+  },
+
+  detach: function AndroidTouchAdapter_detach(aWindow) {
+    if (!this.chromeWin)
+      return;
+
+    Logger.info('AndroidTouchAdapter.detach');
+
+    this.chromeWin.removeEventListener('mousemove', this, true, true);
+    delete this.chromeWin;
+  },
+
+  handleEvent: function AndroidTouchAdapter_handleEvent(aEvent) {
+    // On non-Android we use the shift key to simulate touch.
+    if (Utils.MozBuildApp != 'mobile/android' && !aEvent.shiftKey)
+      return;
+
+    if (aEvent.timeStamp - this._lastExploreTime >= EXPLORE_THROTTLE) {
+      let evt = this.chromeWin.document.createEvent('CustomEvent');
+      evt.initCustomEvent(
+        'mozAccessFuGesture', true, true,
+        {type: 'explore', x: aEvent.screenX, y: aEvent.screenY});
+      this.chromeWin.dispatchEvent(evt);
+      this._lastExploreTime = aEvent.timeStamp;
+    }
+  }
+};
\ No newline at end of file
--- a/accessible/src/jsat/VirtualCursorController.jsm
+++ b/accessible/src/jsat/VirtualCursorController.jsm
@@ -192,55 +192,58 @@ var TraversalRules = {
 
 var VirtualCursorController = {
   exploreByTouch: false,
   editableState: 0,
 
   attach: function attach(aWindow) {
     this.chromeWin = aWindow;
     this.chromeWin.document.addEventListener('keypress', this, true);
-    this.chromeWin.document.addEventListener('mousemove', this, true);
+    this.chromeWin.addEventListener('mozAccessFuGesture', this, true);
   },
 
   detach: function detach() {
     this.chromeWin.document.removeEventListener('keypress', this, true);
-    this.chromeWin.document.removeEventListener('mousemove', this, true);
+    this.chromeWin.removeEventListener('mozAccessFuGesture', this, true);
   },
 
   handleEvent: function VirtualCursorController_handleEvent(aEvent) {
     switch (aEvent.type) {
       case 'keypress':
         this._handleKeypress(aEvent);
         break;
-      case 'mousemove':
-        this._handleMousemove(aEvent);
+      case 'mozAccessFuGesture':
+        this._handleGesture(aEvent);
         break;
     }
   },
 
-  _handleMousemove: function _handleMousemove(aEvent) {
-    // Explore by touch is disabled.
-    if (!this.exploreByTouch)
-      return;
+  _handleGesture: function _handleGesture(aEvent) {
+    let document = Utils.getCurrentContentDoc(this.chromeWin);
+    let detail = aEvent.detail;
+    Logger.info('Gesture', detail.type,
+                '(fingers: ' + detail.touches.length + ')');
 
-    // On non-Android we use the shift key to simulate touch.
-    if (Utils.OS != 'Android' && !aEvent.shiftKey)
-      return;
-
-    // We should not be calling moveToPoint more than 10 times a second.
-    // It is granular enough to feel natural, and it does not hammer the CPU.
-    if (!this._handleMousemove._lastEventTime ||
-        aEvent.timeStamp - this._handleMousemove._lastEventTime >= 100) {
-      this.moveToPoint(Utils.getCurrentContentDoc(this.chromeWin),
-                       aEvent.screenX, aEvent.screenY);
-      this._handleMousemove._lastEventTime = aEvent.timeStamp;
+    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;
+      }
     }
 
-    aEvent.preventDefault();
-    aEvent.stopImmediatePropagation();
   },
 
   _handleKeypress: function _handleKeypress(aEvent) {
     let document = Utils.getCurrentContentDoc(this.chromeWin);
     let target = aEvent.target;
 
     switch (aEvent.keyCode) {
       case 0: