Bug 985541 - Turn GestureEventListener into Finite-state machine. r=kats,drs
authorDmitry Rozhkov <dmitry.rojkov@gmail.com>
Thu, 27 Mar 2014 14:04:39 +0200
changeset 175646 de2d3a740e06cdb3279cbb8e88cb63db62445ebe
parent 175645 852fe5a5434dd6a3598902c1115e63dd32b8566c
child 175647 fe0c6926b8b4c9f75d48f2e1de8c7064cd6ac901
push id26496
push userkwierso@gmail.com
push dateFri, 28 Mar 2014 02:28:34 +0000
treeherdermozilla-central@3c09159e01da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats, drs
bugs985541
milestone31.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 985541 - Turn GestureEventListener into Finite-state machine. r=kats,drs
gfx/layers/ipc/AsyncPanZoomController.cpp
gfx/layers/ipc/AsyncPanZoomController.h
gfx/layers/ipc/GestureEventListener.cpp
gfx/layers/ipc/GestureEventListener.h
gfx/tests/gtest/TestAsyncPanZoomController.cpp
--- a/gfx/layers/ipc/AsyncPanZoomController.cpp
+++ b/gfx/layers/ipc/AsyncPanZoomController.cpp
@@ -589,16 +589,28 @@ nsEventStatus AsyncPanZoomController::Ha
       case MultiTouchInput::MULTITOUCH_START: rv = OnTouchStart(multiTouchInput); break;
       case MultiTouchInput::MULTITOUCH_MOVE: rv = OnTouchMove(multiTouchInput); break;
       case MultiTouchInput::MULTITOUCH_END: rv = OnTouchEnd(multiTouchInput); break;
       case MultiTouchInput::MULTITOUCH_CANCEL: rv = OnTouchCancel(multiTouchInput); break;
       default: NS_WARNING("Unhandled multitouch"); break;
     }
     break;
   }
