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