Bug 1302736 - Fire click events with a clickCount of 2 when the user does a double-tap gesture with double taps not allowed. r=botond
authorKartikaya Gupta <kgupta@mozilla.com>
Thu, 29 Sep 2016 10:05:25 -0400
changeset 315834 40b602b3a8701ad3a4323072cb64a2d40a97c65a
parent 315833 7de1a998b12ee384adec77550f7364eef1283660
child 315835 4d39febef604b36a675216cfa5cb605a8189d212
push idunknown
push userunknown
push dateunknown
reviewersbotond
bugs1302736
milestone52.0a1
Bug 1302736 - Fire click events with a clickCount of 2 when the user does a double-tap gesture with double taps not allowed. r=botond MozReview-Commit-ID: 5qxHMoHXDXh
dom/ipc/TabChild.cpp
gfx/layers/apz/public/GeckoContentController.h
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/src/AsyncPanZoomController.h
gfx/layers/apz/src/GestureEventListener.cpp
gfx/layers/apz/src/GestureEventListener.h
gfx/layers/apz/test/gtest/TestGestureDetector.cpp
gfx/layers/apz/util/APZCCallbackHelper.cpp
gfx/layers/apz/util/APZCCallbackHelper.h
gfx/layers/apz/util/APZEventState.cpp
gfx/layers/apz/util/APZEventState.h
gfx/layers/apz/util/ChromeProcessController.cpp
widget/InputData.h
widget/android/AndroidContentController.cpp
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -1777,22 +1777,30 @@ TabChild::RecvHandleTap(const GeckoConte
   CSSPoint point = APZCCallbackHelper::ApplyCallbackTransform(aPoint / scale, aGuid);
 
   switch (aType) {
   case GeckoContentController::TapType::eSingleTap:
     if (mRemoteFrame) {
       mRemoteFrame->SendTakeFocusForClickFromTap();
     }
     if (mGlobal && mTabChildGlobal) {
-      mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid);
+      mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid, 1);
     }
     break;
   case GeckoContentController::TapType::eDoubleTap:
     HandleDoubleTap(point, aModifiers, aGuid);
     break;