+  default: NS_WARNING("Unhandled input event"); break;
+  }
+
+  mLastEventTime = aEvent.mTime;
+  return rv;
+}
+
+nsEventStatus AsyncPanZoomController::HandleGestureEvent(const InputData& aEvent)
+{
+  nsEventStatus rv = nsEventStatus_eIgnore;
+
+  switch (aEvent.mInputType) {
   case PINCHGESTURE_INPUT: {
     const PinchGestureInput& pinchGestureInput = aEvent.AsPinchGestureInput();
     switch (pinchGestureInput.mType) {
       case PinchGestureInput::PINCHGESTURE_START: rv = OnScaleBegin(pinchGestureInput); break;
       case PinchGestureInput::PINCHGESTURE_SCALE: rv = OnScale(pinchGestureInput); break;
       case PinchGestureInput::PINCHGESTURE_END: rv = OnScaleEnd(pinchGestureInput); break;
       default: NS_WARNING("Unhandled pinch gesture"); break;
     }
--- a/gfx/layers/ipc/AsyncPanZoomController.h
+++ b/gfx/layers/ipc/AsyncPanZoomController.h
@@ -251,16 +251,25 @@ public:
 
   /**
    * Handler for events which should not be intercepted by the touch listener.
    * Does the work for ReceiveInputEvent().
    */
   nsEventStatus HandleInputEvent(const InputData& aEvent);
 
   /**
+   * Handler for gesture events.
+   * Currently some gestures are detected in GestureEventListener that calls
+   * APZC back through this handler in order to avoid recursive calls to
+   * APZC::HandleInputEvent() which is supposed to do the work for
+   * ReceiveInputEvent().
+   */
+  nsEventStatus HandleGestureEvent(const InputData& aEvent);
+
+  /**
    * Populates the provided object (if non-null) with the scrollable guid of this apzc.
    */
   void GetGuid(ScrollableLayerGuid* aGuidOut);
 
   /**
    * Returns the scrollable guid of this apzc.
    */
   ScrollableLayerGuid GetGuid();
--- a/gfx/layers/ipc/GestureEventListener.cpp
+++ b/gfx/layers/ipc/GestureEventListener.cpp
@@ -3,22 +3,19 @@
 /* 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/. */
 
 #include "GestureEventListener.h"
 #include <math.h>                       // for fabsf
 #include <stddef.h>                     // for size_t
 #include "AsyncPanZoomController.h"     // for AsyncPanZoomController
-#include "mozilla/layers/APZCTreeManager.h"  // for APZCTreeManager
 #include "base/task.h"                  // for CancelableTask, etc
 #include "gfxPrefs.h"                   // for gfxPrefs
-#include "mozilla/gfx/BasePoint.h"      // for BasePoint
-#include "mozilla/mozalloc.h"           // for operator new
-#include "nsDebug.h"                    // for NS_WARN_IF_FALSE
+#include "nsDebug.h"                    // for NS_WARNING
 #include "nsMathUtils.h"                // for NS_hypot
 
 namespace mozilla {
 namespace layers {
 
 /**
  * Maximum time for a touch on the screen and corresponding lift of the finger
  * to be considered a tap. This also applies to double taps, except that it is
@@ -29,374 +26,437 @@ static const uint32_t MAX_TAP_TIME = 300
 /**
  * Amount of change in span needed to take us from the GESTURE_WAITING_PINCH
  * state to the GESTURE_PINCH state. This is measured as a change in distance
  * between the fingers used to compute the span ratio. Note that it is a
  * distance, not a displacement.
  */
 static const float PINCH_START_THRESHOLD = 35.0f;
 
+ScreenPoint GetCurrentFocus(const MultiTouchInput& aEvent)
+{
+  const ScreenIntPoint& firstTouch = aEvent.mTouches[0].mScreenPoint,
+                       secondTouch = aEvent.mTouches[1].mScreenPoint;
+  return ScreenPoint(firstTouch + secondTouch) / 2;
+}
+
+float GetCurrentSpan(const MultiTouchInput& aEvent)
+{
+  const ScreenIntPoint& firstTouch = aEvent.mTouches[0].mScreenPoint,
+                       secondTouch = aEvent.mTouches[1].mScreenPoint;
+  ScreenIntPoint delta = secondTouch - firstTouch;
+  return float(NS_hypot(delta.x, delta.y));
+}
+
 GestureEventListener::GestureEventListener(AsyncPanZoomController* aAsyncPanZoomController)
   : mAsyncPanZoomController(aAsyncPanZoomController),
     mState(GESTURE_NONE),
     mSpanChange(0.0f),
-    mTapStartTime(0),
-    mLastTapEndTime(0),
+    mPreviousSpan(0.0f),
     mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0, 0)
 {
 }
 
 GestureEventListener::~GestureEventListener()
 {
 }
 
 nsEventStatus GestureEventListener::HandleInputEvent(const MultiTouchInput& aEvent)
 {
+  nsEventStatus rv = nsEventStatus_eIgnore;
+
   // Cache the current event since it may become the single or long tap that we
   // send.
   mLastTouchInput = aEvent;
 
-  switch (aEvent.mType)
-  {
+  switch (aEvent.mType) {
   case MultiTouchInput::MULTITOUCH_START:
-  case MultiTouchInput::MULTITOUCH_ENTER: {
+  case MultiTouchInput::MULTITOUCH_ENTER:
+    mTouches.Clear();
     for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
-      bool foundAlreadyExistingTouch = false;
-      for (size_t j = 0; j < mTouches.Length(); j++) {
-        if (mTouches[j].mIdentifier == aEvent.mTouches[i].mIdentifier) {
-          foundAlreadyExistingTouch = true;
-          break;
-        }
-      }
-
-      // If we didn't find a touch in our list that matches this, then add it.
-      if (!foundAlreadyExistingTouch) {
-        mTouches.AppendElement(aEvent.mTouches[i]);
-      }
-    }
-
-    size_t length = mTouches.Length();
-    if (length == 1) {
-      mTapStartTime = aEvent.mTime;
-      mTouchStartPosition = aEvent.mTouches[0].mScreenPoint;
-      if (mState == GESTURE_NONE) {
-        mState = GESTURE_WAITING_SINGLE_TAP;
-
-        mLongTapTimeoutTask =
-          NewRunnableMethod(this, &GestureEventListener::TimeoutLongTap);
-
-        mAsyncPanZoomController->PostDelayedTask(
-          mLongTapTimeoutTask,
-          gfxPrefs::UiClickHoldContextMenusDelay());
-      }
-    } else if (length == 2) {
-      // Another finger has been added; it can't be a tap anymore.
-      HandleTapCancel(aEvent);
+      mTouches.AppendElement(aEvent.mTouches[i]);
     }
 
-    break;
-  }
-  case MultiTouchInput::MULTITOUCH_MOVE: {
-    // If we move too much, bail out of the tap.
-    ScreenIntPoint delta = aEvent.mTouches[0].mScreenPoint - mTouchStartPosition;
-    if (mTouches.Length() == 1 &&
-        NS_hypot(delta.x, delta.y) > AsyncPanZoomController::GetTouchStartTolerance())
-    {
-      HandleTapCancel(aEvent);
-    }
-
-    size_t eventTouchesMatched = 0;
-    for (size_t i = 0; i < mTouches.Length(); i++) {
-      bool isTouchRemoved = true;
-      for (size_t j = 0; j < aEvent.mTouches.Length(); j++) {
-        if (mTouches[i].mIdentifier == aEvent.mTouches[j].mIdentifier) {
-          eventTouchesMatched++;
-          isTouchRemoved = false;
-          mTouches[i] = aEvent.mTouches[j];
-        }
-      }
-      if (isTouchRemoved) {
-        // this touch point was lifted, so remove it from our list
-        mTouches.RemoveElementAt(i);
-        i--;
-      }
+    if (aEvent.mTouches.Length() == 1) {
+      rv = HandleInputTouchSingleStart();
+    } else {
+      rv = HandleInputTouchMultiStart();
     }
-
-    NS_WARN_IF_FALSE(eventTouchesMatched == aEvent.mTouches.Length(), "Touch moved, but not in list");
-
     break;
-  }
+  case MultiTouchInput::MULTITOUCH_MOVE:
+    rv = HandleInputTouchMove();
+    break;
   case MultiTouchInput::MULTITOUCH_END:
-  case MultiTouchInput::MULTITOUCH_LEAVE: {
+  case MultiTouchInput::MULTITOUCH_LEAVE:
     for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
-      bool foundAlreadyExistingTouch = false;
-      for (size_t j = 0; j < mTouches.Length() && !foundAlreadyExistingTouch; j++) {
+      for (size_t j = 0; j < mTouches.Length(); j++) {
         if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) {
-          foundAlreadyExistingTouch = true;
           mTouches.RemoveElementAt(j);
+          break;
         }
       }
-      NS_WARN_IF_FALSE(foundAlreadyExistingTouch, "Touch ended, but not in list");
-    }
-
-    if (mState == GESTURE_WAITING_DOUBLE_TAP) {
-      CancelDoubleTapTimeoutTask();
-      if (mTapStartTime - mLastTapEndTime > MAX_TAP_TIME ||
-          aEvent.mTime - mTapStartTime > MAX_TAP_TIME) {
-        // Either the time between taps or the last tap took too long
-        // confirm previous tap and handle current tap seperately
-        TimeoutDoubleTap();
-        mState = GESTURE_WAITING_SINGLE_TAP;
-      } else {
-        // We were waiting for a double tap and it has arrived.
-        HandleDoubleTap(aEvent);
-        mState = GESTURE_NONE;
-      }
     }
 
-    if (mState == GESTURE_LONG_TAP_UP) {
-      HandleLongTapUpEvent(aEvent);
-      mState = GESTURE_NONE;
-    } else if (mState == GESTURE_WAITING_SINGLE_TAP &&
-        aEvent.mTime - mTapStartTime > MAX_TAP_TIME) {
-      // Extended taps are immediately dispatched as single taps
-      CancelLongTapTimeoutTask();
-      HandleSingleTapConfirmedEvent(aEvent);
-      mState = GESTURE_NONE;
-    } else if (mState == GESTURE_WAITING_SINGLE_TAP) {
-      CancelLongTapTimeoutTask();
-      nsEventStatus tapupEvent = HandleSingleTapUpEvent(aEvent);
-
-      if (tapupEvent == nsEventStatus_eIgnore) {
-        // We were not waiting for anything but a single tap has happened that
-        // may turn into a double tap. Wait a while and if it doesn't turn into
-        // a double tap, send a single tap instead.
-        mState = GESTURE_WAITING_DOUBLE_TAP;
-
-        mDoubleTapTimeoutTask =
-          NewRunnableMethod(this, &GestureEventListener::TimeoutDoubleTap);
-
-        mAsyncPanZoomController->PostDelayedTask(
-          mDoubleTapTimeoutTask,
-          MAX_TAP_TIME);
-
-      } else if (tapupEvent == nsEventStatus_eConsumeNoDefault) {
-        // We sent the tapup into content without waiting for a double tap
-        mState = GESTURE_NONE;
-      }
-    }
-
-    mLastTapEndTime = aEvent.mTime;
-
-    if (!mTouches.Length()) {
-      mSpanChange = 0.0f;
-    }
-
+    rv = HandleInputTouchEnd();
     break;
-  }
   case MultiTouchInput::MULTITOUCH_CANCEL:
-    // FIXME: we should probably clear a bunch of gesture state here
+    mTouches.Clear();
+    rv = HandleInputTouchCancel();
     break;
   }
 
-  return HandlePinchGestureEvent(aEvent);
+  return rv;
 }
 
-nsEventStatus GestureEventListener::HandlePinchGestureEvent(const MultiTouchInput& aEvent)
+nsEventStatus GestureEventListener::HandleInputTouchSingleStart()
+{
+  switch (mState) {
+  case GESTURE_NONE:
+    SetState(GESTURE_FIRST_SINGLE_TOUCH_DOWN);
+    mTouchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint;
+
+    CreateLongTapTimeoutTask();
+    CreateMaxTapTimeoutTask();
+    break;
+  case GESTURE_FIRST_SINGLE_TOUCH_UP:
+    SetState(GESTURE_SECOND_SINGLE_TOUCH_DOWN);
+    break;
+  default:
+    NS_WARNING("Unhandled state upon single touch start");
+    SetState(GESTURE_NONE);
+    break;
+  }
+
+  return nsEventStatus_eIgnore;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchMultiStart()
 {
   nsEventStatus rv = nsEventStatus_eIgnore;
 
-  if (aEvent.mType == MultiTouchInput::MULTITOUCH_CANCEL) {
-    mTouches.Clear();
-    mState = GESTURE_NONE;
-    return rv;
-  }
-
-  if (mTouches.Length() > 1) {
-    const ScreenIntPoint& firstTouch = mTouches[0].mScreenPoint,
-                         secondTouch = mTouches[1].mScreenPoint;
-    ScreenPoint focusPoint = ScreenPoint(firstTouch + secondTouch) / 2;
-    ScreenIntPoint delta = secondTouch - firstTouch;
-    float currentSpan = float(NS_hypot(delta.x, delta.y));
-
-    switch (mState) {
-    case GESTURE_NONE:
-      mPreviousSpan = currentSpan;
-      mState = GESTURE_WAITING_PINCH;
-      // Deliberately fall through. If the user pinched and took their fingers
-      // off the screen such that they still had 1 left on it, we want there to
-      // be no resistance. We should only reset |mSpanChange| once all fingers
-      // are off the screen.
-    case GESTURE_WAITING_PINCH: {
-      mSpanChange += fabsf(currentSpan - mPreviousSpan);
-      if (mSpanChange > PINCH_START_THRESHOLD) {
-        PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_START,
-                                     aEvent.mTime,
-                                     focusPoint,
-                                     currentSpan,
-                                     currentSpan,
-                                     aEvent.modifiers);
-
-        mAsyncPanZoomController->HandleInputEvent(pinchEvent);
-
-        mState = GESTURE_PINCH;
-      }
-
-      break;
-    }
-    case GESTURE_PINCH: {
-      PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_SCALE,
-                                   aEvent.mTime,
-                                   focusPoint,
-                                   currentSpan,
-                                   mPreviousSpan,
-                                   aEvent.modifiers);
-
-      mAsyncPanZoomController->HandleInputEvent(pinchEvent);
-      break;
-    }
-    default:
-      // What?
-      break;
-    }
-
-    mPreviousSpan = currentSpan;
-
+  switch (mState) {
+  case GESTURE_NONE:
+    SetState(GESTURE_MULTI_TOUCH_DOWN);
+    break;
+  case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+    CancelLongTapTimeoutTask();
+    CancelMaxTapTimeoutTask();
+    SetState(GESTURE_MULTI_TOUCH_DOWN);
+    // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+    rv = nsEventStatus_eConsumeNoDefault;
+    break;
+  case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+    CancelLongTapTimeoutTask();
+    SetState(GESTURE_MULTI_TOUCH_DOWN);
+    // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
     rv = nsEventStatus_eConsumeNoDefault;
-  } else if (mState == GESTURE_PINCH) {
-    PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_END,
-                                 aEvent.mTime,
-                                 ScreenPoint(),
-                                 1.0f,
-                                 1.0f,
-                                 aEvent.modifiers);
-    mAsyncPanZoomController->HandleInputEvent(pinchEvent);
-
-    mState = GESTURE_NONE;
-
-    // If the user left a finger on the screen, spoof a touch start event and
-    // send it to APZC so that they can continue panning from that point.
-    if (mTouches.Length() == 1) {
-      MultiTouchInput touchEvent(MultiTouchInput::MULTITOUCH_START,
-                                 aEvent.mTime,
-                                 aEvent.modifiers);
-      touchEvent.mTouches.AppendElement(mTouches[0]);
-      mAsyncPanZoomController->HandleInputEvent(touchEvent);
-
-      // The spoofed touch start will get back to GEL and make us enter the
-      // GESTURE_WAITING_SINGLE_TAP state, but this isn't a new touch, so there
-      // is no condition under which this touch should turn into any tap.
-      mState = GESTURE_NONE;
-    }
-
+    break;
+  case GESTURE_FIRST_SINGLE_TOUCH_UP:
+    // Cancel wait for double tap
+    CancelMaxTapTimeoutTask();
+    SetState(GESTURE_MULTI_TOUCH_DOWN);
+    // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+    rv = nsEventStatus_eConsumeNoDefault;
+    break;
+  case GESTURE_SECOND_SINGLE_TOUCH_DOWN:
+    // Cancel wait for single tap
+    CancelMaxTapTimeoutTask();
+    SetState(GESTURE_MULTI_TOUCH_DOWN);
+    // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
     rv = nsEventStatus_eConsumeNoDefault;
-  } else if (mState == GESTURE_WAITING_PINCH) {
-    mState = GESTURE_NONE;
+    break;
+  case GESTURE_LONG_TOUCH_DOWN:
+    SetState(GESTURE_MULTI_TOUCH_DOWN);
+    break;
+  case GESTURE_MULTI_TOUCH_DOWN:
+  case GESTURE_PINCH:
+    // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+    rv = nsEventStatus_eConsumeNoDefault;
+    break;
+  default:
+    NS_WARNING("Unhandled state upon multitouch start");
+    SetState(GESTURE_NONE);
+    break;
   }
 
   return rv;
 }
 
-nsEventStatus GestureEventListener::HandleSingleTapUpEvent(const MultiTouchInput& aEvent)
-{
-  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_UP, aEvent.mTime,
-      aEvent.mTouches[0].mScreenPoint, aEvent.modifiers);
-  return mAsyncPanZoomController->HandleInputEvent(tapEvent);
-}
-
-nsEventStatus GestureEventListener::HandleSingleTapConfirmedEvent(const MultiTouchInput& aEvent)
+nsEventStatus GestureEventListener::HandleInputTouchMove()
 {
-  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_CONFIRMED, aEvent.mTime,
-      aEvent.mTouches[0].mScreenPoint, aEvent.modifiers);
-  return mAsyncPanZoomController->HandleInputEvent(tapEvent);
-}
+  nsEventStatus rv = nsEventStatus_eIgnore;
+
+  switch (mState) {
+  case GESTURE_NONE:
+  case GESTURE_LONG_TOUCH_DOWN:
+    // Ignore this input signal as the corresponding events get handled by APZC
+    break;
+
+  case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+  case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+  case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
+    // If we move too much, bail out of the tap.
+    ScreenIntPoint delta = mLastTouchInput.mTouches[0].mScreenPoint - mTouchStartPosition;
+    if (NS_hypot(delta.x, delta.y) > AsyncPanZoomController::GetTouchStartTolerance()) {
+      CancelLongTapTimeoutTask();
+      CancelMaxTapTimeoutTask();
+      SetState(GESTURE_NONE);
+    }
+    break;
+  }
+
+  case GESTURE_MULTI_TOUCH_DOWN: {
+    if (mLastTouchInput.mTouches.Length() < 2) {
+      NS_WARNING("Wrong input: less than 2 moving points in GESTURE_MULTI_TOUCH_DOWN state");
+      break;
+    }
+
+    float currentSpan = GetCurrentSpan(mLastTouchInput);
+
+    mSpanChange += fabsf(currentSpan - mPreviousSpan);
+    if (mSpanChange > PINCH_START_THRESHOLD) {
+      SetState(GESTURE_PINCH);
+      PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_START,
+                                   mLastTouchInput.mTime,
+                                   GetCurrentFocus(mLastTouchInput),
+                                   currentSpan,
+                                   currentSpan,
+                                   mLastTouchInput.modifiers);
 
-nsEventStatus GestureEventListener::HandleLongTapEvent(const MultiTouchInput& aEvent)
-{
-  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG, aEvent.mTime,
-      aEvent.mTouches[0].mScreenPoint, aEvent.modifiers);
-  return mAsyncPanZoomController->HandleInputEvent(tapEvent);
+      mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+    }
+    rv = nsEventStatus_eConsumeNoDefault;
+    mPreviousSpan = currentSpan;
+    break;
+  }
+
+  case GESTURE_PINCH: {
+    if (mLastTouchInput.mTouches.Length() < 2) {
+      NS_WARNING("Wrong input: less than 2 moving points in GESTURE_PINCH state");
+      // Prevent APZC::OnTouchMove() from handling this wrong input
+      rv = nsEventStatus_eConsumeNoDefault;
+      break;
+    }
+
+    float currentSpan = GetCurrentSpan(mLastTouchInput);
+
+    PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_SCALE,
+                                 mLastTouchInput.mTime,
+                                 GetCurrentFocus(mLastTouchInput),
+                                 currentSpan,
+                                 mPreviousSpan,
+                                 mLastTouchInput.modifiers);
+
+    mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+    rv = nsEventStatus_eConsumeNoDefault;
+    mPreviousSpan = currentSpan;
+
+    break;
+  }
+
+  default:
+    NS_WARNING("Unhandled state upon touch move");
+    SetState(GESTURE_NONE);
+    break;
+  }
+
+  return rv;
 }
 
