Bug 942929 - Use longtapup event to handle firing clicks when longtap not handled. r=kats
authorDale Harvey <dale@arandomurl.com>
Thu, 12 Dec 2013 00:39:06 +0000
changeset 175943 d1d7d42059c82dc5d78b5f93fc3756954c655956
parent 175942 53647f64657232c9efc410238d9e572bd6fd51d7
child 175944 7d9b2fcfb7aa27a3c5e6aef5db74c7042350ea41
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs942929
milestone29.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 942929 - Use longtapup event to handle firing clicks when longtap not handled. r=kats
dom/ipc/PBrowser.ipdl
dom/ipc/TabChild.cpp
dom/ipc/TabChild.h
dom/ipc/TabParent.cpp
dom/ipc/TabParent.h
gfx/layers/ipc/AsyncPanZoomController.cpp
gfx/layers/ipc/AsyncPanZoomController.h
gfx/layers/ipc/GeckoContentController.h
gfx/layers/ipc/GestureEventListener.cpp
gfx/layers/ipc/GestureEventListener.h
gfx/tests/gtest/TestAsyncPanZoomController.cpp
layout/ipc/RenderFrameParent.cpp
widget/InputData.h
widget/android/AndroidBridge.cpp
widget/android/AndroidBridge.h
widget/gonk/ParentProcessController.h
widget/windows/winrt/APZController.cpp
widget/windows/winrt/APZController.h
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -342,16 +342,25 @@ child:
     /**
      * 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(CSSIntPoint point);
 
     /**
+     * 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(CSSIntPoint point);
+
+    /**
      * Notifies the child that the parent has begun or finished transforming
      * the visible child content area. Useful for showing/hiding scrollbars.
      */
     NotifyTransformBegin(ViewID aViewId);
     NotifyTransformEnd(ViewID aViewId);
 
     /**
      * Sending an activate message moves focus to the child.
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -280,16 +280,17 @@ TabChild::TabChild(ContentChild* aManage
   , mOldViewportWidth(0.0f)
   , mLastBackgroundColor(NS_RGB(255, 255, 255))
   , mDidFakeShow(false)
   , mNotified(false)
   , mContentDocumentIsDisplayed(false)
   , mTriedBrowserInit(false)
   , mOrientation(eScreenOrientation_PortraitPrimary)
   , mUpdateHitRegion(false)
+  , mContextMenuHandled(false)
 {
 }
 
 NS_IMETHODIMP
 TabChild::HandleEvent(nsIDOMEvent* aEvent)
 {
   nsAutoString eventType;
   aEvent->GetType(eventType);
@@ -1656,16 +1657,28 @@ TabChild::RecvHandleLongTap(const CSSInt
 
   DispatchMouseEvent(NS_LITERAL_STRING("contextmenu"), aPoint, 2, 1, 0, false,
                      nsIDOMMouseEvent::MOZ_SOURCE_TOUCH);
 
   return true;
 }
 
 bool
+TabChild::RecvHandleLongTapUp(const CSSIntPoint& aPoint)
+{
+  if (mContextMenuHandled) {
+    mContextMenuHandled = false;
+    return true;
+  }
+
+  RecvHandleSingleTap(aPoint);
+  return true;
+}
+
+bool
 TabChild::RecvNotifyTransformBegin(const ViewID& aViewId)
 {
   nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
   if (sf) {
     nsIScrollbarOwner* scrollbarOwner = do_QueryFrame(sf);
     if (scrollbarOwner) {
       scrollbarOwner->ScrollbarActivityStarted();
     }
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -212,16 +212,17 @@ public:
                                          const FileDescriptor& aFileDescriptor)
                                          MOZ_OVERRIDE;
     virtual bool RecvShow(const nsIntSize& size);
     virtual bool RecvUpdateDimensions(const nsRect& rect, const nsIntSize& size, const ScreenOrientation& orientation);
     virtual bool RecvUpdateFrame(const mozilla::layers::FrameMetrics& aFrameMetrics);
     virtual bool RecvHandleDoubleTap(const CSSIntPoint& aPoint);
     virtual bool RecvHandleSingleTap(const CSSIntPoint& aPoint);
     virtual bool RecvHandleLongTap(const CSSIntPoint& aPoint);
+    virtual bool RecvHandleLongTapUp(const CSSIntPoint& aPoint);
     virtual bool RecvNotifyTransformBegin(const ViewID& aViewId);
     virtual bool RecvNotifyTransformEnd(const ViewID& aViewId);
     virtual bool RecvActivate();
     virtual bool RecvDeactivate();
     virtual bool RecvMouseEvent(const nsString& aType,
                                 const float&    aX,
                                 const float&    aY,
                                 const int32_t&  aButton,
@@ -491,16 +492,17 @@ private:
     nscolor mLastBackgroundColor;
     ScrollingBehavior mScrolling;
     bool mDidFakeShow;
     bool mNotified;
     bool mContentDocumentIsDisplayed;
     bool mTriedBrowserInit;
     ScreenOrientation mOrientation;
     bool mUpdateHitRegion;
+    bool mContextMenuHandled;
 
     DISALLOW_EVIL_CONSTRUCTORS(TabChild);
 };
 
 }
 }
 
 #endif // mozilla_dom_TabChild_h
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -516,16 +516,23 @@ void TabParent::HandleSingleTap(const CS
 
 void TabParent::HandleLongTap(const CSSIntPoint& aPoint, int32_t aModifiers)
 {
   if (!mIsDestroyed) {
     unused << SendHandleLongTap(aPoint);
   }
 }
 
+void TabParent::HandleLongTapUp(const CSSIntPoint& aPoint, int32_t aModifiers)
+{
+  if (!mIsDestroyed) {
+    unused << SendHandleLongTapUp(aPoint);
+  }
+}
+
 void TabParent::NotifyTransformBegin(ViewID aViewId)
 {
   if (!mIsDestroyed) {
     unused << SendNotifyTransformBegin(aViewId);
   }
 }
 
 void TabParent::NotifyTransformEnd(ViewID aViewId)
@@ -708,16 +715,25 @@ bool TabParent::SendHandleLongTap(const 
 {
   if (mIsDestroyed) {
     return false;
   }
 
   return PBrowserParent::SendHandleLongTap(AdjustTapToChildWidget(aPoint));
 }
 
+bool TabParent::SendHandleLongTapUp(const CSSIntPoint& aPoint)
+{
+  if (mIsDestroyed) {
+    return false;
+  }
+
+  return PBrowserParent::SendHandleLongTapUp(AdjustTapToChildWidget(aPoint));
+}
+
 bool TabParent::SendHandleDoubleTap(const CSSIntPoint& aPoint)
 {
   if (mIsDestroyed) {
     return false;
   }
 
   return PBrowserParent::SendHandleDoubleTap(AdjustTapToChildWidget(aPoint));
 }
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -191,16 +191,17 @@ public:
     // message-sending functions under a layer of indirection and
     // eating the return values
     void Show(const nsIntSize& size);
     void UpdateDimensions(const nsRect& rect, const nsIntSize& size);
     void UpdateFrame(const layers::FrameMetrics& aFrameMetrics);
     void HandleDoubleTap(const CSSIntPoint& aPoint, int32_t aModifiers);
     void HandleSingleTap(const CSSIntPoint& aPoint, int32_t aModifiers);
     void HandleLongTap(const CSSIntPoint& aPoint, int32_t aModifiers);
+    void HandleLongTapUp(const CSSIntPoint& aPoint, int32_t aModifiers);
     void NotifyTransformBegin(ViewID aViewId);
     void NotifyTransformEnd(ViewID aViewId);
     void Activate();
     void Deactivate();
 
     bool MapEventCoordinatesForChildProcess(mozilla::WidgetEvent* aEvent);
     void MapEventCoordinatesForChildProcess(const LayoutDeviceIntPoint& aOffset,
                                             mozilla::WidgetEvent* aEvent);
@@ -212,16 +213,17 @@ public:
                       int32_t aCharCode, int32_t aModifiers,
                       bool aPreventDefault);
     bool SendRealMouseEvent(mozilla::WidgetMouseEvent& event);
     bool SendMouseWheelEvent(mozilla::WidgetWheelEvent& event);
     bool SendRealKeyEvent(mozilla::WidgetKeyboardEvent& event);
     bool SendRealTouchEvent(WidgetTouchEvent& event);
     bool SendHandleSingleTap(const CSSIntPoint& aPoint);
     bool SendHandleLongTap(const CSSIntPoint& aPoint);
+    bool SendHandleLongTapUp(const CSSIntPoint& aPoint);
     bool SendHandleDoubleTap(const CSSIntPoint& aPoint);
 
     virtual PDocumentRendererParent*
     AllocPDocumentRendererParent(const nsRect& documentRect, const gfxMatrix& transform,
                                  const nsString& bgcolor,
                                  const uint32_t& renderFlags, const bool& flushLayout,
                                  const nsIntSize& renderSize);
     virtual bool DeallocPDocumentRendererParent(PDocumentRendererParent* actor);
--- a/gfx/layers/ipc/AsyncPanZoomController.cpp
+++ b/gfx/layers/ipc/AsyncPanZoomController.cpp
@@ -513,16 +513,17 @@ nsEventStatus AsyncPanZoomController::Ha
       default: NS_WARNING("Unhandled pinch gesture"); break;
     }
     break;
   }
   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_CANCEL: rv = OnCancelTap(tapGestureInput); break;
       default: NS_WARNING("Unhandled tap gesture"); break;
     }
     break;
   }
@@ -816,16 +817,30 @@ nsEventStatus AsyncPanZoomController::On
     if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
       controller->HandleLongTap(geckoScreenPoint, modifiers);
       return nsEventStatus_eConsumeNoDefault;
     }
   }
   return nsEventStatus_eIgnore;
 }
 
+nsEventStatus AsyncPanZoomController::OnLongPressUp(const TapGestureInput& aEvent) {
+  APZC_LOG("%p got a long-tap-up in state %d\n", this, mState);
+  nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
+  if (controller) {
+    int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
+    CSSIntPoint geckoScreenPoint;
+    if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
+      controller->HandleLongTapUp(geckoScreenPoint, modifiers);
+      return nsEventStatus_eConsumeNoDefault;
+    }
+  }
+  return nsEventStatus_eIgnore;
+}
+
 nsEventStatus AsyncPanZoomController::OnSingleTapUp(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a single-tap-up in state %d\n", this, mState);
   nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
   // If mAllowZoom is true we wait for a call to OnSingleTapConfirmed before
   // sending event to content
   if (controller && !mAllowZoom) {
     int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
     CSSIntPoint geckoScreenPoint;
--- a/gfx/layers/ipc/AsyncPanZoomController.h
+++ b/gfx/layers/ipc/AsyncPanZoomController.h
@@ -345,40 +345,33 @@ protected:
 
   /**
    * Helper method for scales ending. Redraws the screen if necessary and does
    * any cleanup after a scale has ended.
    */
   nsEventStatus OnScaleEnd(const PinchGestureInput& aEvent);
 
   /**
-   * Helper method for long press gestures.
-   *
-   * XXX: Implement this.
+   * Helper methods for long press gestures.
    */
   nsEventStatus OnLongPress(const TapGestureInput& aEvent);
