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