-nsEventStatus GestureEventListener::HandleLongTapUpEvent(const MultiTouchInput& aEvent)
+nsEventStatus GestureEventListener::HandleInputTouchEnd()
 {
-  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG_UP, aEvent.mTime,
-      aEvent.mTouches[0].mScreenPoint, aEvent.modifiers);
-  return mAsyncPanZoomController->HandleInputEvent(tapEvent);
-}
+  nsEventStatus rv = nsEventStatus_eIgnore;
 
-nsEventStatus GestureEventListener::HandleTapCancel(const MultiTouchInput& aEvent)
-{
-  mTapStartTime = 0;
-
-  switch (mState)
-  {
-  case GESTURE_WAITING_SINGLE_TAP:
-    CancelLongTapTimeoutTask();
-    mState = GESTURE_NONE;
+  switch (mState) {
+  case GESTURE_NONE:
+    // GEL doesn't have a dedicated state for PANNING handled in APZC thus ignore.
     break;
 
-  case GESTURE_WAITING_DOUBLE_TAP:
-  case GESTURE_LONG_TAP_UP:
-    mState = GESTURE_NONE;
+  case GESTURE_FIRST_SINGLE_TOUCH_DOWN: {
+    CancelLongTapTimeoutTask();
+    CancelMaxTapTimeoutTask();
+    TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_UP,
+                             mLastTouchInput.mTime,
+                             mLastTouchInput.mTouches[0].mScreenPoint,
+                             mLastTouchInput.modifiers);
+    nsEventStatus tapupStatus = mAsyncPanZoomController->HandleGestureEvent(tapEvent);
+    if (tapupStatus == nsEventStatus_eIgnore) {
+      SetState(GESTURE_FIRST_SINGLE_TOUCH_UP);
+      CreateMaxTapTimeoutTask();
+    } else {
+      // We sent the tapup into content without waiting for a double tap
+      SetState(GESTURE_NONE);
+    }
     break;
-  default:
+  }
+
+  case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
+    CancelMaxTapTimeoutTask();
+    SetState(GESTURE_NONE);
+    TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_DOUBLE,
+                             mLastTouchInput.mTime,
+                             mLastTouchInput.mTouches[0].mScreenPoint,
+                             mLastTouchInput.modifiers);
+    mAsyncPanZoomController->HandleGestureEvent(tapEvent);
     break;
   }
 
