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