1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.webkit;
18
19import android.content.Context;
20import android.os.Handler;
21import android.os.Looper;
22import android.os.Message;
23import android.os.SystemClock;
24import android.util.Log;
25import android.view.MotionEvent;
26import android.view.ViewConfiguration;
27
28/**
29 * Perform asynchronous dispatch of input events in a {@link WebView}.
30 *
31 * This dispatcher is shared by the UI thread ({@link WebViewClassic}) and web kit
32 * thread ({@link WebViewCore}).  The UI thread enqueues events for
33 * processing, waits for the web kit thread to handle them, and then performs
34 * additional processing depending on the outcome.
35 *
36 * How it works:
37 *
38 * 1. The web view thread receives an input event from the input system on the UI
39 * thread in its {@link WebViewClassic#onTouchEvent} handler.  It sends the input event
40 * to the dispatcher, then immediately returns true to the input system to indicate that
41 * it will handle the event.
42 *
43 * 2. The web kit thread is notified that an event has been enqueued.  Meanwhile additional
44 * events may be enqueued from the UI thread.  In some cases, the dispatcher may decide to
45 * coalesce motion events into larger batches or to cancel events that have been
46 * sitting in the queue for too long.
47 *
48 * 3. The web kit thread wakes up and handles all input events that are waiting for it.
49 * After processing each input event, it informs the dispatcher whether the web application
50 * has decided to handle the event itself and to prevent default event handling.
51 *
52 * 4. If web kit indicates that it wants to prevent default event handling, then web kit
53 * consumes the remainder of the gesture and web view receives a cancel event if
54 * needed.  Otherwise, the web view handles the gesture on the UI thread normally.
55 *
56 * 5. If the web kit thread takes too long to handle an input event, then it loses the
57 * right to handle it.  The dispatcher synthesizes a cancellation event for web kit and
58 * then tells the web view on the UI thread to handle the event that timed out along
59 * with the rest of the gesture.
60 *
61 * One thing to keep in mind about the dispatcher is that what goes into the dispatcher
62 * is not necessarily what the web kit or UI thread will see.  As mentioned above, the
63 * dispatcher may tweak the input event stream to improve responsiveness.  Both web view and
64 * web kit are guaranteed to perceive a consistent stream of input events but
65 * they might not always see the same events (especially if one decides
66 * to prevent the other from handling a particular gesture).
67 *
68 * This implementation very deliberately does not refer to the {@link WebViewClassic}
69 * or {@link WebViewCore} classes, preferring to communicate with them only via
70 * interfaces to avoid unintentional coupling to their implementation details.
71 *
72 * Currently, the input dispatcher only handles pointer events (includes touch,
73 * hover and scroll events).  In principle, it could be extended to handle trackball
74 * and key events if needed.
75 *
76 * @hide
77 */
78final class WebViewInputDispatcher {
79    private static final String TAG = "WebViewInputDispatcher";
80    private static final boolean DEBUG = false;
81    // This enables batching of MotionEvents. It will combine multiple MotionEvents
82    // together into a single MotionEvent if more events come in while we are
83    // still waiting on the processing of a previous event.
84    // If this is set to false, we will instead opt to drop ACTION_MOVE
85    // events we cannot keep up with.
86    // TODO: If batching proves to be working well, remove this
87    private static final boolean ENABLE_EVENT_BATCHING = true;
88
89    private final Object mLock = new Object();
90
91    // Pool of queued input events.  (guarded by mLock)
92    private static final int MAX_DISPATCH_EVENT_POOL_SIZE = 10;
93    private DispatchEvent mDispatchEventPool;
94    private int mDispatchEventPoolSize;
95
96    // Posted state, tracks events posted to the dispatcher.  (guarded by mLock)
97    private final TouchStream mPostTouchStream = new TouchStream();
98    private boolean mPostSendTouchEventsToWebKit;
99    private boolean mPostDoNotSendTouchEventsToWebKitUntilNextGesture;
100    private boolean mPostLongPressScheduled;
101    private boolean mPostClickScheduled;
102    private boolean mPostShowTapHighlightScheduled;
103    private boolean mPostHideTapHighlightScheduled;
104    private int mPostLastWebKitXOffset;
105    private int mPostLastWebKitYOffset;
106    private float mPostLastWebKitScale;
107
108    // State for event tracking (click, longpress, double tap, etc..)
109    private boolean mIsDoubleTapCandidate;
110    private boolean mIsTapCandidate;
111    private float mInitialDownX;
112    private float mInitialDownY;
113    private float mTouchSlopSquared;
114    private float mDoubleTapSlopSquared;
115
116    // Web kit state, tracks events observed by web kit.  (guarded by mLock)
117    private final DispatchEventQueue mWebKitDispatchEventQueue = new DispatchEventQueue();
118    private final TouchStream mWebKitTouchStream = new TouchStream();
119    private final WebKitCallbacks mWebKitCallbacks;
120    private final WebKitHandler mWebKitHandler;
121    private boolean mWebKitDispatchScheduled;
122    private boolean mWebKitTimeoutScheduled;
123    private long mWebKitTimeoutTime;
124
125    // UI state, tracks events observed by the UI.  (guarded by mLock)
126    private final DispatchEventQueue mUiDispatchEventQueue = new DispatchEventQueue();
127    private final TouchStream mUiTouchStream = new TouchStream();
128    private final UiCallbacks mUiCallbacks;
129    private final UiHandler mUiHandler;
130    private boolean mUiDispatchScheduled;
131
132    // Give up on web kit handling of input events when this timeout expires.
133    private static final long WEBKIT_TIMEOUT_MILLIS = 200;
134    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
135    private static final int LONG_PRESS_TIMEOUT =
136            ViewConfiguration.getLongPressTimeout() + TAP_TIMEOUT;
137    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
138    private static final int PRESSED_STATE_DURATION = ViewConfiguration.getPressedStateDuration();
139
140    /**
141     * Event type: Indicates a touch event type.
142     *
143     * This event is delivered together with a {@link MotionEvent} with one of the
144     * following actions: {@link MotionEvent#ACTION_DOWN}, {@link MotionEvent#ACTION_MOVE},
145     * {@link MotionEvent#ACTION_UP}, {@link MotionEvent#ACTION_POINTER_DOWN},
146     * {@link MotionEvent#ACTION_POINTER_UP}, {@link MotionEvent#ACTION_CANCEL}.
147     */
148    public static final int EVENT_TYPE_TOUCH = 0;
149
150    /**
151     * Event type: Indicates a hover event type.
152     *
153     * This event is delivered together with a {@link MotionEvent} with one of the
154     * following actions: {@link MotionEvent#ACTION_HOVER_ENTER},
155     * {@link MotionEvent#ACTION_HOVER_MOVE}, {@link MotionEvent#ACTION_HOVER_MOVE}.
156     */
157    public static final int EVENT_TYPE_HOVER = 1;
158
159    /**
160     * Event type: Indicates a scroll event type.
161     *
162     * This event is delivered together with a {@link MotionEvent} with action
163     * {@link MotionEvent#ACTION_SCROLL}.
164     */
165    public static final int EVENT_TYPE_SCROLL = 2;
166
167    /**
168     * Event type: Indicates a long-press event type.
169     *
170     * This event is delivered in the middle of a sequence of {@link #EVENT_TYPE_TOUCH} events.
171     * It includes a {@link MotionEvent} with action {@link MotionEvent#ACTION_MOVE}
172     * that indicates the current touch coordinates of the long-press.
173     *
174     * This event is sent when the current touch gesture has been held longer than
175     * the long-press interval.
176     */
177    public static final int EVENT_TYPE_LONG_PRESS = 3;
178
179    /**
180     * Event type: Indicates a click event type.
181     *
182     * This event is delivered after a sequence of {@link #EVENT_TYPE_TOUCH} events that
183     * comprise a complete gesture ending with {@link MotionEvent#ACTION_UP}.
184     * It includes a {@link MotionEvent} with action {@link MotionEvent#ACTION_UP}
185     * that indicates the location of the click.
186     *
187     * This event is sent shortly after the end of a touch after the double-tap
188     * interval has expired to indicate a click.
189     */
190    public static final int EVENT_TYPE_CLICK = 4;
191
192    /**
193     * Event type: Indicates a double-tap event type.
194     *
195     * This event is delivered after a sequence of {@link #EVENT_TYPE_TOUCH} events that
196     * comprise a complete gesture ending with {@link MotionEvent#ACTION_UP}.
197     * It includes a {@link MotionEvent} with action {@link MotionEvent#ACTION_UP}
198     * that indicates the location of the double-tap.
199     *
200     * This event is sent immediately after a sequence of two touches separated
201     * in time by no more than the double-tap interval and separated in space
202     * by no more than the double-tap slop.
203     */
204    public static final int EVENT_TYPE_DOUBLE_TAP = 5;
205
206    /**
207     * Event type: Indicates that a hit test should be performed
208     */
209    public static final int EVENT_TYPE_HIT_TEST = 6;
210
211    /**
212     * Flag: This event is private to this queue.  Do not forward it.
213     */
214    public static final int FLAG_PRIVATE = 1 << 0;
215
216    /**
217     * Flag: This event is currently being processed by web kit.
218     * If a timeout occurs, make a copy of it before forwarding the event to another queue.
219     */
220    public static final int FLAG_WEBKIT_IN_PROGRESS = 1 << 1;
221
222    /**
223     * Flag: A timeout occurred while waiting for web kit to process this input event.
224     */
225    public static final int FLAG_WEBKIT_TIMEOUT = 1 << 2;
226
227    /**
228     * Flag: Indicates that the event was transformed for delivery to web kit.
229     * The event must be transformed back before being delivered to the UI.
230     */
231    public static final int FLAG_WEBKIT_TRANSFORMED_EVENT = 1 << 3;
232
233    public WebViewInputDispatcher(UiCallbacks uiCallbacks, WebKitCallbacks webKitCallbacks) {
234        this.mUiCallbacks = uiCallbacks;
235        mUiHandler = new UiHandler(uiCallbacks.getUiLooper());
236
237        this.mWebKitCallbacks = webKitCallbacks;
238        mWebKitHandler = new WebKitHandler(webKitCallbacks.getWebKitLooper());
239
240        ViewConfiguration config = ViewConfiguration.get(mUiCallbacks.getContext());
241        mDoubleTapSlopSquared = config.getScaledDoubleTapSlop();
242        mDoubleTapSlopSquared = (mDoubleTapSlopSquared * mDoubleTapSlopSquared);
243        mTouchSlopSquared = config.getScaledTouchSlop();
244        mTouchSlopSquared = (mTouchSlopSquared * mTouchSlopSquared);
245    }
246
247    /**
248     * Sets whether web kit wants to receive touch events.
249     *
250     * @param enable True to enable dispatching of touch events to web kit, otherwise
251     * web kit will be skipped.
252     */
253    public void setWebKitWantsTouchEvents(boolean enable) {
254        if (DEBUG) {
255            Log.d(TAG, "webkitWantsTouchEvents: " + enable);
256        }
257        synchronized (mLock) {
258            if (mPostSendTouchEventsToWebKit != enable) {
259                if (!enable) {
260                    enqueueWebKitCancelTouchEventIfNeededLocked();
261                }
262                mPostSendTouchEventsToWebKit = enable;
263            }
264        }
265    }
266
267    /**
268     * Posts a pointer event to the dispatch queue.
269     *
270     * @param event The event to post.
271     * @param webKitXOffset X offset to apply to events before dispatching them to web kit.
272     * @param webKitYOffset Y offset to apply to events before dispatching them to web kit.
273     * @param webKitScale The scale factor to apply to translated events before dispatching
274     * them to web kit.
275     * @return True if the dispatcher will handle the event, false if the event is unsupported.
276     */
277    public boolean postPointerEvent(MotionEvent event,
278            int webKitXOffset, int webKitYOffset, float webKitScale) {
279        if (event == null) {
280            throw new IllegalArgumentException("event cannot be null");
281        }
282
283        if (DEBUG) {
284            Log.d(TAG, "postPointerEvent: " + event);
285        }
286
287        final int action = event.getActionMasked();
288        final int eventType;
289        switch (action) {
290            case MotionEvent.ACTION_DOWN:
291            case MotionEvent.ACTION_MOVE:
292            case MotionEvent.ACTION_UP:
293            case MotionEvent.ACTION_POINTER_DOWN:
294            case MotionEvent.ACTION_POINTER_UP:
295            case MotionEvent.ACTION_CANCEL:
296                eventType = EVENT_TYPE_TOUCH;
297                break;
298            case MotionEvent.ACTION_SCROLL:
299                eventType = EVENT_TYPE_SCROLL;
300                break;
301            case MotionEvent.ACTION_HOVER_ENTER:
302            case MotionEvent.ACTION_HOVER_MOVE:
303            case MotionEvent.ACTION_HOVER_EXIT:
304                eventType = EVENT_TYPE_HOVER;
305                break;
306            default:
307                return false; // currently unsupported event type
308        }
309
310        synchronized (mLock) {
311            // Ensure that the event is consistent and should be delivered.
312            MotionEvent eventToEnqueue = event;
313            if (eventType == EVENT_TYPE_TOUCH) {
314                eventToEnqueue = mPostTouchStream.update(event);
315                if (eventToEnqueue == null) {
316                    if (DEBUG) {
317                        Log.d(TAG, "postPointerEvent: dropped event " + event);
318                    }
319                    unscheduleLongPressLocked();
320                    unscheduleClickLocked();
321                    hideTapCandidateLocked();
322                    return false;
323                }
324
325                if (action == MotionEvent.ACTION_DOWN && mPostSendTouchEventsToWebKit) {
326                    if (mUiCallbacks.shouldInterceptTouchEvent(eventToEnqueue)) {
327                        mPostDoNotSendTouchEventsToWebKitUntilNextGesture = true;
328                    } else if (mPostDoNotSendTouchEventsToWebKitUntilNextGesture) {
329                        // Recover from a previous web kit timeout.
330                        mPostDoNotSendTouchEventsToWebKitUntilNextGesture = false;
331                    }
332                }
333            }
334
335            // Copy the event because we need to retain ownership.
336            if (eventToEnqueue == event) {
337                eventToEnqueue = event.copy();
338            }
339
340            DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, eventType, 0,
341                    webKitXOffset, webKitYOffset, webKitScale);
342            updateStateTrackersLocked(d, event);
343            enqueueEventLocked(d);
344        }
345        return true;
346    }
347
348    private void scheduleLongPressLocked() {
349        unscheduleLongPressLocked();
350        mPostLongPressScheduled = true;
351        mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_LONG_PRESS,
352                LONG_PRESS_TIMEOUT);
353    }
354
355    private void unscheduleLongPressLocked() {
356        if (mPostLongPressScheduled) {
357            mPostLongPressScheduled = false;
358            mUiHandler.removeMessages(UiHandler.MSG_LONG_PRESS);
359        }
360    }
361
362    private void postLongPress() {
363        synchronized (mLock) {
364            if (!mPostLongPressScheduled) {
365                return;
366            }
367            mPostLongPressScheduled = false;
368
369            MotionEvent event = mPostTouchStream.getLastEvent();
370            if (event == null) {
371                return;
372            }
373
374            switch (event.getActionMasked()) {
375                case MotionEvent.ACTION_DOWN:
376                case MotionEvent.ACTION_MOVE:
377                case MotionEvent.ACTION_POINTER_DOWN:
378                case MotionEvent.ACTION_POINTER_UP:
379                    break;
380                default:
381                    return;
382            }
383
384            MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
385            eventToEnqueue.setAction(MotionEvent.ACTION_MOVE);
386            DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_LONG_PRESS, 0,
387                    mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale);
388            enqueueEventLocked(d);
389        }
390    }
391
392    private void hideTapCandidateLocked() {
393        unscheduleHideTapHighlightLocked();
394        unscheduleShowTapHighlightLocked();
395        mUiCallbacks.showTapHighlight(false);
396    }
397
398    private void showTapCandidateLocked() {
399        unscheduleHideTapHighlightLocked();
400        unscheduleShowTapHighlightLocked();
401        mUiCallbacks.showTapHighlight(true);
402    }
403
404    private void scheduleShowTapHighlightLocked() {
405        unscheduleShowTapHighlightLocked();
406        mPostShowTapHighlightScheduled = true;
407        mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_SHOW_TAP_HIGHLIGHT,
408                TAP_TIMEOUT);
409    }
410
411    private void unscheduleShowTapHighlightLocked() {
412        if (mPostShowTapHighlightScheduled) {
413            mPostShowTapHighlightScheduled = false;
414            mUiHandler.removeMessages(UiHandler.MSG_SHOW_TAP_HIGHLIGHT);
415        }
416    }
417
418    private void scheduleHideTapHighlightLocked() {
419        unscheduleHideTapHighlightLocked();
420        mPostHideTapHighlightScheduled = true;
421        mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_HIDE_TAP_HIGHLIGHT,
422                PRESSED_STATE_DURATION);
423    }
424
425    private void unscheduleHideTapHighlightLocked() {
426        if (mPostHideTapHighlightScheduled) {
427            mPostHideTapHighlightScheduled = false;
428            mUiHandler.removeMessages(UiHandler.MSG_HIDE_TAP_HIGHLIGHT);
429        }
430    }
431
432    private void postShowTapHighlight(boolean show) {
433        synchronized (mLock) {
434            if (show) {
435                if (!mPostShowTapHighlightScheduled) {
436                    return;
437                }
438                mPostShowTapHighlightScheduled = false;
439            } else {
440                if (!mPostHideTapHighlightScheduled) {
441                    return;
442                }
443                mPostHideTapHighlightScheduled = false;
444            }
445            mUiCallbacks.showTapHighlight(show);
446        }
447    }
448
449    private void scheduleClickLocked() {
450        unscheduleClickLocked();
451        mPostClickScheduled = true;
452        mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_CLICK, DOUBLE_TAP_TIMEOUT);
453    }
454
455    private void unscheduleClickLocked() {
456        if (mPostClickScheduled) {
457            mPostClickScheduled = false;
458            mUiHandler.removeMessages(UiHandler.MSG_CLICK);
459        }
460    }
461
462    private void postClick() {
463        synchronized (mLock) {
464            if (!mPostClickScheduled) {
465                return;
466            }
467            mPostClickScheduled = false;
468
469            MotionEvent event = mPostTouchStream.getLastEvent();
470            if (event == null || event.getAction() != MotionEvent.ACTION_UP) {
471                return;
472            }
473
474            showTapCandidateLocked();
475            MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
476            DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_CLICK, 0,
477                    mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale);
478            enqueueEventLocked(d);
479        }
480    }
481
482    private void checkForDoubleTapOnDownLocked(MotionEvent event) {
483        mIsDoubleTapCandidate = false;
484        if (!mPostClickScheduled) {
485            return;
486        }
487        int deltaX = (int) mInitialDownX - (int) event.getX();
488        int deltaY = (int) mInitialDownY - (int) event.getY();
489        if ((deltaX * deltaX + deltaY * deltaY) < mDoubleTapSlopSquared) {
490            unscheduleClickLocked();
491            mIsDoubleTapCandidate = true;
492        }
493    }
494
495    private boolean isClickCandidateLocked(MotionEvent event) {
496        if (event == null
497                || event.getActionMasked() != MotionEvent.ACTION_UP
498                || !mIsTapCandidate) {
499            return false;
500        }
501        long downDuration = event.getEventTime() - event.getDownTime();
502        return downDuration < LONG_PRESS_TIMEOUT;
503    }
504
505    private void enqueueDoubleTapLocked(MotionEvent event) {
506        MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
507        DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_DOUBLE_TAP, 0,
508                mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale);
509        enqueueEventLocked(d);
510    }
511
512    private void enqueueHitTestLocked(MotionEvent event) {
513        mUiCallbacks.clearPreviousHitTest();
514        MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
515        DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_HIT_TEST, 0,
516                mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale);
517        enqueueEventLocked(d);
518    }
519
520    private void checkForSlopLocked(MotionEvent event) {
521        if (!mIsTapCandidate) {
522            return;
523        }
524        int deltaX = (int) mInitialDownX - (int) event.getX();
525        int deltaY = (int) mInitialDownY - (int) event.getY();
526        if ((deltaX * deltaX + deltaY * deltaY) > mTouchSlopSquared) {
527            unscheduleLongPressLocked();
528            mIsTapCandidate = false;
529            hideTapCandidateLocked();
530        }
531    }
532
533    private void updateStateTrackersLocked(DispatchEvent d, MotionEvent event) {
534        mPostLastWebKitXOffset = d.mWebKitXOffset;
535        mPostLastWebKitYOffset = d.mWebKitYOffset;
536        mPostLastWebKitScale = d.mWebKitScale;
537        int action = event != null ? event.getAction() : MotionEvent.ACTION_CANCEL;
538        if (d.mEventType != EVENT_TYPE_TOUCH) {
539            return;
540        }
541
542        if (action == MotionEvent.ACTION_CANCEL
543                || event.getPointerCount() > 1) {
544            unscheduleLongPressLocked();
545            unscheduleClickLocked();
546            hideTapCandidateLocked();
547            mIsDoubleTapCandidate = false;
548            mIsTapCandidate = false;
549            hideTapCandidateLocked();
550        } else if (action == MotionEvent.ACTION_DOWN) {
551            checkForDoubleTapOnDownLocked(event);
552            scheduleLongPressLocked();
553            mIsTapCandidate = true;
554            mInitialDownX = event.getX();
555            mInitialDownY = event.getY();
556            enqueueHitTestLocked(event);
557            if (mIsDoubleTapCandidate) {
558                hideTapCandidateLocked();
559            } else {
560                scheduleShowTapHighlightLocked();
561            }
562        } else if (action == MotionEvent.ACTION_UP) {
563            unscheduleLongPressLocked();
564            if (isClickCandidateLocked(event)) {
565                if (mIsDoubleTapCandidate) {
566                    hideTapCandidateLocked();
567                    enqueueDoubleTapLocked(event);
568                } else {
569                    scheduleClickLocked();
570                }
571            } else {
572                hideTapCandidateLocked();
573            }
574        } else if (action == MotionEvent.ACTION_MOVE) {
575            checkForSlopLocked(event);
576        }
577    }
578
579    /**
580     * Dispatches pending web kit events.
581     * Must only be called from the web kit thread.
582     *
583     * This method may be used to flush the queue of pending input events
584     * immediately.  This method may help to reduce input dispatch latency
585     * if called before certain expensive operations such as drawing.
586     */
587    public void dispatchWebKitEvents() {
588        dispatchWebKitEvents(false);
589    }
590
591    private void dispatchWebKitEvents(boolean calledFromHandler) {
592        for (;;) {
593            // Get the next event, but leave it in the queue so we can move it to the UI
594            // queue if a timeout occurs.
595            DispatchEvent d;
596            MotionEvent event;
597            final int eventType;
598            int flags;
599            synchronized (mLock) {
600                if (!ENABLE_EVENT_BATCHING) {
601                    drainStaleWebKitEventsLocked();
602                }
603                d = mWebKitDispatchEventQueue.mHead;
604                if (d == null) {
605                    if (mWebKitDispatchScheduled) {
606                        mWebKitDispatchScheduled = false;
607                        if (!calledFromHandler) {
608                            mWebKitHandler.removeMessages(
609                                    WebKitHandler.MSG_DISPATCH_WEBKIT_EVENTS);
610                        }
611                    }
612                    return;
613                }
614
615                event = d.mEvent;
616                if (event != null) {
617                    event.offsetLocation(d.mWebKitXOffset, d.mWebKitYOffset);
618                    event.scale(d.mWebKitScale);
619                    d.mFlags |= FLAG_WEBKIT_TRANSFORMED_EVENT;
620                }
621
622                eventType = d.mEventType;
623                if (eventType == EVENT_TYPE_TOUCH) {
624                    event = mWebKitTouchStream.update(event);
625                    if (DEBUG && event == null && d.mEvent != null) {
626                        Log.d(TAG, "dispatchWebKitEvents: dropped event " + d.mEvent);
627                    }
628                }
629
630                d.mFlags |= FLAG_WEBKIT_IN_PROGRESS;
631                flags = d.mFlags;
632            }
633
634            // Handle the event.
635            final boolean preventDefault;
636            if (event == null) {
637                preventDefault = false;
638            } else {
639                preventDefault = dispatchWebKitEvent(event, eventType, flags);
640            }
641
642            synchronized (mLock) {
643                flags = d.mFlags;
644                d.mFlags = flags & ~FLAG_WEBKIT_IN_PROGRESS;
645                boolean recycleEvent = event != d.mEvent;
646
647                if ((flags & FLAG_WEBKIT_TIMEOUT) != 0) {
648                    // A timeout occurred!
649                    recycleDispatchEventLocked(d);
650                } else {
651                    // Web kit finished in a timely manner.  Dequeue the event.
652                    assert mWebKitDispatchEventQueue.mHead == d;
653                    mWebKitDispatchEventQueue.dequeue();
654
655                    updateWebKitTimeoutLocked();
656
657                    if ((flags & FLAG_PRIVATE) != 0) {
658                        // Event was intended for web kit only.  All done.
659                        recycleDispatchEventLocked(d);
660                    } else if (preventDefault) {
661                        // Web kit has decided to consume the event!
662                        if (d.mEventType == EVENT_TYPE_TOUCH) {
663                            enqueueUiCancelTouchEventIfNeededLocked();
664                            unscheduleLongPressLocked();
665                        }
666                    } else {
667                        // Web kit is being friendly.  Pass the event to the UI.
668                        enqueueUiEventUnbatchedLocked(d);
669                    }
670                }
671
672                if (event != null && recycleEvent) {
673                    event.recycle();
674                }
675
676                if (eventType == EVENT_TYPE_CLICK) {
677                    scheduleHideTapHighlightLocked();
678                }
679            }
680        }
681    }
682
683    // Runs on web kit thread.
684    private boolean dispatchWebKitEvent(MotionEvent event, int eventType, int flags) {
685        if (DEBUG) {
686            Log.d(TAG, "dispatchWebKitEvent: event=" + event
687                    + ", eventType=" + eventType + ", flags=" + flags);
688        }
689        boolean preventDefault = mWebKitCallbacks.dispatchWebKitEvent(
690                this, event, eventType, flags);
691        if (DEBUG) {
692            Log.d(TAG, "dispatchWebKitEvent: preventDefault=" + preventDefault);
693        }
694        return preventDefault;
695    }
696
697    private boolean isMoveEventLocked(DispatchEvent d) {
698        return d.mEvent != null
699                && d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE;
700    }
701
702    private void drainStaleWebKitEventsLocked() {
703        DispatchEvent d = mWebKitDispatchEventQueue.mHead;
704        while (d != null && d.mNext != null
705                && isMoveEventLocked(d)
706                && isMoveEventLocked(d.mNext)) {
707            DispatchEvent next = d.mNext;
708            skipWebKitEventLocked(d);
709            d = next;
710        }
711        mWebKitDispatchEventQueue.mHead = d;
712    }
713
714    // Called by WebKit when it doesn't care about the rest of the touch stream
715    public void skipWebkitForRemainingTouchStream() {
716        // Just treat this like a timeout
717        handleWebKitTimeout();
718    }
719
720    // Runs on UI thread in response to the web kit thread appearing to be unresponsive.
721    private void handleWebKitTimeout() {
722        synchronized (mLock) {
723            if (!mWebKitTimeoutScheduled) {
724                return;
725            }
726            mWebKitTimeoutScheduled = false;
727
728            if (DEBUG) {
729                Log.d(TAG, "handleWebKitTimeout: timeout occurred!");
730            }
731
732            // Drain the web kit event queue.
733            DispatchEvent d = mWebKitDispatchEventQueue.dequeueList();
734
735            // If web kit was processing an event (must be at the head of the list because
736            // it can only do one at a time), then clone it or ignore it.
737            if ((d.mFlags & FLAG_WEBKIT_IN_PROGRESS) != 0) {
738                d.mFlags |= FLAG_WEBKIT_TIMEOUT;
739                if ((d.mFlags & FLAG_PRIVATE) != 0) {
740                    d = d.mNext; // the event is private to web kit, ignore it
741                } else {
742                    d = copyDispatchEventLocked(d);
743                    d.mFlags &= ~FLAG_WEBKIT_IN_PROGRESS;
744                }
745            }
746
747            // Enqueue all non-private events for handling by the UI thread.
748            while (d != null) {
749                DispatchEvent next = d.mNext;
750                skipWebKitEventLocked(d);
751                d = next;
752            }
753
754            // Tell web kit to cancel all pending touches.
755            // This also prevents us from sending web kit any more touches until the
756            // next gesture begins.  (As required to ensure touch event stream consistency.)
757            enqueueWebKitCancelTouchEventIfNeededLocked();
758        }
759    }
760
761    private void skipWebKitEventLocked(DispatchEvent d) {
762        d.mNext = null;
763        if ((d.mFlags & FLAG_PRIVATE) != 0) {
764            recycleDispatchEventLocked(d);
765        } else {
766            d.mFlags |= FLAG_WEBKIT_TIMEOUT;
767            enqueueUiEventUnbatchedLocked(d);
768        }
769    }
770
771    /**
772     * Dispatches pending UI events.
773     * Must only be called from the UI thread.
774     *
775     * This method may be used to flush the queue of pending input events
776     * immediately.  This method may help to reduce input dispatch latency
777     * if called before certain expensive operations such as drawing.
778     */
779    public void dispatchUiEvents() {
780        dispatchUiEvents(false);
781    }
782
783    private void dispatchUiEvents(boolean calledFromHandler) {
784        for (;;) {
785            MotionEvent event;
786            final int eventType;
787            final int flags;
788            synchronized (mLock) {
789                DispatchEvent d = mUiDispatchEventQueue.dequeue();
790                if (d == null) {
791                    if (mUiDispatchScheduled) {
792                        mUiDispatchScheduled = false;
793                        if (!calledFromHandler) {
794                            mUiHandler.removeMessages(UiHandler.MSG_DISPATCH_UI_EVENTS);
795                        }
796                    }
797                    return;
798                }
799
800                event = d.mEvent;
801                if (event != null && (d.mFlags & FLAG_WEBKIT_TRANSFORMED_EVENT) != 0) {
802                    event.scale(1.0f / d.mWebKitScale);
803                    event.offsetLocation(-d.mWebKitXOffset, -d.mWebKitYOffset);
804                    d.mFlags &= ~FLAG_WEBKIT_TRANSFORMED_EVENT;
805                }
806
807                eventType = d.mEventType;
808                if (eventType == EVENT_TYPE_TOUCH) {
809                    event = mUiTouchStream.update(event);
810                    if (DEBUG && event == null && d.mEvent != null) {
811                        Log.d(TAG, "dispatchUiEvents: dropped event " + d.mEvent);
812                    }
813                }
814
815                flags = d.mFlags;
816
817                if (event == d.mEvent) {
818                    d.mEvent = null; // retain ownership of event, don't recycle it yet
819                }
820                recycleDispatchEventLocked(d);
821
822                if (eventType == EVENT_TYPE_CLICK) {
823                    scheduleHideTapHighlightLocked();
824                }
825            }
826
827            // Handle the event.
828            if (event != null) {
829                dispatchUiEvent(event, eventType, flags);
830                event.recycle();
831            }
832        }
833    }
834
835    // Runs on UI thread.
836    private void dispatchUiEvent(MotionEvent event, int eventType, int flags) {
837        if (DEBUG) {
838            Log.d(TAG, "dispatchUiEvent: event=" + event
839                    + ", eventType=" + eventType + ", flags=" + flags);
840        }
841        mUiCallbacks.dispatchUiEvent(event, eventType, flags);
842    }
843
844    private void enqueueEventLocked(DispatchEvent d) {
845        if (!shouldSkipWebKit(d)) {
846            enqueueWebKitEventLocked(d);
847        } else {
848            enqueueUiEventLocked(d);
849        }
850    }
851
852    private boolean shouldSkipWebKit(DispatchEvent d) {
853        switch (d.mEventType) {
854            case EVENT_TYPE_CLICK:
855            case EVENT_TYPE_HOVER:
856            case EVENT_TYPE_SCROLL:
857            case EVENT_TYPE_HIT_TEST:
858                return false;
859            case EVENT_TYPE_TOUCH:
860                // TODO: This should be cleaned up. We now have WebViewInputDispatcher
861                // and WebViewClassic both checking for slop and doing their own
862                // thing - they should be consolidated. And by consolidated, I mean
863                // WebViewClassic's version should just be deleted.
864                // The reason this is done is because webpages seem to expect
865                // that they only get an ontouchmove if the slop has been exceeded.
866                if (mIsTapCandidate && d.mEvent != null
867                        && d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE) {
868                    return true;
869                }
870                return !mPostSendTouchEventsToWebKit
871                        || mPostDoNotSendTouchEventsToWebKitUntilNextGesture;
872        }
873        return true;
874    }
875
876    private void enqueueWebKitCancelTouchEventIfNeededLocked() {
877        // We want to cancel touch events that were delivered to web kit.
878        // Enqueue a null event at the end of the queue if needed.
879        if (mWebKitTouchStream.isCancelNeeded() || !mWebKitDispatchEventQueue.isEmpty()) {
880            DispatchEvent d = obtainDispatchEventLocked(null, EVENT_TYPE_TOUCH, FLAG_PRIVATE,
881                    0, 0, 1.0f);
882            enqueueWebKitEventUnbatchedLocked(d);
883            mPostDoNotSendTouchEventsToWebKitUntilNextGesture = true;
884        }
885    }
886
887    private void enqueueWebKitEventLocked(DispatchEvent d) {
888        if (batchEventLocked(d, mWebKitDispatchEventQueue.mTail)) {
889            if (DEBUG) {
890                Log.d(TAG, "enqueueWebKitEventLocked: batched event " + d.mEvent);
891            }
892            recycleDispatchEventLocked(d);
893        } else {
894            enqueueWebKitEventUnbatchedLocked(d);
895        }
896    }
897
898    private void enqueueWebKitEventUnbatchedLocked(DispatchEvent d) {
899        if (DEBUG) {
900            Log.d(TAG, "enqueueWebKitEventUnbatchedLocked: enqueued event " + d.mEvent);
901        }
902        mWebKitDispatchEventQueue.enqueue(d);
903        scheduleWebKitDispatchLocked();
904        updateWebKitTimeoutLocked();
905    }
906
907    private void scheduleWebKitDispatchLocked() {
908        if (!mWebKitDispatchScheduled) {
909            mWebKitHandler.sendEmptyMessage(WebKitHandler.MSG_DISPATCH_WEBKIT_EVENTS);
910            mWebKitDispatchScheduled = true;
911        }
912    }
913
914    private void updateWebKitTimeoutLocked() {
915        DispatchEvent d = mWebKitDispatchEventQueue.mHead;
916        if (d != null && mWebKitTimeoutScheduled && mWebKitTimeoutTime == d.mTimeoutTime) {
917            return;
918        }
919        if (mWebKitTimeoutScheduled) {
920            mUiHandler.removeMessages(UiHandler.MSG_WEBKIT_TIMEOUT);
921            mWebKitTimeoutScheduled = false;
922        }
923        if (d != null) {
924            mUiHandler.sendEmptyMessageAtTime(UiHandler.MSG_WEBKIT_TIMEOUT, d.mTimeoutTime);
925            mWebKitTimeoutScheduled = true;
926            mWebKitTimeoutTime = d.mTimeoutTime;
927        }
928    }
929
930    private void enqueueUiCancelTouchEventIfNeededLocked() {
931        // We want to cancel touch events that were delivered to the UI.
932        // Enqueue a null event at the end of the queue if needed.
933        if (mUiTouchStream.isCancelNeeded() || !mUiDispatchEventQueue.isEmpty()) {
934            DispatchEvent d = obtainDispatchEventLocked(null, EVENT_TYPE_TOUCH, FLAG_PRIVATE,
935                    0, 0, 1.0f);
936            enqueueUiEventUnbatchedLocked(d);
937        }
938    }
939
940    private void enqueueUiEventLocked(DispatchEvent d) {
941        if (batchEventLocked(d, mUiDispatchEventQueue.mTail)) {
942            if (DEBUG) {
943                Log.d(TAG, "enqueueUiEventLocked: batched event " + d.mEvent);
944            }
945            recycleDispatchEventLocked(d);
946        } else {
947            enqueueUiEventUnbatchedLocked(d);
948        }
949    }
950
951    private void enqueueUiEventUnbatchedLocked(DispatchEvent d) {
952        if (DEBUG) {
953            Log.d(TAG, "enqueueUiEventUnbatchedLocked: enqueued event " + d.mEvent);
954        }
955        mUiDispatchEventQueue.enqueue(d);
956        scheduleUiDispatchLocked();
957    }
958
959    private void scheduleUiDispatchLocked() {
960        if (!mUiDispatchScheduled) {
961            mUiHandler.sendEmptyMessage(UiHandler.MSG_DISPATCH_UI_EVENTS);
962            mUiDispatchScheduled = true;
963        }
964    }
965
966    private boolean batchEventLocked(DispatchEvent in, DispatchEvent tail) {
967        if (!ENABLE_EVENT_BATCHING) {
968            return false;
969        }
970        if (tail != null && tail.mEvent != null && in.mEvent != null
971                && in.mEventType == tail.mEventType
972                && in.mFlags == tail.mFlags
973                && in.mWebKitXOffset == tail.mWebKitXOffset
974                && in.mWebKitYOffset == tail.mWebKitYOffset
975                && in.mWebKitScale == tail.mWebKitScale) {
976            return tail.mEvent.addBatch(in.mEvent);
977        }
978        return false;
979    }
980
981    private DispatchEvent obtainDispatchEventLocked(MotionEvent event,
982            int eventType, int flags, int webKitXOffset, int webKitYOffset, float webKitScale) {
983        DispatchEvent d = obtainUninitializedDispatchEventLocked();
984        d.mEvent = event;
985        d.mEventType = eventType;
986        d.mFlags = flags;
987        d.mTimeoutTime = SystemClock.uptimeMillis() + WEBKIT_TIMEOUT_MILLIS;
988        d.mWebKitXOffset = webKitXOffset;
989        d.mWebKitYOffset = webKitYOffset;
990        d.mWebKitScale = webKitScale;
991        if (DEBUG) {
992            Log.d(TAG, "Timeout time: " + (d.mTimeoutTime - SystemClock.uptimeMillis()));
993        }
994        return d;
995    }
996
997    private DispatchEvent copyDispatchEventLocked(DispatchEvent d) {
998        DispatchEvent copy = obtainUninitializedDispatchEventLocked();
999        if (d.mEvent != null) {
1000            copy.mEvent = d.mEvent.copy();
1001        }
1002        copy.mEventType = d.mEventType;
1003        copy.mFlags = d.mFlags;
1004        copy.mTimeoutTime = d.mTimeoutTime;
1005        copy.mWebKitXOffset = d.mWebKitXOffset;
1006        copy.mWebKitYOffset = d.mWebKitYOffset;
1007        copy.mWebKitScale = d.mWebKitScale;
1008        copy.mNext = d.mNext;
1009        return copy;
1010    }
1011
1012    private DispatchEvent obtainUninitializedDispatchEventLocked() {
1013        DispatchEvent d = mDispatchEventPool;
1014        if (d != null) {
1015            mDispatchEventPoolSize -= 1;
1016            mDispatchEventPool = d.mNext;
1017            d.mNext = null;
1018        } else {
1019            d = new DispatchEvent();
1020        }
1021        return d;
1022    }
1023
1024    private void recycleDispatchEventLocked(DispatchEvent d) {
1025        if (d.mEvent != null) {
1026            d.mEvent.recycle();
1027            d.mEvent = null;
1028        }
1029
1030        if (mDispatchEventPoolSize < MAX_DISPATCH_EVENT_POOL_SIZE) {
1031            mDispatchEventPoolSize += 1;
1032            d.mNext = mDispatchEventPool;
1033            mDispatchEventPool = d;
1034        }
1035    }
1036
1037    /* Implemented by {@link WebViewClassic} to perform operations on the UI thread. */
1038    public static interface UiCallbacks {
1039        /**
1040         * Gets the UI thread's looper.
1041         * @return The looper.
1042         */
1043        public Looper getUiLooper();
1044
1045        /**
1046         * Gets the UI's context
1047         * @return The context
1048         */
1049        public Context getContext();
1050
1051        /**
1052         * Dispatches an event to the UI.
1053         * @param event The event.
1054         * @param eventType The event type.
1055         * @param flags The event's dispatch flags.
1056         */
1057        public void dispatchUiEvent(MotionEvent event, int eventType, int flags);
1058
1059        /**
1060         * Asks the UI thread whether this touch event stream should be
1061         * intercepted based on the touch down event.
1062         * @param event The touch down event.
1063         * @return true if the UI stream wants the touch stream without going
1064         * through webkit or false otherwise.
1065         */
1066        public boolean shouldInterceptTouchEvent(MotionEvent event);
1067
1068        /**
1069         * Inform's the UI that it should show the tap highlight
1070         * @param show True if it should show the highlight, false if it should hide it
1071         */
1072        public void showTapHighlight(boolean show);
1073
1074        /**
1075         * Called when we are sending a new EVENT_TYPE_HIT_TEST to WebKit, so
1076         * previous hit tests should be cleared as they are obsolete.
1077         */
1078        public void clearPreviousHitTest();
1079    }
1080
1081    /* Implemented by {@link WebViewCore} to perform operations on the web kit thread. */
1082    public static interface WebKitCallbacks {
1083        /**
1084         * Gets the web kit thread's looper.
1085         * @return The looper.
1086         */
1087        public Looper getWebKitLooper();
1088
1089        /**
1090         * Dispatches an event to web kit.
1091         * @param dispatcher The WebViewInputDispatcher sending the event
1092         * @param event The event.
1093         * @param eventType The event type.
1094         * @param flags The event's dispatch flags.
1095         * @return True if web kit wants to prevent default event handling.
1096         */
1097        public boolean dispatchWebKitEvent(WebViewInputDispatcher dispatcher,
1098                MotionEvent event, int eventType, int flags);
1099    }
1100
1101    // Runs on UI thread.
1102    private final class UiHandler extends Handler {
1103        public static final int MSG_DISPATCH_UI_EVENTS = 1;
1104        public static final int MSG_WEBKIT_TIMEOUT = 2;
1105        public static final int MSG_LONG_PRESS = 3;
1106        public static final int MSG_CLICK = 4;
1107        public static final int MSG_SHOW_TAP_HIGHLIGHT = 5;
1108        public static final int MSG_HIDE_TAP_HIGHLIGHT = 6;
1109
1110        public UiHandler(Looper looper) {
1111            super(looper);
1112        }
1113
1114        @Override
1115        public void handleMessage(Message msg) {
1116            switch (msg.what) {
1117                case MSG_DISPATCH_UI_EVENTS:
1118                    dispatchUiEvents(true);
1119                    break;
1120                case MSG_WEBKIT_TIMEOUT:
1121                    handleWebKitTimeout();
1122                    break;
1123                case MSG_LONG_PRESS:
1124                    postLongPress();
1125                    break;
1126                case MSG_CLICK:
1127                    postClick();
1128                    break;
1129                case MSG_SHOW_TAP_HIGHLIGHT:
1130                    postShowTapHighlight(true);
1131                    break;
1132                case MSG_HIDE_TAP_HIGHLIGHT:
1133                    postShowTapHighlight(false);
1134                    break;
1135                default:
1136                    throw new IllegalStateException("Unknown message type: " + msg.what);
1137            }
1138        }
1139    }
1140
1141    // Runs on web kit thread.
1142    private final class WebKitHandler extends Handler {
1143        public static final int MSG_DISPATCH_WEBKIT_EVENTS = 1;
1144
1145        public WebKitHandler(Looper looper) {
1146            super(looper);
1147        }
1148
1149        @Override
1150        public void handleMessage(Message msg) {
1151            switch (msg.what) {
1152                case MSG_DISPATCH_WEBKIT_EVENTS:
1153                    dispatchWebKitEvents(true);
1154                    break;
1155                default:
1156                    throw new IllegalStateException("Unknown message type: " + msg.what);
1157            }
1158        }
1159    }
1160
1161    private static final class DispatchEvent {
1162        public DispatchEvent mNext;
1163
1164        public MotionEvent mEvent;
1165        public int mEventType;
1166        public int mFlags;
1167        public long mTimeoutTime;
1168        public int mWebKitXOffset;
1169        public int mWebKitYOffset;
1170        public float mWebKitScale;
1171    }
1172
1173    private static final class DispatchEventQueue {
1174        public DispatchEvent mHead;
1175        public DispatchEvent mTail;
1176
1177        public boolean isEmpty() {
1178            return mHead != null;
1179        }
1180
1181        public void enqueue(DispatchEvent d) {
1182            if (mHead == null) {
1183                mHead = d;
1184                mTail = d;
1185            } else {
1186                mTail.mNext = d;
1187                mTail = d;
1188            }
1189        }
1190
1191        public DispatchEvent dequeue() {
1192            DispatchEvent d = mHead;
1193            if (d != null) {
1194                DispatchEvent next = d.mNext;
1195                if (next == null) {
1196                    mHead = null;
1197                    mTail = null;
1198                } else {
1199                    mHead = next;
1200                    d.mNext = null;
1201                }
1202            }
1203            return d;
1204        }
1205
1206        public DispatchEvent dequeueList() {
1207            DispatchEvent d = mHead;
1208            if (d != null) {
1209                mHead = null;
1210                mTail = null;
1211            }
1212            return d;
1213        }
1214    }
1215
1216    /**
1217     * Keeps track of a stream of touch events so that we can discard touch
1218     * events that would make the stream inconsistent.
1219     */
1220    private static final class TouchStream {
1221        private MotionEvent mLastEvent;
1222
1223        /**
1224         * Gets the last touch event that was delivered.
1225         * @return The last touch event, or null if none.
1226         */
1227        public MotionEvent getLastEvent() {
1228            return mLastEvent;
1229        }
1230
1231        /**
1232         * Updates the touch event stream.
1233         * @param event The event that we intend to send, or null to cancel the
1234         * touch event stream.
1235         * @return The event that we should actually send, or null if no event should
1236         * be sent because the proposed event would make the stream inconsistent.
1237         */
1238        public MotionEvent update(MotionEvent event) {
1239            if (event == null) {
1240                if (isCancelNeeded()) {
1241                    event = mLastEvent;
1242                    if (event != null) {
1243                        event.setAction(MotionEvent.ACTION_CANCEL);
1244                        mLastEvent = null;
1245                    }
1246                }
1247                return event;
1248            }
1249
1250            switch (event.getActionMasked()) {
1251                case MotionEvent.ACTION_MOVE:
1252                case MotionEvent.ACTION_UP:
1253                case MotionEvent.ACTION_POINTER_DOWN:
1254                case MotionEvent.ACTION_POINTER_UP:
1255                    if (mLastEvent == null
1256                            || mLastEvent.getAction() == MotionEvent.ACTION_UP) {
1257                        return null;
1258                    }
1259                    updateLastEvent(event);
1260                    return event;
1261
1262                case MotionEvent.ACTION_DOWN:
1263                    updateLastEvent(event);
1264                    return event;
1265
1266                case MotionEvent.ACTION_CANCEL:
1267                    if (mLastEvent == null) {
1268                        return null;
1269                    }
1270                    updateLastEvent(null);
1271                    return event;
1272
1273                default:
1274                    return null;
1275            }
1276        }
1277
1278        /**
1279         * Returns true if there is a gesture in progress that may need to be canceled.
1280         * @return True if cancel is needed.
1281         */
1282        public boolean isCancelNeeded() {
1283            return mLastEvent != null && mLastEvent.getAction() != MotionEvent.ACTION_UP;
1284        }
1285
1286        private void updateLastEvent(MotionEvent event) {
1287            if (mLastEvent != null) {
1288                mLastEvent.recycle();
1289            }
1290            mLastEvent = event != null ? MotionEvent.obtainNoHistory(event) : null;
1291        }
1292    }
1293}