-  return nsEventStatus_eConsumeDoDefault;
-}
+  case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+    CancelLongTapTimeoutTask();
+    SetState(GESTURE_NONE);
+    TriggerSingleTapConfirmedEvent();
+    break;
+
+  case GESTURE_LONG_TOUCH_DOWN: {
+    SetState(GESTURE_NONE);
+    TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG_UP,
+                             mLastTouchInput.mTime,
+                             mLastTouchInput.mTouches[0].mScreenPoint,
+                             mLastTouchInput.modifiers);
+    mAsyncPanZoomController->HandleGestureEvent(tapEvent);
+    break;
+  }
+
+  case GESTURE_MULTI_TOUCH_DOWN:
+    if (mTouches.Length() < 2) {
+      SetState(GESTURE_NONE);
+    }
+    break;
 
-nsEventStatus GestureEventListener::HandleDoubleTap(const MultiTouchInput& aEvent)
-{
-  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_DOUBLE, aEvent.mTime,
-      aEvent.mTouches[0].mScreenPoint, aEvent.modifiers);
-  return mAsyncPanZoomController->HandleInputEvent(tapEvent);
+  case GESTURE_PINCH:
+    if (mTouches.Length() < 2) {
+      SetState(GESTURE_NONE);
+      PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_END,
+                                   mLastTouchInput.mTime,
+                                   ScreenPoint(),
+                                   1.0f,
+                                   1.0f,
+                                   mLastTouchInput.modifiers);
+      mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+    }
+    rv = nsEventStatus_eConsumeNoDefault;
+    break;
+
+  default:
+    NS_WARNING("Unhandled state upon touch end");
+    SetState(GESTURE_NONE);
+    break;
+  }
+
+  return rv;
 }
 
-void GestureEventListener::TimeoutDoubleTap()
+nsEventStatus GestureEventListener::HandleInputTouchCancel()
+{
+  SetState(GESTURE_NONE);
+  return nsEventStatus_eIgnore;
+}
+
+void GestureEventListener::HandleInputTimeoutLongTap()
 {
-  mDoubleTapTimeoutTask = nullptr;
-  // If we haven't gotten another tap by now, reset the state and treat it as a
-  // single tap. It couldn't have been a double tap.
-  if (mState == GESTURE_WAITING_DOUBLE_TAP) {
-    mState = GESTURE_NONE;
+  mLongTapTimeoutTask = nullptr;
 
-    HandleSingleTapConfirmedEvent(mLastTouchInput);
+  switch (mState) {
+  case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+    // just in case MAX_TAP_TIME > ContextMenuDelay cancel MAX_TAP timer
+    // and fall through
+    CancelMaxTapTimeoutTask();
+  case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: {
+    SetState(GESTURE_LONG_TOUCH_DOWN);
+    TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG,
+                             mLastTouchInput.mTime,
+                             mLastTouchInput.mTouches[0].mScreenPoint,
+                             mLastTouchInput.modifiers);
+    mAsyncPanZoomController->HandleGestureEvent(tapEvent);
+    break;
+  }
+  default:
+    NS_WARNING("Unhandled state upon long tap timeout");
+    SetState(GESTURE_NONE);
+    break;
   }
 }
 