+  case GeckoContentController::TapType::eSecondTap:
+    if (mRemoteFrame) {
+      mRemoteFrame->SendTakeFocusForClickFromTap();
+    }
+    if (mGlobal && mTabChildGlobal) {
+      mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid, 2);
+    }
+    break;
   case GeckoContentController::TapType::eLongTap:
     if (mGlobal && mTabChildGlobal) {
       mAPZEventState->ProcessLongTap(presShell, point, scale, aModifiers, aGuid,
           aInputBlockId);
     }
     break;
   case GeckoContentController::TapType::eLongTapUp:
     if (mGlobal && mTabChildGlobal) {
--- a/gfx/layers/apz/public/GeckoContentController.h
+++ b/gfx/layers/apz/public/GeckoContentController.h
@@ -38,20 +38,26 @@ public:
 
   /**
    * Different types of tap-related events that can be sent in
    * the HandleTap function. The names should be relatively self-explanatory.
    * Note that the eLongTapUp will always be preceded by an eLongTap, but not
    * all eLongTap notifications will be followed by an eLongTapUp (for instance,
    * if the user moves their finger after triggering the long-tap but before
    * lifting it).
+   * The difference between eDoubleTap and eSecondTap is subtle - the eDoubleTap
+   * is for an actual double-tap "gesture" while eSecondTap is for the same user
+   * input but where a double-tap gesture is not allowed. This is used to fire
+   * a click event with detail=2 to web content (similar to what a mouse double-
+   * click would do).
    */
   enum class TapType {
     eSingleTap,
     eDoubleTap,
+    eSecondTap,
     eLongTap,
     eLongTapUp,
 
     eSentinel,
   };
 
   /**
    * Requests handling of a tap event. |aPoint| is in LD pixels, relative to the
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -1054,16 +1054,17 @@ nsEventStatus AsyncPanZoomController::Ha
   case TAPGESTURE_INPUT: {
     const TapGestureInput& tapGestureInput = aEvent.AsTapGestureInput();
     switch (tapGestureInput.mType) {
       case TapGestureInput::TAPGESTURE_LONG: rv = OnLongPress(tapGestureInput); break;
       case TapGestureInput::TAPGESTURE_LONG_UP: rv = OnLongPressUp(tapGestureInput); break;
       case TapGestureInput::TAPGESTURE_UP: rv = OnSingleTapUp(tapGestureInput); break;
       case TapGestureInput::TAPGESTURE_CONFIRMED: rv = OnSingleTapConfirmed(tapGestureInput); break;
       case TapGestureInput::TAPGESTURE_DOUBLE: rv = OnDoubleTap(tapGestureInput); break;
+      case TapGestureInput::TAPGESTURE_SECOND: rv = OnSecondTap(tapGestureInput); break;
       case TapGestureInput::TAPGESTURE_CANCEL: rv = OnCancelTap(tapGestureInput); break;
       default: NS_WARNING("Unhandled tap gesture"); break;
     }
     break;
   }
   default: NS_WARNING("Unhandled input event"); break;
   }
 
@@ -2114,16 +2115,22 @@ nsEventStatus AsyncPanZoomController::On
             aEvent.modifiers, GetGuid(), GetCurrentTouchBlock()->GetBlockId());
       }
     }
     return nsEventStatus_eConsumeNoDefault;
   }
   return nsEventStatus_eIgnore;
 }
 
+nsEventStatus AsyncPanZoomController::OnSecondTap(const TapGestureInput& aEvent)
+{
+  APZC_LOG("%p got a second-tap in state %d\n", this, mState);
+  return GenerateSingleTap(TapType::eSecondTap, aEvent.mPoint, aEvent.modifiers);
+}
+
 nsEventStatus AsyncPanZoomController::OnCancelTap(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a cancel-tap in state %d\n", this, mState);
   // XXX: Implement this.
   return nsEventStatus_eIgnore;
 }
 
 
 ScreenToParentLayerMatrix4x4 AsyncPanZoomController::GetTransformToThis() const {
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -474,16 +474,21 @@ protected:
   nsEventStatus OnSingleTapConfirmed(const TapGestureInput& aEvent);
 
   /**
    * Helper method for double taps.
    */
   nsEventStatus OnDoubleTap(const TapGestureInput& aEvent);
 
   /**
+   * Helper method for double taps where the double-tap gesture is disabled.
+   */
+  nsEventStatus OnSecondTap(const TapGestureInput& aEvent);
+
+  /**
    * Helper method to cancel any gesture currently going to Gecko. Used
    * primarily when a user taps the screen over some clickable content but then
    * pans down instead of letting go (i.e. to cancel a previous touch so that a
    * new one can properly take effect.
    */
   nsEventStatus OnCancelTap(const TapGestureInput& aEvent);
 
   /**
--- a/gfx/layers/apz/src/GestureEventListener.cpp
+++ b/gfx/layers/apz/src/GestureEventListener.cpp
@@ -191,28 +191,25 @@ nsEventStatus GestureEventListener::Hand
     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;
     break;
   case GESTURE_FIRST_SINGLE_TOUCH_UP:
+  case GESTURE_SECOND_SINGLE_TOUCH_DOWN:
     // Cancel wait for double tap
     CancelMaxTapTimeoutTask();
+    MOZ_ASSERT(mSingleTapSent.isSome());
+    if (!mSingleTapSent.value()) {
+      TriggerSingleTapConfirmedEvent();
+    }
+    mSingleTapSent = Nothing();
     SetState(GESTURE_MULTI_TOUCH_DOWN);
-    TriggerSingleTapConfirmedEvent();
-    // 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);
-    TriggerSingleTapConfirmedEvent();
     // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
     rv = nsEventStatus_eConsumeNoDefault;
     break;
   case GESTURE_LONG_TOUCH_DOWN:
     SetState(GESTURE_MULTI_TOUCH_DOWN);
     break;
   case GESTURE_MULTI_TOUCH_DOWN:
   case GESTURE_PINCH:
@@ -255,16 +252,17 @@ nsEventStatus GestureEventListener::Hand
 
   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.
     if (MoveDistanceIsLarge()) {
       CancelLongTapTimeoutTask();
       CancelMaxTapTimeoutTask();
+      mSingleTapSent = Nothing();
       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");
@@ -341,31 +339,31 @@ nsEventStatus GestureEventListener::Hand
     // GEL doesn't have a dedicated state for PANNING handled in APZC thus ignore.
     break;
 
   case GESTURE_FIRST_SINGLE_TOUCH_DOWN: {
     CancelLongTapTimeoutTask();
     CancelMaxTapTimeoutTask();
     nsEventStatus tapupStatus = mAsyncPanZoomController->HandleGestureEvent(
         CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_UP));
-    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);
-    }
+    mSingleTapSent = Some(tapupStatus != nsEventStatus_eIgnore);
+    SetState(GESTURE_FIRST_SINGLE_TOUCH_UP);
+    CreateMaxTapTimeoutTask();
     break;
   }
 
   case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
     CancelMaxTapTimeoutTask();
-    SetState(GESTURE_NONE);
+    MOZ_ASSERT(mSingleTapSent.isSome());
     mAsyncPanZoomController->HandleGestureEvent(
-        CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_DOUBLE));
+        CreateTapEvent(mLastTouchInput,
+            mSingleTapSent.value() ? TapGestureInput::TAPGESTURE_SECOND
+                                   : TapGestureInput::TAPGESTURE_DOUBLE));
+    mSingleTapSent = Nothing();
+    SetState(GESTURE_NONE);
     break;
   }
 
   case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
     CancelLongTapTimeoutTask();
     SetState(GESTURE_NONE);
     TriggerSingleTapConfirmedEvent();
     break;
@@ -412,16 +410,17 @@ nsEventStatus GestureEventListener::Hand
     break;
   }
 
   return rv;
 }
 
 nsEventStatus GestureEventListener::HandleInputTouchCancel()
 {
+  mSingleTapSent = Nothing();
   SetState(GESTURE_NONE);
   CancelMaxTapTimeoutTask();
   CancelLongTapTimeoutTask();
   return nsEventStatus_eIgnore;
 }
 
 void GestureEventListener::HandleInputTimeoutLongTap()
 {
@@ -453,20 +452,22 @@ void GestureEventListener::HandleInputTi
   GEL_LOG("Running max-tap timeout task in state %d\n", mState);
 
   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);
-    if (!aDuringFastFling) {
+    MOZ_ASSERT(mSingleTapSent.isSome());
+    if (!aDuringFastFling && !mSingleTapSent.value()) {
       TriggerSingleTapConfirmedEvent();
     }
+    mSingleTapSent = Nothing();
+    SetState(GESTURE_NONE);
   } else {
     NS_WARNING("Unhandled state upon MAX_TAP timeout");
     SetState(GESTURE_NONE);
   }
 }
 
 void GestureEventListener::TriggerSingleTapConfirmedEvent()
 {
--- a/gfx/layers/apz/src/GestureEventListener.h
+++ b/gfx/layers/apz/src/GestureEventListener.h
@@ -226,14 +226,23 @@ private:
    * GESTURE_FIRST_SINGLE_TOUCH_UP and GESTURE_SECOND_SINGLE_TOUCH_DOWN states.
    *
    * CancelMaxTapTimeoutTask: Cancel the mMaxTapTimeoutTask and also set
    * it to null.
    */
   RefPtr<CancelableRunnable> mMaxTapTimeoutTask;
   void CancelMaxTapTimeoutTask();
   void CreateMaxTapTimeoutTask();
+
+  /**
+   * Tracks whether the single-tap event was already sent to content. This is
+   * needed because it affects how the double-tap gesture, if detected, is
+   * handled. The value is only valid in states GESTURE_FIRST_SINGLE_TOUCH_UP and
+   * GESTURE_SECOND_SINGLE_TOUCH_DOWN; to more easily catch violations it is
+   * stored in a Maybe which is set to Nothing() at all other times.
+   */
+  Maybe<bool> mSingleTapSent;
 };
 
 } // namespace layers
 } // namespace mozilla
 
 #endif