+  nsEventStatus OnLongPressUp(const TapGestureInput& aEvent);
 
   /**
    * Helper method for single tap gestures.
-   *
-   * XXX: Implement this.
    */
   nsEventStatus OnSingleTapUp(const TapGestureInput& aEvent);
 
   /**
    * Helper method for a single tap confirmed.
-   *
-   * XXX: Implement this.
    */
   nsEventStatus OnSingleTapConfirmed(const TapGestureInput& aEvent);
 
   /**
    * Helper method for double taps.
-   *
-   * XXX: Implement this.
    */
   nsEventStatus OnDoubleTap(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.
--- a/gfx/layers/ipc/GeckoContentController.h
+++ b/gfx/layers/ipc/GeckoContentController.h
@@ -45,16 +45,23 @@ public:
 
   /**
    * Requests handling a long tap. |aPoint| is in CSS pixels, relative to the
    * current scroll offset.
    */
   virtual void HandleLongTap(const CSSIntPoint& aPoint, int32_t aModifiers) = 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
+   */
+  virtual void HandleLongTapUp(const CSSIntPoint& aPoint, int32_t aModifiers) = 0;
+
+  /**
    * Requests sending a mozbrowserasyncscroll domevent to embedder.
    * |aContentRect| is in CSS pixels, relative to the current cssPage.
    * |aScrollableSize| is the current content width/height in CSS pixels.
    */
   virtual void SendAsyncScrollDOMEvent(bool aIsRoot,
                                        const CSSRect &aContentRect,
                                        const CSSSize &aScrollableSize) = 0;
 
--- a/gfx/layers/ipc/GestureEventListener.cpp
+++ b/gfx/layers/ipc/GestureEventListener.cpp
@@ -154,17 +154,20 @@ nsEventStatus GestureEventListener::Hand
         mState = GESTURE_WAITING_SINGLE_TAP;
       } else {
         // We were waiting for a double tap and it has arrived.
         HandleDoubleTap(event);
         mState = GESTURE_NONE;
       }
     }
 
