Bug 1009733 - Rewrite much of the APZC input block handling code. r=botond
authorKartikaya Gupta <kgupta@mozilla.com>
Wed, 16 Jul 2014 08:33:50 -0400
changeset 216298 c892d4e318ad69f880cb7d13956c7ee9bc8e155d
parent 216297 d69317ef2dea4a6e5f996c5d3f4e660b295e6a2a
child 216299 c056276fb1bb5d671dc93d78794aabecfefe4132
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbotond
bugs1009733
milestone33.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 1009733 - Rewrite much of the APZC input block handling code. r=botond
dom/ipc/PBrowser.ipdl
dom/ipc/TabChild.cpp
dom/ipc/TabChild.h
gfx/layers/apz/public/GeckoContentController.h
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/src/AsyncPanZoomController.h
gfx/layers/apz/src/TouchBlockState.cpp
gfx/layers/apz/src/TouchBlockState.h
gfx/layers/moz.build
gfx/tests/gtest/TestAsyncPanZoomController.cpp
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -396,58 +396,26 @@ child:
     LoadURL(nsCString uri);
 
     CacheFileDescriptor(nsString path, FileDescriptor fd);
 
     UpdateDimensions(nsRect rect, nsIntSize size, ScreenOrientation orientation) compress;
 
     UpdateFrame(FrameMetrics frame);
 
-    /**
-     * Acknowledge the receipt of a scroll offset update from the content
-     * process. This is used to manage concurrent scroll updates from many
-     * sources.
-     */
+    // The following methods correspond to functions on the GeckoContentController
+    // interface in gfx/layers/apz/public/GeckoContentController.h. Refer to documentation
+    // in that file for these functions.
     AcknowledgeScrollUpdate(ViewID aScrollId, uint32_t aScrollGeneration);
-
-    /**
-     * Requests handling of a double tap. |point| is in CSS pixels, relative to
-     * the scroll offset. This message is expected to round-trip back to
-     * ZoomToRect() with a rect indicating where we should zoom to.
-     */
     HandleDoubleTap(CSSPoint point, ScrollableLayerGuid aGuid);
-
-    /**
-     * Requests handling of a single tap. |point| is in CSS pixels, relative to
-     * the scroll offset. This message is expected to send a "mousedown" and
-     * "mouseup" series of events at this point.
-     */
     HandleSingleTap(CSSPoint point, ScrollableLayerGuid aGuid);
+    HandleLongTap(CSSPoint point, ScrollableLayerGuid aGuid);
+    HandleLongTapUp(CSSPoint point, ScrollableLayerGuid aGuid);
+    NotifyAPZStateChange(ViewID aViewId, APZStateChange aChange, int aArg);
 
