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