--- a/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
+++ b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
@@ -494,17 +494,18 @@ TEST_F(APZCGestureDetectorTester, Double
 
   apzc->AssertStateIsReset();
 }
 
 TEST_F(APZCGestureDetectorTester, DoubleTapNotZoomable) {
   MakeApzcWaitForMainThread();
   MakeApzcUnzoomable();
 
-  EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(2);
+  EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+  EXPECT_CALL(*mcc, HandleTap(TapType::eSecondTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
   EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(0);
 
   uint64_t blockIds[2];
   DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
 
   // responses to the two touchstarts
   apzc->ContentReceivedInputBlock(blockIds[0], false);
   apzc->ContentReceivedInputBlock(blockIds[1], false);
--- a/gfx/layers/apz/util/APZCCallbackHelper.cpp
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -473,30 +473,31 @@ APZCCallbackHelper::DispatchWidgetEvent(
   return status;
 }
 
 nsEventStatus
 APZCCallbackHelper::DispatchSynthesizedMouseEvent(EventMessage aMsg,
                                                   uint64_t aTime,
                                                   const LayoutDevicePoint& aRefPoint,
                                                   Modifiers aModifiers,
+                                                  int32_t aClickCount,
                                                   nsIWidget* aWidget)
 {
   MOZ_ASSERT(aMsg == eMouseMove || aMsg == eMouseDown ||
              aMsg == eMouseUp || aMsg == eMouseLongTap);
 
   WidgetMouseEvent event(true, aMsg, aWidget,
                          WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal);
   event.mRefPoint = LayoutDeviceIntPoint::Truncate(aRefPoint.x, aRefPoint.y);
   event.mTime = aTime;
   event.button = WidgetMouseEvent::eLeftButton;
   event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_TOUCH;
   event.mIgnoreRootScrollFrame = true;
   if (aMsg != eMouseMove) {
-    event.mClickCount = 1;
+    event.mClickCount = aClickCount;
   }
   event.mModifiers = aModifiers;
   // Real touch events will generate corresponding pointer events. We set
   // convertToPointer to false to prevent the synthesized mouse events generate
   // pointer events again.
   event.convertToPointer = false;
   return DispatchWidgetEvent(event);
 }
@@ -520,27 +521,28 @@ APZCCallbackHelper::DispatchMouseEvent(c
       &defaultPrevented, false, /* aIsWidgetEventSynthesized = */ false);
   return defaultPrevented;
 }
 
 
 void
 APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint,
                                        Modifiers aModifiers,
+                                       int32_t aClickCount,
                                        nsIWidget* aWidget)
 {
   if (aWidget->Destroyed()) {
     return;
   }
   APZCCH_LOG("Dispatching single-tap component events to %s\n",
     Stringify(aPoint).c_str());
   int time = 0;
-  DispatchSynthesizedMouseEvent(eMouseMove, time, aPoint, aModifiers, aWidget);
-  DispatchSynthesizedMouseEvent(eMouseDown, time, aPoint, aModifiers, aWidget);
-  DispatchSynthesizedMouseEvent(eMouseUp, time, aPoint, aModifiers, aWidget);
+  DispatchSynthesizedMouseEvent(eMouseMove, time, aPoint, aModifiers, aClickCount, aWidget);
+  DispatchSynthesizedMouseEvent(eMouseDown, time, aPoint, aModifiers, aClickCount, aWidget);
+  DispatchSynthesizedMouseEvent(eMouseUp, time, aPoint, aModifiers, aClickCount, aWidget);
 }
 
 static dom::Element*
 GetDisplayportElementFor(nsIScrollableFrame* aScrollableFrame)
 {
   if (!aScrollableFrame) {
     return nullptr;
   }
--- a/gfx/layers/apz/util/APZCCallbackHelper.h
+++ b/gfx/layers/apz/util/APZCCallbackHelper.h
@@ -103,16 +103,17 @@ public:
     static nsEventStatus DispatchWidgetEvent(WidgetGUIEvent& aEvent);
 
     /* Synthesize a mouse event with the given parameters, and dispatch it
      * via the given widget. */
     static nsEventStatus DispatchSynthesizedMouseEvent(EventMessage aMsg,
                                                        uint64_t aTime,
                                                        const LayoutDevicePoint& aRefPoint,
                                                        Modifiers aModifiers,
+                                                       int32_t aClickCount,
                                                        nsIWidget* aWidget);
 
     /* Dispatch a mouse event with the given parameters.
      * Return whether or not any listeners have called preventDefault on the event. */
     static bool DispatchMouseEvent(const nsCOMPtr<nsIPresShell>& aPresShell,
                                    const nsString& aType,
                                    const CSSPoint& aPoint,
                                    int32_t aButton,
@@ -120,16 +121,17 @@ public:
                                    int32_t aModifiers,
                                    bool aIgnoreRootScrollFrame,
                                    unsigned short aInputSourceArg);
 
     /* Fire a single-tap event at the given point. The event is dispatched
      * via the given widget. */
     static void FireSingleTapEvent(const LayoutDevicePoint& aPoint,
                                    Modifiers aModifiers,
+                                   int32_t aClickCount,
                                    nsIWidget* aWidget);
 
     /* Perform hit-testing on the touch points of |aEvent| to determine
      * which scrollable frames they target. If any of these frames don't have
      * a displayport, set one.
      *
      * If any displayports need to be set, the actual notification to APZ is
      * sent to the compositor, which will then post a message back to APZ's
--- a/gfx/layers/apz/util/APZEventState.cpp
+++ b/gfx/layers/apz/util/APZEventState.cpp
@@ -124,29 +124,31 @@ APZEventState::~APZEventState()
 class DelayedFireSingleTapEvent final : public nsITimerCallback
 {
 public:
   NS_DECL_ISUPPORTS
 
   DelayedFireSingleTapEvent(nsWeakPtr aWidget,
                             LayoutDevicePoint& aPoint,
                             Modifiers aModifiers,
+                            int32_t aClickCount,
                             nsITimer* aTimer)
     : mWidget(aWidget)
     , mPoint(aPoint)
     , mModifiers(aModifiers)
+    , mClickCount(aClickCount)
     // Hold the reference count until we are called back.
     , mTimer(aTimer)
   {
   }
 
   NS_IMETHOD Notify(nsITimer*) override
   {
     if (nsCOMPtr<nsIWidget> widget = do_QueryReferent(mWidget)) {
-      APZCCallbackHelper::FireSingleTapEvent(mPoint, mModifiers, widget);
+      APZCCallbackHelper::FireSingleTapEvent(mPoint, mModifiers, mClickCount, widget);
     }
     mTimer = nullptr;
     return NS_OK;
   }
 
   void ClearTimer() {
     mTimer = nullptr;
   }
@@ -154,26 +156,28 @@ public:
 private:
   ~DelayedFireSingleTapEvent()
   {
   }
 
   nsWeakPtr mWidget;
   LayoutDevicePoint mPoint;
   Modifiers mModifiers;
+  int32_t mClickCount;
   nsCOMPtr<nsITimer> mTimer;
 };
 
 NS_IMPL_ISUPPORTS(DelayedFireSingleTapEvent, nsITimerCallback)
 
 void
 APZEventState::ProcessSingleTap(const CSSPoint& aPoint,
                                 const CSSToLayoutDeviceScale& aScale,
                                 Modifiers aModifiers,
-                                const ScrollableLayerGuid& aGuid)
+                                const ScrollableLayerGuid& aGuid,
+                                int32_t aClickCount)
 {
   APZES_LOG("Handling single tap at %s on %s with %d\n",
     Stringify(aPoint).c_str(), Stringify(aGuid).c_str(), mTouchEndCancelled);
 
   nsCOMPtr<nsIWidget> widget = GetWidget();
   if (!widget) {
     return;
   }
@@ -182,24 +186,24 @@ APZEventState::ProcessSingleTap(const CS
     return;
   }
 
   LayoutDevicePoint ldPoint = aPoint * aScale;
   if (!mActiveElementManager->ActiveElementUsesStyle()) {
     // If the active element isn't visually affected by the :active style, we
     // have no need to wait the extra sActiveDurationMs to make the activation
     // visually obvious to the user.
-    APZCCallbackHelper::FireSingleTapEvent(ldPoint, aModifiers, widget);
+    APZCCallbackHelper::FireSingleTapEvent(ldPoint, aModifiers, aClickCount, widget);
     return;
   }
 
   APZES_LOG("Active element uses style, scheduling timer for click event\n");
   nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID);
   RefPtr<DelayedFireSingleTapEvent> callback =
-    new DelayedFireSingleTapEvent(mWidget, ldPoint, aModifiers, timer);
+    new DelayedFireSingleTapEvent(mWidget, ldPoint, aModifiers, aClickCount, timer);
   nsresult rv = timer->InitWithCallback(callback,
                                         sActiveDurationMs,
                                         nsITimer::TYPE_ONE_SHOT);
   if (NS_FAILED(rv)) {
     // Make |callback| not hold the timer, so they will both be destructed when
     // we leave the scope of this function.
     callback->ClearTimer();
   }
@@ -227,18 +231,19 @@ APZEventState::FireContextmenuEvents(con
     // and so we should remove any activation
     mActiveElementManager->ClearActivation();
   } else {
     // If no one handle context menu, fire MOZLONGTAP event
     LayoutDevicePoint ldPoint = aPoint * aScale;
     int time = 0;
     nsEventStatus status =
         APZCCallbackHelper::DispatchSynthesizedMouseEvent(eMouseLongTap, time,
-                                                          ldPoint,
-                                                          aModifiers, aWidget);
+                                                          ldPoint, aModifiers,
+                                                          /*clickCount*/ 1,
+                                                          aWidget);
     eventHandled = (status == nsEventStatus_eConsumeNoDefault);
     APZES_LOG("MOZLONGTAP event handled: %d\n", eventHandled);
   }
 
   return eventHandled;
 }
 
 void
