mobile/android/base/gfx/TouchEventHandler.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko.gfx;
michael@0 7
michael@0 8 import org.mozilla.gecko.Tab;
michael@0 9 import org.mozilla.gecko.Tabs;
michael@0 10
michael@0 11 import android.content.Context;
michael@0 12 import android.os.SystemClock;
michael@0 13 import android.util.Log;
michael@0 14 import android.view.GestureDetector;
michael@0 15 import android.view.MotionEvent;
michael@0 16 import android.view.View;
michael@0 17
michael@0 18 import java.util.LinkedList;
michael@0 19 import java.util.Queue;
michael@0 20
michael@0 21 /**
michael@0 22 * This class handles incoming touch events from the user and sends them to
michael@0 23 * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom
michael@0 24 * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD.
michael@0 25 *
michael@0 26 * In the following code/comments, a "block" of events refers to a contiguous
michael@0 27 * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to
michael@0 28 * but not including the next DOWN or POINTER_DOWN event.
michael@0 29 *
michael@0 30 * "Dispatching" an event refers to performing the default actions for the event,
michael@0 31 * which at our level of abstraction just means sending it off to the gesture
michael@0 32 * detectors and the pan/zoom controller.
michael@0 33 *
michael@0 34 * If an event is "default-prevented" that means one or more listeners in Gecko
michael@0 35 * has called preventDefault() on the event, which means that the default action
michael@0 36 * for that event should not occur. Usually we care about a "block" of events being
michael@0 37 * default-prevented, which means that the DOWN/POINTER_DOWN event that started
michael@0 38 * the block, or the first MOVE event following that, were prevent-defaulted.
michael@0 39 *
michael@0 40 * A "default-prevented notification" is when we here in Java-land receive a notification
michael@0 41 * from gecko as to whether or not a block of events was default-prevented. This happens
michael@0 42 * at some point after the first or second event in the block is processed in Gecko.
michael@0 43 * This code assumes we get EXACTLY ONE default-prevented notification for each block
michael@0 44 * of events.
michael@0 45 *
michael@0 46 * Note that even if all events are default-prevented, we still send specific types
michael@0 47 * of notifications to the pan/zoom controller. The notifications are needed
michael@0 48 * to respond to user actions a timely manner regardless of default-prevention,
michael@0 49 * and fix issues like bug 749384.
michael@0 50 */
michael@0 51 final class TouchEventHandler implements Tabs.OnTabsChangedListener {
michael@0 52 private static final String LOGTAG = "GeckoTouchEventHandler";
michael@0 53
michael@0 54 // The time limit for listeners to respond with preventDefault on touchevents
michael@0 55 // before we begin panning the page
michael@0 56 private final int EVENT_LISTENER_TIMEOUT = 200;
michael@0 57
michael@0 58 private final View mView;
michael@0 59 private final GestureDetector mGestureDetector;
michael@0 60 private final SimpleScaleGestureDetector mScaleGestureDetector;
michael@0 61 private final JavaPanZoomController mPanZoomController;
michael@0 62
michael@0 63 // the queue of events that we are holding on to while waiting for a preventDefault
michael@0 64 // notification
michael@0 65 private final Queue<MotionEvent> mEventQueue;
michael@0 66 private final ListenerTimeoutProcessor mListenerTimeoutProcessor;
michael@0 67
michael@0 68 // whether or not we should wait for touch listeners to respond (this state is
michael@0 69 // per-tab and is updated when we switch tabs).
michael@0 70 private boolean mWaitForTouchListeners;
michael@0 71
michael@0 72 // true if we should hold incoming events in our queue. this is re-set for every
michael@0 73 // block of events, this is cleared once we find out if the block has been
michael@0 74 // default-prevented or not (or we time out waiting for that).
michael@0 75 private boolean mHoldInQueue;
michael@0 76
michael@0 77 // false if the current event block has been default-prevented. In this case,
michael@0 78 // we still pass the event to both Gecko and the pan/zoom controller, but the
michael@0 79 // latter will not use it to scroll content. It may still use the events for
michael@0 80 // other things, such as making the dynamic toolbar visible.
michael@0 81 private boolean mAllowDefaultAction;
michael@0 82
michael@0 83 // this next variable requires some explanation. strap yourself in.
michael@0 84 //
michael@0 85 // for each block of events, we do two things: (1) send the events to gecko and expect
michael@0 86 // exactly one default-prevented notification in return, and (2) kick off a delayed
michael@0 87 // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in
michael@0 88 // a timely fashion.
michael@0 89 // since events are constantly coming in, we need to be able to handle more than one
michael@0 90 // block of events in the queue.
michael@0 91 //
michael@0 92 // this means that there are ordering restrictions on these that we can take advantage of,
michael@0 93 // and need to abide by. blocks of events in the queue will always be in the order that
michael@0 94 // the user generated them. default-prevented notifications we get from gecko will be in
michael@0 95 // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that
michael@0 96 // have been posted will also fire in the same order as the blocks of events in the queue.
michael@0 97 // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple
michael@0 98 // ListenerTimeoutProcessor firings, and that interleaving is not predictable.
michael@0 99 //
michael@0 100 // therefore, we need to make sure that for each block of events, we process the queued
michael@0 101 // events exactly once, either when we get the default-prevented notification, or when the
michael@0 102 // timeout expires (whichever happens first). there is no way to associate the
michael@0 103 // default-prevented notification with a particular block of events other than via ordering,
michael@0 104 //
michael@0 105 // so what we do to accomplish this is to track a "processing balance", which is the number
michael@0 106 // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors
michael@0 107 // that have fired. (think "balance" as in teeter-totter balance). this value is:
michael@0 108 // - zero when we are in a state where the next default-prevented notification we expect
michael@0 109 // to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to
michael@0 110 // the next block of events in the queue.
michael@0 111 // - positive when we are in a state where we have received more default-prevented notifications
michael@0 112 // than ListenerTimeoutProcessors. This means that the next default-prevented notification
michael@0 113 // does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors
michael@0 114 // need to be ignored as they are for blocks we have already processed. (n is the absolute value
michael@0 115 // of the balance.)
michael@0 116 // - negative when we are in a state where we have received more ListenerTimeoutProcessors than
michael@0 117 // default-prevented notifications. This means that the next ListenerTimeoutProcessor that
michael@0 118 // we receive does correspond to the block at the head of the queue, but the next n
michael@0 119 // default-prevented notifications need to be ignored as they are for blocks we have already
michael@0 120 // processed. (n is the absolute value of the balance.)
michael@0 121 private int mProcessingBalance;
michael@0 122
michael@0 123 TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) {
michael@0 124 mView = view;
michael@0 125
michael@0 126 mEventQueue = new LinkedList<MotionEvent>();
michael@0 127 mPanZoomController = panZoomController;
michael@0 128 mGestureDetector = new GestureDetector(context, mPanZoomController);
michael@0 129 mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController);
michael@0 130 mListenerTimeoutProcessor = new ListenerTimeoutProcessor();
michael@0 131 mAllowDefaultAction = true;
michael@0 132
michael@0 133 mGestureDetector.setOnDoubleTapListener(mPanZoomController);
michael@0 134
michael@0 135 Tabs.registerOnTabsChangedListener(this);
michael@0 136 }
michael@0 137
michael@0 138 public void destroy() {
michael@0 139 Tabs.unregisterOnTabsChangedListener(this);
michael@0 140 }
michael@0 141
michael@0 142 /* This function MUST be called on the UI thread */
michael@0 143 public boolean handleEvent(MotionEvent event) {
michael@0 144 if (isDownEvent(event)) {
michael@0 145 // this is the start of a new block of events! whee!
michael@0 146 mHoldInQueue = mWaitForTouchListeners;
michael@0 147
michael@0 148 // Set mAllowDefaultAction to true so that in the event we dispatch events, the
michael@0 149 // PanZoomController doesn't treat them as if they've been prevent-defaulted
michael@0 150 // when they haven't.
michael@0 151 mAllowDefaultAction = true;
michael@0 152 if (mHoldInQueue) {
michael@0 153 // if the new block we are starting is the current block (i.e. there are no
michael@0 154 // other blocks waiting in the queue, then we should let the pan/zoom controller
michael@0 155 // know we are waiting for the touch listeners to run
michael@0 156 if (mEventQueue.isEmpty()) {
michael@0 157 mPanZoomController.startingNewEventBlock(event, true);
michael@0 158 }
michael@0 159 } else {
michael@0 160 // we're not going to be holding this block of events in the queue, but we need
michael@0 161 // a marker of some sort so that the processEventBlock loop deals with the blocks
michael@0 162 // in the right order as notifications come in. we use a single null event in
michael@0 163 // the queue as a placeholder for a block of events that has already been dispatched.
michael@0 164 mEventQueue.add(null);
michael@0 165 mPanZoomController.startingNewEventBlock(event, false);
michael@0 166 }
michael@0 167
michael@0 168 // set the timeout so that we dispatch these events and update mProcessingBalance
michael@0 169 // if we don't get a default-prevented notification
michael@0 170 mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT);
michael@0 171 }
michael@0 172
michael@0 173 // if we need to hold the events, add it to the queue, otherwise dispatch
michael@0 174 // it directly.
michael@0 175 if (mHoldInQueue) {
michael@0 176 mEventQueue.add(MotionEvent.obtain(event));
michael@0 177 } else {
michael@0 178 dispatchEvent(event, mAllowDefaultAction);
michael@0 179 }
michael@0 180
michael@0 181 return false;
michael@0 182 }
michael@0 183
michael@0 184 /**
michael@0 185 * This function is how gecko sends us a default-prevented notification. It is called
michael@0 186 * once gecko knows definitively whether the block of events has had preventDefault
michael@0 187 * called on it (either on the initial down event that starts the block, or on
michael@0 188 * the first event following that down event).
michael@0 189 *
michael@0 190 * This function MUST be called on the UI thread.
michael@0 191 */
michael@0 192 public void handleEventListenerAction(boolean allowDefaultAction) {
michael@0 193 if (mProcessingBalance > 0) {
michael@0 194 // this event listener that triggered this took too long, and the corresponding
michael@0 195 // ListenerTimeoutProcessor runnable already ran for the event in question. the
michael@0 196 // block of events this is for has already been processed, so we don't need to
michael@0 197 // do anything here.
michael@0 198 } else {
michael@0 199 processEventBlock(allowDefaultAction);
michael@0 200 }
michael@0 201 mProcessingBalance--;
michael@0 202 }
michael@0 203
michael@0 204 /* This function MUST be called on the UI thread. */
michael@0 205 public void setWaitForTouchListeners(boolean aValue) {
michael@0 206 mWaitForTouchListeners = aValue;
michael@0 207 }
michael@0 208
michael@0 209 private boolean isDownEvent(MotionEvent event) {
michael@0 210 int action = (event.getAction() & MotionEvent.ACTION_MASK);
michael@0 211 return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
michael@0 212 }
michael@0 213
michael@0 214 private boolean touchFinished(MotionEvent event) {
michael@0 215 int action = (event.getAction() & MotionEvent.ACTION_MASK);
michael@0 216 return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
michael@0 217 }
michael@0 218
michael@0 219 /**
michael@0 220 * Dispatch the event to the gesture detectors and the pan/zoom controller.
michael@0 221 */
michael@0 222 private void dispatchEvent(MotionEvent event, boolean allowDefaultAction) {
michael@0 223 if (allowDefaultAction) {
michael@0 224 if (mGestureDetector.onTouchEvent(event)) {
michael@0 225 return;
michael@0 226 }
michael@0 227 mScaleGestureDetector.onTouchEvent(event);
michael@0 228 if (mScaleGestureDetector.isInProgress()) {
michael@0 229 return;
michael@0 230 }
michael@0 231 }
michael@0 232 mPanZoomController.handleEvent(event, !allowDefaultAction);
michael@0 233 }
michael@0 234
michael@0 235 /**
michael@0 236 * Process the block of events at the head of the queue now that we know
michael@0 237 * whether it has been default-prevented or not.
michael@0 238 */
michael@0 239 private void processEventBlock(boolean allowDefaultAction) {
michael@0 240 if (mEventQueue.isEmpty()) {
michael@0 241 Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
michael@0 242 return;
michael@0 243 }
michael@0 244
michael@0 245 // the odd loop condition is because the first event in the queue will
michael@0 246 // always be a DOWN or POINTER_DOWN event, and we want to process all
michael@0 247 // the events in the queue starting at that one, up to but not including
michael@0 248 // the next DOWN or POINTER_DOWN event.
michael@0 249
michael@0 250 MotionEvent event = mEventQueue.poll();
michael@0 251 while (true) {
michael@0 252 // event being null here is valid and represents a block of events
michael@0 253 // that has already been dispatched.
michael@0 254
michael@0 255 if (event != null) {
michael@0 256 dispatchEvent(event, allowDefaultAction);
michael@0 257 event.recycle();
michael@0 258 event = null;
michael@0 259 }
michael@0 260 if (mEventQueue.isEmpty()) {
michael@0 261 // we have processed the backlog of events, and are all caught up.
michael@0 262 // now we can set clear the hold flag and set the dispatch flag so
michael@0 263 // that the handleEvent() function can do the right thing for all
michael@0 264 // remaining events in this block (which is still ongoing) without
michael@0 265 // having to put them in the queue.
michael@0 266 mHoldInQueue = false;
michael@0 267 mAllowDefaultAction = allowDefaultAction;
michael@0 268 break;
michael@0 269 }
michael@0 270 event = mEventQueue.peek();
michael@0 271 if (event == null || isDownEvent(event)) {
michael@0 272 // we have finished processing the block we were interested in.
michael@0 273 // now we wait for the next call to processEventBlock
michael@0 274 if (event != null) {
michael@0 275 mPanZoomController.startingNewEventBlock(event, true);
michael@0 276 }
michael@0 277 break;
michael@0 278 }
michael@0 279 // pop the event we peeked above, as it is still part of the block and
michael@0 280 // we want to keep processing
michael@0 281 mEventQueue.remove();
michael@0 282 }
michael@0 283 }
michael@0 284
michael@0 285 private class ListenerTimeoutProcessor implements Runnable {
michael@0 286 /* This MUST be run on the UI thread */
michael@0 287 @Override
michael@0 288 public void run() {
michael@0 289 if (mProcessingBalance < 0) {
michael@0 290 // gecko already responded with default-prevented notification, and so
michael@0 291 // the block of events this ListenerTimeoutProcessor corresponds to have
michael@0 292 // already been removed from the queue.
michael@0 293 } else {
michael@0 294 processEventBlock(true);
michael@0 295 }
michael@0 296 mProcessingBalance++;
michael@0 297 }
michael@0 298 }
michael@0 299
michael@0 300 // Tabs.OnTabsChangedListener implementation
michael@0 301
michael@0 302 @Override
michael@0 303 public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
michael@0 304 if ((Tabs.getInstance().isSelectedTab(tab) && msg == Tabs.TabEvents.STOP) || msg == Tabs.TabEvents.SELECTED) {
michael@0 305 mWaitForTouchListeners = tab.getHasTouchListeners();
michael@0 306 }
michael@0 307 }
michael@0 308 }

mercurial