-    if (mState == GESTURE_WAITING_SINGLE_TAP &&
+    if (mState == GESTURE_LONG_TAP_UP) {
+      HandleLongTapUpEvent(event);
+      mState = GESTURE_NONE;
+    } else if (mState == GESTURE_WAITING_SINGLE_TAP &&
         event.mTime - mTapStartTime > MAX_TAP_TIME) {
       // Extended taps are immediately dispatched as single taps
       CancelLongTapTimeoutTask();
       HandleSingleTapConfirmedEvent(event);
       mState = GESTURE_NONE;
     } else if (mState == GESTURE_WAITING_SINGLE_TAP) {
       CancelLongTapTimeoutTask();
       nsEventStatus tapupEvent = HandleSingleTapUpEvent(event);
@@ -309,28 +312,36 @@ nsEventStatus GestureEventListener::Hand
 
 nsEventStatus GestureEventListener::HandleLongTapEvent(const MultiTouchInput& aEvent)
 {
   TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG, aEvent.mTime,
       aEvent.mTouches[0].mScreenPoint, aEvent.modifiers);
   return mAsyncPanZoomController->HandleInputEvent(tapEvent);
 }
 
+nsEventStatus GestureEventListener::HandleLongTapUpEvent(const MultiTouchInput& aEvent)
+{
+  TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_LONG_UP, aEvent.mTime,
+      aEvent.mTouches[0].mScreenPoint, aEvent.modifiers);
+  return mAsyncPanZoomController->HandleInputEvent(tapEvent);
+}
+
 nsEventStatus GestureEventListener::HandleTapCancel(const MultiTouchInput& aEvent)
 {
   mTapStartTime = 0;
 
   switch (mState)
   {
   case GESTURE_WAITING_SINGLE_TAP:
     CancelLongTapTimeoutTask();
     mState = GESTURE_NONE;
     break;
 
   case GESTURE_WAITING_DOUBLE_TAP:
+  case GESTURE_LONG_TAP_UP:
     mState = GESTURE_NONE;
     break;
   default:
     break;
   }
 
   return nsEventStatus_eConsumeDoDefault;
 }