-void GestureEventListener::CancelDoubleTapTimeoutTask() {
-  if (mDoubleTapTimeoutTask) {
-    mDoubleTapTimeoutTask->Cancel();
-    mDoubleTapTimeoutTask = nullptr;
+void GestureEventListener::HandleInputTimeoutMaxTap()
+{
+  mMaxTapTimeoutTask = nullptr;
+
+  if (mState == GESTURE_FIRST_SINGLE_TOUCH_DOWN) {
+    SetState(GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN);
+  } else if (mState == GESTURE_FIRST_SINGLE_TOUCH_UP ||
+             mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) {
+    SetState(GESTURE_NONE);
+    TriggerSingleTapConfirmedEvent();
+  } else {
+    NS_WARNING("Unhandled state upon MAX_TAP timeout");
+    SetState(GESTURE_NONE);
   }
 }
 
-void GestureEventListener::TimeoutLongTap()
+void GestureEventListener::TriggerSingleTapConfirmedEvent()
 {
-  mLongTapTimeoutTask = nullptr;
-  // If the tap has not been released, this is a long press.
-  if (mState == GESTURE_WAITING_SINGLE_TAP) {
-    mState = GESTURE_LONG_TAP_UP;
+  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_CONFIRMED,
+                           mLastTouchInput.mTime,
+                           mLastTouchInput.mTouches[0].mScreenPoint,
+                           mLastTouchInput.modifiers);
+  mAsyncPanZoomController->HandleGestureEvent(tapEvent);
+}
 
-    HandleLongTapEvent(mLastTouchInput);
+void GestureEventListener::SetState(GestureState aState)
+{
+  mState = aState;
+
+  if (mState == GESTURE_NONE) {
+    mSpanChange = 0.0f;
+    mPreviousSpan = 0.0f;
+  } else if (mState == GESTURE_MULTI_TOUCH_DOWN) {
+    mPreviousSpan = GetCurrentSpan(mLastTouchInput);
   }
 }
 
-void GestureEventListener::CancelLongTapTimeoutTask() {
+void GestureEventListener::CancelLongTapTimeoutTask()
+{
+  if (mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) {
+    // being in this state means the task has been canceled already
+    return;
+  }
+
   if (mLongTapTimeoutTask) {
     mLongTapTimeoutTask->Cancel();
     mLongTapTimeoutTask = nullptr;
   }
 }
 
-AsyncPanZoomController* GestureEventListener::GetAsyncPanZoomController() {
-  return mAsyncPanZoomController;
+void GestureEventListener::CreateLongTapTimeoutTask()
+{
+  mLongTapTimeoutTask =
+    NewRunnableMethod(this, &GestureEventListener::HandleInputTimeoutLongTap);
+
+  mAsyncPanZoomController->PostDelayedTask(
+    mLongTapTimeoutTask,
+    gfxPrefs::UiClickHoldContextMenusDelay());
 }
 
-void GestureEventListener::CancelGesture() {
-  mTouches.Clear();
-  mState = GESTURE_NONE;
+void GestureEventListener::CancelMaxTapTimeoutTask()
+{
+  if (mState == GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN) {
+    // being in this state means the timer has just been triggered
+    return;
+  }
+
+  if (mMaxTapTimeoutTask) {
+    mMaxTapTimeoutTask->Cancel();
+    mMaxTapTimeoutTask = nullptr;
+  }
+}
+
+void GestureEventListener::CreateMaxTapTimeoutTask()
+{
+  mMaxTapTimeoutTask =
+    NewRunnableMethod(this, &GestureEventListener::HandleInputTimeoutMaxTap);
+
+  mAsyncPanZoomController->PostDelayedTask(
+    mMaxTapTimeoutTask,
+    MAX_TAP_TIME);
 }
 
 }
 }
--- a/gfx/layers/ipc/GestureEventListener.h
+++ b/gfx/layers/ipc/GestureEventListener.h
@@ -2,20 +2,18 @@
 /* vim: set sw=2 ts=8 et tw=80 : */
 /* 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/. */
 
 #ifndef mozilla_layers_GestureEventListener_h
 #define mozilla_layers_GestureEventListener_h
 
-#include <stdint.h>                     // for uint64_t
 #include "InputData.h"                  // for MultiTouchInput, etc
 #include "Units.h"                      // for ScreenIntPoint
-#include "mozilla/Assertions.h"         // for MOZ_ASSERT_HELPER2
 #include "mozilla/EventForwards.h"      // for nsEventStatus
 #include "nsAutoPtr.h"                  // for nsRefPtr
 #include "nsISupportsImpl.h"
 #include "nsTArray.h"                   // for nsTArray
 
 class CancelableTask;
 
 namespace mozilla {
@@ -51,128 +49,105 @@ public:
 
   /**
    * General input handler for a touch event. If the touch event is not a part
    * of a gesture, then we pass it along to AsyncPanZoomController. Otherwise,
    * it gets consumed here and never forwarded along.
    */
   nsEventStatus HandleInputEvent(const MultiTouchInput& aEvent);
 
-  /**
-   * Cancels any currently active gesture. May not properly handle situations
-   * that require extra work at the gesture's end, like a pinch which only
-   * requests a repaint once it has ended.
-   */
-  void CancelGesture();
+protected:
 
   /**
-   * Returns the AsyncPanZoomController stored on this class and used for
-   * callbacks.
+   * States of GEL finite-state machine.
    */
-  AsyncPanZoomController* GetAsyncPanZoomController();
+  enum GestureState {
+    // This is the initial and final state of any gesture.
+    // In this state there's no gesture going on, and we don't think we're
+    // about to enter one.
+    // Allowed next states: GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_MULTI_TOUCH_DOWN.
+    GESTURE_NONE,
+
+    // A touch start with a single touch point has just happened.
+    // After having gotten into this state we start timers for MAX_TAP_TIME and
+    // gfxPrefs::UiClickHoldContextMenusDelay().
+    // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE,
+    //                      GESTURE_FIRST_SINGLE_TOUCH_UP, GESTURE_LONG_TOUCH_DOWN,
+    //                      GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN.
+    GESTURE_FIRST_SINGLE_TOUCH_DOWN,
 
-protected:
-  enum GestureState {
-    // There's no gesture going on, and we don't think we're about to enter one.
-    GESTURE_NONE,
+    // While in GESTURE_FIRST_SINGLE_TOUCH_DOWN state a MAX_TAP_TIME timer got
+    // triggered. Now we'll trigger either a single tap if a user lifts her
+    // finger or a long tap if gfxPrefs::UiClickHoldContextMenusDelay() happens
+    // first.
+    // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE,
+    //                      GESTURE_LONG_TOUCH_DOWN.
+    GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN,
+
+    // A user put her finger down and lifted it up quickly enough.
+    // After having gotten into this state we clear the timer for MAX_TAP_TIME.
+    // Allowed next states: GESTURE_SECOND_SINGLE_TOUCH_DOWN, GESTURE_NONE,
+    //                      GESTURE_MULTI_TOUCH_DOWN.
+    GESTURE_FIRST_SINGLE_TOUCH_UP,
+
+    // A user put down her finger again right after a single tap thus the
+    // gesture can't be a single tap, but rather a double tap. But we're
+    // still not sure about that until the user lifts her finger again.
+    // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE.
+    GESTURE_SECOND_SINGLE_TOUCH_DOWN,
+
+    // A long touch has happened, but the user still keeps her finger down.
+    // We'll trigger a "long tap up" event when the finger is up.
+    // Allowed next states: GESTURE_NONE, GESTURE_MULTI_TOUCH_DOWN.
+    GESTURE_LONG_TOUCH_DOWN,
+
     // We have detected that two or more fingers are on the screen, but there
     // hasn't been enough movement yet to make us start actually zooming the
     // screen.
-    GESTURE_WAITING_PINCH,
+    // Allowed next states: GESTURE_PINCH, GESTURE_NONE
+    GESTURE_MULTI_TOUCH_DOWN,
+
     // There are two or more fingers on the screen, and the user has already
     // pinched enough for us to start zooming the screen.
-    GESTURE_PINCH,
-    // A touch start has happened and it may turn into a tap. We use this
-    // because, if we put down two fingers and then lift them very quickly, this
-    // may be mistaken for a tap.
-    GESTURE_WAITING_SINGLE_TAP,
-    // A single tap has happened for sure, and we're waiting for a second tap.
-    GESTURE_WAITING_DOUBLE_TAP,
-    // A long tap has happened, wait for the tap to be released in case we need
-    // to fire a click event in the case the long tap was not handled.
-    GESTURE_LONG_TAP_UP
+    // Allowed next states: GESTURE_NONE
+    GESTURE_PINCH
   };
 
   /**
-   * Attempts to handle the event as a pinch event. If it is not a pinch event,
-   * then we simply tell the next consumer to consume the event instead.
+   * These HandleInput* functions comprise input alphabet of the GEL
+   * finite-state machine triggering state transitions.
    */
-  nsEventStatus HandlePinchGestureEvent(const MultiTouchInput& aEvent);
-
-  /**
-   * Attempts to handle the event as a single tap event, which highlights links
-   * before opening them. In general, this will not attempt to block the touch
-   * event from being passed along to AsyncPanZoomController since APZC needs to
-   * know about touches ending (and we only know if a touch was a tap once it
-   * ends).
-   */
-  nsEventStatus HandleSingleTapUpEvent(const MultiTouchInput& aEvent);
+  nsEventStatus HandleInputTouchSingleStart();
+  nsEventStatus HandleInputTouchMultiStart();
+  nsEventStatus HandleInputTouchEnd();
+  nsEventStatus HandleInputTouchMove();
+  nsEventStatus HandleInputTouchCancel();
+  void HandleInputTimeoutLongTap();
+  void HandleInputTimeoutMaxTap();
 
-  /**
-   * Attempts to handle a single tap confirmation. This is what will actually
-   * open links, etc. In general, this will not attempt to block the touch event
-   * from being passed along to AsyncPanZoomController since APZC needs to know
-   * about touches ending (and we only know if a touch was a tap once it ends).
-   */
-  nsEventStatus HandleSingleTapConfirmedEvent(const MultiTouchInput& aEvent);
-
-  /**
-   * Attempts to handle a long tap confirmation. This is what will use
-   * for context menu.
-   */
-  nsEventStatus HandleLongTapEvent(const MultiTouchInput& aEvent);
+  void TriggerSingleTapConfirmedEvent();
 
   /**
-   * Attempts to handle release of long tap. This is used to fire click
-   * events in the case the context menu was not invoked.
-   */
-  nsEventStatus HandleLongTapUpEvent(const MultiTouchInput& aEvent);
-
-  /**
-   * Attempts to handle a tap event cancellation. This happens when we think
-   * something was a tap but it actually wasn't. In general, this will not
-   * attempt to block the touch event from being passed along to
-   * AsyncPanZoomController since APZC needs to know about touches ending (and
-   * we only know if a touch was a tap once it ends).
+   * Do actual state transition and reset substates.
    */
-  nsEventStatus HandleTapCancel(const MultiTouchInput& aEvent);
-
-  /**
-   * Attempts to handle a double tap. This happens when we get two single taps
-   * within a short time. In general, this will not attempt to block the touch
-   * event from being passed along to AsyncPanZoomController since APZC needs to
-   * know about touches ending (and we only know if a touch was a double tap
-   * once it ends).
-   */
-  nsEventStatus HandleDoubleTap(const MultiTouchInput& aEvent);
-
-  /**
-   * Times out a single tap we think may be turned into a double tap. This will
-   * also send a single tap if we're still in the "GESTURE_WAITING_DOUBLE_TAP"
-   * state when this is called. This should be called a short time after a
-   * single tap is detected, and the delay on it should be enough that the user
-   * has time to tap again (to make a double tap).
-   */
-  void TimeoutDoubleTap();
-  /**
-   * Times out a long tap. This should be called a 'long' time after a single
-   * tap is detected.
-   */
-  void TimeoutLongTap();
+  void SetState(GestureState aState);
 
   nsRefPtr<AsyncPanZoomController> mAsyncPanZoomController;
 
   /**
    * Array containing all active touches. When a touch happens it, gets added to
    * this array, even if we choose not to handle it. When it ends, we remove it.
+   * We need to maintain this array in order to detect the end of the
+   * "multitouch" states because touch start events contain all current touches,
+   * but touch end events contain only those touches that have gone.
    */
   nsTArray<SingleTouchData> mTouches;
 
   /**
-   * Current gesture we're dealing with.
+   * Current state we're dealing with.
    */
   GestureState mState;
 
   /**
    * Total change in span since we detected a pinch gesture. Only used when we
    * are in the |GESTURE_WAITING_PINCH| state and need to know how far zoomed
    * out we are compared to our original pinch span. Note that this does _not_
    * continue to be updated once we jump into the |GESTURE_PINCH| state.
@@ -181,64 +156,57 @@ protected:
 
   /**
    * Previous span calculated for the purposes of setting inside a
    * PinchGestureInput.
    */
   float mPreviousSpan;
 
   /**
-   * Stores the time a touch started, used for detecting a tap gesture. Only
-   * valid when there's exactly one touch in mTouches. This is the time that the
-   * first touch was inserted into the array. This is a uint64_t because it is
-   * initialized from interactions with InputData, which stores its timestamps as
-   * a uint64_t.
-   */
-  uint64_t mTapStartTime;
-
-  /**
-   * Stores the time the last tap ends (finger leaves the screen). This is used
-   * when mDoubleTapTimeoutTask cannot be scheduled in time and consecutive
-   * taps are falsely regarded as double taps.
-   */
-  uint64_t mLastTapEndTime;
-
-  /**
-   * Cached copy of the last touch input, only valid when in the
-   * "GESTURE_WAITING_DOUBLE_TAP" state. This is used to forward along to
-   * AsyncPanZoomController if a single tap needs to be sent (since it is sent
-   * shortly after the user actually taps, since we need to wait for a double
-   * tap).
+   * Cached copy of the last touch input.
    */
   MultiTouchInput mLastTouchInput;
 
   /**
-   * Task used to timeout a double tap. This gets posted to the UI thread such
-   * that it runs a short time after a single tap happens. We cache it so that
-   * we can cancel it if a double tap actually comes in.
-   * CancelDoubleTapTimeoutTask: Cancel the mDoubleTapTimeoutTask and also set
-   * it to null.
+   * Position of the last touch starting. This is only valid during an attempt
+   * to determine if a touch is a tap. If a touch point moves away from
+   * mTouchStartPosition to the distance greater than
+   * AsyncPanZoomController::GetTouchStartTolerance() while in
+   * GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN
+   * or GESTURE_SECOND_SINGLE_TOUCH_DOWN then we're certain the gesture is
+   * not tap.
    */
-  CancelableTask *mDoubleTapTimeoutTask;
-  inline void CancelDoubleTapTimeoutTask();
+  ScreenIntPoint mTouchStartPosition;
 
   /**
    * Task used to timeout a long tap. This gets posted to the UI thread such
    * that it runs a time when a single tap happens. We cache it so that
    * we can cancel it if any other touch event happens.
+   *
+   * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN
+   * and GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN states.
+   *
    * CancelLongTapTimeoutTask: Cancel the mLongTapTimeoutTask and also set
    * it to null.
    */
   CancelableTask *mLongTapTimeoutTask;
-  inline void CancelLongTapTimeoutTask();
+  void CancelLongTapTimeoutTask();
+  void CreateLongTapTimeoutTask();
 
   /**
-   * Position of the last touch starting. This is only valid during an attempt
-   * to determine if a touch is a tap. This means that it is used in both the
-   * "GESTURE_WAITING_SINGLE_TAP" and "GESTURE_WAITING_DOUBLE_TAP" states.
+   * Task used to timeout a single tap or a double tap.
+   *
+   * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+   * GESTURE_FIRST_SINGLE_TOUCH_UP and GESTURE_SECOND_SINGLE_TOUCH_DOWN states.
+   *
+   * CancelMaxTapTimeoutTask: Cancel the mMaxTapTimeoutTask and also set
+   * it to null.
    */
-  ScreenIntPoint mTouchStartPosition;
+  CancelableTask *mMaxTapTimeoutTask;
+  void CancelMaxTapTimeoutTask();
+  void CreateMaxTapTimeoutTask();
+
 };
 
 }
 }
 
 #endif
--- a/gfx/tests/gtest/TestAsyncPanZoomController.cpp
+++ b/gfx/tests/gtest/TestAsyncPanZoomController.cpp
@@ -60,49 +60,48 @@ public:
   MOCK_METHOD3(HandleLongTapUp, void(const CSSPoint&, int32_t, const ScrollableLayerGuid&));
   MOCK_METHOD3(SendAsyncScrollDOMEvent, void(bool aIsRoot, const CSSRect &aContentRect, const CSSSize &aScrollableSize));
   MOCK_METHOD2(PostDelayedTask, void(Task* aTask, int aDelayMs));
 };
 
 class MockContentControllerDelayed : public MockContentController {
 public:
   MockContentControllerDelayed()
-    : mCurrentTask(nullptr)
   {
   }
 
   void PostDelayedTask(Task* aTask, int aDelayMs) {
-    // Ensure we're not clobbering an existing task
-    EXPECT_TRUE(nullptr == mCurrentTask);
-    mCurrentTask = aTask;
+    mTaskQueue.AppendElement(aTask);
   }
 
   void CheckHasDelayedTask() {
-    EXPECT_TRUE(nullptr != mCurrentTask);
+    EXPECT_TRUE(mTaskQueue.Length() > 0);
   }
 
   void ClearDelayedTask() {
-    mCurrentTask = nullptr;
+    mTaskQueue.RemoveElementAt(0);
+  }
+
+  void DestroyOldestTask() {
+    delete mTaskQueue[0];
+    mTaskQueue.RemoveElementAt(0);
   }
 
   // Note that deleting mCurrentTask is important in order to
   // release the reference to the callee object. Without this
   // that object might be leaked. This is also why we don't
-  // expose mCurrentTask to any users of MockContentControllerDelayed.
+  // expose mTaskQueue to any users of MockContentControllerDelayed.
   void RunDelayedTask() {
-    // Running mCurrentTask may call PostDelayedTask, so we should
-    // keep a local copy of mCurrentTask and operate on that
-    Task* local = mCurrentTask;
-    mCurrentTask = nullptr;
-    local->Run();
-    delete local;
+    mTaskQueue[0]->Run();
+    delete mTaskQueue[0];
+    mTaskQueue.RemoveElementAt(0);
   }
 
 private:
-  Task *mCurrentTask;
+  nsTArray<Task*> mTaskQueue;
 };
 
 
 class TestAPZCContainerLayer : public ContainerLayer {
   public:
     TestAPZCContainerLayer()
       : ContainerLayer(nullptr, nullptr)
     {}
@@ -279,29 +278,29 @@ void DoPanTest(bool aShouldTriggerScroll
   EXPECT_EQ(pointOut, ScreenPoint());
   EXPECT_EQ(viewTransformOut, ViewTransform());
 
   apzc->Destroy();
 }
 
 static void
 ApzcPinch(AsyncPanZoomController* aApzc, int aFocusX, int aFocusY, float aScale) {
-  aApzc->HandleInputEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_START,
+  aApzc->HandleGestureEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_START,
                                             0,
                                             ScreenPoint(aFocusX, aFocusY),
                                             10.0,
                                             10.0,
                                             0));
-  aApzc->HandleInputEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+  aApzc->HandleGestureEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
                                             0,
                                             ScreenPoint(aFocusX, aFocusY),
                                             10.0 * aScale,
                                             10.0,
                                             0));
