| /* |
| * 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; |
| } |
| } |
| } |