blob: f64547f255dbaedac2c55c6b445cbfbd45cc1e6f [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.webkit;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
/**
* Perform asynchronous dispatch of input events in a {@link WebView}.
*
* This dispatcher is shared by the UI thread ({@link WebViewClassic}) and web kit
* thread ({@link WebViewCore}). The UI thread enqueues events for
* processing, waits for the web kit thread to handle them, and then performs
* additional processing depending on the outcome.
*
* How it works:
*
* 1. The web view thread receives an input event from the input system on the UI
* thread in its {@link WebViewClassic#onTouchEvent} handler. It sends the input event
* to the dispatcher, then immediately returns true to the input system to indicate that
* it will handle the event.
*
* 2. The web kit thread is notified that an event has been enqueued. Meanwhile additional
* events may be enqueued from the UI thread. In some cases, the dispatcher may decide to
* coalesce motion events into larger batches or to cancel events that have been
* sitting in the queue for too long.
*
* 3. The web kit thread wakes up and handles all input events that are waiting for it.
* After processing each input event, it informs the dispatcher whether the web application
* has decided to handle the event itself and to prevent default event handling.
*
* 4. If web kit indicates that it wants to prevent default event handling, then web kit
* consumes the remainder of the gesture and web view receives a cancel event if
* needed. Otherwise, the web view handles the gesture on the UI thread normally.
*
* 5. If the web kit thread takes too long to handle an input event, then it loses the
* right to handle it. The dispatcher synthesizes a cancellation event for web kit and
* then tells the web view on the UI thread to handle the event that timed out along
* with the rest of the gesture.
*
* One thing to keep in mind about the dispatcher is that what goes into the dispatcher
* is not necessarily what the web kit or UI thread will see. As mentioned above, the
* dispatcher may tweak the input event stream to improve responsiveness. Both web view and
* web kit are guaranteed to perceive a consistent stream of input events but
* they might not always see the same events (especially if one decides
* to prevent the other from handling a particular gesture).
*
* This implementation very deliberately does not refer to the {@link WebViewClassic}
* or {@link WebViewCore} classes, preferring to communicate with them only via
* interfaces to avoid unintentional coupling to their implementation details.
*
* Currently, the input dispatcher only handles pointer events (includes touch,
* hover and scroll events). In principle, it could be extended to handle trackball
* and key events if needed.
*
* @hide
*/
final class WebViewInputDispatcher {
private static final String TAG = "WebViewInputDispatcher";
private static final boolean DEBUG = false;
// This enables batching of MotionEvents. It will combine multiple MotionEvents
// together into a single MotionEvent if more events come in while we are
// still waiting on the processing of a previous event.
// If this is set to false, we will instead opt to drop ACTION_MOVE
// events we cannot keep up with.
// TODO: If batching proves to be working well, remove this
private static final boolean ENABLE_EVENT_BATCHING = true;
private final Object mLock = new Object();
// Pool of queued input events. (guarded by mLock)
private static final int MAX_DISPATCH_EVENT_POOL_SIZE = 10;
private DispatchEvent mDispatchEventPool;
private int mDispatchEventPoolSize;
// Posted state, tracks events posted to the dispatcher. (guarded by mLock)
private final TouchStream mPostTouchStream = new TouchStream();
private boolean mPostSendTouchEventsToWebKit;
private boolean mPostDoNotSendTouchEventsToWebKitUntilNextGesture;
private boolean mPostLongPressScheduled;
private boolean mPostClickScheduled;
private boolean mPostShowTapHighlightScheduled;
private boolean mPostHideTapHighlightScheduled;
private int mPostLastWebKitXOffset;
private int mPostLastWebKitYOffset;
private float mPostLastWebKitScale;
// State for event tracking (click, longpress, double tap, etc..)
private boolean mIsDoubleTapCandidate;
private boolean mIsTapCandidate;
private float mInitialDownX;
private float mInitialDownY;
private float mTouchSlopSquared;
private float mDoubleTapSlopSquared;
// Web kit state, tracks events observed by web kit. (guarded by mLock)
private final DispatchEventQueue mWebKitDispatchEventQueue = new DispatchEventQueue();
private final TouchStream mWebKitTouchStream = new TouchStream();
private final WebKitCallbacks mWebKitCallbacks;
private final WebKitHandler mWebKitHandler;
private boolean mWebKitDispatchScheduled;
private boolean mWebKitTimeoutScheduled;
private long mWebKitTimeoutTime;
// UI state, tracks events observed by the UI. (guarded by mLock)
private final DispatchEventQueue mUiDispatchEventQueue = new DispatchEventQueue();
private final TouchStream mUiTouchStream = new TouchStream();
private final UiCallbacks mUiCallbacks;
private final UiHandler mUiHandler;
private boolean mUiDispatchScheduled;
// Give up on web kit handling of input events when this timeout expires.
private static final long WEBKIT_TIMEOUT_MILLIS = 200;
private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
private static final int LONG_PRESS_TIMEOUT =
ViewConfiguration.getLongPressTimeout() + TAP_TIMEOUT;
private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
private static final int PRESSED_STATE_DURATION = ViewConfiguration.getPressedStateDuration();
/**
* Event type: Indicates a touch event type.
*
* This event is delivered together with a {@link MotionEvent} with one of the
* following actions: {@link MotionEvent#ACTION_DOWN}, {@link MotionEvent#ACTION_MOVE},
* {@link MotionEvent#ACTION_UP}, {@link MotionEvent#ACTION_POINTER_DOWN},
* {@link MotionEvent#ACTION_POINTER_UP}, {@link MotionEvent#ACTION_CANCEL}.
*/
public static final int EVENT_TYPE_TOUCH = 0;
/**
* Event type: Indicates a hover event type.
*
* This event is delivered together with a {@link MotionEvent} with one of the
* following actions: {@link MotionEvent#ACTION_HOVER_ENTER},
* {@link MotionEvent#ACTION_HOVER_MOVE}, {@link MotionEvent#ACTION_HOVER_MOVE}.
*/
public static final int EVENT_TYPE_HOVER = 1;
/**
* Event type: Indicates a scroll event type.
*
* This event is delivered together with a {@link MotionEvent} with action
* {@link MotionEvent#ACTION_SCROLL}.
*/
public static final int EVENT_TYPE_SCROLL = 2;
/**
* Event type: Indicates a long-press event type.
*
* This event is delivered in the middle of a sequence of {@link #EVENT_TYPE_TOUCH} events.
* It includes a {@link MotionEvent} with action {@link MotionEvent#ACTION_MOVE}
* that indicates the current touch coordinates of the long-press.
*
* This event is sent when the current touch gesture has been held longer than
* the long-press interval.
*/
public static final int EVENT_TYPE_LONG_PRESS = 3;
/**
* Event type: Indicates a click event type.
*
* This event is delivered after a sequence of {@link #EVENT_TYPE_TOUCH} events that
* comprise a complete gesture ending with {@link MotionEvent#ACTION_UP}.
* It includes a {@link MotionEvent} with action {@link MotionEvent#ACTION_UP}
* that indicates the location of the click.
*
* This event is sent shortly after the end of a touch after the double-tap
* interval has expired to indicate a click.
*/
public static final int EVENT_TYPE_CLICK = 4;
/**
* Event type: Indicates a double-tap event type.
*
* This event is delivered after a sequence of {@link #EVENT_TYPE_TOUCH} events that
* comprise a complete gesture ending with {@link MotionEvent#ACTION_UP}.
* It includes a {@link MotionEvent} with action {@link MotionEvent#ACTION_UP}
* that indicates the location of the double-tap.
*
* This event is sent immediately after a sequence of two touches separated
* in time by no more than the double-tap interval and separated in space
* by no more than the double-tap slop.
*/
public static final int EVENT_TYPE_DOUBLE_TAP = 5;
/**
* Event type: Indicates that a hit test should be performed
*/
public static final int EVENT_TYPE_HIT_TEST = 6;
/**
* Flag: This event is private to this queue. Do not forward it.
*/
public static final int FLAG_PRIVATE = 1 << 0;
/**
* Flag: This event is currently being processed by web kit.
* If a timeout occurs, make a copy of it before forwarding the event to another queue.
*/
public static final int FLAG_WEBKIT_IN_PROGRESS = 1 << 1;
/**
* Flag: A timeout occurred while waiting for web kit to process this input event.
*/
public static final int FLAG_WEBKIT_TIMEOUT = 1 << 2;
/**
* Flag: Indicates that the event was transformed for delivery to web kit.
* The event must be transformed back before being delivered to the UI.
*/
public static final int FLAG_WEBKIT_TRANSFORMED_EVENT = 1 << 3;
public WebViewInputDispatcher(UiCallbacks uiCallbacks, WebKitCallbacks webKitCallbacks) {
this.mUiCallbacks = uiCallbacks;
mUiHandler = new UiHandler(uiCallbacks.getUiLooper());
this.mWebKitCallbacks = webKitCallbacks;
mWebKitHandler = new WebKitHandler(webKitCallbacks.getWebKitLooper());
ViewConfiguration config = ViewConfiguration.get(mUiCallbacks.getContext());
mDoubleTapSlopSquared = config.getScaledDoubleTapSlop();
mDoubleTapSlopSquared = (mDoubleTapSlopSquared * mDoubleTapSlopSquared);
mTouchSlopSquared = config.getScaledTouchSlop();
mTouchSlopSquared = (mTouchSlopSquared * mTouchSlopSquared);
}
/**
* Sets whether web kit wants to receive touch events.
*
* @param enable True to enable dispatching of touch events to web kit, otherwise
* web kit will be skipped.
*/
public void setWebKitWantsTouchEvents(boolean enable) {
if (DEBUG) {
Log.d(TAG, "webkitWantsTouchEvents: " + enable);
}
synchronized (mLock) {
if (mPostSendTouchEventsToWebKit != enable) {
if (!enable) {
enqueueWebKitCancelTouchEventIfNeededLocked();
}
mPostSendTouchEventsToWebKit = enable;
}
}
}
/**
* Posts a pointer event to the dispatch queue.
*
* @param event The event to post.
* @param webKitXOffset X offset to apply to events before dispatching them to web kit.
* @param webKitYOffset Y offset to apply to events before dispatching them to web kit.
* @param webKitScale The scale factor to apply to translated events before dispatching
* them to web kit.
* @return True if the dispatcher will handle the event, false if the event is unsupported.
*/
public boolean postPointerEvent(MotionEvent event,
int webKitXOffset, int webKitYOffset, float webKitScale) {
if (event == null) {
throw new IllegalArgumentException("event cannot be null");
}
if (DEBUG) {
Log.d(TAG, "postPointerEvent: " + event);
}
final int action = event.getActionMasked();
final int eventType;
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
eventType = EVENT_TYPE_TOUCH;
break;
case MotionEvent.ACTION_SCROLL:
eventType = EVENT_TYPE_SCROLL;
break;
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_HOVER_EXIT:
eventType = EVENT_TYPE_HOVER;
break;
default:
return false; // currently unsupported event type
}
synchronized (mLock) {
// Ensure that the event is consistent and should be delivered.
MotionEvent eventToEnqueue = event;
if (eventType == EVENT_TYPE_TOUCH) {
eventToEnqueue = mPostTouchStream.update(event);
if (eventToEnqueue == null) {
if (DEBUG) {
Log.d(TAG, "postPointerEvent: dropped event " + event);
}
unscheduleLongPressLocked();
unscheduleClickLocked();
hideTapCandidateLocked();
return false;
}
if (action == MotionEvent.ACTION_DOWN && mPostSendTouchEventsToWebKit) {
if (mUiCallbacks.shouldInterceptTouchEvent(eventToEnqueue)) {
mPostDoNotSendTouchEventsToWebKitUntilNextGesture = true;
} else if (mPostDoNotSendTouchEventsToWebKitUntilNextGesture) {
// Recover from a previous web kit timeout.
mPostDoNotSendTouchEventsToWebKitUntilNextGesture = false;
}
}
}
// Copy the event because we need to retain ownership.
if (eventToEnqueue == event) {
eventToEnqueue = event.copy();
}
DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, eventType, 0,
webKitXOffset, webKitYOffset, webKitScale);
updateStateTrackersLocked(d, event);
enqueueEventLocked(d);
}
return true;
}
private void scheduleLongPressLocked() {
unscheduleLongPressLocked();
mPostLongPressScheduled = true;
mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_LONG_PRESS,
LONG_PRESS_TIMEOUT);
}
private void unscheduleLongPressLocked() {
if (mPostLongPressScheduled) {
mPostLongPressScheduled = false;
mUiHandler.removeMessages(UiHandler.MSG_LONG_PRESS);
}
}
private void postLongPress() {
synchronized (mLock) {
if (!mPostLongPressScheduled) {
return;
}
mPostLongPressScheduled = false;
MotionEvent event = mPostTouchStream.getLastEvent();
if (event == null) {
return;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
break;
default:
return;
}
MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
eventToEnqueue.setAction(MotionEvent.ACTION_MOVE);
DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_LONG_PRESS, 0,
mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale);
enqueueEventLocked(d);
}
}
private void hideTapCandidateLocked() {
unscheduleHideTapHighlightLocked();
unscheduleShowTapHighlightLocked();
mUiCallbacks.showTapHighlight(false);
}
private void showTapCandidateLocked() {
unscheduleHideTapHighlightLocked();
unscheduleShowTapHighlightLocked();
mUiCallbacks.showTapHighlight(true);
}
private void scheduleShowTapHighlightLocked() {
unscheduleShowTapHighlightLocked();
mPostShowTapHighlightScheduled = true;
mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_SHOW_TAP_HIGHLIGHT,
TAP_TIMEOUT);
}
private void unscheduleShowTapHighlightLocked() {
if (mPostShowTapHighlightScheduled) {
mPostShowTapHighlightScheduled = false;
mUiHandler.removeMessages(UiHandler.MSG_SHOW_TAP_HIGHLIGHT);
}
}
private void scheduleHideTapHighlightLocked() {
unscheduleHideTapHighlightLocked();
mPostHideTapHighlightScheduled = true;
mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_HIDE_TAP_HIGHLIGHT,
PRESSED_STATE_DURATION);
}
private void unscheduleHideTapHighlightLocked() {
if (mPostHideTapHighlightScheduled) {
mPostHideTapHighlightScheduled = false;
mUiHandler.removeMessages(UiHandler.MSG_HIDE_TAP_HIGHLIGHT);
}
}
private void postShowTapHighlight(boolean show) {
synchronized (mLock) {
if (show) {
if (!mPostShowTapHighlightScheduled) {
return;
}
mPostShowTapHighlightScheduled = false;
} else {
if (!mPostHideTapHighlightScheduled) {
return;
}
mPostHideTapHighlightScheduled = false;
}
mUiCallbacks.showTapHighlight(show);
}
}
private void scheduleClickLocked() {
unscheduleClickLocked();
mPostClickScheduled = true;
mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_CLICK, DOUBLE_TAP_TIMEOUT);
}
private void unscheduleClickLocked() {
if (mPostClickScheduled) {
mPostClickScheduled = false;
mUiHandler.removeMessages(UiHandler.MSG_CLICK);
}
}
private void postClick() {
synchronized (mLock) {
if (!mPostClickScheduled) {
return;
}
mPostClickScheduled = false;
MotionEvent event = mPostTouchStream.getLastEvent();
if (event == null || event.getAction() != MotionEvent.ACTION_UP) {
return;
}
showTapCandidateLocked();
MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_CLICK, 0,
mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale);
enqueueEventLocked(d);
}
}
private void checkForDoubleTapOnDownLocked(MotionEvent event) {
mIsDoubleTapCandidate = false;
if (!mPostClickScheduled) {
return;
}
int deltaX = (int) mInitialDownX - (int) event.getX();
int deltaY = (int) mInitialDownY - (int) event.getY();
if ((deltaX * deltaX + deltaY * deltaY) < mDoubleTapSlopSquared) {
unscheduleClickLocked();
mIsDoubleTapCandidate = true;
}
}
private boolean isClickCandidateLocked(MotionEvent event) {
if (event == null
|| event.getActionMasked() != MotionEvent.ACTION_UP
|| !mIsTapCandidate) {
return false;
}
long downDuration = event.getEventTime() - event.getDownTime();
return downDuration < LONG_PRESS_TIMEOUT;
}
private void enqueueDoubleTapLocked(MotionEvent event) {
MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_DOUBLE_TAP, 0,
mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale);
enqueueEventLocked(d);
}
private void enqueueHitTestLocked(MotionEvent event) {
mUiCallbacks.clearPreviousHitTest();
MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_HIT_TEST, 0,
mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale);
enqueueEventLocked(d);
}
private void checkForSlopLocked(MotionEvent event) {
if (!mIsTapCandidate) {
return;
}
int deltaX = (int) mInitialDownX - (int) event.getX();
int deltaY = (int) mInitialDownY - (int) event.getY();
if ((deltaX * deltaX + deltaY * deltaY) > mTouchSlopSquared) {
unscheduleLongPressLocked();
mIsTapCandidate = false;
hideTapCandidateLocked();
}
}
private void updateStateTrackersLocked(DispatchEvent d, MotionEvent event) {
mPostLastWebKitXOffset = d.mWebKitXOffset;
mPostLastWebKitYOffset = d.mWebKitYOffset;
mPostLastWebKitScale = d.mWebKitScale;
int action = event != null ? event.getAction() : MotionEvent.ACTION_CANCEL;
if (d.mEventType != EVENT_TYPE_TOUCH) {
return;
}
if (action == MotionEvent.ACTION_CANCEL
|| event.getPointerCount() > 1) {
unscheduleLongPressLocked();
unscheduleClickLocked();
hideTapCandidateLocked();
mIsDoubleTapCandidate = false;
mIsTapCandidate = false;
hideTapCandidateLocked();
} else if (action == MotionEvent.ACTION_DOWN) {
checkForDoubleTapOnDownLocked(event);
scheduleLongPressLocked();
mIsTapCandidate = true;
mInitialDownX = event.getX();
mInitialDownY = event.getY();
enqueueHitTestLocked(event);
if (mIsDoubleTapCandidate) {
hideTapCandidateLocked();
} else {
scheduleShowTapHighlightLocked();
}
} else if (action == MotionEvent.ACTION_UP) {
unscheduleLongPressLocked();
if (isClickCandidateLocked(event)) {
if (mIsDoubleTapCandidate) {
hideTapCandidateLocked();
enqueueDoubleTapLocked(event);
} else {
scheduleClickLocked();
}
} else {
hideTapCandidateLocked();
}
} else if (action == MotionEvent.ACTION_MOVE) {
checkForSlopLocked(event);
}
}
/**
* Dispatches pending web kit events.
* Must only be called from the web kit thread.
*
* This method may be used to flush the queue of pending input events
* immediately. This method may help to reduce input dispatch latency
* if called before certain expensive operations such as drawing.
*/
public void dispatchWebKitEvents() {
dispatchWebKitEvents(false);
}
private void dispatchWebKitEvents(boolean calledFromHandler) {
for (;;) {
// Get the next event, but leave it in the queue so we can move it to the UI
// queue if a timeout occurs.
DispatchEvent d;
MotionEvent event;
final int eventType;
int flags;
synchronized (mLock) {
if (!ENABLE_EVENT_BATCHING) {
drainStaleWebKitEventsLocked();
}
d = mWebKitDispatchEventQueue.mHead;
if (d == null) {
if (mWebKitDispatchScheduled) {
mWebKitDispatchScheduled = false;
if (!calledFromHandler) {
mWebKitHandler.removeMessages(
WebKitHandler.MSG_DISPATCH_WEBKIT_EVENTS);
}
}
return;
}
event = d.mEvent;
if (event != null) {
event.offsetLocation(d.mWebKitXOffset, d.mWebKitYOffset);
event.scale(d.mWebKitScale);
d.mFlags |= FLAG_WEBKIT_TRANSFORMED_EVENT;
}
eventType = d.mEventType;
if (eventType == EVENT_TYPE_TOUCH) {
event = mWebKitTouchStream.update(event);
if (DEBUG && event == null && d.mEvent != null) {
Log.d(TAG, "dispatchWebKitEvents: dropped event " + d.mEvent);
}
}
d.mFlags |= FLAG_WEBKIT_IN_PROGRESS;
flags = d.mFlags;
}
// Handle the event.
final boolean preventDefault;
if (event == null) {
preventDefault = false;
} else {
preventDefault = dispatchWebKitEvent(event, eventType, flags);
}
synchronized (mLock) {
flags = d.mFlags;
d.mFlags = flags & ~FLAG_WEBKIT_IN_PROGRESS;
boolean recycleEvent = event != d.mEvent;
if ((flags & FLAG_WEBKIT_TIMEOUT) != 0) {
// A timeout occurred!
recycleDispatchEventLocked(d);
} else {
// Web kit finished in a timely manner. Dequeue the event.
assert mWebKitDispatchEventQueue.mHead == d;
mWebKitDispatchEventQueue.dequeue();
updateWebKitTimeoutLocked();
if ((flags & FLAG_PRIVATE) != 0) {
// Event was intended for web kit only. All done.
recycleDispatchEventLocked(d);
} else if (preventDefault) {
// Web kit has decided to consume the event!
if (d.mEventType == EVENT_TYPE_TOUCH) {
enqueueUiCancelTouchEventIfNeededLocked();
unscheduleLongPressLocked();
}
} else {
// Web kit is being friendly. Pass the event to the UI.
enqueueUiEventUnbatchedLocked(d);
}
}
if (event != null && recycleEvent) {
event.recycle();
}
if (eventType == EVENT_TYPE_CLICK) {
scheduleHideTapHighlightLocked();
}
}
}
}
// Runs on web kit thread.
private boolean dispatchWebKitEvent(MotionEvent event, int eventType, int flags) {
if (DEBUG) {
Log.d(TAG, "dispatchWebKitEvent: event=" + event
+ ", eventType=" + eventType + ", flags=" + flags);
}
boolean preventDefault = mWebKitCallbacks.dispatchWebKitEvent(
this, event, eventType, flags);
if (DEBUG) {
Log.d(TAG, "dispatchWebKitEvent: preventDefault=" + preventDefault);
}
return preventDefault;
}
private boolean isMoveEventLocked(DispatchEvent d) {
return d.mEvent != null
&& d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE;
}
private void drainStaleWebKitEventsLocked() {
DispatchEvent d = mWebKitDispatchEventQueue.mHead;
while (d != null && d.mNext != null
&& isMoveEventLocked(d)
&& isMoveEventLocked(d.mNext)) {
DispatchEvent next = d.mNext;
skipWebKitEventLocked(d);
d = next;
}
mWebKitDispatchEventQueue.mHead = d;
}
// Called by WebKit when it doesn't care about the rest of the touch stream
public void skipWebkitForRemainingTouchStream() {
// Just treat this like a timeout
handleWebKitTimeout();
}
// Runs on UI thread in response to the web kit thread appearing to be unresponsive.
private void handleWebKitTimeout() {
synchronized (mLock) {
if (!mWebKitTimeoutScheduled) {
return;
}
mWebKitTimeoutScheduled = false;
if (DEBUG) {
Log.d(TAG, "handleWebKitTimeout: timeout occurred!");
}
// Drain the web kit event queue.
DispatchEvent d = mWebKitDispatchEventQueue.dequeueList();
// If web kit was processing an event (must be at the head of the list because
// it can only do one at a time), then clone it or ignore it.
if ((d.mFlags & FLAG_WEBKIT_IN_PROGRESS) != 0) {
d.mFlags |= FLAG_WEBKIT_TIMEOUT;
if ((d.mFlags & FLAG_PRIVATE) != 0) {
d = d.mNext; // the event is private to web kit, ignore it
} else {
d = copyDispatchEventLocked(d);
d.mFlags &= ~FLAG_WEBKIT_IN_PROGRESS;
}
}
// Enqueue all non-private events for handling by the UI thread.
while (d != null) {
DispatchEvent next = d.mNext;
skipWebKitEventLocked(d);
d = next;
}
// Tell web kit to cancel all pending touches.
// This also prevents us from sending web kit any more touches until the
// next gesture begins. (As required to ensure touch event stream consistency.)
enqueueWebKitCancelTouchEventIfNeededLocked();
}
}
private void skipWebKitEventLocked(DispatchEvent d) {
d.mNext = null;
if ((d.mFlags & FLAG_PRIVATE) != 0) {
recycleDispatchEventLocked(d);
} else {
d.mFlags |= FLAG_WEBKIT_TIMEOUT;
enqueueUiEventUnbatchedLocked(d);
}
}
/**
* Dispatches pending UI events.
* Must only be called from the UI thread.
*
* This method may be used to flush the queue of pending input events
* immediately. This method may help to reduce input dispatch latency
* if called before certain expensive operations such as drawing.
*/
public void dispatchUiEvents() {
dispatchUiEvents(false);
}
private void dispatchUiEvents(boolean calledFromHandler) {
for (;;) {
MotionEvent event;
final int eventType;
final int flags;
synchronized (mLock) {
DispatchEvent d = mUiDispatchEventQueue.dequeue();
if (d == null) {
if (mUiDispatchScheduled) {
mUiDispatchScheduled = false;
if (!calledFromHandler) {
mUiHandler.removeMessages(UiHandler.MSG_DISPATCH_UI_EVENTS);
}
}
return;
}
event = d.mEvent;
if (event != null && (d.mFlags & FLAG_WEBKIT_TRANSFORMED_EVENT) != 0) {
event.scale(1.0f / d.mWebKitScale);
event.offsetLocation(-d.mWebKitXOffset, -d.mWebKitYOffset);
d.mFlags &= ~FLAG_WEBKIT_TRANSFORMED_EVENT;
}
eventType = d.mEventType;
if (eventType == EVENT_TYPE_TOUCH) {
event = mUiTouchStream.update(event);
if (DEBUG && event == null && d.mEvent != null) {
Log.d(TAG, "dispatchUiEvents: dropped event " + d.mEvent);
}
}
flags = d.mFlags;
if (event == d.mEvent) {
d.mEvent = null; // retain ownership of event, don't recycle it yet
}
recycleDispatchEventLocked(d);
if (eventType == EVENT_TYPE_CLICK) {
scheduleHideTapHighlightLocked();
}
}
// Handle the event.
if (event != null) {
dispatchUiEvent(event, eventType, flags);
event.recycle();
}
}
}
// Runs on UI thread.
private void dispatchUiEvent(MotionEvent event, int eventType, int flags) {
if (DEBUG) {
Log.d(TAG, "dispatchUiEvent: event=" + event
+ ", eventType=" + eventType + ", flags=" + flags);
}
mUiCallbacks.dispatchUiEvent(event, eventType, flags);
}
private void enqueueEventLocked(DispatchEvent d) {
if (!shouldSkipWebKit(d)) {
enqueueWebKitEventLocked(d);
} else {
enqueueUiEventLocked(d);
}
}
private boolean shouldSkipWebKit(DispatchEvent d) {
switch (d.mEventType) {
case EVENT_TYPE_CLICK:
case EVENT_TYPE_HOVER:
case EVENT_TYPE_SCROLL:
case EVENT_TYPE_HIT_TEST:
return false;
case EVENT_TYPE_TOUCH:
// TODO: This should be cleaned up. We now have WebViewInputDispatcher
// and WebViewClassic both checking for slop and doing their own
// thing - they should be consolidated. And by consolidated, I mean
// WebViewClassic's version should just be deleted.
// The reason this is done is because webpages seem to expect
// that they only get an ontouchmove if the slop has been exceeded.
if (mIsTapCandidate && d.mEvent != null
&& d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE) {
return true;
}
return !mPostSendTouchEventsToWebKit
|| mPostDoNotSendTouchEventsToWebKitUntilNextGesture;
}
return true;
}
private void enqueueWebKitCancelTouchEventIfNeededLocked() {
// We want to cancel touch events that were delivered to web kit.
// Enqueue a null event at the end of the queue if needed.
if (mWebKitTouchStream.isCancelNeeded() || !mWebKitDispatchEventQueue.isEmpty()) {
DispatchEvent d = obtainDispatchEventLocked(null, EVENT_TYPE_TOUCH, FLAG_PRIVATE,
0, 0, 1.0f);
enqueueWebKitEventUnbatchedLocked(d);
mPostDoNotSendTouchEventsToWebKitUntilNextGesture = true;
}
}
private void enqueueWebKitEventLocked(DispatchEvent d) {
if (batchEventLocked(d, mWebKitDispatchEventQueue.mTail)) {
if (DEBUG) {
Log.d(TAG, "enqueueWebKitEventLocked: batched event " + d.mEvent);
}
recycleDispatchEventLocked(d);
} else {
enqueueWebKitEventUnbatchedLocked(d);
}
}
private void enqueueWebKitEventUnbatchedLocked(DispatchEvent d) {
if (DEBUG) {
Log.d(TAG, "enqueueWebKitEventUnbatchedLocked: enqueued event " + d.mEvent);
}
mWebKitDispatchEventQueue.enqueue(d);
scheduleWebKitDispatchLocked();
updateWebKitTimeoutLocked();
}
private void scheduleWebKitDispatchLocked() {
if (!mWebKitDispatchScheduled) {
mWebKitHandler.sendEmptyMessage(WebKitHandler.MSG_DISPATCH_WEBKIT_EVENTS);
mWebKitDispatchScheduled = true;
}
}
private void updateWebKitTimeoutLocked() {
DispatchEvent d = mWebKitDispatchEventQueue.mHead;
if (d != null && mWebKitTimeoutScheduled && mWebKitTimeoutTime == d.mTimeoutTime) {
return;
}
if (mWebKitTimeoutScheduled) {
mUiHandler.removeMessages(UiHandler.MSG_WEBKIT_TIMEOUT);
mWebKitTimeoutScheduled = false;
}
if (d != null) {
mUiHandler.sendEmptyMessageAtTime(UiHandler.MSG_WEBKIT_TIMEOUT, d.mTimeoutTime);
mWebKitTimeoutScheduled = true;
mWebKitTimeoutTime = d.mTimeoutTime;
}
}
private void enqueueUiCancelTouchEventIfNeededLocked() {
// We want to cancel touch events that were delivered to the UI.
// Enqueue a null event at the end of the queue if needed.
if (mUiTouchStream.isCancelNeeded() || !mUiDispatchEventQueue.isEmpty()) {
DispatchEvent d = obtainDispatchEventLocked(null, EVENT_TYPE_TOUCH, FLAG_PRIVATE,
0, 0, 1.0f);
enqueueUiEventUnbatchedLocked(d);
}
}
private void enqueueUiEventLocked(DispatchEvent d) {
if (batchEventLocked(d, mUiDispatchEventQueue.mTail)) {
if (DEBUG) {
Log.d(TAG, "enqueueUiEventLocked: batched event " + d.mEvent);
}
recycleDispatchEventLocked(d);
} else {
enqueueUiEventUnbatchedLocked(d);
}
}
private void enqueueUiEventUnbatchedLocked(DispatchEvent d) {
if (DEBUG) {
Log.d(TAG, "enqueueUiEventUnbatchedLocked: enqueued event " + d.mEvent);
}
mUiDispatchEventQueue.enqueue(d);
scheduleUiDispatchLocked();
}
private void scheduleUiDispatchLocked() {
if (!mUiDispatchScheduled) {
mUiHandler.sendEmptyMessage(UiHandler.MSG_DISPATCH_UI_EVENTS);
mUiDispatchScheduled = true;
}
}
private boolean batchEventLocked(DispatchEvent in, DispatchEvent tail) {
if (!ENABLE_EVENT_BATCHING) {
return false;
}
if (tail != null && tail.mEvent != null && in.mEvent != null
&& in.mEventType == tail.mEventType
&& in.mFlags == tail.mFlags
&& in.mWebKitXOffset == tail.mWebKitXOffset
&& in.mWebKitYOffset == tail.mWebKitYOffset
&& in.mWebKitScale == tail.mWebKitScale) {
return tail.mEvent.addBatch(in.mEvent);
}
return false;
}
private DispatchEvent obtainDispatchEventLocked(MotionEvent event,
int eventType, int flags, int webKitXOffset, int webKitYOffset, float webKitScale) {
DispatchEvent d = obtainUninitializedDispatchEventLocked();
d.mEvent = event;
d.mEventType = eventType;
d.mFlags = flags;
d.mTimeoutTime = SystemClock.uptimeMillis() + WEBKIT_TIMEOUT_MILLIS;
d.mWebKitXOffset = webKitXOffset;
d.mWebKitYOffset = webKitYOffset;
d.mWebKitScale = webKitScale;
if (DEBUG) {
Log.d(TAG, "Timeout time: " + (d.mTimeoutTime - SystemClock.uptimeMillis()));
}
return d;
}
private DispatchEvent copyDispatchEventLocked(DispatchEvent d) {
DispatchEvent copy = obtainUninitializedDispatchEventLocked();
if (d.mEvent != null) {
copy.mEvent = d.mEvent.copy();
}
copy.mEventType = d.mEventType;
copy.mFlags = d.mFlags;
copy.mTimeoutTime = d.mTimeoutTime;
copy.mWebKitXOffset = d.mWebKitXOffset;
copy.mWebKitYOffset = d.mWebKitYOffset;
copy.mWebKitScale = d.mWebKitScale;
copy.mNext = d.mNext;
return copy;
}
private DispatchEvent obtainUninitializedDispatchEventLocked() {
DispatchEvent d = mDispatchEventPool;
if (d != null) {
mDispatchEventPoolSize -= 1;
mDispatchEventPool = d.mNext;
d.mNext = null;
} else {
d = new DispatchEvent();
}
return d;
}
private void recycleDispatchEventLocked(DispatchEvent d) {
if (d.mEvent != null) {
d.mEvent.recycle();
d.mEvent = null;
}
if (mDispatchEventPoolSize < MAX_DISPATCH_EVENT_POOL_SIZE) {
mDispatchEventPoolSize += 1;
d.mNext = mDispatchEventPool;
mDispatchEventPool = d;
}
}
/* Implemented by {@link WebViewClassic} to perform operations on the UI thread. */
public static interface UiCallbacks {
/**
* Gets the UI thread's looper.
* @return The looper.
*/
public Looper getUiLooper();
/**
* Gets the UI's context
* @return The context
*/
public Context getContext();
/**
* Dispatches an event to the UI.
* @param event The event.
* @param eventType The event type.
* @param flags The event's dispatch flags.
*/
public void dispatchUiEvent(MotionEvent event, int eventType, int flags);
/**
* Asks the UI thread whether this touch event stream should be
* intercepted based on the touch down event.
* @param event The touch down event.
* @return true if the UI stream wants the touch stream without going
* through webkit or false otherwise.
*/
public boolean shouldInterceptTouchEvent(MotionEvent event);
/**
* Inform's the UI that it should show the tap highlight
* @param show True if it should show the highlight, false if it should hide it
*/
public void showTapHighlight(boolean show);
/**
* Called when we are sending a new EVENT_TYPE_HIT_TEST to WebKit, so
* previous hit tests should be cleared as they are obsolete.
*/
public void clearPreviousHitTest();
}
/* Implemented by {@link WebViewCore} to perform operations on the web kit thread. */
public static interface WebKitCallbacks {
/**
* Gets the web kit thread's looper.
* @return The looper.
*/
public Looper getWebKitLooper();
/**
* Dispatches an event to web kit.
* @param dispatcher The WebViewInputDispatcher sending the event
* @param event The event.
* @param eventType The event type.
* @param flags The event's dispatch flags.
* @return True if web kit wants to prevent default event handling.
*/
public boolean dispatchWebKitEvent(WebViewInputDispatcher dispatcher,
MotionEvent event, int eventType, int flags);
}
// Runs on UI thread.
private final class UiHandler extends Handler {
public static final int MSG_DISPATCH_UI_EVENTS = 1;
public static final int MSG_WEBKIT_TIMEOUT = 2;
public static final int MSG_LONG_PRESS = 3;
public static final int MSG_CLICK = 4;
public static final int MSG_SHOW_TAP_HIGHLIGHT = 5;
public static final int MSG_HIDE_TAP_HIGHLIGHT = 6;
public UiHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DISPATCH_UI_EVENTS:
dispatchUiEvents(true);
break;
case MSG_WEBKIT_TIMEOUT:
handleWebKitTimeout();
break;
case MSG_LONG_PRESS:
postLongPress();
break;
case MSG_CLICK:
postClick();
break;
case MSG_SHOW_TAP_HIGHLIGHT:
postShowTapHighlight(true);
break;
case MSG_HIDE_TAP_HIGHLIGHT:
postShowTapHighlight(false);
break;
default:
throw new IllegalStateException("Unknown message type: " + msg.what);
}
}
}
// Runs on web kit thread.
private final class WebKitHandler extends Handler {
public static final int MSG_DISPATCH_WEBKIT_EVENTS = 1;
public WebKitHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DISPATCH_WEBKIT_EVENTS:
dispatchWebKitEvents(true);
break;
default:
throw new IllegalStateException("Unknown message type: " + msg.what);
}
}
}
private static final class DispatchEvent {
public DispatchEvent mNext;
public MotionEvent mEvent;
public int mEventType;
public int mFlags;
public long mTimeoutTime;
public int mWebKitXOffset;
public int mWebKitYOffset;
public float mWebKitScale;
}
private static final class DispatchEventQueue {
public DispatchEvent mHead;
public DispatchEvent mTail;
public boolean isEmpty() {
return mHead != null;
}
public void enqueue(DispatchEvent d) {
if (mHead == null) {
mHead = d;
mTail = d;
} else {
mTail.mNext = d;
mTail = d;
}
}
public DispatchEvent dequeue() {
DispatchEvent d = mHead;
if (d != null) {
DispatchEvent next = d.mNext;
if (next == null) {
mHead = null;
mTail = null;
} else {
mHead = next;
d.mNext = null;
}
}
return d;
}
public DispatchEvent dequeueList() {
DispatchEvent d = mHead;
if (d != null) {
mHead = null;
mTail = null;
}
return d;
}
}
/**
* Keeps track of a stream of touch events so that we can discard touch
* events that would make the stream inconsistent.
*/
private static final class TouchStream {
private MotionEvent mLastEvent;
/**
* Gets the last touch event that was delivered.
* @return The last touch event, or null if none.
*/
public MotionEvent getLastEvent() {
return mLastEvent;
}
/**
* Updates the touch event stream.
* @param event The event that we intend to send, or null to cancel the
* touch event stream.
* @return The event that we should actually send, or null if no event should
* be sent because the proposed event would make the stream inconsistent.
*/
public MotionEvent update(MotionEvent event) {
if (event == null) {
if (isCancelNeeded()) {
event = mLastEvent;
if (event != null) {
event.setAction(MotionEvent.ACTION_CANCEL);
mLastEvent = null;
}
}
return event;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
if (mLastEvent == null
|| mLastEvent.getAction() == MotionEvent.ACTION_UP) {
return null;
}
updateLastEvent(event);
return event;
case MotionEvent.ACTION_DOWN:
updateLastEvent(event);
return event;
case MotionEvent.ACTION_CANCEL:
if (mLastEvent == null) {
return null;
}
updateLastEvent(null);
return event;
default:
return null;
}
}
/**
* Returns true if there is a gesture in progress that may need to be canceled.
* @return True if cancel is needed.
*/
public boolean isCancelNeeded() {
return mLastEvent != null && mLastEvent.getAction() != MotionEvent.ACTION_UP;
}
private void updateLastEvent(MotionEvent event) {
if (mLastEvent != null) {
mLastEvent.recycle();
}
mLastEvent = event != null ? MotionEvent.obtainNoHistory(event) : null;
}
}
}