diff options
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.java | 306 |
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 0000000000..1c227de20b --- /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++; + } + } +} |