@@ -361,17 +372,17 @@ void GestureEventListener::CancelDoubleT
   }
 }
 
 void GestureEventListener::TimeoutLongTap()
 {
   mLongTapTimeoutTask = nullptr;
   // If the tap has not been released, this is a long press.
   if (mState == GESTURE_WAITING_SINGLE_TAP) {
-    mState = GESTURE_NONE;
+    mState = GESTURE_LONG_TAP_UP;
 
     HandleLongTapEvent(mLastTouchInput);
   }
 }
 
 void GestureEventListener::CancelLongTapTimeoutTask() {
   if (mLongTapTimeoutTask) {
     mLongTapTimeoutTask->Cancel();
--- a/gfx/layers/ipc/GestureEventListener.h
+++ b/gfx/layers/ipc/GestureEventListener.h
@@ -80,17 +80,20 @@ protected:
     // There are two or more fingers on the screen, and the user has already
     // pinched enough for us to start zooming the screen.
     GESTURE_PINCH,
     // A touch start has happened and it may turn into a tap. We use this
     // because, if we put down two fingers and then lift them very quickly, this
     // may be mistaken for a tap.
     GESTURE_WAITING_SINGLE_TAP,
     // A single tap has happened for sure, and we're waiting for a second tap.
-    GESTURE_WAITING_DOUBLE_TAP
+    GESTURE_WAITING_DOUBLE_TAP,
+    // A long tap has happened, wait for the tap to be released in case we need
+    // to fire a click event in the case the long tap was not handled.
+    GESTURE_LONG_TAP_UP
   };
 
   /**
    * Attempts to handle the event as a pinch event. If it is not a pinch event,
    * then we simply tell the next consumer to consume the event instead.
    *
    * |aClearTouches| marks whether or not to terminate any pinch currently
    * happening.
@@ -116,16 +119,22 @@ protected:
 
   /**
    * Attempts to handle a long tap confirmation. This is what will use
    * for context menu.
    */
   nsEventStatus HandleLongTapEvent(const MultiTouchInput& aEvent);
 
   /**
+   * Attempts to handle release of long tap. This is used to fire click
+   * events in the case the context menu was not invoked.
+   */
+  nsEventStatus HandleLongTapUpEvent(const MultiTouchInput& aEvent);
+
+  /**
    * Attempts to handle a tap event cancellation. This happens when we think
    * something was a tap but it actually wasn't. In general, this will not
    * attempt to block the touch event from being passed along to
    * AsyncPanZoomController since APZC needs to know about touches ending (and
    * we only know if a touch was a tap once it ends).
    */
   nsEventStatus HandleTapCancel(const MultiTouchInput& aEvent);
 
--- a/gfx/tests/gtest/TestAsyncPanZoomController.cpp
+++ b/gfx/tests/gtest/TestAsyncPanZoomController.cpp
@@ -8,36 +8,56 @@
 
 #include "mozilla/Attributes.h"
 #include "mozilla/layers/AsyncCompositionManager.h" // for ViewTransform
 #include "mozilla/layers/AsyncPanZoomController.h"
 #include "mozilla/layers/LayerManagerComposite.h"
 #include "mozilla/layers/GeckoContentController.h"
 #include "mozilla/layers/CompositorParent.h"
 #include "mozilla/layers/APZCTreeManager.h"
+#include "base/task.h"
 #include "Layers.h"
 #include "TestLayers.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace mozilla::layers;
 using ::testing::_;
 using ::testing::NiceMock; 
 using ::testing::AtLeast;
 
+class Task;
+
 class MockContentController : public GeckoContentController {
 public:
   MOCK_METHOD1(RequestContentRepaint, void(const FrameMetrics&));
   MOCK_METHOD2(HandleDoubleTap, void(const CSSIntPoint&, int32_t));
   MOCK_METHOD2(HandleSingleTap, void(const CSSIntPoint&, int32_t));
   MOCK_METHOD2(HandleLongTap, void(const CSSIntPoint&, int32_t));
+  MOCK_METHOD2(HandleLongTapUp, void(const CSSIntPoint&, int32_t));
   MOCK_METHOD3(SendAsyncScrollDOMEvent, void(bool aIsRoot, const CSSRect &aContentRect, const CSSSize &aScrollableSize));
   MOCK_METHOD2(PostDelayedTask, void(Task* aTask, int aDelayMs));
 };
 
+class MockContentControllerDelayed : public MockContentController {
+public:
+
+  void PostDelayedTask(Task* aTask, int aDelayMs) {
+    mCurrentTask = aTask;
+  }
+
+  Task* GetDelayedTask() {
+    return mCurrentTask;
+  }
+
+private:
+  Task *mCurrentTask;
+};
+
+
 class TestAPZCContainerLayer : public ContainerLayer {
   public:
     TestAPZCContainerLayer()
       : ContainerLayer(nullptr, nullptr)
     {}
   void RemoveChild(Layer* aChild) {}
   void InsertAfter(Layer* aChild, Layer* aAfter) {}
   void ComputeEffectiveTransforms(const gfx3DMatrix& aTransformToSurface) {}
@@ -147,30 +167,37 @@ ApzcPinch(AsyncPanZoomController* aApzc,
                                             // note: negative values here tell APZC
                                             //       not to turn the pinch into a pan
                                             -1.0,
                                             -1.0,
                                             0));
 }
 
 static nsEventStatus