-  aApzc->HandleInputEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_END,
+  aApzc->HandleGestureEvent(PinchGestureInput(PinchGestureInput::PINCHGESTURE_END,
                                             0,
                                             ScreenPoint(aFocusX, aFocusY),
                                             // note: negative values here tell APZC
                                             //       not to turn the pinch into a pan
                                             -1.0,
                                             -1.0,
                                             0));
 }
@@ -319,18 +318,20 @@ ApzcUp(AsyncPanZoomController* apzc, int
   mti.mTouches.AppendElement(SingleTouchData(0, ScreenIntPoint(aX, aY), ScreenSize(0, 0), 0, 0));
   return apzc->ReceiveInputEvent(mti);
 }
 
 static nsEventStatus
 ApzcTap(AsyncPanZoomController* apzc, int aX, int aY, int& aTime, int aTapLength, MockContentControllerDelayed* mcc = nullptr) {
   nsEventStatus status = ApzcDown(apzc, aX, aY, aTime);
   if (mcc != nullptr) {
-    // There will be a delayed task posted for the long-tap timeout, but
-    // if we were provided a non-null mcc we want to clear it.
+    // There will be delayed tasks posted for the long-tap and MAX_TAP timeouts, but
+    // if we were provided a non-null mcc we want to clear them.
+    mcc->CheckHasDelayedTask();
+    mcc->ClearDelayedTask();
     mcc->CheckHasDelayedTask();
     mcc->ClearDelayedTask();
   }
   EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
   aTime += aTapLength;
   return ApzcUp(apzc, aX, aY, aTime);
 }
 
