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