-ApzcTap(AsyncPanZoomController* apzc, int aX, int aY, int& aTime, int aTapLength) {
+ApzcDown(AsyncPanZoomController* apzc, int aX, int aY, int& aTime) {
   MultiTouchInput mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_START, aTime, 0);
   mti.mTouches.AppendElement(SingleTouchData(0, ScreenIntPoint(aX, aY), ScreenSize(0, 0), 0, 0));
-  nsEventStatus status = apzc->ReceiveInputEvent(mti);
-  EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
-  // APZC should be in TOUCHING state
+  return apzc->ReceiveInputEvent(mti);
+}
 
-  aTime += aTapLength;
-
-  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, aTime, 0);
+static nsEventStatus
+ApzcUp(AsyncPanZoomController* apzc, int aX, int aY, int& aTime) {
+  MultiTouchInput mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, aTime, 0);
   mti.mTouches.AppendElement(SingleTouchData(0, ScreenIntPoint(aX, aY), ScreenSize(0, 0), 0, 0));
   return apzc->ReceiveInputEvent(mti);
 }
 
+static nsEventStatus
+ApzcTap(AsyncPanZoomController* apzc, int aX, int aY, int& aTime, int aTapLength) {
+  nsEventStatus status = ApzcDown(apzc, aX, aY, aTime);
+  EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
+  aTime += aTapLength;
+  return ApzcUp(apzc, aX, aY, aTime);
+}
+
 TEST(AsyncPanZoomController, Constructor) {
   // RefCounted class can't live in the stack
   nsRefPtr<MockContentController> mcc = new NiceMock<MockContentController>();
   nsRefPtr<TestAsyncPanZoomController> apzc = new TestAsyncPanZoomController(0, mcc);
   apzc->SetFrameMetrics(TestFrameMetrics());
 }
 
 TEST(AsyncPanZoomController, Pinch) {
@@ -482,16 +509,49 @@ TEST(AsyncPanZoomController, MediumPress
 
   int time = 0;
   nsEventStatus status = ApzcTap(apzc, 10, 10, time, 400);
   EXPECT_EQ(nsEventStatus_eIgnore, status);
 
   apzc->Destroy();
 }
 
+TEST(AsyncPanZoomController, LongPress) {
+  nsRefPtr<MockContentControllerDelayed> mcc = new MockContentControllerDelayed();
+  nsRefPtr<TestAPZCTreeManager> tm = new TestAPZCTreeManager();
+  nsRefPtr<TestAsyncPanZoomController> apzc = new TestAsyncPanZoomController(
+    0, mcc, tm, AsyncPanZoomController::USE_GESTURE_DETECTOR);
+
+  apzc->SetFrameMetrics(TestFrameMetrics());
+  apzc->NotifyLayersUpdated(TestFrameMetrics(), true);
+  apzc->UpdateZoomConstraints(false, CSSToScreenScale(1.0), CSSToScreenScale(1.0));
+
+  int time = 0;
+
+  nsEventStatus status = ApzcDown(apzc, 10, 10, time);
+  EXPECT_EQ(nsEventStatus_eConsumeNoDefault, status);
+
+  Task* t = mcc->GetDelayedTask();
+
+  EXPECT_TRUE(nullptr != t);
+  EXPECT_CALL(*mcc, HandleLongTap(CSSIntPoint(10, 10), 0)).Times(1);
+  EXPECT_CALL(*mcc, HandleLongTapUp(CSSIntPoint(10, 10), 0)).Times(1);
+  EXPECT_CALL(*mcc, SendAsyncScrollDOMEvent(_,_,_)).Times(AtLeast(1));
+
+  // Manually invoke the longpress while the touch is currently down.
+  t->Run();
+
+  time += 1000;
+
+  status = ApzcUp(apzc, 10, 10, time);
+  EXPECT_EQ(nsEventStatus_eIgnore, status);
+
+  apzc->Destroy();
+}
+
 // Layer tree for HitTesting1
 static already_AddRefed<mozilla::layers::Layer>
 CreateTestLayerTree1(nsRefPtr<LayerManager>& aLayerManager, nsTArray<nsRefPtr<Layer> >& aLayers) {
   const char* layerTreeSyntax = "c(ttcc)";
   // LayerID                     0 1234
   nsIntRegion layerVisibleRegion[] = {
     nsIntRegion(nsIntRect(0,0,100,100)),
     nsIntRegion(nsIntRect(0,0,100,100)),
--- a/layout/ipc/RenderFrameParent.cpp
+++ b/layout/ipc/RenderFrameParent.cpp
@@ -562,16 +562,34 @@ public:
       return;
     }
     if (mRenderFrame) {
       TabParent* browser = static_cast<TabParent*>(mRenderFrame->Manager());
       browser->HandleLongTap(aPoint, aModifiers);
     }
   }
 
+  virtual void HandleLongTapUp(const CSSIntPoint& aPoint,
+                             int32_t aModifiers) MOZ_OVERRIDE
+  {
+    if (MessageLoop::current() != mUILoop) {
+      // We have to send this message from the "UI thread" (main
+      // thread).
+      mUILoop->PostTask(
+        FROM_HERE,
+        NewRunnableMethod(this, &RemoteContentController::HandleLongTapUp,
+                          aPoint, aModifiers));
+      return;
+    }
+    if (mRenderFrame) {
+      TabParent* browser = static_cast<TabParent*>(mRenderFrame->Manager());
+      browser->HandleLongTapUp(aPoint, aModifiers);
+    }
+  }
+
   void ClearRenderFrame() { mRenderFrame = nullptr; }
 
   virtual void SendAsyncScrollDOMEvent(bool aIsRoot,
                                        const CSSRect& aContentRect,
                                        const CSSSize& aContentSize) MOZ_OVERRIDE
   {
     if (MessageLoop::current() != mUILoop) {
       mUILoop->PostTask(
--- a/widget/InputData.h
+++ b/widget/InputData.h
@@ -234,16 +234,17 @@ public:
  * determining whether or not the user was trying to do a gesture.
  */
 class TapGestureInput : public InputData
 {
 public:
   enum TapGestureType
   {
     TAPGESTURE_LONG,
+    TAPGESTURE_LONG_UP,
     TAPGESTURE_UP,
     TAPGESTURE_CONFIRMED,
     TAPGESTURE_DOUBLE,
     TAPGESTURE_CANCEL
   };
 
   TapGestureInput(TapGestureType aType,
                   uint32_t aTime,
--- a/widget/android/AndroidBridge.cpp
+++ b/widget/android/AndroidBridge.cpp
@@ -1998,16 +1998,21 @@ void
 AndroidBridge::HandleLongTap(const CSSIntPoint& aPoint, int32_t aModifiers)
 {
     nsCString data = nsPrintfCString("{ \"x\": %d, \"y\": %d }", aPoint.x, aPoint.y);
     nsAppShell::gAppShell->PostEvent(AndroidGeckoEvent::MakeBroadcastEvent(
             NS_LITERAL_CSTRING("Gesture:LongPress"), data));
 }
 
 void
+AndroidBridge::HandleLongTapUp(const CSSIntPoint& aPoint, int32_t aModifiers)
+{
+}
+
+void
 AndroidBridge::SendAsyncScrollDOMEvent(bool aIsRoot,
                                        const CSSRect& aContentRect,
                                        const CSSSize& aScrollableSize)
 {
     // FIXME implement this
 }
 
 void
--- a/widget/android/AndroidBridge.h
+++ b/widget/android/AndroidBridge.h
@@ -404,16 +404,17 @@ private:
 
 public:
     NativePanZoomController* SetNativePanZoomController(jobject obj);
     // GeckoContentController methods
     void RequestContentRepaint(const mozilla::layers::FrameMetrics& aFrameMetrics) MOZ_OVERRIDE;
     void HandleDoubleTap(const CSSIntPoint& aPoint, int32_t aModifiers) MOZ_OVERRIDE;
     void HandleSingleTap(const CSSIntPoint& aPoint, int32_t aModifiers) MOZ_OVERRIDE;
     void HandleLongTap(const CSSIntPoint& aPoint, int32_t aModifiers) MOZ_OVERRIDE;
+    void HandleLongTapUp(const CSSIntPoint& aPoint, int32_t aModifiers) MOZ_OVERRIDE;
     void SendAsyncScrollDOMEvent(bool aIsRoot,
                                  const CSSRect& aContentRect,
                                  const CSSSize& aScrollableSize) MOZ_OVERRIDE;
     void PostDelayedTask(Task* aTask, int aDelayMs) MOZ_OVERRIDE;
     int64_t RunDelayedTasks();
 };
 
 class AutoJObject {
--- a/widget/gonk/ParentProcessController.h
+++ b/widget/gonk/ParentProcessController.h
@@ -18,16 +18,18 @@ class ParentProcessController : public m
 public:
     virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics) MOZ_OVERRIDE;
     virtual void PostDelayedTask(Task* aTask, int aDelayMs) MOZ_OVERRIDE;
 
     // No-ops
     virtual void HandleDoubleTap(const CSSIntPoint& aPoint, int32_t aModifiers) MOZ_OVERRIDE {}
     virtual void HandleSingleTap(const CSSIntPoint& aPoint, int32_t aModifiers) MOZ_OVERRIDE {}
     virtual void HandleLongTap(const CSSIntPoint& aPoint, int32_t aModifiers) MOZ_OVERRIDE {}
+    virtual void HandleLongTapUp(const CSSIntPoint& aPoint, int32_t aModifiers) MOZ_OVERRIDE {}
+
     virtual void SendAsyncScrollDOMEvent(bool aIsRoot,
                                          const CSSRect &aContentRect,
                                          const CSSSize &aScrollableSize) MOZ_OVERRIDE {}
 };
 
 }
 }
 
--- a/widget/windows/winrt/APZController.cpp
+++ b/widget/windows/winrt/APZController.cpp
@@ -296,16 +296,21 @@ APZController::HandleSingleTap(const CSS
   MetroUtils::FireObserver("Gesture:SingleTap", data.get());
 }
 
 void
 APZController::HandleLongTap(const CSSIntPoint& aPoint, int32_t aModifiers)
 {
 }
 
+void
+APZController::HandleLongTapUp(const CSSIntPoint& aPoint, int32_t aModifiers)
+{
+}
+
 // requests that we send a mozbrowserasyncscroll domevent. not in use.
 void
 APZController::SendAsyncScrollDOMEvent(bool aIsRoot,
                                        const CSSRect &aContentRect,
                                        const CSSSize &aScrollableSize)
 {
 }
 
--- a/widget/windows/winrt/APZController.h
+++ b/widget/windows/winrt/APZController.h
@@ -30,16 +30,17 @@ public:
   {
   }
 
   // GeckoContentController interface
   virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics);
   virtual void HandleDoubleTap(const mozilla::CSSIntPoint& aPoint, int32_t aModifiers);
   virtual void HandleSingleTap(const mozilla::CSSIntPoint& aPoint, int32_t aModifiers);
   virtual void HandleLongTap(const mozilla::CSSIntPoint& aPoint, int32_t aModifiers);
+  virtual void HandleLongTapUp(const mozilla::CSSIntPoint& aPoint, int32_t aModifiers);
   virtual void SendAsyncScrollDOMEvent(bool aIsRoot, const mozilla::CSSRect &aContentRect, const mozilla::CSSSize &aScrollableSize);
   virtual void PostDelayedTask(Task* aTask, int aDelayMs);
   virtual void NotifyTransformBegin(const ScrollableLayerGuid& aGuid);
   virtual void NotifyTransformEnd(const ScrollableLayerGuid& aGuid);
   
   void SetWidgetListener(nsIWidgetListener* aWidgetListener);
   void UpdateScrollOffset(const mozilla::layers::ScrollableLayerGuid& aScrollLayerId, CSSIntPoint& aScrollOffset);