--- a/gfx/layers/apz/util/APZEventState.h
+++ b/gfx/layers/apz/util/APZEventState.h
@@ -44,17 +44,18 @@ public:
   APZEventState(nsIWidget* aWidget,
                 ContentReceivedInputBlockCallback&& aCallback);
 
   NS_INLINE_DECL_REFCOUNTING(APZEventState);
 
   void ProcessSingleTap(const CSSPoint& aPoint,
                         const CSSToLayoutDeviceScale& aScale,
                         Modifiers aModifiers,
-                        const ScrollableLayerGuid& aGuid);
+                        const ScrollableLayerGuid& aGuid,
+                        int32_t aClickCount);
   void ProcessLongTap(const nsCOMPtr<nsIPresShell>& aUtils,
                       const CSSPoint& aPoint,
                       const CSSToLayoutDeviceScale& aScale,
                       Modifiers aModifiers,
                       const ScrollableLayerGuid& aGuid,
                       uint64_t aInputBlockId);
   void ProcessLongTapUp(const nsCOMPtr<nsIPresShell>& aPresShell,
                         const CSSPoint& aPoint,
--- a/gfx/layers/apz/util/ChromeProcessController.cpp
+++ b/gfx/layers/apz/util/ChromeProcessController.cpp
@@ -182,21 +182,24 @@ ChromeProcessController::HandleTap(TapTy
   if (!presShell->GetPresContext()) {
     return;
   }
   CSSToLayoutDeviceScale scale(presShell->GetPresContext()->CSSToDevPixelScale());
   CSSPoint point = APZCCallbackHelper::ApplyCallbackTransform(aPoint / scale, aGuid);
 
   switch (aType) {
   case TapType::eSingleTap:
-    mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid);
+    mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid, 1);
     break;
   case TapType::eDoubleTap:
     HandleDoubleTap(point, aModifiers, aGuid);
     break;
+  case TapType::eSecondTap:
+    mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid, 2);
+    break;
   case TapType::eLongTap:
     mAPZEventState->ProcessLongTap(presShell, point, scale, aModifiers, aGuid,
         aInputBlockId);
     break;
   case TapType::eLongTapUp:
     mAPZEventState->ProcessLongTapUp(presShell, point, scale, aModifiers);
     break;
   case TapType::eSentinel:
--- a/widget/InputData.h
+++ b/widget/InputData.h
@@ -482,16 +482,17 @@ public:
   {
     // Warning, this enum is serialized and sent over IPC. If you reorder, add,
     // or remove a value, you need to update its ParamTraits<> in nsGUIEventIPC.h
     TAPGESTURE_LONG,
     TAPGESTURE_LONG_UP,
     TAPGESTURE_UP,
     TAPGESTURE_CONFIRMED,
     TAPGESTURE_DOUBLE,
+    TAPGESTURE_SECOND, // See GeckoContentController::TapType::eSecondTap
     TAPGESTURE_CANCEL,
 
     // Used as an upper bound for ContiguousEnumSerializer
     TAPGESTURE_SENTINEL,
   };
 
   // Construct a tap gesture from a Screen point.
   // mLocalPoint remains (0,0) unless it's set later.
--- a/widget/android/AndroidContentController.cpp
+++ b/widget/android/AndroidContentController.cpp
@@ -88,17 +88,18 @@ AndroidContentController::HandleTap(TapT
                                     const ScrollableLayerGuid& aGuid,
                                     uint64_t aInputBlockId)
 {
     // This function will get invoked first on the Java UI thread, and then
     // again on the main thread (because of the code in ChromeProcessController::
     // HandleTap). We want to post the SingleTap message once; it can be
     // done from either thread but we need access to the callback transform
     // so we do it from the main thread.
-    if (NS_IsMainThread() && aType == TapType::eSingleTap) {
+    if (NS_IsMainThread() &&
+        (aType == TapType::eSingleTap || aType == TapType::eSecondTap)) {
         DispatchSingleTapToObservers(aPoint, aGuid);
     }
 
     ChromeProcessController::HandleTap(aType, aPoint, aModifiers, aGuid, aInputBlockId);
 }
 
 void
 AndroidContentController::PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs)