1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/gfx/TouchEventHandler.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,308 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.gfx; 1.10 + 1.11 +import org.mozilla.gecko.Tab; 1.12 +import org.mozilla.gecko.Tabs; 1.13 + 1.14 +import android.content.Context; 1.15 +import android.os.SystemClock; 1.16 +import android.util.Log; 1.17 +import android.view.GestureDetector; 1.18 +import android.view.MotionEvent; 1.19 +import android.view.View; 1.20 + 1.21 +import java.util.LinkedList; 1.22 +import java.util.Queue; 1.23 + 1.24 +/** 1.25 + * This class handles incoming touch events from the user and sends them to 1.26 + * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom 1.27 + * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD. 1.28 + * 1.29 + * In the following code/comments, a "block" of events refers to a contiguous 1.30 + * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to 1.31 + * but not including the next DOWN or POINTER_DOWN event. 1.32 + * 1.33 + * "Dispatching" an event refers to performing the default actions for the event, 1.34 + * which at our level of abstraction just means sending it off to the gesture 1.35 + * detectors and the pan/zoom controller. 1.36 + * 1.37 + * If an event is "default-prevented" that means one or more listeners in Gecko 1.38 + * has called preventDefault() on the event, which means that the default action 1.39 + * for that event should not occur. Usually we care about a "block" of events being 1.40 + * default-prevented, which means that the DOWN/POINTER_DOWN event that started 1.41 + * the block, or the first MOVE event following that, were prevent-defaulted. 1.42 + * 1.43 + * A "default-prevented notification" is when we here in Java-land receive a notification 1.44 + * from gecko as to whether or not a block of events was default-prevented. This happens 1.45 + * at some point after the first or second event in the block is processed in Gecko. 1.46 + * This code assumes we get EXACTLY ONE default-prevented notification for each block 1.47 + * of events. 1.48 + * 1.49 + * Note that even if all events are default-prevented, we still send specific types 1.50 + * of notifications to the pan/zoom controller. The notifications are needed 1.51 + * to respond to user actions a timely manner regardless of default-prevention, 1.52 + * and fix issues like bug 749384. 1.53 + */ 1.54 +final class TouchEventHandler implements Tabs.OnTabsChangedListener { 1.55 + private static final String LOGTAG = "GeckoTouchEventHandler"; 1.56 + 1.57 + // The time limit for listeners to respond with preventDefault on touchevents 1.58 + // before we begin panning the page 1.59 + private final int EVENT_LISTENER_TIMEOUT = 200; 1.60 + 1.61 + private final View mView; 1.62 + private final GestureDetector mGestureDetector; 1.63 + private final SimpleScaleGestureDetector mScaleGestureDetector; 1.64 + private final JavaPanZoomController mPanZoomController; 1.65 + 1.66 + // the queue of events that we are holding on to while waiting for a preventDefault 1.67 + // notification 1.68 + private final Queue<MotionEvent> mEventQueue; 1.69 + private final ListenerTimeoutProcessor mListenerTimeoutProcessor; 1.70 + 1.71 + // whether or not we should wait for touch listeners to respond (this state is 1.72 + // per-tab and is updated when we switch tabs). 1.73 + private boolean mWaitForTouchListeners; 1.74 + 1.75 + // true if we should hold incoming events in our queue. this is re-set for every 1.76 + // block of events, this is cleared once we find out if the block has been 1.77 + // default-prevented or not (or we time out waiting for that). 1.78 + private boolean mHoldInQueue; 1.79 + 1.80 + // false if the current event block has been default-prevented. In this case, 1.81 + // we still pass the event to both Gecko and the pan/zoom controller, but the 1.82 + // latter will not use it to scroll content. It may still use the events for 1.83 + // other things, such as making the dynamic toolbar visible. 1.84 + private boolean mAllowDefaultAction; 1.85 + 1.86 + // this next variable requires some explanation. strap yourself in. 1.87 + // 1.88 + // for each block of events, we do two things: (1) send the events to gecko and expect 1.89 + // exactly one default-prevented notification in return, and (2) kick off a delayed 1.90 + // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in 1.91 + // a timely fashion. 1.92 + // since events are constantly coming in, we need to be able to handle more than one 1.93 + // block of events in the queue. 1.94 + // 1.95 + // this means that there are ordering restrictions on these that we can take advantage of, 1.96 + // and need to abide by. blocks of events in the queue will always be in the order that 1.97 + // the user generated them. default-prevented notifications we get from gecko will be in 1.98 + // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that 1.99 + // have been posted will also fire in the same order as the blocks of events in the queue. 1.100 + // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple 1.101 + // ListenerTimeoutProcessor firings, and that interleaving is not predictable. 1.102 + // 1.103 + // therefore, we need to make sure that for each block of events, we process the queued 1.104 + // events exactly once, either when we get the default-prevented notification, or when the 1.105 + // timeout expires (whichever happens first). there is no way to associate the 1.106 + // default-prevented notification with a particular block of events other than via ordering, 1.107 + // 1.108 + // so what we do to accomplish this is to track a "processing balance", which is the number 1.109 + // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors 1.110 + // that have fired. (think "balance" as in teeter-totter balance). this value is: 1.111 + // - zero when we are in a state where the next default-prevented notification we expect 1.112 + // to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to 1.113 + // the next block of events in the queue. 1.114 + // - positive when we are in a state where we have received more default-prevented notifications 1.115 + // than ListenerTimeoutProcessors. This means that the next default-prevented notification 1.116 + // does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors 1.117 + // need to be ignored as they are for blocks we have already processed. (n is the absolute value 1.118 + // of the balance.) 1.119 + // - negative when we are in a state where we have received more ListenerTimeoutProcessors than 1.120 + // default-prevented notifications. This means that the next ListenerTimeoutProcessor that 1.121 + // we receive does correspond to the block at the head of the queue, but the next n 1.122 + // default-prevented notifications need to be ignored as they are for blocks we have already 1.123 + // processed. (n is the absolute value of the balance.) 1.124 + private int mProcessingBalance; 1.125 + 1.126 + TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) { 1.127 + mView = view; 1.128 + 1.129 + mEventQueue = new LinkedList<MotionEvent>(); 1.130 + mPanZoomController = panZoomController; 1.131 + mGestureDetector = new GestureDetector(context, mPanZoomController); 1.132 + mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController); 1.133 + mListenerTimeoutProcessor = new ListenerTimeoutProcessor(); 1.134 + mAllowDefaultAction = true; 1.135 + 1.136 + mGestureDetector.setOnDoubleTapListener(mPanZoomController); 1.137 + 1.138 + Tabs.registerOnTabsChangedListener(this); 1.139 + } 1.140 + 1.141 + public void destroy() { 1.142 + Tabs.unregisterOnTabsChangedListener(this); 1.143 + } 1.144 + 1.145 + /* This function MUST be called on the UI thread */ 1.146 + public boolean handleEvent(MotionEvent event) { 1.147 + if (isDownEvent(event)) { 1.148 + // this is the start of a new block of events! whee! 1.149 + mHoldInQueue = mWaitForTouchListeners; 1.150 + 1.151 + // Set mAllowDefaultAction to true so that in the event we dispatch events, the 1.152 + // PanZoomController doesn't treat them as if they've been prevent-defaulted 1.153 + // when they haven't. 1.154 + mAllowDefaultAction = true; 1.155 + if (mHoldInQueue) { 1.156 + // if the new block we are starting is the current block (i.e. there are no 1.157 + // other blocks waiting in the queue, then we should let the pan/zoom controller 1.158 + // know we are waiting for the touch listeners to run 1.159 + if (mEventQueue.isEmpty()) { 1.160 + mPanZoomController.startingNewEventBlock(event, true); 1.161 + } 1.162 + } else { 1.163 + // we're not going to be holding this block of events in the queue, but we need 1.164 + // a marker of some sort so that the processEventBlock loop deals with the blocks 1.165 + // in the right order as notifications come in. we use a single null event in 1.166 + // the queue as a placeholder for a block of events that has already been dispatched. 1.167 + mEventQueue.add(null); 1.168 + mPanZoomController.startingNewEventBlock(event, false); 1.169 + } 1.170 + 1.171 + // set the timeout so that we dispatch these events and update mProcessingBalance 1.172 + // if we don't get a default-prevented notification 1.173 + mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT); 1.174 + } 1.175 + 1.176 + // if we need to hold the events, add it to the queue, otherwise dispatch 1.177 + // it directly. 1.178 + if (mHoldInQueue) { 1.179 + mEventQueue.add(MotionEvent.obtain(event)); 1.180 + } else { 1.181 + dispatchEvent(event, mAllowDefaultAction); 1.182 + } 1.183 + 1.184 + return false; 1.185 + } 1.186 + 1.187 + /** 1.188 + * This function is how gecko sends us a default-prevented notification. It is called 1.189 + * once gecko knows definitively whether the block of events has had preventDefault 1.190 + * called on it (either on the initial down event that starts the block, or on 1.191 + * the first event following that down event). 1.192 + * 1.193 + * This function MUST be called on the UI thread. 1.194 + */ 1.195 + public void handleEventListenerAction(boolean allowDefaultAction) { 1.196 + if (mProcessingBalance > 0) { 1.197 + // this event listener that triggered this took too long, and the corresponding 1.198 + // ListenerTimeoutProcessor runnable already ran for the event in question. the 1.199 + // block of events this is for has already been processed, so we don't need to 1.200 + // do anything here. 1.201 + } else { 1.202 + processEventBlock(allowDefaultAction); 1.203 + } 1.204 + mProcessingBalance--; 1.205 + } 1.206 + 1.207 + /* This function MUST be called on the UI thread. */ 1.208 + public void setWaitForTouchListeners(boolean aValue) { 1.209 + mWaitForTouchListeners = aValue; 1.210 + } 1.211 + 1.212 + private boolean isDownEvent(MotionEvent event) { 1.213 + int action = (event.getAction() & MotionEvent.ACTION_MASK); 1.214 + return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN); 1.215 + } 1.216 + 1.217 + private boolean touchFinished(MotionEvent event) { 1.218 + int action = (event.getAction() & MotionEvent.ACTION_MASK); 1.219 + return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL); 1.220 + } 1.221 + 1.222 + /** 1.223 + * Dispatch the event to the gesture detectors and the pan/zoom controller. 1.224 + */ 1.225 + private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) { 1.226 + if (allowDefaultAction) { 1.227 + if (mGestureDetector.onTouchEvent(event)) { 1.228 + return; 1.229 + } 1.230 + mScaleGestureDetector.onTouchEvent(event); 1.231 + if (mScaleGestureDetector.isInProgress()) { 1.232 + return; 1.233 + } 1.234 + } 1.235 + mPanZoomController.handleEvent(event, !allowDefaultAction); 1.236 + } 1.237 + 1.238 + /** 1.239 + * Process the block of events at the head of the queue now that we know 1.240 + * whether it has been default-prevented or not. 1.241 + */ 1.242 + private void processEventBlock(boolean allowDefaultAction) { 1.243 + if (mEventQueue.isEmpty()) { 1.244 + Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception()); 1.245 + return; 1.246 + } 1.247 + 1.248 + // the odd loop condition is because the first event in the queue will 1.249 + // always be a DOWN or POINTER_DOWN event, and we want to process all 1.250 + // the events in the queue starting at that one, up to but not including 1.251 + // the next DOWN or POINTER_DOWN event. 1.252 + 1.253 + MotionEvent event = mEventQueue.poll(); 1.254 + while (true) { 1.255 + // event being null here is valid and represents a block of events 1.256 + // that has already been dispatched. 1.257 + 1.258 + if (event != null) { 1.259 + dispatchEvent(event, allowDefaultAction); 1.260 + event.recycle(); 1.261 + event = null; 1.262 + } 1.263 + if (mEventQueue.isEmpty()) { 1.264 + // we have processed the backlog of events, and are all caught up. 1.265 + // now we can set clear the hold flag and set the dispatch flag so 1.266 + // that the handleEvent() function can do the right thing for all 1.267 + // remaining events in this block (which is still ongoing) without 1.268 + // having to put them in the queue. 1.269 + mHoldInQueue = false; 1.270 + mAllowDefaultAction = allowDefaultAction; 1.271 + break; 1.272 + } 1.273 + event = mEventQueue.peek(); 1.274 + if (event == null || isDownEvent(event)) { 1.275 + // we have finished processing the block we were interested in. 1.276 + // now we wait for the next call to processEventBlock 1.277 + if (event != null) { 1.278 + mPanZoomController.startingNewEventBlock(event, true); 1.279 + } 1.280 + break; 1.281 + } 1.282 + // pop the event we peeked above, as it is still part of the block and 1.283 + // we want to keep processing 1.284 + mEventQueue.remove(); 1.285 + } 1.286 + } 1.287 + 1.288 + private class ListenerTimeoutProcessor implements Runnable { 1.289 + /* This MUST be run on the UI thread */ 1.290 + @Override 1.291 + public void run() { 1.292 + if (mProcessingBalance < 0) { 1.293 + // gecko already responded with default-prevented notification, and so 1.294 + // the block of events this ListenerTimeoutProcessor corresponds to have 1.295 + // already been removed from the queue. 1.296 + } else { 1.297 + processEventBlock(true); 1.298 + } 1.299 + mProcessingBalance++; 1.300 + } 1.301 + } 1.302 + 1.303 + // Tabs.OnTabsChangedListener implementation 1.304 + 1.305 + @Override 1.306 + public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { 1.307 + if ((Tabs.getInstance().isSelectedTab(tab) && msg == Tabs.TabEvents.STOP) || msg == Tabs.TabEvents.SELECTED) { 1.308 + mWaitForTouchListeners = tab.getHasTouchListeners(); 1.309 + } 1.310 + } 1.311 +}