summaryrefslogtreecommitdiffstats
path: root/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java')
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java306
1 files changed, 306 insertions, 0 deletions
diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java b/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
new file mode 100644
index 000000000..1c227de20
--- /dev/null
+++ b/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java
@@ -0,0 +1,306 @@
+/* -*- 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 android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * 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.
+ *
+ * Note that even if all events are default-prevented, we still send specific types
+ * of notifications to the pan/zoom controller. The notifications are needed
+ * to respond to user actions a timely manner regardless of default-prevention,
+ * and fix issues like bug 749384.
+ */
+public final class TouchEventHandler {
+ 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 = 200;
+
+ private final View mView;
+ private final GestureDetector mGestureDetector;
+ private final SimpleScaleGestureDetector mScaleGestureDetector;
+ private final JavaPanZoomController mPanZoomController;
+
+ // the queue of events that we are holding on to while waiting for a preventDefault
+ // notification
+ private final Queue<MotionEvent> mEventQueue;
+ private final ListenerTimeoutProcessor mListenerTimeoutProcessor;
+
+ // 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, View view, JavaPanZoomController panZoomController) {
+ mView = view;
+
+ mEventQueue = new LinkedList<MotionEvent>();
+ mPanZoomController = panZoomController;
+ mGestureDetector = new GestureDetector(context, mPanZoomController);
+ mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
+ mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
+ mDispatchEvents = true;
+
+ mGestureDetector.setOnDoubleTapListener(mPanZoomController);
+ }
+
+ void destroy() {
+ }
+
+ /* This function MUST be called on the UI thread */
+ public boolean handleEvent(MotionEvent event) {
+ if (isDownEvent(event)) {
+ // this is the start of a new block of events! whee!
+ mHoldInQueue = mWaitForTouchListeners;
+
+ // Set mDispatchEvents to true so that we are guaranteed to either queue these
+ // events or dispatch them. The only time we should not do either is once we've
+ // heard back from content to preventDefault this block.
+ mDispatchEvents = true;
+ if (mHoldInQueue) {
+ // if the new block we are starting is the current block (i.e. there are no
+ // other blocks waiting in the queue, then we should let the pan/zoom controller
+ // know we are waiting for the touch listeners to run
+ if (mEventQueue.isEmpty()) {
+ mPanZoomController.startingNewEventBlock(event, true);
+ }
+ } else {
+ // we're not going to be holding this block of events in the queue, but we need
+ // a marker of some sort so that the processEventBlock loop deals with the blocks
+ // in the right order as notifications come in. we use a single null event in
+ // the queue as a placeholder for a block of events that has already been dispatched.
+ mEventQueue.add(null);
+ mPanZoomController.startingNewEventBlock(event, false);
+ }
+
+ // set the timeout so that we dispatch these events and update mProcessingBalance
+ // if we don't get a default-prevented notification
+ mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
+ }
+
+ // 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);
+ } else if (touchFinished(event)) {
+ mPanZoomController.preventedTouchFinished();
+ }
+
+ 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;
+ }
+
+ private boolean isDownEvent(MotionEvent event) {
+ int action = (event.getAction() & MotionEvent.ACTION_MASK);
+ return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
+ }
+
+ private boolean touchFinished(MotionEvent event) {
+ int action = (event.getAction() & MotionEvent.ACTION_MASK);
+ return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
+ }
+
+ /**
+ * 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;
+ }
+ mPanZoomController.handleEvent(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));
+ }
+
+ if (mEventQueue.isEmpty()) {
+ Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
+ return;
+ }
+
+ // 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) {
+ // event being null here is valid and represents a block of events
+ // that has already been dispatched.
+
+ if (event != null) {
+ // for each event we process, only dispatch it if the block hasn't been
+ // default-prevented.
+ if (allowDefaultAction) {
+ dispatchEvent(event);
+ } else if (touchFinished(event)) {
+ mPanZoomController.preventedTouchFinished();
+ }
+ }
+ if (mEventQueue.isEmpty()) {
+ // 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;
+ }
+ event = mEventQueue.peek();
+ if (event == null || isDownEvent(event)) {
+ // we have finished processing the block we were interested in.
+ // now we wait for the next call to processEventBlock
+ if (event != null) {
+ mPanZoomController.startingNewEventBlock(event, true);
+ }
+ 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++;
+ }
+ }
+}