From e685c4edc8cf219b4174b3eb2d05ae8a0ebe50b5 Mon Sep 17 00:00:00 2001 From: Kartikaya Gupta Date: Sat, 7 Apr 2012 03:09:26 -0400 Subject: [PATCH] Bug 742019 - Rewrite how we handle touch events so we don't break panning, and don't introduce unnecessary latency. r=wesj --- embedding/android/GeckoAppShell.java | 2 +- mobile/android/base/GeckoApp.java | 4 +- mobile/android/base/GeckoAppShell.java | 6 +- mobile/android/base/Makefile.in | 1 + mobile/android/base/gfx/LayerController.java | 115 +------ mobile/android/base/gfx/LayerView.java | 51 +-- .../android/base/gfx/TouchEventHandler.java | 294 ++++++++++++++++++ mobile/android/base/ui/PanZoomController.java | 3 +- .../base/ui/SimpleScaleGestureDetector.java | 3 +- widget/android/AndroidBridge.cpp | 6 +- widget/android/AndroidBridge.h | 4 +- widget/android/nsWindow.cpp | 54 +++- 12 files changed, 362 insertions(+), 181 deletions(-) create mode 100644 mobile/android/base/gfx/TouchEventHandler.java diff --git a/embedding/android/GeckoAppShell.java b/embedding/android/GeckoAppShell.java index a954ad8edf44..14af705c5604 100644 --- a/embedding/android/GeckoAppShell.java +++ b/embedding/android/GeckoAppShell.java @@ -1846,7 +1846,7 @@ public class GeckoAppShell } // This is only used in Native Fennec. - public static void setPreventPanning(final boolean aPreventPanning) { } + public static void notifyDefaultPrevented(boolean defaultPrevented) { } public static short getScreenOrientation() { return GeckoScreenOrientationListener.getInstance().getScreenOrientation(); diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 1dfb96eff155..2518f992307d 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -991,7 +991,7 @@ abstract public class GeckoApp if (Tabs.getInstance().isSelectedTab(tab)) { mMainHandler.post(new Runnable() { public void run() { - mLayerController.setWaitForTouchListeners(true); + mLayerController.getView().getTouchEventHandler().setWaitForTouchListeners(true); } }); } @@ -2805,7 +2805,7 @@ abstract public class GeckoApp LayerController layerController = getLayerController(); layerController.setLayerClient(mLayerClient); - layerController.setOnTouchListener(new View.OnTouchListener() { + layerController.getView().getTouchEventHandler().setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View view, MotionEvent event) { if (event == null) return true; diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 702990729273..c8293fc2446e 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -1201,11 +1201,11 @@ public class GeckoAppShell }); } - public static void setPreventPanning(final boolean aPreventPanning) { + public static void notifyDefaultPrevented(final boolean defaultPrevented) { getMainHandler().post(new Runnable() { public void run() { - LayerController layerController = GeckoApp.mAppContext.getLayerController(); - layerController.preventPanning(aPreventPanning); + LayerView view = GeckoApp.mAppContext.getLayerController().getView(); + view.getTouchEventHandler().handleEventListenerAction(!defaultPrevented); } }); } diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 16e4a0338b4a..2314b4b5eb44 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -144,6 +144,7 @@ FENNEC_JAVA_FILES = \ gfx/TextureGenerator.java \ gfx/TextureReaper.java \ gfx/TileLayer.java \ + gfx/TouchEventHandler.java \ gfx/ViewTransform.java \ gfx/ViewportMetrics.java \ gfx/VirtualLayer.java \ diff --git a/mobile/android/base/gfx/LayerController.java b/mobile/android/base/gfx/LayerController.java index efb9f3eb350c..1173066da3b2 100644 --- a/mobile/android/base/gfx/LayerController.java +++ b/mobile/android/base/gfx/LayerController.java @@ -38,33 +38,20 @@ package org.mozilla.gecko.gfx; -import org.mozilla.gecko.gfx.IntSize; import org.mozilla.gecko.gfx.Layer; import org.mozilla.gecko.ui.PanZoomController; import org.mozilla.gecko.ui.SimpleScaleGestureDetector; import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoEvent; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.Tab; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Point; import android.graphics.PointF; -import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; -import android.view.MotionEvent; import android.view.GestureDetector; -import android.view.ScaleGestureDetector; import android.view.View.OnTouchListener; -import android.view.ViewConfiguration; -import java.lang.Math; -import java.util.Timer; -import java.util.TimerTask; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -75,7 +62,7 @@ import java.util.regex.Pattern; * * Many methods require that the monitor be held, with a synchronized (controller) { ... } block. */ -public class LayerController implements Tabs.OnTabsChangedListener { +public class LayerController { private static final String LOGTAG = "GeckoLayerController"; private Layer mRootLayer; /* The root layer. */ @@ -95,15 +82,12 @@ public class LayerController implements Tabs.OnTabsChangedListener { * fields. */ private volatile ImmutableViewportMetrics mViewportMetrics; /* The current viewport metrics. */ - private boolean mWaitForTouchListeners; - - private PanZoomController mPanZoomController; /* * The panning and zooming controller, which interprets pan and zoom gestures for us and * updates our visible rect appropriately. */ + private PanZoomController mPanZoomController; - private OnTouchListener mOnTouchListener; /* The touch listener. */ private GeckoLayerClient mLayerClient; /* The layer client. */ /* The new color for the checkerboard. */ @@ -112,14 +96,6 @@ public class LayerController implements Tabs.OnTabsChangedListener { private boolean mForceRedraw; - /* The time limit for pages to respond with preventDefault on touchevents - * before we begin panning the page */ - private int mTimeout = 200; - - private boolean allowDefaultActions = true; - private Timer allowDefaultTimer = null; - private PointF initialTouchLocation = null; - private static Pattern sColorPattern; public LayerController(Context context) { @@ -130,14 +106,6 @@ public class LayerController implements Tabs.OnTabsChangedListener { mPanZoomController = new PanZoomController(this); mView = new LayerView(context, this); mCheckerboardShouldShowChecks = true; - - Tabs.registerOnTabsChangedListener(this); - - mTimeout = ViewConfiguration.getLongPressTimeout(); - } - - public void onDestroy() { - Tabs.unregisterOnTabsChangedListener(this); } public void setRoot(Layer layer) { mRootLayer = layer; } @@ -293,10 +261,6 @@ public class LayerController implements Tabs.OnTabsChangedListener { public boolean post(Runnable action) { return mView.post(action); } - public void setOnTouchListener(OnTouchListener onTouchListener) { - mOnTouchListener = onTouchListener; - } - /** * The view as well as the controller itself use this method to notify the layer client that * the geometry changed. @@ -366,81 +330,6 @@ public class LayerController implements Tabs.OnTabsChangedListener { return layerPoint; } - /* - * Gesture detection. This is handled only at a high level in this class; we dispatch to the - * pan/zoom controller to do the dirty work. - */ - public boolean onTouchEvent(MotionEvent event) { - int action = event.getAction(); - PointF point = new PointF(event.getX(), event.getY()); - - // this will only match the first touchstart in a series - if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { - initialTouchLocation = point; - allowDefaultActions = !mWaitForTouchListeners; - - // if we have a timer, this may be a double tap, - // cancel the current timer but don't clear the event queue - if (allowDefaultTimer != null) { - allowDefaultTimer.cancel(); - } else { - // if we don't have a timer, make sure we remove any old events - mView.clearEventQueue(); - } - allowDefaultTimer = new Timer(); - allowDefaultTimer.schedule(new TimerTask() { - public void run() { - post(new Runnable() { - public void run() { - preventPanning(false); - } - }); - } - }, mTimeout); - } - - // After the initial touch, ignore touch moves until they exceed a minimum distance. - if (initialTouchLocation != null && (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE) { - if (PointUtils.subtract(point, initialTouchLocation).length() > PanZoomController.PAN_THRESHOLD) { - initialTouchLocation = null; - } else { - return !allowDefaultActions; - } - } - - // send the event to content - if (mOnTouchListener != null) - mOnTouchListener.onTouch(mView, event); - - return !allowDefaultActions; - } - - public void preventPanning(boolean aValue) { - if (allowDefaultTimer != null) { - allowDefaultTimer.cancel(); - allowDefaultTimer = null; - } - if (aValue == allowDefaultActions) { - allowDefaultActions = !aValue; - - if (aValue) { - mView.clearEventQueue(); - mPanZoomController.cancelTouch(); - } else { - mView.processEventQueue(); - } - } - } - - public void onTabChanged(Tab tab, Tabs.TabEvents msg) { - if ((Tabs.getInstance().isSelectedTab(tab) && msg == Tabs.TabEvents.STOP) || msg == Tabs.TabEvents.SELECTED) { - mWaitForTouchListeners = tab.getHasTouchListeners(); - } - } - public void setWaitForTouchListeners(boolean aValue) { - mWaitForTouchListeners = aValue; - } - /** Retrieves whether we should show checkerboard checks or not. */ public boolean checkerboardShouldShowChecks() { return mCheckerboardShouldShowChecks; diff --git a/mobile/android/base/gfx/LayerView.java b/mobile/android/base/gfx/LayerView.java index b22ce9b2a87a..547e711ee091 100644 --- a/mobile/android/base/gfx/LayerView.java +++ b/mobile/android/base/gfx/LayerView.java @@ -43,20 +43,16 @@ import org.mozilla.gecko.GeckoInputConnection; import org.mozilla.gecko.gfx.FloatSize; import org.mozilla.gecko.gfx.InputConnectionHandler; import org.mozilla.gecko.gfx.LayerController; -import org.mozilla.gecko.ui.SimpleScaleGestureDetector; import android.content.Context; import android.opengl.GLSurfaceView; import android.view.View; -import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; -import android.view.ScaleGestureDetector; import android.widget.RelativeLayout; import android.util.Log; import java.nio.IntBuffer; -import java.util.LinkedList; import org.mozilla.gecko.GeckoApp; import android.content.Context; @@ -77,18 +73,16 @@ import javax.microedition.khronos.opengles.GL10; * Note that LayerView is accessed by Robocop via reflection. */ public class LayerView extends SurfaceView implements SurfaceHolder.Callback { + private static String LOGTAG = "GeckoLayerView"; + private Context mContext; private LayerController mController; + private TouchEventHandler mTouchEventHandler; private GLController mGLController; private InputConnectionHandler mInputConnectionHandler; private LayerRenderer mRenderer; - private GestureDetector mGestureDetector; - private SimpleScaleGestureDetector mScaleGestureDetector; private long mRenderTime; private boolean mRenderTimeReset; - private static String LOGTAG = "GeckoLayerView"; - /* List of events to be processed if the page does not prevent them. Should only be touched on the main thread */ - private LinkedList mEventQueue = new LinkedList(); /* Must be a PAINT_xxx constant */ private int mPaintState = PAINT_NONE; @@ -111,54 +105,21 @@ public class LayerView extends SurfaceView implements SurfaceHolder.Callback { mGLController = new GLController(this); mContext = context; mController = controller; + mTouchEventHandler = new TouchEventHandler(context, this, mController); mRenderer = new LayerRenderer(this); - mGestureDetector = new GestureDetector(context, controller.getGestureListener()); - mScaleGestureDetector = - new SimpleScaleGestureDetector(controller.getScaleGestureListener()); - mGestureDetector.setOnDoubleTapListener(controller.getDoubleTapListener()); mInputConnectionHandler = null; setFocusable(true); setFocusableInTouchMode(true); } - private void addToEventQueue(MotionEvent event) { - MotionEvent copy = MotionEvent.obtain(event); - mEventQueue.add(copy); - } - - public void processEventQueue() { - MotionEvent event = mEventQueue.poll(); - while(event != null) { - processEvent(event); - event = mEventQueue.poll(); - } - } - - public void clearEventQueue() { - mEventQueue.clear(); - } - @Override public boolean onTouchEvent(MotionEvent event) { - if (mController.onTouchEvent(event)) { - addToEventQueue(event); - return true; - } - return processEvent(event); - } - - private boolean processEvent(MotionEvent event) { - if (mGestureDetector.onTouchEvent(event)) - return true; - mScaleGestureDetector.onTouchEvent(event); - if (mScaleGestureDetector.isInProgress()) - return true; - mController.getPanZoomController().onTouchEvent(event); - return true; + return mTouchEventHandler.handleEvent(event); } public LayerController getController() { return mController; } + public TouchEventHandler getTouchEventHandler() { return mTouchEventHandler; } /** The LayerRenderer calls this to indicate that the window has changed size. */ public void setViewportSize(IntSize size) { diff --git a/mobile/android/base/gfx/TouchEventHandler.java b/mobile/android/base/gfx/TouchEventHandler.java new file mode 100644 index 000000000000..707ea1b3b157 --- /dev/null +++ b/mobile/android/base/gfx/TouchEventHandler.java @@ -0,0 +1,294 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import java.util.LinkedList; +import java.util.Queue; +import android.content.Context; +import android.graphics.PointF; +import android.os.SystemClock; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.View.OnTouchListener; +import org.mozilla.gecko.ui.SimpleScaleGestureDetector; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; + +/** + * This class handles incoming touch events from the user and sends them to + * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom + * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD. + * + * In the following code/comments, a "block" of events refers to a contiguous + * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to + * but not including the next DOWN or POINTER_DOWN event. + * + * "Dispatching" an event refers to performing the default actions for the event, + * which at our level of abstraction just means sending it off to the gesture + * detectors and the pan/zoom controller. + * + * If an event is "default-prevented" that means one or more listeners in Gecko + * has called preventDefault() on the event, which means that the default action + * for that event should not occur. Usually we care about a "block" of events being + * default-prevented, which means that the DOWN/POINTER_DOWN event that started + * the block, or the first MOVE event following that, were prevent-defaulted. + * + * A "default-prevented notification" is when we here in Java-land receive a notification + * from gecko as to whether or not a block of events was default-prevented. This happens + * at some point after the first or second event in the block is processed in Gecko. + * This code assumes we get EXACTLY ONE default-prevented notification for each block + * of events. + */ +public final class TouchEventHandler implements Tabs.OnTabsChangedListener { + private static final String LOGTAG = "GeckoTouchEventHandler"; + + // The time limit for listeners to respond with preventDefault on touchevents + // before we begin panning the page + private final int EVENT_LISTENER_TIMEOUT = ViewConfiguration.getLongPressTimeout(); + + private final LayerView mView; + private final LayerController mController; + private final GestureDetector mGestureDetector; + private final SimpleScaleGestureDetector mScaleGestureDetector; + + // the queue of events that we are holding on to while waiting for a preventDefault + // notification + private final Queue mEventQueue; + private final ListenerTimeoutProcessor mListenerTimeoutProcessor; + + // the listener we use to notify gecko of touch events + private OnTouchListener mOnTouchListener; + + // whether or not we should wait for touch listeners to respond (this state is + // per-tab and is updated when we switch tabs). + private boolean mWaitForTouchListeners; + + // true if we should hold incoming events in our queue. this is re-set for every + // block of events, this is cleared once we find out if the block has been + // default-prevented or not (or we time out waiting for that). + private boolean mHoldInQueue; + + // true if we should dispatch incoming events to the gesture detector and the pan/zoom + // controller. if this is false, then the current block of events has been + // default-prevented, and we should not dispatch these events (although we'll still send + // them to gecko listeners). + private boolean mDispatchEvents; + + // this next 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 default-prevented notification in return, and (2) kick off a delayed + // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in + // a timely fashion. + // since events are constantly coming in, we need to be able to handle more than one + // block of events in the queue. + // + // this means that there are ordering restrictions on these that we can take advantage of, + // and need to abide by. blocks of events in the queue will always be in the order that + // the user generated them. default-prevented notifications we get from gecko will be in + // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that + // have been posted will also fire in the same order as the blocks of events in the queue. + // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple + // ListenerTimeoutProcessor firings, 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 default-prevented notification, or when the + // timeout expires (whichever happens first). there is no way to associate the + // default-prevented notification with a particular block of events other than via ordering, + // + // so what we do to accomplish this is to track a "processing balance", which is the number + // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors + // that have fired. (think "balance" as in teeter-totter balance). this value is: + // - zero when we are in a state where the next default-prevented notification we expect + // to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to + // the next block of events in the queue. + // - positive when we are in a state where we have received more default-prevented notifications + // than ListenerTimeoutProcessors. This means that the next default-prevented notification + // does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors + // need to be ignored as they are for blocks we have already processed. (n is the absolute value + // of the balance.) + // - negative when we are in a state where we have received more ListenerTimeoutProcessors than + // default-prevented notifications. This means that the next ListenerTimeoutProcessor that + // we receive does correspond to the block at the head of the queue, but the next n + // default-prevented notifications need to be ignored as they are for blocks we have already + // processed. (n is the absolute value of the balance.) + private int mProcessingBalance; + + TouchEventHandler(Context context, LayerView view, LayerController controller) { + mView = view; + mController = controller; + + mEventQueue = new LinkedList(); + mGestureDetector = new GestureDetector(context, controller.getGestureListener()); + mScaleGestureDetector = new SimpleScaleGestureDetector(controller.getScaleGestureListener()); + mListenerTimeoutProcessor = new ListenerTimeoutProcessor(); + mDispatchEvents = true; + + mGestureDetector.setOnDoubleTapListener(controller.getDoubleTapListener()); + Tabs.registerOnTabsChangedListener(this); + } + + /* This function MUST be called on the UI thread */ + public boolean handleEvent(MotionEvent event) { + // if we don't have gecko listeners, just dispatch the event + // and be done with it, no extra work needed. + if (mOnTouchListener == null) { + dispatchEvent(event); + return true; + } + + if (isDownEvent(event)) { + // this is the start of a new block of events! whee! + mHoldInQueue = mWaitForTouchListeners; + if (mHoldInQueue) { + // if we're holding the events in the queue, set the timeout so that + // we dispatch these events if we don't get a default-prevented notification + mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT); + } else { + // if we're not holding these events, then we still need to pretend like + // we did and had a ListenerTimeoutProcessor fire so that when we get + // the default-prevented notification for this block, it doesn't accidentally + // act upon some other block + mProcessingBalance++; + } + } + + // if we need to hold the events, add it to the queue. if we need to dispatch + // it directly, do that. it is possible that both mHoldInQueue and mDispatchEvents + // are false, in which case we are processing a block of events that we know + // has been default-prevented. in that case we don't keep the events as we don't + // need them (but we still pass them to the gecko listener). + if (mHoldInQueue) { + mEventQueue.add(MotionEvent.obtain(event)); + } else if (mDispatchEvents) { + dispatchEvent(event); + } + + // notify gecko of the event + mOnTouchListener.onTouch(mView, event); + + return true; + } + + /** + * This function is how gecko sends us a default-prevented notification. It is called + * once gecko knows definitively whether the block of events has had preventDefault + * called on it (either on the initial down event that starts the block, or on + * the first event following that down event). + * + * This function MUST be called on the UI thread. + */ + public void handleEventListenerAction(boolean allowDefaultAction) { + if (mProcessingBalance > 0) { + // this event listener that triggered this took too long, and the corresponding + // ListenerTimeoutProcessor runnable already ran for the event in question. the + // block of events this is for has already been processed, so we don't need to + // do anything here. + } else { + processEventBlock(allowDefaultAction); + } + mProcessingBalance--; + } + + /* This function MUST be called on the UI thread. */ + public void setWaitForTouchListeners(boolean aValue) { + mWaitForTouchListeners = aValue; + } + + /* This function MUST be called on the UI thread. */ + public void setOnTouchListener(OnTouchListener onTouchListener) { + mOnTouchListener = onTouchListener; + } + + private boolean isDownEvent(MotionEvent event) { + int action = (event.getAction() & MotionEvent.ACTION_MASK); + return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN); + } + + /** + * Dispatch the event to the gesture detectors and the pan/zoom controller. + */ + private void dispatchEvent(MotionEvent event) { + if (mGestureDetector.onTouchEvent(event)) { + return; + } + mScaleGestureDetector.onTouchEvent(event); + if (mScaleGestureDetector.isInProgress()) { + return; + } + mController.getPanZoomController().onTouchEvent(event); + } + + /** + * Process the block of events at the head of the queue now that we know + * whether it has been default-prevented or not. + */ + private void processEventBlock(boolean allowDefaultAction) { + if (!allowDefaultAction) { + // if the block has been default-prevented, cancel whatever stuff we had in + // progress in the gesture detector and pan zoom controller + long now = SystemClock.uptimeMillis(); + dispatchEvent(MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0)); + } + + // the odd loop condition is because the first event in the queue will + // always be a DOWN or POINTER_DOWN event, and we want to process all + // the events in the queue starting at that one, up to but not including + // the next DOWN or POINTER_DOWN event. + + MotionEvent event = mEventQueue.poll(); + while (true) { + // for each event we process, only dispatch it if the block hasn't been + // default-prevented. + if (allowDefaultAction) { + dispatchEvent(event); + } + event = mEventQueue.peek(); + if (event == null) { + // we have processed the backlog of events, and are all caught up. + // now we can set clear the hold flag and set the dispatch flag so + // that the handleEvent() function can do the right thing for all + // remaining events in this block (which is still ongoing) without + // having to put them in the queue. + mHoldInQueue = false; + mDispatchEvents = allowDefaultAction; + break; + } + if (isDownEvent(event)) { + // we have finished processing the block we were interested in. + // now we wait for the next call to processEventBlock + break; + } + // pop the event we peeked above, as it is still part of the block and + // we want to keep processing + mEventQueue.remove(); + } + } + + private class ListenerTimeoutProcessor implements Runnable { + /* This MUST be run on the UI thread */ + public void run() { + if (mProcessingBalance < 0) { + // gecko already responded with default-prevented notification, and so + // the block of events this ListenerTimeoutProcessor corresponds to have + // already been removed from the queue. + } else { + processEventBlock(true); + } + mProcessingBalance++; + } + } + + // Tabs.OnTabsChangedListener implementation + + public void onTabChanged(Tab tab, Tabs.TabEvents msg) { + if ((Tabs.getInstance().isSelectedTab(tab) && msg == Tabs.TabEvents.STOP) || msg == Tabs.TabEvents.SELECTED) { + mWaitForTouchListeners = tab.getHasTouchListeners(); + } + } +} diff --git a/mobile/android/base/ui/PanZoomController.java b/mobile/android/base/ui/PanZoomController.java index bf39714c1be0..915fc46ee121 100644 --- a/mobile/android/base/ui/PanZoomController.java +++ b/mobile/android/base/ui/PanZoomController.java @@ -367,6 +367,7 @@ public class PanZoomController private boolean onTouchCancel(MotionEvent event) { mState = PanZoomState.NOTHING; + cancelTouch(); // ensure we snap back if we're overscrolled bounce(); return false; @@ -901,7 +902,7 @@ public class PanZoomController return true; } - public void cancelTouch() { + private void cancelTouch() { GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", ""); GeckoAppShell.sendEventToGecko(e); } diff --git a/mobile/android/base/ui/SimpleScaleGestureDetector.java b/mobile/android/base/ui/SimpleScaleGestureDetector.java index 69ebcb8012b2..1561b2416b4b 100644 --- a/mobile/android/base/ui/SimpleScaleGestureDetector.java +++ b/mobile/android/base/ui/SimpleScaleGestureDetector.java @@ -131,11 +131,12 @@ public class SimpleScaleGestureDetector { private void onTouchEnd(MotionEvent event) { mLastEventTime = event.getEventTime(); + boolean isCancel = (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_CANCEL; int id = event.getPointerId(getActionIndex(event)); ListIterator iterator = mPointerInfo.listIterator(); while (iterator.hasNext()) { PointerInfo pointerInfo = iterator.next(); - if (pointerInfo.getId() != id) { + if (!(isCancel || pointerInfo.getId() == id)) { continue; } diff --git a/widget/android/AndroidBridge.cpp b/widget/android/AndroidBridge.cpp index 8ca5e573a263..35952840e5dc 100644 --- a/widget/android/AndroidBridge.cpp +++ b/widget/android/AndroidBridge.cpp @@ -142,7 +142,7 @@ AndroidBridge::Init(JNIEnv *jEnv, jGetDpi = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "getDpi", "()I"); jSetFullScreen = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "setFullScreen", "(Z)V"); jShowInputMethodPicker = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "showInputMethodPicker", "()V"); - jSetPreventPanning = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "setPreventPanning", "(Z)V"); + jNotifyDefaultPrevented = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "notifyDefaultPrevented", "(Z)V"); jHideProgressDialog = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "hideProgressDialog", "()V"); jPerformHapticFeedback = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "performHapticFeedback", "(Z)V"); jVibrate1 = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "vibrate", "(J)V"); @@ -1961,12 +1961,12 @@ NS_IMETHODIMP nsAndroidBridge::SetDrawMetadataProvider(nsIAndroidDrawMetadataPro } void -AndroidBridge::SetPreventPanning(bool aPreventPanning) { +AndroidBridge::NotifyDefaultPrevented(bool aDefaultPrevented) { JNIEnv *env = GetJNIEnv(); if (!env) return; - env->CallStaticVoidMethod(mGeckoAppShellClass, jSetPreventPanning, (jboolean)aPreventPanning); + env->CallStaticVoidMethod(mGeckoAppShellClass, jNotifyDefaultPrevented, (jboolean)aDefaultPrevented); } diff --git a/widget/android/AndroidBridge.h b/widget/android/AndroidBridge.h index 7e2c5cf1f8d9..555e2f640424 100644 --- a/widget/android/AndroidBridge.h +++ b/widget/android/AndroidBridge.h @@ -262,7 +262,7 @@ public: void ShowInputMethodPicker(); - void SetPreventPanning(bool aPreventPanning); + void NotifyDefaultPrevented(bool aDefaultPrevented); void HideProgressDialogOnce(); @@ -498,7 +498,7 @@ protected: jmethodID jGetDpi; jmethodID jSetFullScreen; jmethodID jShowInputMethodPicker; - jmethodID jSetPreventPanning; + jmethodID jNotifyDefaultPrevented; jmethodID jHideProgressDialog; jmethodID jPerformHapticFeedback; jmethodID jVibrate1; diff --git a/widget/android/nsWindow.cpp b/widget/android/nsWindow.cpp index 44a2fe96d909..d9017ed58040 100644 --- a/widget/android/nsWindow.cpp +++ b/widget/android/nsWindow.cpp @@ -1442,28 +1442,66 @@ getDistance(const nsIntPoint &p1, const nsIntPoint &p2) bool nsWindow::OnMultitouchEvent(AndroidGeckoEvent *ae) { + // This is set to true once we have called SetPreventPanning() exactly + // once for a given sequence of touch events. It is reset on the start + // of the next sequence. + static bool sDefaultPreventedNotified = false; + static bool sLastWasDownEvent = false; + + bool preventDefaultActions = false; + bool isDownEvent = false; switch (ae->Action() & AndroidMotionEvent::ACTION_MASK) { case AndroidMotionEvent::ACTION_DOWN: case AndroidMotionEvent::ACTION_POINTER_DOWN: { nsTouchEvent event(true, NS_TOUCH_START, this); - return DispatchMultitouchEvent(event, ae); + preventDefaultActions = DispatchMultitouchEvent(event, ae); + isDownEvent = true; + break; } case AndroidMotionEvent::ACTION_MOVE: { nsTouchEvent event(true, NS_TOUCH_MOVE, this); - return DispatchMultitouchEvent(event, ae); + preventDefaultActions = DispatchMultitouchEvent(event, ae); + break; } case AndroidMotionEvent::ACTION_UP: case AndroidMotionEvent::ACTION_POINTER_UP: { nsTouchEvent event(true, NS_TOUCH_END, this); - return DispatchMultitouchEvent(event, ae); + preventDefaultActions = DispatchMultitouchEvent(event, ae); + break; } case AndroidMotionEvent::ACTION_OUTSIDE: case AndroidMotionEvent::ACTION_CANCEL: { nsTouchEvent event(true, NS_TOUCH_CANCEL, this); - return DispatchMultitouchEvent(event, ae); + preventDefaultActions = DispatchMultitouchEvent(event, ae); + break; } } - return false; + + // if the last event we got was a down event, then by now we know for sure whether + // this block has been default-prevented or not. if we haven't already sent the + // notification for this block, do so now. + if (sLastWasDownEvent && !sDefaultPreventedNotified) { + // if this event is a down event, that means it's the start of a new block, and the + // previous block should not be default-prevented + bool defaultPrevented = isDownEvent ? false : preventDefaultActions; + AndroidBridge::Bridge()->NotifyDefaultPrevented(defaultPrevented); + sDefaultPreventedNotified = true; + } + + // now, if this event is a down event, then we might already know that it has been + // default-prevented. if so, we send the notification right away; otherwise we wait + // for the next event. + if (isDownEvent) { + if (preventDefaultActions) { + AndroidBridge::Bridge()->NotifyDefaultPrevented(true); + sDefaultPreventedNotified = true; + } else { + sDefaultPreventedNotified = false; + } + } + sLastWasDownEvent = isDownEvent; + + return preventDefaultActions; } bool @@ -1503,11 +1541,7 @@ nsWindow::DispatchMultitouchEvent(nsTouchEvent &event, AndroidGeckoEvent *ae) nsEventStatus status; DispatchEvent(&event, status); - bool preventPanning = (status == nsEventStatus_eConsumeNoDefault); - if (preventPanning || action == AndroidMotionEvent::ACTION_MOVE) { - AndroidBridge::Bridge()->SetPreventPanning(preventPanning); - } - return preventPanning; + return (status == nsEventStatus_eConsumeNoDefault); } void