michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.gfx; michael@0: michael@0: import org.mozilla.gecko.Tab; michael@0: import org.mozilla.gecko.Tabs; michael@0: michael@0: import android.content.Context; michael@0: import android.os.SystemClock; michael@0: import android.util.Log; michael@0: import android.view.GestureDetector; michael@0: import android.view.MotionEvent; michael@0: import android.view.View; michael@0: michael@0: import java.util.LinkedList; michael@0: import java.util.Queue; michael@0: michael@0: /** michael@0: * This class handles incoming touch events from the user and sends them to michael@0: * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom michael@0: * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD. michael@0: * michael@0: * In the following code/comments, a "block" of events refers to a contiguous michael@0: * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to michael@0: * but not including the next DOWN or POINTER_DOWN event. michael@0: * michael@0: * "Dispatching" an event refers to performing the default actions for the event, michael@0: * which at our level of abstraction just means sending it off to the gesture michael@0: * detectors and the pan/zoom controller. michael@0: * michael@0: * If an event is "default-prevented" that means one or more listeners in Gecko michael@0: * has called preventDefault() on the event, which means that the default action michael@0: * for that event should not occur. Usually we care about a "block" of events being michael@0: * default-prevented, which means that the DOWN/POINTER_DOWN event that started michael@0: * the block, or the first MOVE event following that, were prevent-defaulted. michael@0: * michael@0: * A "default-prevented notification" is when we here in Java-land receive a notification michael@0: * from gecko as to whether or not a block of events was default-prevented. This happens michael@0: * at some point after the first or second event in the block is processed in Gecko. michael@0: * This code assumes we get EXACTLY ONE default-prevented notification for each block michael@0: * of events. michael@0: * michael@0: * Note that even if all events are default-prevented, we still send specific types michael@0: * of notifications to the pan/zoom controller. The notifications are needed michael@0: * to respond to user actions a timely manner regardless of default-prevention, michael@0: * and fix issues like bug 749384. michael@0: */ michael@0: final class TouchEventHandler implements Tabs.OnTabsChangedListener { michael@0: private static final String LOGTAG = "GeckoTouchEventHandler"; michael@0: michael@0: // The time limit for listeners to respond with preventDefault on touchevents michael@0: // before we begin panning the page michael@0: private final int EVENT_LISTENER_TIMEOUT = 200; michael@0: michael@0: private final View mView; michael@0: private final GestureDetector mGestureDetector; michael@0: private final SimpleScaleGestureDetector mScaleGestureDetector; michael@0: private final JavaPanZoomController mPanZoomController; michael@0: michael@0: // the queue of events that we are holding on to while waiting for a preventDefault michael@0: // notification michael@0: private final Queue mEventQueue; michael@0: private final ListenerTimeoutProcessor mListenerTimeoutProcessor; michael@0: michael@0: // whether or not we should wait for touch listeners to respond (this state is michael@0: // per-tab and is updated when we switch tabs). michael@0: private boolean mWaitForTouchListeners; michael@0: michael@0: // true if we should hold incoming events in our queue. this is re-set for every michael@0: // block of events, this is cleared once we find out if the block has been michael@0: // default-prevented or not (or we time out waiting for that). michael@0: private boolean mHoldInQueue; michael@0: michael@0: // false if the current event block has been default-prevented. In this case, michael@0: // we still pass the event to both Gecko and the pan/zoom controller, but the michael@0: // latter will not use it to scroll content. It may still use the events for michael@0: // other things, such as making the dynamic toolbar visible. michael@0: private boolean mAllowDefaultAction; michael@0: michael@0: // this next variable requires some explanation. strap yourself in. michael@0: // michael@0: // for each block of events, we do two things: (1) send the events to gecko and expect michael@0: // exactly one default-prevented notification in return, and (2) kick off a delayed michael@0: // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in michael@0: // a timely fashion. michael@0: // since events are constantly coming in, we need to be able to handle more than one michael@0: // block of events in the queue. michael@0: // michael@0: // this means that there are ordering restrictions on these that we can take advantage of, michael@0: // and need to abide by. blocks of events in the queue will always be in the order that michael@0: // the user generated them. default-prevented notifications we get from gecko will be in michael@0: // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that michael@0: // have been posted will also fire in the same order as the blocks of events in the queue. michael@0: // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple michael@0: // ListenerTimeoutProcessor firings, and that interleaving is not predictable. michael@0: // michael@0: // therefore, we need to make sure that for each block of events, we process the queued michael@0: // events exactly once, either when we get the default-prevented notification, or when the michael@0: // timeout expires (whichever happens first). there is no way to associate the michael@0: // default-prevented notification with a particular block of events other than via ordering, michael@0: // michael@0: // so what we do to accomplish this is to track a "processing balance", which is the number michael@0: // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors michael@0: // that have fired. (think "balance" as in teeter-totter balance). this value is: michael@0: // - zero when we are in a state where the next default-prevented notification we expect michael@0: // to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to michael@0: // the next block of events in the queue. michael@0: // - positive when we are in a state where we have received more default-prevented notifications michael@0: // than ListenerTimeoutProcessors. This means that the next default-prevented notification michael@0: // does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors michael@0: // need to be ignored as they are for blocks we have already processed. (n is the absolute value michael@0: // of the balance.) michael@0: // - negative when we are in a state where we have received more ListenerTimeoutProcessors than michael@0: // default-prevented notifications. This means that the next ListenerTimeoutProcessor that michael@0: // we receive does correspond to the block at the head of the queue, but the next n michael@0: // default-prevented notifications need to be ignored as they are for blocks we have already michael@0: // processed. (n is the absolute value of the balance.) michael@0: private int mProcessingBalance; michael@0: michael@0: TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) { michael@0: mView = view; michael@0: michael@0: mEventQueue = new LinkedList(); michael@0: mPanZoomController = panZoomController; michael@0: mGestureDetector = new GestureDetector(context, mPanZoomController); michael@0: mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController); michael@0: mListenerTimeoutProcessor = new ListenerTimeoutProcessor(); michael@0: mAllowDefaultAction = true; michael@0: michael@0: mGestureDetector.setOnDoubleTapListener(mPanZoomController); michael@0: michael@0: Tabs.registerOnTabsChangedListener(this); michael@0: } michael@0: michael@0: public void destroy() { michael@0: Tabs.unregisterOnTabsChangedListener(this); michael@0: } michael@0: michael@0: /* This function MUST be called on the UI thread */ michael@0: public boolean handleEvent(MotionEvent event) { michael@0: if (isDownEvent(event)) { michael@0: // this is the start of a new block of events! whee! michael@0: mHoldInQueue = mWaitForTouchListeners; michael@0: michael@0: // Set mAllowDefaultAction to true so that in the event we dispatch events, the michael@0: // PanZoomController doesn't treat them as if they've been prevent-defaulted michael@0: // when they haven't. michael@0: mAllowDefaultAction = true; michael@0: if (mHoldInQueue) { michael@0: // if the new block we are starting is the current block (i.e. there are no michael@0: // other blocks waiting in the queue, then we should let the pan/zoom controller michael@0: // know we are waiting for the touch listeners to run michael@0: if (mEventQueue.isEmpty()) { michael@0: mPanZoomController.startingNewEventBlock(event, true); michael@0: } michael@0: } else { michael@0: // we're not going to be holding this block of events in the queue, but we need michael@0: // a marker of some sort so that the processEventBlock loop deals with the blocks michael@0: // in the right order as notifications come in. we use a single null event in michael@0: // the queue as a placeholder for a block of events that has already been dispatched. michael@0: mEventQueue.add(null); michael@0: mPanZoomController.startingNewEventBlock(event, false); michael@0: } michael@0: michael@0: // set the timeout so that we dispatch these events and update mProcessingBalance michael@0: // if we don't get a default-prevented notification michael@0: mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT); michael@0: } michael@0: michael@0: // if we need to hold the events, add it to the queue, otherwise dispatch michael@0: // it directly. michael@0: if (mHoldInQueue) { michael@0: mEventQueue.add(MotionEvent.obtain(event)); michael@0: } else { michael@0: dispatchEvent(event, mAllowDefaultAction); michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * This function is how gecko sends us a default-prevented notification. It is called michael@0: * once gecko knows definitively whether the block of events has had preventDefault michael@0: * called on it (either on the initial down event that starts the block, or on michael@0: * the first event following that down event). michael@0: * michael@0: * This function MUST be called on the UI thread. michael@0: */ michael@0: public void handleEventListenerAction(boolean allowDefaultAction) { michael@0: if (mProcessingBalance > 0) { michael@0: // this event listener that triggered this took too long, and the corresponding michael@0: // ListenerTimeoutProcessor runnable already ran for the event in question. the michael@0: // block of events this is for has already been processed, so we don't need to michael@0: // do anything here. michael@0: } else { michael@0: processEventBlock(allowDefaultAction); michael@0: } michael@0: mProcessingBalance--; michael@0: } michael@0: michael@0: /* This function MUST be called on the UI thread. */ michael@0: public void setWaitForTouchListeners(boolean aValue) { michael@0: mWaitForTouchListeners = aValue; michael@0: } michael@0: michael@0: private boolean isDownEvent(MotionEvent event) { michael@0: int action = (event.getAction() & MotionEvent.ACTION_MASK); michael@0: return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN); michael@0: } michael@0: michael@0: private boolean touchFinished(MotionEvent event) { michael@0: int action = (event.getAction() & MotionEvent.ACTION_MASK); michael@0: return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL); michael@0: } michael@0: michael@0: /** michael@0: * Dispatch the event to the gesture detectors and the pan/zoom controller. michael@0: */ michael@0: private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) { michael@0: if (allowDefaultAction) { michael@0: if (mGestureDetector.onTouchEvent(event)) { michael@0: return; michael@0: } michael@0: mScaleGestureDetector.onTouchEvent(event); michael@0: if (mScaleGestureDetector.isInProgress()) { michael@0: return; michael@0: } michael@0: } michael@0: mPanZoomController.handleEvent(event, !allowDefaultAction); michael@0: } michael@0: michael@0: /** michael@0: * Process the block of events at the head of the queue now that we know michael@0: * whether it has been default-prevented or not. michael@0: */ michael@0: private void processEventBlock(boolean allowDefaultAction) { michael@0: if (mEventQueue.isEmpty()) { michael@0: Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception()); michael@0: return; michael@0: } michael@0: michael@0: // the odd loop condition is because the first event in the queue will michael@0: // always be a DOWN or POINTER_DOWN event, and we want to process all michael@0: // the events in the queue starting at that one, up to but not including michael@0: // the next DOWN or POINTER_DOWN event. michael@0: michael@0: MotionEvent event = mEventQueue.poll(); michael@0: while (true) { michael@0: // event being null here is valid and represents a block of events michael@0: // that has already been dispatched. michael@0: michael@0: if (event != null) { michael@0: dispatchEvent(event, allowDefaultAction); michael@0: event.recycle(); michael@0: event = null; michael@0: } michael@0: if (mEventQueue.isEmpty()) { michael@0: // we have processed the backlog of events, and are all caught up. michael@0: // now we can set clear the hold flag and set the dispatch flag so michael@0: // that the handleEvent() function can do the right thing for all michael@0: // remaining events in this block (which is still ongoing) without michael@0: // having to put them in the queue. michael@0: mHoldInQueue = false; michael@0: mAllowDefaultAction = allowDefaultAction; michael@0: break; michael@0: } michael@0: event = mEventQueue.peek(); michael@0: if (event == null || isDownEvent(event)) { michael@0: // we have finished processing the block we were interested in. michael@0: // now we wait for the next call to processEventBlock michael@0: if (event != null) { michael@0: mPanZoomController.startingNewEventBlock(event, true); michael@0: } michael@0: break; michael@0: } michael@0: // pop the event we peeked above, as it is still part of the block and michael@0: // we want to keep processing michael@0: mEventQueue.remove(); michael@0: } michael@0: } michael@0: michael@0: private class ListenerTimeoutProcessor implements Runnable { michael@0: /* This MUST be run on the UI thread */ michael@0: @Override michael@0: public void run() { michael@0: if (mProcessingBalance < 0) { michael@0: // gecko already responded with default-prevented notification, and so michael@0: // the block of events this ListenerTimeoutProcessor corresponds to have michael@0: // already been removed from the queue. michael@0: } else { michael@0: processEventBlock(true); michael@0: } michael@0: mProcessingBalance++; michael@0: } michael@0: } michael@0: michael@0: // Tabs.OnTabsChangedListener implementation michael@0: michael@0: @Override michael@0: public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { michael@0: if ((Tabs.getInstance().isSelectedTab(tab) && msg == Tabs.TabEvents.STOP) || msg == Tabs.TabEvents.SELECTED) { michael@0: mWaitForTouchListeners = tab.getHasTouchListeners(); michael@0: } michael@0: } michael@0: }