Thu, 22 Jan 2015 13:21:57 +0100
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 | } |