@@ -771,16 +772,26 @@ DoLongPressTest(bool aShouldUseTouchActi
 
   mcc->CheckHasDelayedTask();
 
   // Manually invoke the longpress while the touch is currently down.
   check.Call("preHandleLongTap");
   mcc->RunDelayedTask();
   check.Call("postHandleLongTap");
 
+  // Destroy pending MAX_TAP timeout task
+  mcc->DestroyOldestTask();
+  // There should be a TimeoutContentResponse task in the queue still
+  // Clear the waiting-for-content timeout task, then send the signal that
+  // content has handled this long tap. This takes the place of the
+  // "contextmenu" event.
+  mcc->CheckHasDelayedTask();
+  mcc->ClearDelayedTask();
+  apzc->ContentReceivedTouch(true);
+
   time += 1000;
 
   status = ApzcUp(apzc, 10, 10, time);
   EXPECT_EQ(nsEventStatus_eIgnore, status);
 
   // To get a LongTapUp event, we must kick APZC to flush its event queue. This
   // would normally happen if we had a (Tab|RenderFrame)(Parent|Child)
   // mechanism.
@@ -830,16 +841,18 @@ TEST_F(AsyncPanZoomControllerTester, Lon
 
   mcc->CheckHasDelayedTask();
 
   // Manually invoke the longpress while the touch is currently down.
   check.Call("preHandleLongTap");
   mcc->RunDelayedTask();
   check.Call("postHandleLongTap");
 
+  // Destroy pending MAX_TAP timeout task
+  mcc->DestroyOldestTask();
   // Clear the waiting-for-content timeout task, then send the signal that
   // content has handled this long tap. This takes the place of the
   // "contextmenu" event.
   mcc->ClearDelayedTask();
   apzc->ContentReceivedTouch(true);
 
   time += 1000;