-    /**
-     * Requests handling of a long tap. |point| is in CSS pixels, relative to
-     * the scroll offset. This message is expected to send a "contextmenu"
-     * events at this point.
-     */
-    HandleLongTap(CSSPoint point, ScrollableLayerGuid aGuid);
-
-    /**
-     * Requests handling of releasing a long tap. |aPoint| is in CSS pixels,
-     * relative to the current scroll offset. In the case the "contextmenu"
-     * event generated by the preceding HandleLongTap call was not handled,
-     * this message is expected to generate a "mousedown" and "mouseup"
-     * series of events
-     */
-    HandleLongTapUp(CSSPoint point, ScrollableLayerGuid aGuid);
-
-    /**
-     * Notifies the child about various APZ state changes.
-     * See GeckoContentController::NotifyAPZStateChange() for details.
-     */
-    NotifyAPZStateChange(ViewID aViewId, APZStateChange aChange, int aArg);
 
     /**
      * Sending an activate message moves focus to the child.
      */
     Activate();
 
     Deactivate();
 
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -697,19 +697,17 @@ TabChild::TabChild(nsIContentChild* aMan
   , mTapHoldTimer(nullptr)
   , mAppPackageFileDescriptorRecved(false)
   , mLastBackgroundColor(NS_RGB(255, 255, 255))
   , mDidFakeShow(false)
   , mNotified(false)
   , mTriedBrowserInit(false)
   , mOrientation(eScreenOrientation_PortraitPrimary)
   , mUpdateHitRegion(false)
-  , mContextMenuHandled(false)
-  , mLongTapEventHandled(false)
-  , mWaitingTouchListeners(false)
+  , mPendingTouchPreventedResponse(false)
   , mIgnoreKeyPressEvent(false)
   , mActiveElementManager(new ActiveElementManager())
   , mHasValidInnerSize(false)
   , mUniqueId(0)
 {
   if (!sActiveDurationMsSet) {
     Preferences::AddIntVarCache(&sActiveDurationMs,
                                 "ui.touch_activation.duration_ms",
@@ -1800,50 +1798,40 @@ TabChild::FireSingleTapEvent(LayoutDevic
 
 bool
 TabChild::RecvHandleLongTap(const CSSPoint& aPoint, const ScrollableLayerGuid& aGuid)
 {
   if (!mGlobal || !mTabChildGlobal) {
     return true;
   }
 
-  mContextMenuHandled =
+  bool eventHandled =
       DispatchMouseEvent(NS_LITERAL_STRING("contextmenu"),
                          APZCCallbackHelper::ApplyCallbackTransform(aPoint, aGuid),
                          2, 1, 0, false,
                          nsIDOMMouseEvent::MOZ_SOURCE_TOUCH);
 
   // If no one handle context menu, fire MOZLONGTAP event
-  if (!mContextMenuHandled) {
+  if (!eventHandled) {
     LayoutDevicePoint currentPoint =
       APZCCallbackHelper::ApplyCallbackTransform(aPoint, aGuid) * mWidget->GetDefaultScale();
     int time = 0;
     nsEventStatus status =
       DispatchSynthesizedMouseEvent(NS_MOUSE_MOZLONGTAP, time, currentPoint, mWidget);
-    mLongTapEventHandled = (status == nsEventStatus_eConsumeNoDefault);
+    eventHandled = (status == nsEventStatus_eConsumeNoDefault);
   }
 
-  SendContentReceivedTouch(aGuid, mContextMenuHandled || mLongTapEventHandled);
+  SendContentReceivedTouch(aGuid, eventHandled);
 
   return true;
 }
 
 bool
 TabChild::RecvHandleLongTapUp(const CSSPoint& aPoint, const ScrollableLayerGuid& aGuid)
 {
-  if (mContextMenuHandled) {
-    mContextMenuHandled = false;
-    return true;
-  }
-
-  if (mLongTapEventHandled) {
-    mLongTapEventHandled = false;
-    return true;
-  }
-
   RecvHandleSingleTap(aPoint, aGuid);
   return true;
 }
 
 bool
 TabChild::RecvNotifyAPZStateChange(const ViewID& aViewId,
                                    const APZStateChange& aChange,
                                    const int& aArg)
@@ -2090,43 +2078,42 @@ TabChild::RecvRealTouchEvent(const Widge
     UpdateTapState(localEvent, status);
     return true;
   }
 
   if (aEvent.message == NS_TOUCH_START && localEvent.touches.Length() > 0) {
     mActiveElementManager->SetTargetElement(localEvent.touches[0]->GetTarget());
   }
 
-  nsCOMPtr<nsPIDOMWindow> outerWindow = do_GetInterface(WebNavigation());
-  nsCOMPtr<nsPIDOMWindow> innerWindow = outerWindow ? outerWindow->GetCurrentInnerWindow() : nullptr;
-
-  if (!innerWindow || (!innerWindow->HasTouchEventListeners() &&
-                       !innerWindow->MayHaveTouchCaret())) {
-    SendContentReceivedTouch(aGuid, false);
-    return true;
-  }
-
   bool isTouchPrevented = nsIPresShell::gPreventMouseEvents ||
                           localEvent.mFlags.mMultipleActionsPrevented;
   switch (aEvent.message) {
   case NS_TOUCH_START: {
+    if (mPendingTouchPreventedResponse) {
+      // We can enter here if we get two TOUCH_STARTs in a row and didn't
+      // respond to the first one. Respond to it now.
+      SendContentReceivedTouch(mPendingTouchPreventedGuid, false);
+      mPendingTouchPreventedResponse = false;
+    }
     if (isTouchPrevented) {
       SendContentReceivedTouch(aGuid, isTouchPrevented);
     } else {
-      mWaitingTouchListeners = true;
+      mPendingTouchPreventedResponse = true;
+      mPendingTouchPreventedGuid = aGuid;
     }
     break;
   }
 
   case NS_TOUCH_MOVE:
   case NS_TOUCH_END:
   case NS_TOUCH_CANCEL: {
-    if (mWaitingTouchListeners) {
-      SendContentReceivedTouch(aGuid, isTouchPrevented);
-      mWaitingTouchListeners = false;
+    if (mPendingTouchPreventedResponse) {
+      MOZ_ASSERT(aGuid == mPendingTouchPreventedGuid);
+      SendContentReceivedTouch(mPendingTouchPreventedGuid, isTouchPrevented);
+      mPendingTouchPreventedResponse = false;
     }
     break;
   }
 
   default:
     NS_WARNING("Unknown touch event type");
   }
 
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -581,19 +581,18 @@ private:
     nsAutoTArray<nsAutoPtr<CachedFileDescriptorInfo>, 1>
         mCachedFileDescriptorInfos;
     nscolor mLastBackgroundColor;
     bool mDidFakeShow;
     bool mNotified;
     bool mTriedBrowserInit;
     ScreenOrientation mOrientation;
     bool mUpdateHitRegion;
-    bool mContextMenuHandled;
-    bool mLongTapEventHandled;
-    bool mWaitingTouchListeners;
+    bool mPendingTouchPreventedResponse;
+    ScrollableLayerGuid mPendingTouchPreventedGuid;
     void FireSingleTapEvent(LayoutDevicePoint aPoint);
 
     bool mIgnoreKeyPressEvent;
     nsRefPtr<ActiveElementManager> mActiveElementManager;
     bool mHasValidInnerSize;
     uint64_t mUniqueId;
 
     DISALLOW_EVIL_CONSTRUCTORS(TabChild);
--- a/gfx/layers/apz/public/GeckoContentController.h
+++ b/gfx/layers/apz/public/GeckoContentController.h
@@ -63,17 +63,18 @@ public:
                              int32_t aModifiers,
                              const ScrollableLayerGuid& aGuid) = 0;
 
   /**
    * Requests handling of releasing a long tap. |aPoint| is in CSS pixels,
    * relative to the current scroll offset. HandleLongTapUp will always be
    * preceeded by HandleLongTap. However not all calls to HandleLongTap will
    * be followed by a HandleLongTapUp (for example, if the user drags
-   * around between the long-tap and lifting their finger).
+   * around between the long-tap and lifting their finger, or if content
+   * notifies the APZ that the long-tap event was prevent-defaulted).
    */
   virtual void HandleLongTapUp(const CSSPoint& aPoint,
                                int32_t aModifiers,
                                const ScrollableLayerGuid& aGuid) = 0;
 
   /**
    * Requests sending a mozbrowserasyncscroll domevent to embedder.
    * |aContentRect| is in CSS pixels, relative to the current cssPage.
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -10,16 +10,17 @@
 #include <algorithm>                    // for max, min
 #include "AnimationCommon.h"            // for ComputedTimingFunction
 #include "AsyncPanZoomController.h"     // for AsyncPanZoomController, etc
 #include "Compositor.h"                 // for Compositor
 #include "CompositorParent.h"           // for CompositorParent
 #include "FrameMetrics.h"               // for FrameMetrics, etc
 #include "GestureEventListener.h"       // for GestureEventListener
 #include "InputData.h"                  // for MultiTouchInput, etc
+#include "TouchBlockState.h"            // for TouchBlockState
 #include "Units.h"                      // for CSSRect, CSSPoint, etc
 #include "UnitTransforms.h"             // for TransformTo
 #include "base/message_loop.h"          // for MessageLoop
 #include "base/task.h"                  // for NewRunnableMethod, etc
 #include "base/tracked.h"               // for FROM_HERE
 #include "gfxPrefs.h"                   // for gfxPrefs
 #include "gfxTypes.h"                   // for gfxFloat
 #include "mozilla/Assertions.h"         // for MOZ_ASSERT, etc
@@ -312,24 +313,16 @@ typedef GeckoContentController::APZState
  * documentation for the skate size multipliers above).
  *
  * "apz.zoom_animation_duration_ms"
  * This controls how long the zoom-to-rect animation takes.
  * Units: ms
  */
 
 /**
- * Default touch behavior (is used when not touch behavior is set).
- */
-static const uint32_t DefaultTouchBehavior = AllowedTouchBehavior::VERTICAL_PAN |
-                                             AllowedTouchBehavior::HORIZONTAL_PAN |
-                                             AllowedTouchBehavior::PINCH_ZOOM |
-                                             AllowedTouchBehavior::DOUBLE_TAP_ZOOM;
-
-/**
  * Angle from axis within which we stay axis-locked
  */
 static const double AXIS_LOCK_ANGLE = M_PI / 6.0; // 30 degrees
 
 /**
  * The distance in inches the user must pan before axis lock can be broken
  */
 static const float AXIS_BREAKOUT_THRESHOLD = 1.0f/32.0f;
@@ -692,27 +685,26 @@ AsyncPanZoomController::AsyncPanZoomCont
                                                GestureBehavior aGestures)
   :  mLayersId(aLayersId),
      mPaintThrottler(GetFrameTime()),
      mGeckoContentController(aGeckoContentController),
      mRefPtrMonitor("RefPtrMonitor"),
      mSharingFrameMetricsAcrossProcesses(false),
      mMonitor("AsyncPanZoomController"),
      mState(NOTHING),
-     mContentResponseTimeoutTask(nullptr),
      mX(MOZ_THIS_IN_INITIALIZER_LIST()),
      mY(MOZ_THIS_IN_INITIALIZER_LIST()),
      mPanDirRestricted(false),
      mZoomConstraints(false, false, MIN_ZOOM, MAX_ZOOM),
      mLastSampleTime(GetFrameTime()),
      mLastAsyncScrollTime(GetFrameTime()),
      mLastAsyncScrollOffset(0, 0),
      mCurrentAsyncScrollOffset(0, 0),
      mAsyncScrollTimeoutTask(nullptr),
-     mHandlingTouchQueue(false),
+     mTouchBlockBalance(0),
      mTreeManager(aTreeManager),
      mScrollParentId(FrameMetrics::NULL_SCROLL_ID),
      mAPZCId(sAsyncPanZoomControllerCount++),
      mSharedFrameMetricsBuffer(nullptr),
      mSharedLock(nullptr)
 {
   MOZ_COUNT_CTOR(AsyncPanZoomController);
 
@@ -754,16 +746,18 @@ AsyncPanZoomController::GetGestureEventL
   return listener.forget();
 }
 
 void
 AsyncPanZoomController::Destroy()
 {
   CancelAnimation();
 
+  mTouchBlockQueue.Clear();
+
   { // scope the lock
     MonitorAutoLock lock(mRefPtrMonitor);
     mGeckoContentController = nullptr;
     mGestureEventListener = nullptr;
   }
   mPrevSibling = nullptr;
   mLastChild = nullptr;
   mParent = nullptr;
@@ -797,49 +791,60 @@ AsyncPanZoomController::GetTouchStartTol
 }
 
 /* static */AsyncPanZoomController::AxisLockMode AsyncPanZoomController::GetAxisLockMode()
 {
   return static_cast<AxisLockMode>(gfxPrefs::APZAxisLockMode());
 }
 
 nsEventStatus AsyncPanZoomController::ReceiveInputEvent(const InputData& aEvent) {
-  if (aEvent.mInputType == MULTITOUCH_INPUT &&
-      aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) {
-    // Starting a new touch block, clear old touch block state.
-    mTouchBlockState = TouchBlockState();
+  if (aEvent.mInputType != MULTITOUCH_INPUT) {
+    return HandleInputEvent(aEvent);
   }
 
-  // If we may have touch listeners and touch action property is enabled, we
-  // enable the machinery that allows touch listeners to preventDefault any touch inputs
-  // and also waits for the allowed touch behavior values to be received from the outside.
-  // This should not happen unless there are actually touch listeners and touch-action property
-  // enable as it introduces potentially unbounded lag because it causes a round-trip through
-  // content.  Usually, if content is responding in a timely fashion, this only introduces a
-  // nearly constant few hundred ms of lag.
-  if ((mFrameMetrics.mMayHaveTouchListeners || mFrameMetrics.mMayHaveTouchCaret) &&
-      aEvent.mInputType == MULTITOUCH_INPUT &&
-      (mState == NOTHING || mState == TOUCHING || IsPanningState(mState))) {
-    const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
-    if (multiTouchInput.mType == MultiTouchInput::MULTITOUCH_START) {
-      SetState(WAITING_CONTENT_RESPONSE);
+  TouchBlockState* block = nullptr;
+  if (aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) {
+    block = StartNewTouchBlock(false);
+    APZC_LOG("%p started new touch block %p\n", this, block);
+    if (mFrameMetrics.mMayHaveTouchListeners || mFrameMetrics.mMayHaveTouchCaret) {
+      // Content may intercept the touch events and prevent-default them. So we schedule
+      // a timeout to give content time to do that.
+      ScheduleContentResponseTimeout();
+    } else {
+      // Content won't prevent-default this, so we can just pretend like we scheduled
+      // a timeout and it expired. Note that we will still receive a ContentReceivedTouch
+      // callback for this block, and so we need to make sure we adjust the touch balance.
+      APZC_LOG("%p not waiting for content response on block %p\n", this, block);
+      mTouchBlockBalance++;
+      block->TimeoutContentResponse();
     }
+  } else if (mTouchBlockQueue.IsEmpty()) {
+    NS_WARNING("Received a non-start touch event while no touch blocks active!");
+  } else {
+    // this touch is part of the most-recently created block
+    block = mTouchBlockQueue.LastElement().get();
+    APZC_LOG("%p received new event in block %p\n", this, block);
   }
 
-  if (mState == WAITING_CONTENT_RESPONSE || mHandlingTouchQueue) {
-    if (aEvent.mInputType == MULTITOUCH_INPUT) {
-      const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
-      mTouchQueue.AppendElement(multiTouchInput);
-
-      SetContentResponseTimer();
-    }
+  if (!block) {
     return nsEventStatus_eIgnore;
   }
 
-  return HandleInputEvent(aEvent);
+  if (block == CurrentTouchBlock() && block->IsReadyForHandling()) {
+    APZC_LOG("%p's current touch block is ready with preventdefault %d\n",
+        this, block->IsDefaultPrevented());
+    if (block->IsDefaultPrevented()) {
+      return nsEventStatus_eIgnore;
+    }
+    return HandleInputEvent(aEvent);
+  }
+
+  // Otherwise, add it to the queue for the touch block
+  block->AddEvent(aEvent.AsMultiTouchInput());
+  return nsEventStatus_eConsumeDoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent) {
   nsEventStatus rv = nsEventStatus_eIgnore;
 
   switch (aEvent.mInputType) {
   case MULTITOUCH_INPUT: {
     const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
@@ -952,17 +957,16 @@ nsEventStatus AsyncPanZoomController::On
     }
     case TOUCHING:
     case PANNING:
     case PANNING_LOCKED_X:
     case PANNING_LOCKED_Y:
     case CROSS_SLIDING_X:
     case CROSS_SLIDING_Y:
     case PINCHING:
-    case WAITING_CONTENT_RESPONSE:
       NS_WARNING("Received impossible touch in OnTouchStart");
       break;
     default:
       NS_WARNING("Unhandled case in OnTouchStart");
       break;
   }
 
   return nsEventStatus_eConsumeNoDefault;
@@ -987,19 +991,17 @@ nsEventStatus AsyncPanZoomController::On
     case TOUCHING: {
       float panThreshold = GetTouchStartTolerance();
       UpdateWithTouchAtDevicePoint(aEvent);
 
       if (PanDistance() < panThreshold) {
         return nsEventStatus_eIgnore;
       }
 
-      if (gfxPrefs::TouchActionEnabled() &&
-          (GetTouchBehavior(0) & AllowedTouchBehavior::VERTICAL_PAN) &&
-          (GetTouchBehavior(0) & AllowedTouchBehavior::HORIZONTAL_PAN)) {
+      if (gfxPrefs::TouchActionEnabled() && CurrentTouchBlock()->TouchActionAllowsPanningXY()) {
         // User tries to trigger a touch behavior. If allowed touch behavior is vertical pan
         // + horizontal pan (touch-action value is equal to AUTO) we can return ConsumeNoDefault
         // status immediately to trigger cancel event further. It should happen independent of
         // the parent type (whether it is scrolling or not).
         StartPanning(aEvent);
         return nsEventStatus_eConsumeNoDefault;
       }
 
@@ -1012,17 +1014,16 @@ nsEventStatus AsyncPanZoomController::On
       TrackTouch(aEvent);
       return nsEventStatus_eConsumeNoDefault;
 
     case PINCHING:
       // The scale gesture listener should have handled this.
       NS_WARNING("Gesture listener should have handled pinching in OnTouchMove.");
       return nsEventStatus_eIgnore;
 
-    case WAITING_CONTENT_RESPONSE:
     case SNAP_BACK:  // Should not receive a touch-move in the SNAP_BACK state
                      // as touch blocks that begin in an overscrolled state
                      // are ignored.
       NS_WARNING("Received impossible touch in OnTouchMove");
       break;
   }
 
   return nsEventStatus_eConsumeNoDefault;
@@ -1087,17 +1088,16 @@ nsEventStatus AsyncPanZoomController::On
     return nsEventStatus_eConsumeNoDefault;
 
   case PINCHING:
     SetState(NOTHING);
     // Scale gesture listener should have handled this.
     NS_WARNING("Gesture listener should have handled pinching in OnTouchEnd.");
     return nsEventStatus_eIgnore;
 
-  case WAITING_CONTENT_RESPONSE:
   case SNAP_BACK:  // Should not receive a touch-move in the SNAP_BACK state
                    // as touch blocks that begin in an overscrolled state
                    // are ignored.
     NS_WARNING("Received impossible touch in OnTouchEnd");
     break;
   }
 
   return nsEventStatus_eConsumeNoDefault;
@@ -1108,34 +1108,36 @@ nsEventStatus AsyncPanZoomController::On
   OnTouchEndOrCancel();
   SetState(NOTHING);
   return nsEventStatus_eConsumeNoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::OnScaleBegin(const PinchGestureInput& aEvent) {
   APZC_LOG("%p got a scale-begin in state %d\n", this, mState);
 
-  if (!TouchActionAllowPinchZoom()) {
+  // Note that there may not be a touch block at this point, if we received the
+  // PinchGestureEvent directly from widget code without any touch events.
+  if (HasReadyTouchBlock() && !CurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
     return nsEventStatus_eIgnore;
   }
 
   if (!mZoomConstraints.mAllowZoom) {
     return nsEventStatus_eConsumeNoDefault;
   }
 
   SetState(PINCHING);
   mLastZoomFocus = ToParentLayerCoords(aEvent.mFocusPoint) - mFrameMetrics.mCompositionBounds.TopLeft();
 
   return nsEventStatus_eConsumeNoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
   APZC_LOG("%p got a scale in state %d\n", this, mState);
 
-  if (!TouchActionAllowPinchZoom()) {
+  if (HasReadyTouchBlock() && !CurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
     return nsEventStatus_eIgnore;
   }
 
   if (mState != PINCHING) {
     return nsEventStatus_eConsumeNoDefault;
   }
 
   float prevSpan = aEvent.mPreviousSpan;
@@ -1208,17 +1210,17 @@ nsEventStatus AsyncPanZoomController::On
   }
 
   return nsEventStatus_eConsumeNoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::OnScaleEnd(const PinchGestureInput& aEvent) {
   APZC_LOG("%p got a scale-end in state %d\n", this, mState);
 
-  if (!TouchActionAllowPinchZoom()) {
+  if (HasReadyTouchBlock() && !CurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
     return nsEventStatus_eIgnore;
   }
 
   SetState(NOTHING);
 
   {
     ReentrantMonitorAutoEnter lock(mMonitor);
 
@@ -1348,18 +1350,18 @@ nsEventStatus AsyncPanZoomController::On
 
 nsEventStatus AsyncPanZoomController::OnLongPress(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a long-press in state %d\n", this, mState);
   nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
   if (controller) {
     int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
     CSSPoint geckoScreenPoint;
     if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
-      SetState(WAITING_CONTENT_RESPONSE);
-      SetContentResponseTimer();
+      StartNewTouchBlock(true);
+      ScheduleContentResponseTimeout();
       controller->HandleLongTap(geckoScreenPoint, modifiers, GetGuid());
       return nsEventStatus_eConsumeNoDefault;
     }
   }
   return nsEventStatus_eIgnore;
 }
 
 nsEventStatus AsyncPanZoomController::OnLongPressUp(const TapGestureInput& aEvent) {
@@ -1386,50 +1388,50 @@ nsEventStatus AsyncPanZoomController::Ge
       // calling controller->HandleSingleTap directly might mean that content receives
       // the single tap message before the corresponding touch-up. To avoid that we
       // schedule the singletap message to run on the next spin of the event loop.
       // See bug 965381 for the issue this was causing.
       controller->PostDelayedTask(
         NewRunnableMethod(controller.get(), &GeckoContentController::HandleSingleTap,
                           geckoScreenPoint, modifiers, GetGuid()),
         0);
-      mTouchBlockState.mSingleTapOccurred = true;
+      CurrentTouchBlock()->SetSingleTapOccurred();
       return nsEventStatus_eConsumeNoDefault;
     }
   }
   return nsEventStatus_eIgnore;
 }
 
 void AsyncPanZoomController::OnTouchEndOrCancel() {
   if (nsRefPtr<GeckoContentController> controller = GetGeckoContentController()) {
     controller->NotifyAPZStateChange(
-        GetGuid(), APZStateChange::EndTouch, mTouchBlockState.mSingleTapOccurred);
+        GetGuid(), APZStateChange::EndTouch, CurrentTouchBlock()->SingleTapOccurred());
   }
 }
 
 nsEventStatus AsyncPanZoomController::OnSingleTapUp(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a single-tap-up in state %d\n", this, mState);
   // If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to OnSingleTapConfirmed before
   // sending event to content
-  if (!(mZoomConstraints.mAllowDoubleTapZoom && TouchActionAllowDoubleTapZoom())) {
+  if (!(mZoomConstraints.mAllowDoubleTapZoom && CurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
     return GenerateSingleTap(aEvent.mPoint, aEvent.modifiers);
   }
   return nsEventStatus_eIgnore;
 }
 
 nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a single-tap-confirmed in state %d\n", this, mState);
   return GenerateSingleTap(aEvent.mPoint, aEvent.modifiers);
 }
 
 nsEventStatus AsyncPanZoomController::OnDoubleTap(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a double-tap in state %d\n", this, mState);
   nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
   if (controller) {
-    if (mZoomConstraints.mAllowDoubleTapZoom && TouchActionAllowDoubleTapZoom()) {
+    if (mZoomConstraints.mAllowDoubleTapZoom && CurrentTouchBlock()->TouchActionAllowsDoubleTapZoom()) {
       int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
       CSSPoint geckoScreenPoint;
       if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
         controller->HandleDoubleTap(geckoScreenPoint, modifiers, GetGuid());
       }
     }
     return nsEventStatus_eConsumeNoDefault;
   }
@@ -1446,48 +1448,48 @@ float AsyncPanZoomController::PanDistanc
   ReentrantMonitorAutoEnter lock(mMonitor);
   return NS_hypot(mX.PanDistance(), mY.PanDistance());
 }
 
 const ScreenPoint AsyncPanZoomController::GetVelocityVector() {
   return ScreenPoint(mX.GetVelocity(), mY.GetVelocity());
 }
 
-void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle, TouchBehaviorFlags aBehavior) {
+void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) {
   // Handling of cross sliding will need to be added in this method after touch-action released
   // enabled by default.
-  if ((aBehavior & AllowedTouchBehavior::VERTICAL_PAN) && (aBehavior & AllowedTouchBehavior::HORIZONTAL_PAN)) {
+  if (CurrentTouchBlock()->TouchActionAllowsPanningXY()) {
     if (mX.CanScrollNow() && mY.CanScrollNow()) {
       if (IsCloseToHorizontal(aAngle, AXIS_LOCK_ANGLE)) {
         mY.SetAxisLocked(true);
         SetState(PANNING_LOCKED_X);
       } else if (IsCloseToVertical(aAngle, AXIS_LOCK_ANGLE)) {
         mX.SetAxisLocked(true);
         SetState(PANNING_LOCKED_Y);
       } else {
         SetState(PANNING);
       }
     } else if (mX.CanScrollNow() || mY.CanScrollNow()) {
       SetState(PANNING);
     } else {
       SetState(NOTHING);
     }
-  } else if (aBehavior & AllowedTouchBehavior::HORIZONTAL_PAN) {
+  } else if (CurrentTouchBlock()->TouchActionAllowsPanningX()) {
     // Using bigger angle for panning to keep behavior consistent
     // with IE.
     if (IsCloseToHorizontal(aAngle, ALLOWED_DIRECT_PAN_ANGLE)) {
       mY.SetAxisLocked(true);
       SetState(PANNING_LOCKED_X);
       mPanDirRestricted = true;
     } else {
       // Don't treat these touches as pan/zoom movements since 'touch-action' value
       // requires it.
       SetState(NOTHING);
     }
-  } else if (aBehavior & AllowedTouchBehavior::VERTICAL_PAN) {
+  } else if (CurrentTouchBlock()->TouchActionAllowsPanningY()) {
     if (IsCloseToVertical(aAngle, ALLOWED_DIRECT_PAN_ANGLE)) {
       mX.SetAxisLocked(true);
       SetState(PANNING_LOCKED_Y);
       mPanDirRestricted = true;
     } else {
       SetState(NOTHING);
     }
   } else {
@@ -1556,17 +1558,17 @@ nsEventStatus AsyncPanZoomController::St
   // so the page won't jump when we start panning.
   mX.StartTouch(point.x, aEvent.mTime);
   mY.StartTouch(point.y, aEvent.mTime);
 
   double angle = atan2(dy, dx); // range [-pi, pi]
   angle = fabs(angle); // range [0, pi]
 
   if (gfxPrefs::TouchActionEnabled()) {
-    HandlePanningWithTouchAction(angle, GetTouchBehavior(0));
+    HandlePanningWithTouchAction(angle);
   } else {
     if (GetAxisLockMode() == FREE) {
       SetState(PANNING);
     } else {
       HandlePanning(angle);
     }
   }
 
@@ -2453,117 +2455,164 @@ void AsyncPanZoomController::ZoomToRect(
         endZoomToMetrics.GetZoom()));
 
     // Schedule a repaint now, so the new displayport will be painted before the
     // animation finishes.
     RequestContentRepaint(endZoomToMetrics);
   }
 }
 
-void AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) {
-  mTouchBlockState.mPreventDefaultSet = true;
-  mTouchBlockState.mPreventDefault = aPreventDefault;
-  CheckContentResponse();
+void
+AsyncPanZoomController::ScheduleContentResponseTimeout() {
+  APZC_LOG("%p scheduling content response timeout\n", this);
+  PostDelayedTask(
+    NewRunnableMethod(this, &AsyncPanZoomController::ContentResponseTimeout),
+    gfxPrefs::APZContentResponseTimeout());
 }
 
-void AsyncPanZoomController::CheckContentResponse() {
-  bool canProceedToTouchState = true;
-
-  if (mFrameMetrics.mMayHaveTouchListeners ||
-      mFrameMetrics.mMayHaveTouchCaret) {
-    canProceedToTouchState &= mTouchBlockState.mPreventDefaultSet;
-  }
-
-  if (gfxPrefs::TouchActionEnabled()) {
-    canProceedToTouchState &= mTouchBlockState.mAllowedTouchBehaviorSet;
-  }
-
-  if (!canProceedToTouchState) {
-    return;
+void
+AsyncPanZoomController::ContentResponseTimeout() {
+  mTouchBlockBalance++;
+  APZC_LOG("%p got a content response timeout; balance %d\n", this, mTouchBlockBalance);
+  if (mTouchBlockBalance > 0) {
+    // Find the first touch block in the queue that hasn't already received
+    // the content response timeout callback, and notify it.
+    bool found = false;
+    for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
+      if (mTouchBlockQueue[i]->TimeoutContentResponse()) {
+        found = true;
+        break;
+      }
+    }
+    if (found) {
+      ProcessPendingInputBlocks();
+    } else {
+      NS_WARNING("APZC received more ContentResponseTimeout calls than it has unprocessed touch blocks\n");
+    }
   }
-
-  if (mContentResponseTimeoutTask) {
-    mContentResponseTimeoutTask->Cancel();
-    mContentResponseTimeoutTask = nullptr;
-  }
-
-  if (mState == WAITING_CONTENT_RESPONSE) {
-    if (!mTouchBlockState.mPreventDefault) {
-      SetState(NOTHING);
-    }
-
-    mHandlingTouchQueue = true;
-
-    while (!mTouchQueue.IsEmpty()) {
-      if (!mTouchBlockState.mPreventDefault) {
-        HandleInputEvent(mTouchQueue[0]);
-      }
-
-      if (mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_END ||
-          mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_CANCEL) {
-        mTouchQueue.RemoveElementAt(0);
+}
+
+void
+AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) {
+  mTouchBlockBalance--;
+  APZC_LOG("%p got a content response; balance %d\n", this, mTouchBlockBalance);
+  if (mTouchBlockBalance < 0) {
+    // Find the first touch block in the queue that hasn't already received
+    // its response from content, and notify it.
+    bool found = false;
+    for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
+      if (mTouchBlockQueue[i]->SetContentResponse(aPreventDefault)) {
+        found = true;
         break;
       }
-
-      mTouchQueue.RemoveElementAt(0);
     }
-
-    mHandlingTouchQueue = false;
+    if (found) {
+      ProcessPendingInputBlocks();
+    } else {
+      NS_WARNING("APZC received more ContentReceivedTouch calls than it has unprocessed touch blocks\n");
+    }
   }
 }
 
-bool AsyncPanZoomController::TouchActionAllowPinchZoom() {
-  if (!gfxPrefs::TouchActionEnabled()) {
-    return true;
-  }
-  // Pointer events specification implies all touch points to allow zoom
-  // to perform it.
-  for (size_t i = 0; i < mTouchBlockState.mAllowedTouchBehaviors.Length(); i++) {
-    if (!(mTouchBlockState.mAllowedTouchBehaviors[i] & AllowedTouchBehavior::PINCH_ZOOM)) {
-      return false;
+void
+AsyncPanZoomController::SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+  bool found = false;
+  for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
+    if (mTouchBlockQueue[i]->SetAllowedTouchBehaviors(aBehaviors)) {
+      found = true;
+      break;
     }
   }
-  return true;
+  if (found) {
+    ProcessPendingInputBlocks();
+  } else {
+    NS_WARNING("APZC received more SetAllowedTouchBehavior calls than it has unprocessed touch blocks\n");
+  }
 }
 
-bool AsyncPanZoomController::TouchActionAllowDoubleTapZoom() {
-  if (!gfxPrefs::TouchActionEnabled()) {
-    return true;
+void
+AsyncPanZoomController::ProcessPendingInputBlocks() {
+  while (true) {
+    TouchBlockState* curBlock = CurrentTouchBlock();
+    if (!curBlock->IsReadyForHandling()) {
+      break;
+    }
+
+    APZC_LOG("%p processing input block %p; preventDefault %d\n",
+        this, curBlock, curBlock->IsDefaultPrevented());
+    if (curBlock->IsDefaultPrevented()) {
+      SetState(NOTHING);
+      curBlock->DropEvents();
+    } else {
+      while (curBlock->HasEvents()) {
+        HandleInputEvent(curBlock->RemoveFirstEvent());
+      }
+    }
+    MOZ_ASSERT(!curBlock->HasEvents());
+
+    if (mTouchBlockQueue.Length() == 1) {
+      // If |curBlock| is the only touch block in the queue, then it is still
+      // active and we cannot remove it yet. We only know that a touch block is
+      // over when we start the next one. This block will be removed by the code
+      // in StartNewTouchBlock, where new touch blocks are added.
+      break;
+    }
+
+    // If we get here, we know there are more touch blocks in the queue after
+    // |curBlock|, so we can remove |curBlock| and try to process the next one.
+    APZC_LOG("%p discarding depleted touch block %p\n", this, curBlock);
+    mTouchBlockQueue.RemoveElementAt(0);
   }
-  for (size_t i = 0; i < mTouchBlockState.mAllowedTouchBehaviors.Length(); i++) {
-    if (!(mTouchBlockState.mAllowedTouchBehaviors[i] & AllowedTouchBehavior::DOUBLE_TAP_ZOOM)) {
-      return false;
+}
+
+TouchBlockState*
+AsyncPanZoomController::StartNewTouchBlock(bool aCopyAllowedTouchBehaviorFromCurrent)
+{
+  TouchBlockState* newBlock = new TouchBlockState();
+  if (gfxPrefs::TouchActionEnabled() && aCopyAllowedTouchBehaviorFromCurrent) {
+    newBlock->CopyAllowedTouchBehaviorsFrom(*CurrentTouchBlock());
+  }
+
+  // We're going to start a new block, so clear out any depleted blocks at the head of the queue.
+  // See corresponding comment in ProcessPendingInputBlocks.
+  while (!mTouchBlockQueue.IsEmpty()) {
+    if (mTouchBlockQueue[0]->IsReadyForHandling() && !mTouchBlockQueue[0]->HasEvents()) {
+      APZC_LOG("%p discarding depleted touch block %p\n", this, mTouchBlockQueue[0]);
+      mTouchBlockQueue.RemoveElementAt(0);
+    } else {
+      break;
     }
   }
-  return true;
+
+  // Add the new block to the queue.
+  mTouchBlockQueue.AppendElement(newBlock);
+  return newBlock;
 }
 
-AsyncPanZoomController::TouchBehaviorFlags
-AsyncPanZoomController::GetTouchBehavior(uint32_t touchIndex) {
-  if (touchIndex < mTouchBlockState.mAllowedTouchBehaviors.Length()) {
-    return mTouchBlockState.mAllowedTouchBehaviors[touchIndex];
-  }
-  return DefaultTouchBehavior;
+TouchBlockState*
+AsyncPanZoomController::CurrentTouchBlock()
+{
+  MOZ_ASSERT(!mTouchBlockQueue.IsEmpty());
+  return mTouchBlockQueue[0].get();
+}
+
+bool
+AsyncPanZoomController::HasReadyTouchBlock()
+{
+  return !mTouchBlockQueue.IsEmpty() && mTouchBlockQueue[0]->IsReadyForHandling();
 }
 
 AsyncPanZoomController::TouchBehaviorFlags
 AsyncPanZoomController::GetAllowedTouchBehavior(ScreenIntPoint& aPoint) {
   // Here we need to perform a hit testing over the touch-action regions attached to the
   // layer associated with current apzc.
   // Currently they are in progress, for more info see bug 928833.
   return AllowedTouchBehavior::UNKNOWN;
 }
 
-void AsyncPanZoomController::SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors) {
-  mTouchBlockState.mAllowedTouchBehaviors.Clear();
-  mTouchBlockState.mAllowedTouchBehaviors.AppendElements(aBehaviors);
-  mTouchBlockState.mAllowedTouchBehaviorSet = true;
-  CheckContentResponse();
-}
-
 void AsyncPanZoomController::SetState(PanZoomState aNewState) {
 
   PanZoomState oldState;
 
   // Intentional scoping for mutex
   {
     ReentrantMonitorAutoEnter lock(mMonitor);
     oldState = mState;
@@ -2577,37 +2626,23 @@ void AsyncPanZoomController::SetState(Pa
     } else if (IsTransformingState(oldState) && !IsTransformingState(aNewState)) {
       controller->NotifyAPZStateChange(
           GetGuid(), APZStateChange::TransformEnd);
     }
   }
 }
 
 bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) {
-  return !(aState == NOTHING || aState == TOUCHING || aState == WAITING_CONTENT_RESPONSE);
+  return !(aState == NOTHING || aState == TOUCHING);
 }
 
 bool AsyncPanZoomController::IsPanningState(PanZoomState aState) {
   return (aState == PANNING || aState == PANNING_LOCKED_X || aState == PANNING_LOCKED_Y);
 }
 
-void AsyncPanZoomController::SetContentResponseTimer() {
-  if (!mContentResponseTimeoutTask) {
-    mContentResponseTimeoutTask =
-      NewRunnableMethod(this, &AsyncPanZoomController::TimeoutContentResponse);
-
-    PostDelayedTask(mContentResponseTimeoutTask, gfxPrefs::APZContentResponseTimeout());
-  }
-}
-
-void AsyncPanZoomController::TimeoutContentResponse() {
-  mContentResponseTimeoutTask = nullptr;
-  ContentReceivedTouch(false);
-}
-
 void AsyncPanZoomController::UpdateZoomConstraints(const ZoomConstraints& aConstraints) {
   APZC_LOG("%p updating zoom constraints to %d %d %f %f\n", this, aConstraints.mAllowZoom,
     aConstraints.mAllowDoubleTapZoom, aConstraints.mMinZoom.scale, aConstraints.mMaxZoom.scale);
   if (IsNaN(aConstraints.mMinZoom.scale) || IsNaN(aConstraints.mMaxZoom.scale)) {
     NS_WARNING("APZC received zoom constraints with NaN values; dropping...\n");
     return;
   }
   // inf float values and other bad cases should be sanitized by the code below.
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -10,16 +10,17 @@
 #include "CrossProcessMutex.h"
 #include "mozilla/layers/GeckoContentController.h"
 #include "mozilla/layers/APZCTreeManager.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/Monitor.h"
 #include "mozilla/ReentrantMonitor.h"
 #include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
 #include "mozilla/Atomics.h"
 #include "InputData.h"
 #include "Axis.h"
 #include "TaskThrottler.h"
 #include "gfx3DMatrix.h"
 
 #include "base/message_loop.h"
 
@@ -36,16 +37,17 @@ namespace layers {
 struct ScrollableLayerGuid;
 class CompositorParent;
 class GestureEventListener;
 class ContainerLayer;
 class PCompositorParent;
 struct ViewTransform;
 class AsyncPanZoomAnimation;
 class FlingAnimation;
+class TouchBlockState;
 
 /**
  * Controller for all panning and zooming logic. Any time a user input is
  * detected and it must be processed in some way to affect what the user sees,
  * it goes through here. Listens for any input event from InputData and can
  * optionally handle WidgetGUIEvent-derived touch events, but this must be done
  * on the main thread. Note that this class completely cross-platform.
  *
@@ -106,35 +108,30 @@ public:
   // These methods must only be called on the controller/UI thread.
   //
 
   /**
    * General handler for incoming input events. Manipulates the frame metrics
    * based on what type of input it is. For example, a PinchGestureEvent will
    * cause scaling. This should only be called externally to this class.
    * HandleInputEvent() should be used internally.
+   * This function returns nsEventStatus_eIgnore for events that are ignored,
+   * and nsEventStatus_eConsumeDoDefault for events that are queued for
+   * processing pending a content response.
    */
   nsEventStatus ReceiveInputEvent(const InputData& aEvent);
 
   /**
    * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
    * in. The actual animation is done on the compositor thread after being set
    * up.
    */
   void ZoomToRect(CSSRect aRect);
 
   /**
-   * If we have touch listeners, this should always be called when we know
-   * definitively whether or not content has preventDefaulted any touch events
-   * that have come in. If |aPreventDefault| is true, any touch events in the
-   * queue will be discarded.
-   */
-  void ContentReceivedTouch(bool aPreventDefault);
-
-  /**
    * Updates any zoom constraints contained in the <meta name="viewport"> tag.
    */
   void UpdateZoomConstraints(const ZoomConstraints& aConstraints);
 
   /**
    * Return the zoom constraints last set for this APZC (in the constructor
    * or in UpdateZoomConstraints()).
    */
@@ -299,25 +296,16 @@ public:
    * contains info about allowed touch behavior. If regions info isn't enough it returns
    * UNKNOWN value and we should switch to the fallback approach - asking content.
    * TODO: for now it's only a stub and returns hardcoded magic value. As soon as bug 928833
    * is done we should integrate its logic here.
    */
   TouchBehaviorFlags GetAllowedTouchBehavior(ScreenIntPoint& aPoint);
 
   /**
-   * Sets allowed touch behavior for current touch session.
-   * This method is invoked by the APZCTreeManager which in its turn invoked by
-   * the widget after performing touch-action values retrieving.
-   * Must be called after receiving the TOUCH_START even that started the
-   * touch session.
-   */
-  void SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors);
-
-  /**
    * Returns whether this APZC is for an element marked with the 'scrollgrab'
    * attribute.
    */
   bool HasScrollgrab() const { return mFrameMetrics.GetHasScrollgrab(); }
 
   /**
    * Returns whether this APZC has room to be panned (in any direction).
    */
@@ -342,20 +330,16 @@ protected:
 
     CROSS_SLIDING_X,          /* Panning disabled while user does a horizontal gesture
                                  on a vertically-scrollable view. This used for the
                                  Windows Metro "cross-slide" gesture. */
     CROSS_SLIDING_Y,          /* as above for Y axis */
 
     PINCHING,                 /* nth touch-start, where n > 1. this mode allows pan and zoom */
     ANIMATING_ZOOM,           /* animated zoom to a new rect */
-    WAITING_CONTENT_RESPONSE, /* a state halfway between NOTHING and TOUCHING - the user has
-                                 put a finger down, but we don't yet know if a touch listener has
-                                 prevented the default actions yet and the allowed touch behavior
-                                 was not set yet. we still need to abort animations. */
     SNAP_BACK,                /* snap-back animation to relieve overscroll */
   };
 
   // Protected destructor, to discourage deletion outside of Release():
   ~AsyncPanZoomController();
 
   /**
    * Helper method for touches beginning. Sets everything up for panning and any
@@ -482,17 +466,17 @@ protected:
    * gets only the first one and assumes the rest are either missing or not
    * relevant.
    */
   ScreenIntPoint& GetFirstTouchScreenPoint(const MultiTouchInput& aEvent);
 
   /**
    * Sets the panning state basing on the pan direction angle and current touch-action value.
    */
-  void HandlePanningWithTouchAction(double angle, TouchBehaviorFlags value);
+  void HandlePanningWithTouchAction(double angle);
 
   /**
    * Sets the panning state ignoring the touch action value.
    */
   void HandlePanning(double angle);
 
   /**
    * Update the panning state and axis locks.
@@ -539,98 +523,24 @@ protected:
 
   /**
    * Gets the current frame metrics. This is *not* the Gecko copy stored in the
    * layers code.
    */
   const FrameMetrics& GetFrameMetrics() const;
 
   /**
-   * Sets the timer for content response to a series of touch events, if it
-   * hasn't been already. This is to prevent us from batching up touch events
-   * indefinitely in the case that content doesn't respond with whether or not
-   * it wants to preventDefault. When the timer is fired, the touch event queue
-   * will be flushed.
-   */
-  void SetContentResponseTimer();
-
-  /**
-   * Timeout function for content response. This should be called on a timer
-   * after we get our first touch event in a batch, under the condition that we
-   * waiting for response from content. If a notification comes indicating whether or not
-   * content preventDefaulted a series of touch events and touch behavior values are
-   * set before the timeout, the timeout should be cancelled.
-   */
-  void TimeoutContentResponse();
-
-  /**
    * Timeout function for mozbrowserasyncscroll event. Because we throttle
    * mozbrowserasyncscroll events in some conditions, this function ensures
    * that the last mozbrowserasyncscroll event will be fired after a period of
    * time.
    */
   void FireAsyncScrollOnTimeout();
 
 private:
-  // State related to a single touch block. Does not persist across touch blocks.
-  struct TouchBlockState {
-
-    TouchBlockState()
-      :  mAllowedTouchBehaviorSet(false),
-         mPreventDefault(false),
-         mPreventDefaultSet(false),
-         mSingleTapOccurred(false)
-    {}
-
-    // Values of allowed touch behavior for touch points of this touch block.
-    // Since there are maybe a few current active touch points per time (multitouch case)
-    // and each touch point should have its own value of allowed touch behavior- we're
-    // keeping an array of allowed touch behavior values, not the single value.
-    nsTArray<TouchBehaviorFlags> mAllowedTouchBehaviors;
-
-    // Specifies whether mAllowedTouchBehaviors is set for this touch events block.
-    bool mAllowedTouchBehaviorSet;
-
-    // Flag used to specify that content prevented the default behavior of this
-    // touch events block.
-    bool mPreventDefault;
-
-    // Specifies whether mPreventDefault property is set for this touch events block.
-    bool mPreventDefaultSet;
-
-    // Specifies whether a single tap event was generated during this touch block.
-    bool mSingleTapOccurred;
-  };
-
-  /*
-   * Returns whether current touch behavior values allow pinch-zooming.
-   */
-  bool TouchActionAllowPinchZoom();
-
-  /*
-   * Returns whether current touch behavior values allow double-tap-zooming.
-   */
-  bool TouchActionAllowDoubleTapZoom();
-
-  /*
-   * Returns allowed touch behavior from the mAllowedTouchBehavior array.
-   * In case apzc didn't receive touch behavior values within the timeout
-   * it returns default value.
-   */
-  TouchBehaviorFlags GetTouchBehavior(uint32_t touchIndex);
-
-  /**
-   * To move from the WAITING_CONTENT_RESPONSE state to TOUCHING one we need two
-   * conditions set: get content listeners response (whether they called preventDefault)
-   * and get allowed touch behaviors.
-   * This method checks both conditions and changes (or not changes) state
-   * appropriately.
-   */
-  void CheckContentResponse();
-
   /**
    * Helper to set the current state. Holds the monitor before actually setting
    * it and fires content controller events based on state changes. Always set
    * the state using this call, do not set it directly.
    */
   void SetState(PanZoomState aState);
 
   /**
@@ -729,20 +639,16 @@ private:
   // If we don't do this check, we don't get a ShadowLayersUpdated back.
   FrameMetrics mLastPaintRequestMetrics;
   // The last metrics that we actually sent to Gecko. This allows us to transform
   // inputs into a coordinate space that Gecko knows about. This assumes the pipe
   // through which input events and repaint requests are sent to Gecko operates
   // in a FIFO manner.
   FrameMetrics mLastDispatchedPaintMetrics;
 
-  nsTArray<MultiTouchInput> mTouchQueue;
-
-  CancelableTask* mContentResponseTimeoutTask;
-
   AxisX mX;
   AxisY mY;
 
   // This flag is set to true when we are in a axis-locked pan as a result of
   // the touch-action CSS property.
   bool mPanDirRestricted;
 
   // Most up-to-date constraints on zooming. These should always be reasonable
@@ -766,29 +672,108 @@ private:
   // The current offset drawn on the screen, it may not be sent since we have
   // throttling policy for mozbrowserasyncscroll event.
   CSSPoint mCurrentAsyncScrollOffset;
 
   // The delay task triggered by the throttling mozbrowserasyncscroll event
   // ensures the last mozbrowserasyncscroll event is always been fired.
   CancelableTask* mAsyncScrollTimeoutTask;
 
-  // Flag used to determine whether or not we should try to enter the
-  // WAITING_LISTENERS state. This is used in the case that we are processing a
-  // queued up event block. If set, this means that we are handling this queue
-  // and we don't want to queue the events back up again.
-  bool mHandlingTouchQueue;
-
-  // Stores information about the current touch block.
-  TouchBlockState mTouchBlockState;
-
   nsRefPtr<AsyncPanZoomAnimation> mAnimation;
 
   friend class Axis;
 
+  /* ===================================================================
+   * The functions and members in this section are used to manage
+   * blocks of touch events and the state needed to deal with content
+   * listeners.
+   */
+public:
+  /**
+   * This function is invoked by the APZCTreeManager which in turn is invoked
+   * by the widget when web content decides whether or not it wants to
+   * cancel a block of events. This automatically gets applied to the next
+   * block of events that has not yet been responded to. This function MUST
+   * be invoked exactly once for each touch block.
+   */
+  void ContentReceivedTouch(bool aPreventDefault);
+
+  /**
+   * Sets allowed touch behavior for current touch session.
+   * This method is invoked by the APZCTreeManager which in its turn invoked by
+   * the widget after performing touch-action values retrieving.
+   * Must be called after receiving the TOUCH_START even that started the
+   * touch session.
+   */
+  void SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors);
+
+private:
+  void ScheduleContentResponseTimeout();
+  void ContentResponseTimeout();
+  /**
+   * Processes any pending input blocks that are ready for processing. There
+   * must be at least one input block in the queue when this function is called.
+   */
+  void ProcessPendingInputBlocks();
+  TouchBlockState* StartNewTouchBlock(bool aCopyAllowedTouchBehaviorFromCurrent);
+  TouchBlockState* CurrentTouchBlock();
+  bool HasReadyTouchBlock();
+
+private:
+  // The queue of touch blocks that have not yet been processed by this APZC.
+  nsTArray<UniquePtr<TouchBlockState>> mTouchBlockQueue;
+
+  // This variable requires some explanation. Strap yourself in.
+  //
+  // For each block of events, we do two things: (1) send the events to gecko and expect
+  // exactly one call to ContentReceivedTouch in return, and (2) kick off a timeout
+  // that triggers in case we don't hear from web content in a timely fashion.
+  // Since events are constantly coming in, we need to be able to handle more than one
+  // block of input events sitting in the queue.
+  //
+  // There are ordering restrictions on events that we can take advantage of, and that
+  // we need to abide by. Blocks of events in the queue will always be in the order that
+  // the user generated them. Responses we get from content will be in the same order as
+  // as the blocks of events in the queue. The timeout callbacks that have been posted
+  // will also fire in the same order as the blocks of events in the queue.
+  // HOWEVER, we may get multiple responses from content interleaved with multiple
+  // timeout expirations, and that interleaving is not predictable.
+  //
+  // Therefore, we need to make sure that for each block of events, we process the queued
+  // events exactly once, either when we get the response from content, or when the
+  // timeout expires (whichever happens first). There is no way to associate the timeout
+  // or response from content with a particular block of events other than via ordering.
+  //
+  // So, what we do to accomplish this is to track a "touch block balance", which is the
+  // number of timeout expirations that have fired, minus the number of content responses
+  // that have been received. (Think "balance" as in teeter-totter balance). This
+  // value is:
+  // - zero when we are in a state where the next content response we expect to receive
+  //   and the next timeout expiration we expect to fire both correspond to the next
+  //   unprocessed block of events in the queue.
+  // - negative when we are in a state where we have received more content responses than
+  //   timeout expirations. This means that the next content repsonse we receive will
+  //   correspond to the first unprocessed block, but the next n timeout expirations need
+  //   to be ignored as they are for blocks we have already processed. (n is the absolute
+  //   value of the balance.)
+  // - positive when we are in a state where we have received more timeout expirations
+  //   than content responses. This means that the next timeout expiration that we will
+  //   receive will correspond to the first unprocessed block, but the next n content
+  //   responses need to be ignored as they are for blocks we have already processed.
+  //   (n is the absolute value of the balance.)
+  //
+  // Note that each touch block internally carries flags that indicate whether or not it
+  // has received a content response and/or timeout expiration. However, we cannot rely
+  // on that alone to deliver these notifications to the right input block, because
+  // once an input block has been processed, it can potentially be removed from the queue.
+  // Therefore the information in that block is lost. An alternative approach would
+  // be to keep around those blocks until they have received both the content response
+  // and timeout expiration, but that involves a higher level of memory usage.
+  int32_t mTouchBlockBalance;
+
 
   /* ===================================================================
    * The functions and members in this section are used to manage
    * fling animations and handling overscroll during a fling.
    */
 public:
   /**
    * Take over a fling with the given velocity from another APZC. Used for
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/TouchBlockState.cpp
@@ -0,0 +1,219 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "TouchBlockState.h"
+#include "mozilla/layers/APZCTreeManager.h" // for AllowedTouchBehavior
+#include "gfxPrefs.h"                       // for gfxPrefs
+
+#define TBS_LOG(...)
+// #define TBS_LOG(...) printf_stderr("TBS: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Default touch behavior (is used when no touch behavior is set).
+ */
+static const TouchBlockState::TouchBehaviorFlags kDefaultTouchBehavior =
+    AllowedTouchBehavior::VERTICAL_PAN |
+    AllowedTouchBehavior::HORIZONTAL_PAN |
+    AllowedTouchBehavior::PINCH_ZOOM |
+    AllowedTouchBehavior::DOUBLE_TAP_ZOOM;
+
+TouchBlockState::TouchBlockState()
+  : mAllowedTouchBehaviorSet(false)
+  , mPreventDefault(false)
+  , mContentResponded(false)
+  , mContentResponseTimerExpired(false)
+  , mSingleTapOccurred(false)
+{
+  TBS_LOG("Creating %p\n", this);
+}
+
+bool
+TouchBlockState::SetContentResponse(bool aPreventDefault)
+{
+  if (mContentResponded) {
+    return false;
+  }
+  TBS_LOG("%p got content response %d with timer expired %d\n",
+    this, aPreventDefault, mContentResponseTimerExpired);
+  if (!mContentResponseTimerExpired) {
+    mPreventDefault = aPreventDefault;
+  }
+  mContentResponded = true;
+  return true;
+}
+
+bool
+TouchBlockState::TimeoutContentResponse()
+{
+  if (mContentResponseTimerExpired) {
+    return false;
+  }
+  TBS_LOG("%p got content timer expired with response received %d\n",
+    this, mContentResponded);
+  if (!mContentResponded) {
+    mPreventDefault = false;
+  }
+  mContentResponseTimerExpired = true;
+  return true;
+}
+
+bool
+TouchBlockState::SetAllowedTouchBehaviors(const nsTArray<TouchBehaviorFlags>& aBehaviors)
+{
+  if (mAllowedTouchBehaviorSet) {
+    return false;
+  }
+  TBS_LOG("%p got allowed touch behaviours for %d points\n", this, aBehaviors.Length());
+  mAllowedTouchBehaviors.AppendElements(aBehaviors);
+  mAllowedTouchBehaviorSet = true;
+  return true;
+}
+
+bool
+TouchBlockState::CopyAllowedTouchBehaviorsFrom(const TouchBlockState& aOther)
+{
+  TBS_LOG("%p copying allowed touch behaviours from %p\n", this, &aOther);
+  MOZ_ASSERT(aOther.mAllowedTouchBehaviorSet);
+  return SetAllowedTouchBehaviors(aOther.mAllowedTouchBehaviors);
+}
+
+bool
+TouchBlockState::IsReadyForHandling() const
+{
+  // TODO: for long-tap blocks we probably don't need the touch behaviour?
+  if (gfxPrefs::TouchActionEnabled() && !mAllowedTouchBehaviorSet) {
+    return false;
+  }
+  if (!mContentResponded && !mContentResponseTimerExpired) {
+    return false;
+  }
+  return true;
+}
+
+bool
+TouchBlockState::IsDefaultPrevented() const
+{
+  MOZ_ASSERT(mContentResponded || mContentResponseTimerExpired);
+  return mPreventDefault;
+}
+
+void
+TouchBlockState::SetSingleTapOccurred()
+{
+  TBS_LOG("%p setting single-tap occurred\n", this);
+  mSingleTapOccurred = true;
+}
+
+bool
+TouchBlockState::SingleTapOccurred() const
+{
+  return mSingleTapOccurred;
+}
+
+bool
+TouchBlockState::HasEvents() const
+{
+  return !mEvents.IsEmpty();
+}
+
+void
+TouchBlockState::AddEvent(const MultiTouchInput& aEvent)
+{
+  TBS_LOG("%p adding event of type %d\n", this, aEvent.mType);
+  mEvents.AppendElement(aEvent);
+}
+
+void
+TouchBlockState::DropEvents()
+{
+  TBS_LOG("%p dropping %d events\n", this, mEvents.Length());
+  mEvents.Clear();
+}
+
+MultiTouchInput
+TouchBlockState::RemoveFirstEvent()
+{
+  MOZ_ASSERT(!mEvents.IsEmpty());
+  TBS_LOG("%p returning first of %d events\n", this, mEvents.Length());
+  MultiTouchInput event = mEvents[0];
+  mEvents.RemoveElementAt(0);
+  return event;
+}
+
+bool
+TouchBlockState::TouchActionAllowsPinchZoom() const
+{
+  if (!gfxPrefs::TouchActionEnabled()) {
+    return true;
+  }
+  // Pointer events specification requires that all touch points allow zoom.
+  for (size_t i = 0; i < mAllowedTouchBehaviors.Length(); i++) {
+    if (!(mAllowedTouchBehaviors[i] & AllowedTouchBehavior::PINCH_ZOOM)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+bool
+TouchBlockState::TouchActionAllowsDoubleTapZoom() const
+{
+  if (!gfxPrefs::TouchActionEnabled()) {
+    return true;
+  }
+  for (size_t i = 0; i < mAllowedTouchBehaviors.Length(); i++) {
+    if (!(mAllowedTouchBehaviors[i] & AllowedTouchBehavior::DOUBLE_TAP_ZOOM)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+bool
+TouchBlockState::TouchActionAllowsPanningX() const
+{
+  if (!gfxPrefs::TouchActionEnabled()) {
+    return true;
+  }
+  if (mAllowedTouchBehaviors.IsEmpty()) {
+    return false;
+  }
+  TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+  return (flags & AllowedTouchBehavior::HORIZONTAL_PAN);
+}
+
+bool
+TouchBlockState::TouchActionAllowsPanningY() const
+{
+  if (!gfxPrefs::TouchActionEnabled()) {
+    return true;
+  }
+  if (mAllowedTouchBehaviors.IsEmpty()) {
+    return false;
+  }
+  TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+  return (flags & AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+bool
+TouchBlockState::TouchActionAllowsPanningXY() const
+{
+  if (!gfxPrefs::TouchActionEnabled()) {
+    return true;
+  }
+  if (mAllowedTouchBehaviors.IsEmpty()) {
+    return false;
+  }
+  TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+  return (flags & AllowedTouchBehavior::HORIZONTAL_PAN)
+      && (flags & AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+} // namespace layers
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/TouchBlockState.h
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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_TouchBlockState_h
+#define mozilla_layers_TouchBlockState_h
+
+#include "nsTArray.h"                       // for nsTArray
+#include "InputData.h"                      // for MultiTouchInput
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class represents a single touch block. A touch block is
+ * a set of touch events that can be cancelled by web content via
+ * touch event listeners.
+ *
+ * Every touch-start event creates a new touch block. In this case, the
+ * touch block consists of the touch-start, followed by all touch events
+ * up to but not including the next touch-start (except in the case where
+ * a long-tap happens, see below). Note that in particular we cannot know
+ * when a touch block ends until the next one is started. Most touch
+ * blocks are created by receipt of a touch-start event.
+ *
+ * Every long-tap event also creates a new touch block, since it can also
+ * be consumed by web content. In this case, when the long-tap event is
+ * dispatched to web content, a new touch block is started to hold the remaining
+ * touch events, up to but not including the next touch start (or long-tap).
+ *
+ * Conceptually, each touch block can be cancelled by web content, and
+ * this information is stored in the mPreventDefault flag. Because web
+ * content runs on the Gecko main thread, we cannot always wait for web content's
+ * response. Instead, there is a timeout that sets this flag in the case
+ * where web content doesn't respond in time. The mContentResponded
+ * and mContentResponseTimerExpired flags indicate which of these scenarios
+ * occurred.
+ *
+ * Additionally, if touch-action is enabled, each touch block should
+ * have a set of allowed touch behavior flags; one for each touch point.
+ * This also requires running code on the Gecko main thread, and so may
+ * be populated with some latency. The mAllowedTouchBehaviorSet and
+ * mAllowedTouchBehaviors variables track this information.
+ */
+class TouchBlockState
+{
+public:
+  typedef uint32_t TouchBehaviorFlags;
+
+  TouchBlockState();
+
+  /**
+   * Record whether or not content cancelled this block of events.
+   * @param aPreventDefault true iff the block is cancelled.
+   * @return false if this block has already received a response from
+   *         web content, true if not.
+   */
+  bool SetContentResponse(bool aPreventDefault);
+  /**
+   * Record that content didn't respond in time.
+   * @return false if this block already timed out, true if not.
+   */
+  bool TimeoutContentResponse();
+  /**
+   * Set the allowed touch behavior flags for this block.
+   * @return false if this block already has these flags set, true if not.
+   */
+  bool SetAllowedTouchBehaviors(const nsTArray<TouchBehaviorFlags>& aBehaviors);
+  /**
+   * Copy the allowed touch behavior flags from another block.
+   * @return false if this block already has these flags set, true if not.
+   */
+  bool CopyAllowedTouchBehaviorsFrom(const TouchBlockState& aOther);
+
+  /**
+   * @return true iff this block has received all the information needed
+   *         to properly dispatch the events in the block.
+   */
+  bool IsReadyForHandling() const;
+  /**
+   * @return true iff web content cancelled this block of events.
+   */
+  bool IsDefaultPrevented() const;
+
+  /**
+   * Set a flag that indicates that this touch block triggered a single tap event.
+   */
+  void SetSingleTapOccurred();
+  /**
+   * @return true iff SetSingleTapOccurred was previously called on this block.
+   */
+  bool SingleTapOccurred() const;
+
+  /**
+   * @return true iff there are pending events in this touch block.
+   */
+  bool HasEvents() const;
+  /**
+   * Add a new touch event to the queue of events in this input block.
+   */
+  void AddEvent(const MultiTouchInput& aEvent);
+  /**
+   * Throw away all the events in this input block.
+   */
+  void DropEvents();
+  /**
+   * @return the first event in the queue. The event is removed from the queue
+   *         before it is returned.
+   */
+  MultiTouchInput RemoveFirstEvent();
+
+  /**
+   * @return false iff touch-action is enabled and the allowed touch behaviors for
+   *         this touch block do not allow pinch-zooming.
+   */
+  bool TouchActionAllowsPinchZoom() const;
+  /**
+   * @return false iff touch-action is enabled and the allowed touch behaviors for
+   *         this touch block do not allow double-tap zooming.
+   */
+  bool TouchActionAllowsDoubleTapZoom() const;
+  /**
+   * @return false iff touch-action is enabled and the allowed touch behaviors for
+   *         the first touch point do not allow panning in the specified direction(s).
+   */
+  bool TouchActionAllowsPanningX() const;
+  bool TouchActionAllowsPanningY() const;
+  bool TouchActionAllowsPanningXY() const;
+
+private:
+  nsTArray<TouchBehaviorFlags> mAllowedTouchBehaviors;
+  bool mAllowedTouchBehaviorSet;
+  bool mPreventDefault;
+  bool mContentResponded;
+  bool mContentResponseTimerExpired;
+  bool mSingleTapOccurred;
+  nsTArray<MultiTouchInput> mEvents;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_TouchBlockState_h
--- a/gfx/layers/moz.build
+++ b/gfx/layers/moz.build
@@ -230,16 +230,17 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk
         ]
 
 UNIFIED_SOURCES += [
     'apz/src/APZCTreeManager.cpp',
     'apz/src/AsyncPanZoomController.cpp',
     'apz/src/Axis.cpp',
     'apz/src/GestureEventListener.cpp',
     'apz/src/TaskThrottler.cpp',
+    'apz/src/TouchBlockState.cpp',
     'apz/testutil/APZTestData.cpp',
     'apz/util/ActiveElementManager.cpp',
     'apz/util/APZCCallbackHelper.cpp',
     'basic/BasicCanvasLayer.cpp',
     'basic/BasicColorLayer.cpp',
     'basic/BasicCompositor.cpp',
     'basic/BasicContainerLayer.cpp',
     'basic/BasicImages.cpp',
--- a/gfx/tests/gtest/TestAsyncPanZoomController.cpp
+++ b/gfx/tests/gtest/TestAsyncPanZoomController.cpp
@@ -344,28 +344,31 @@ ApzcPanAndCheckStatus(AsyncPanZoomContro
                       bool expectIgnoredPan,
                       bool hasTouchListeners,
                       nsTArray<uint32_t>* aAllowedTouchBehaviors)
 {
   nsEventStatus statuses[4]; // down, move, move, up
   ApzcPan(aApzc, aTreeManager, aTime, aTouchStartY, aTouchEndY, false, aAllowedTouchBehaviors, &statuses);
 
   nsEventStatus touchStartStatus;
-  if (hasTouchListeners) {
+  if (hasTouchListeners || gfxPrefs::TouchActionEnabled()) {
     // APZC shouldn't consume the start event now, instead queueing it up
-    // waiting for content's response.
-    touchStartStatus = nsEventStatus_eIgnore;
+    // waiting for content's response and/or allowed behavior.
+    touchStartStatus = nsEventStatus_eConsumeDoDefault;
   } else {
     // APZC should go into the touching state and therefore consume the event.
     touchStartStatus = nsEventStatus_eConsumeNoDefault;
   }
   EXPECT_EQ(touchStartStatus, statuses[0]);
 
   nsEventStatus touchMoveStatus;
-  if (expectIgnoredPan) {
+  if (hasTouchListeners) {
+    // APZC will queue up this event while waiting for content's response.
+    touchMoveStatus = nsEventStatus_eConsumeDoDefault;
+  } else if (expectIgnoredPan) {
     // APZC should ignore panning, be in TOUCHING state and therefore return eIgnore.
     // The same applies to all consequent touch move events.
     touchMoveStatus = nsEventStatus_eIgnore;
   } else {
     // APZC should go into the panning state and therefore consume the event.
     touchMoveStatus = nsEventStatus_eConsumeNoDefault;
   }
   EXPECT_EQ(touchMoveStatus, statuses[1]);
@@ -1031,64 +1034,75 @@ TEST_F(APZCGestureDetectorTester, Medium
 class APZCLongPressTester : public APZCGestureDetectorTester {
 protected:
   void DoLongPressTest(uint32_t aBehavior) {
     MakeApzcUnzoomable();
 
     int time = 0;
 
     nsEventStatus status = ApzcDown(apzc, 10, 10, time);
-    EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
+    if (gfxPrefs::TouchActionEnabled()) {
+      // If touch-action is enabled, then the event is queued until the
+      // allowed touch behavior is set.
+      EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
+    } else {
+      // Otherwise, it is processed immediately.
+      EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
+    }
 
-    // SetAllowedTouchBehavior() must be called after sending touch-start.
-    nsTArray<uint32_t> allowedTouchBehaviors;
-    allowedTouchBehaviors.AppendElement(aBehavior);
-    apzc->SetAllowedTouchBehavior(allowedTouchBehaviors);
+    if (gfxPrefs::TouchActionEnabled()) {
+      // SetAllowedTouchBehavior() must be called after sending touch-start.
+      nsTArray<uint32_t> allowedTouchBehaviors;
+      allowedTouchBehaviors.AppendElement(aBehavior);
+      apzc->SetAllowedTouchBehavior(allowedTouchBehaviors);
+    }
+    // Have content "respond" to the touchstart
+    apzc->ContentReceivedTouch(false);
 
     MockFunction<void(std::string checkPointName)> check;
 
     {
       InSequence s;
 
       EXPECT_CALL(check, Call("preHandleLongTap"));
       EXPECT_CALL(*mcc, HandleLongTap(CSSPoint(10, 10), 0, apzc->GetGuid())).Times(1);
       EXPECT_CALL(check, Call("postHandleLongTap"));
 
       EXPECT_CALL(check, Call("preHandleLongTapUp"));
       EXPECT_CALL(*mcc, HandleLongTapUp(CSSPoint(10, 10), 0, apzc->GetGuid())).Times(1);
       EXPECT_CALL(check, Call("postHandleLongTapUp"));
     }
 
+    // There is a longpress event scheduled on a timeout
     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.
+
+    // Dispatching the longpress event starts a new touch block, which
+    // needs a new content response and also has a pending timeout task
+    // in the queue. Deal with those here. We do the content response first
+    // with preventDefault=false, and then we run the timeout task which
+    // "loses the race" and does nothing.
+    apzc->ContentReceivedTouch(false);
     mcc->CheckHasDelayedTask();
-    mcc->ClearDelayedTask();
-    apzc->ContentReceivedTouch(true);
+    mcc->RunDelayedTask();
 
     time += 1000;
 
+    // Finally, simulate lifting the finger. Since the long-press wasn't
+    // prevent-defaulted, we should get a long-tap-up event.
+    check.Call("preHandleLongTapUp");
     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.
-    check.Call("preHandleLongTapUp");
-    apzc->ContentReceivedTouch(false);
     check.Call("postHandleLongTapUp");
 
     apzc->AssertStateIsReset();
   }
 
   void DoLongPressPreventDefaultTest(uint32_t aBehavior) {
     MakeApzcUnzoomable();
 
@@ -1096,22 +1110,33 @@ protected:
     EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
 
     int touchX = 10,
         touchStartY = 10,
         touchEndY = 50;
 
     int time = 0;
     nsEventStatus status = ApzcDown(apzc, touchX, touchStartY, time);
-    EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
+    if (gfxPrefs::TouchActionEnabled()) {
+      // If touch-action is enabled, then the event is queued until the
+      // allowed touch behavior is set.
+      EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
+    } else {
+      // Otherwise, it is processed immediately.
+      EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
+    }
 
-    // SetAllowedTouchBehavior() must be called after sending touch-start.
-    nsTArray<uint32_t> allowedTouchBehaviors;
-    allowedTouchBehaviors.AppendElement(aBehavior);
-    apzc->SetAllowedTouchBehavior(allowedTouchBehaviors);
+    if (gfxPrefs::TouchActionEnabled()) {
+      // SetAllowedTouchBehavior() must be called after sending touch-start.
+      nsTArray<uint32_t> allowedTouchBehaviors;
+      allowedTouchBehaviors.AppendElement(aBehavior);
+      apzc->SetAllowedTouchBehavior(allowedTouchBehaviors);
+    }
+    // Have content "respond" to the touchstart
+    apzc->ContentReceivedTouch(false);
 
     MockFunction<void(std::string checkPointName)> check;
 
     {
       InSequence s;
 
       EXPECT_CALL(check, Call("preHandleLongTap"));
       EXPECT_CALL(*mcc, HandleLongTap(CSSPoint(touchX, touchStartY), 0, apzc->GetGuid())).Times(1);
@@ -1122,38 +1147,37 @@ protected:
 
     // 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();
+
+    // There should be a TimeoutContentResponse task in the queue still,
+    // waiting for the response from the longtap event dispatched above.
+    // Send the signal that content has handled the long-tap, and then run
+    // the timeout task (it will be a no-op because the content "wins" the
+    // race. This takes the place of the "contextmenu" event.
     apzc->ContentReceivedTouch(true);
+    mcc->CheckHasDelayedTask();
+    mcc->RunDelayedTask();
 
     time += 1000;
 
     MultiTouchInput mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, time, TimeStamp(), 0);
     mti.mTouches.AppendElement(SingleTouchData(0, ScreenIntPoint(touchX, touchEndY), ScreenSize(0, 0), 0, 0));
     status = apzc->ReceiveInputEvent(mti);
     EXPECT_EQ(nsEventStatus_eIgnore, status);
 
     EXPECT_CALL(*mcc, HandleLongTapUp(CSSPoint(touchX, touchEndY), 0, apzc->GetGuid())).Times(0);
     status = ApzcUp(apzc, touchX, touchEndY, time);
     EXPECT_EQ(nsEventStatus_eIgnore, status);
 
-    // Flush the event queue. Once the "contextmenu" event is handled, any touch
-    // events that come from the same series of start->n*move->end events should
-    // be discarded, even if only the "contextmenu" event is preventDefaulted.
-    apzc->ContentReceivedTouch(false);
-
     ScreenPoint pointOut;
     ViewTransform viewTransformOut;
     apzc->SampleContentTransformForFrame(testStartTime, &viewTransformOut, pointOut);
 
     EXPECT_EQ(ScreenPoint(), pointOut);
     EXPECT_EQ(ViewTransform(), viewTransformOut);
 
     apzc->AssertStateIsReset();