1// Copyright 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.content.browser; 6 7import android.content.Context; 8import android.os.Bundle; 9import android.os.SystemClock; 10import android.util.Log; 11import android.view.InputDevice; 12import android.view.MotionEvent; 13import android.view.ViewConfiguration; 14 15import org.chromium.base.CommandLine; 16import org.chromium.content.browser.LongPressDetector.LongPressDelegate; 17import org.chromium.content.browser.third_party.GestureDetector; 18import org.chromium.content.browser.third_party.GestureDetector.OnDoubleTapListener; 19import org.chromium.content.browser.third_party.GestureDetector.OnGestureListener; 20import org.chromium.content.common.ContentSwitches; 21import org.chromium.content.common.TraceEvent; 22 23import java.util.ArrayDeque; 24import java.util.Deque; 25 26/** 27 * This class handles all MotionEvent handling done in ContentViewCore including the gesture 28 * recognition. It sends all related native calls through the interface MotionEventDelegate. 29 */ 30class ContentViewGestureHandler implements LongPressDelegate { 31 32 private static final String TAG = "ContentViewGestureHandler"; 33 /** 34 * Used for GESTURE_FLING_START x velocity 35 */ 36 static final String VELOCITY_X = "Velocity X"; 37 /** 38 * Used for GESTURE_FLING_START y velocity 39 */ 40 static final String VELOCITY_Y = "Velocity Y"; 41 /** 42 * Used for GESTURE_SCROLL_BY x distance 43 */ 44 static final String DISTANCE_X = "Distance X"; 45 /** 46 * Used for GESTURE_SCROLL_BY y distance 47 */ 48 static final String DISTANCE_Y = "Distance Y"; 49 /** 50 * Used in GESTURE_SINGLE_TAP_CONFIRMED to check whether ShowPress has been called before. 51 */ 52 static final String SHOW_PRESS = "ShowPress"; 53 /** 54 * Used for GESTURE_PINCH_BY delta 55 */ 56 static final String DELTA = "Delta"; 57 58 /** 59 * Used by UMA stat for tracking accidental double tap navigations. Specifies the amount of 60 * time after a double tap within which actions will be recorded to the UMA stat. 61 */ 62 private static final long ACTION_AFTER_DOUBLE_TAP_WINDOW_MS = 5000; 63 64 private final Bundle mExtraParamBundleSingleTap; 65 private final Bundle mExtraParamBundleFling; 66 private final Bundle mExtraParamBundleScroll; 67 private final Bundle mExtraParamBundleDoubleTapDragZoom; 68 private final Bundle mExtraParamBundlePinchBy; 69 private GestureDetector mGestureDetector; 70 private final ZoomManager mZoomManager; 71 private LongPressDetector mLongPressDetector; 72 private OnGestureListener mListener; 73 private OnDoubleTapListener mDoubleTapListener; 74 private MotionEvent mCurrentDownEvent; 75 private final MotionEventDelegate mMotionEventDelegate; 76 77 // Queue of motion events. 78 private final Deque<MotionEvent> mPendingMotionEvents = new ArrayDeque<MotionEvent>(); 79 80 // All events are forwarded to the GestureDetector, bypassing Javascript. 81 private static final int NO_TOUCH_HANDLER = 0; 82 83 // All events are forwarded as normal to Javascript, and if unconsumed to the GestureDetector. 84 // * Activated from the renderer by way of |hasTouchEventHandlers(true)|. 85 private static final int HAS_TOUCH_HANDLER = 1; 86 87 // Events in the current gesture are forwarded to the GestureDetector, bypassing Javascript. 88 // * Activated if the touch down for the current gesture had no Javascript consumer. 89 private static final int NO_TOUCH_HANDLER_FOR_GESTURE = 2; 90 91 // Events in the current gesture are forwarded to Javascript, and not to the GestureDetector. 92 // * Activated if *any* touch event in the current sequence was consumed by Javascript. 93 private static final int JAVASCRIPT_CONSUMING_GESTURE = 3; 94 95 private static final int TOUCH_HANDLING_STATE_DEFAULT = NO_TOUCH_HANDLER; 96 97 private int mTouchHandlingState = TOUCH_HANDLING_STATE_DEFAULT; 98 99 // Remember whether onShowPress() is called. If it is not, in onSingleTapConfirmed() 100 // we will first show the press state, then trigger the click. 101 private boolean mShowPressIsCalled; 102 103 // Whether a sent GESTURE_TAP_DOWN event has yet to be accompanied by a corresponding 104 // GESTURE_SINGLE_TAP_UP, GESTURE_SINGLE_TAP_CONFIRMED, GESTURE_TAP_CANCEL or 105 // GESTURE_DOUBLE_TAP. 106 private boolean mNeedsTapEndingEvent; 107 108 // This flag is used for ignoring the remaining touch events, i.e., All the events until the 109 // next ACTION_DOWN. This is automatically set to false on the next ACTION_DOWN. 110 private boolean mIgnoreRemainingTouchEvents; 111 112 // TODO(klobag): this is to avoid a bug in GestureDetector. With multi-touch, 113 // mAlwaysInTapRegion is not reset. So when the last finger is up, onSingleTapUp() 114 // will be mistakenly fired. 115 private boolean mIgnoreSingleTap; 116 117 // True from right before we send the first scroll event until the last finger is raised. 118 private boolean mTouchScrolling; 119 120 // TODO(wangxianzhu): For now it is true after a fling is started until the next 121 // touch. Should reset it to false on end of fling if the UI is able to know when the 122 // fling ends. 123 private boolean mFlingMayBeActive; 124 125 private boolean mSeenFirstScrollEvent; 126 127 private boolean mPinchInProgress = false; 128 129 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); 130 131 //On single tap this will store the x, y coordinates of the touch. 132 private int mSingleTapX; 133 private int mSingleTapY; 134 135 // Indicate current double tap mode state. 136 private int mDoubleTapMode = DOUBLE_TAP_MODE_NONE; 137 138 // x, y coordinates for an Anchor on double tap drag zoom. 139 private float mDoubleTapDragZoomAnchorX; 140 private float mDoubleTapDragZoomAnchorY; 141 142 // On double tap this will store the y coordinates of the touch. 143 private float mDoubleTapY; 144 145 // Double tap drag zoom sensitive (speed). 146 private static final float DOUBLE_TAP_DRAG_ZOOM_SPEED = 0.005f; 147 148 // Used to track the last rawX/Y coordinates for moves. This gives absolute scroll distance. 149 // Useful for full screen tracking. 150 private float mLastRawX = 0; 151 private float mLastRawY = 0; 152 153 // Cache of square of the scaled touch slop so we don't have to calculate it on every touch. 154 private int mScaledTouchSlopSquare; 155 156 // Object that keeps track of and updates scroll snapping behavior. 157 private final SnapScrollController mSnapScrollController; 158 159 // Used to track the accumulated scroll error over time. This is used to remove the 160 // rounding error we introduced by passing integers to webkit. 161 private float mAccumulatedScrollErrorX = 0; 162 private float mAccumulatedScrollErrorY = 0; 163 164 // The page's viewport and scale sometimes allow us to disable double tap gesture detection, 165 // according to the logic in ContentViewCore.onRenderCoordinatesUpdated(). 166 private boolean mShouldDisableDoubleTap; 167 168 // Keeps track of the last long press event, if we end up opening a context menu, we would need 169 // to potentially use the event to send GESTURE_TAP_CANCEL to remove ::active styling 170 private MotionEvent mLastLongPressEvent; 171 172 // Whether the click delay should always be disabled by sending clicks for double tap gestures. 173 private final boolean mDisableClickDelay; 174 175 // Used for tracking UMA ActionAfterDoubleTap to tell user's immediate 176 // action after a double tap. 177 private long mLastDoubleTapTimeMs; 178 179 static final int GESTURE_SHOW_PRESSED_STATE = 0; 180 static final int GESTURE_DOUBLE_TAP = 1; 181 static final int GESTURE_SINGLE_TAP_UP = 2; 182 static final int GESTURE_SINGLE_TAP_CONFIRMED = 3; 183 static final int GESTURE_SINGLE_TAP_UNCONFIRMED = 4; 184 static final int GESTURE_LONG_PRESS = 5; 185 static final int GESTURE_SCROLL_START = 6; 186 static final int GESTURE_SCROLL_BY = 7; 187 static final int GESTURE_SCROLL_END = 8; 188 static final int GESTURE_FLING_START = 9; 189 static final int GESTURE_FLING_CANCEL = 10; 190 static final int GESTURE_PINCH_BEGIN = 11; 191 static final int GESTURE_PINCH_BY = 12; 192 static final int GESTURE_PINCH_END = 13; 193 static final int GESTURE_TAP_CANCEL = 14; 194 static final int GESTURE_LONG_TAP = 15; 195 static final int GESTURE_TAP_DOWN = 16; 196 197 // These have to be kept in sync with content/port/common/input_event_ack_state.h 198 static final int INPUT_EVENT_ACK_STATE_UNKNOWN = 0; 199 static final int INPUT_EVENT_ACK_STATE_CONSUMED = 1; 200 static final int INPUT_EVENT_ACK_STATE_NOT_CONSUMED = 2; 201 static final int INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS = 3; 202 static final int INPUT_EVENT_ACK_STATE_IGNORED = 4; 203 204 // Return values of sendPendingEventToNative(); 205 static final int EVENT_FORWARDED_TO_NATIVE = 0; 206 static final int EVENT_DROPPED = 1; 207 static final int EVENT_NOT_FORWARDED = 2; 208 209 private final float mPxToDp; 210 211 static final int DOUBLE_TAP_MODE_NONE = 0; 212 static final int DOUBLE_TAP_MODE_DRAG_DETECTION_IN_PROGRESS = 1; 213 static final int DOUBLE_TAP_MODE_DRAG_ZOOM = 2; 214 static final int DOUBLE_TAP_MODE_DISABLED = 3; 215 216 /** 217 * This is an interface to handle MotionEvent related communication with the native side also 218 * access some ContentView specific parameters. 219 */ 220 public interface MotionEventDelegate { 221 /** 222 * Send a raw {@link MotionEvent} to the native side 223 * @param timeMs Time of the event in ms. 224 * @param action The action type for the event. 225 * @param pts The TouchPoint array to be sent for the event. 226 * @return Whether the event was sent to the native side successfully or not. 227 */ 228 public boolean sendTouchEvent(long timeMs, int action, TouchPoint[] pts); 229 230 /** 231 * Send a gesture event to the native side. 232 * @param type The type of the gesture event. 233 * @param timeMs The time the gesture event occurred at. 234 * @param x The x location for the gesture event. 235 * @param y The y location for the gesture event. 236 * @param extraParams A bundle that holds specific extra parameters for certain gestures. 237 * This is read-only and should not be modified in this function. 238 * Refer to gesture type definition for more information. 239 * @return Whether the gesture was sent successfully. 240 */ 241 boolean sendGesture(int type, long timeMs, int x, int y, Bundle extraParams); 242 243 /** 244 * Show the zoom picker UI. 245 */ 246 public void invokeZoomPicker(); 247 248 /** 249 * Send action after dobule tap for UMA stat tracking. 250 * @param type The action that occured 251 * @param clickDelayEnabled Whether the tap down delay is active 252 */ 253 public void sendActionAfterDoubleTapUMA(int type, boolean clickDelayEnabled); 254 255 /** 256 * Send single tap UMA. 257 * @param type The tap type: delayed or undelayed 258 */ 259 public void sendSingleTapUMA(int type); 260 } 261 262 ContentViewGestureHandler( 263 Context context, MotionEventDelegate delegate, ZoomManager zoomManager) { 264 mExtraParamBundleSingleTap = new Bundle(); 265 mExtraParamBundleFling = new Bundle(); 266 mExtraParamBundleScroll = new Bundle(); 267 mExtraParamBundleDoubleTapDragZoom = new Bundle(); 268 mExtraParamBundlePinchBy = new Bundle(); 269 270 mLongPressDetector = new LongPressDetector(context, this); 271 mMotionEventDelegate = delegate; 272 mZoomManager = zoomManager; 273 mSnapScrollController = new SnapScrollController(context, mZoomManager); 274 mPxToDp = 1.0f / context.getResources().getDisplayMetrics().density; 275 276 mDisableClickDelay = CommandLine.isInitialized() && 277 CommandLine.getInstance().hasSwitch(ContentSwitches.DISABLE_CLICK_DELAY); 278 279 initGestureDetectors(context); 280 } 281 282 /** 283 * Used to override the default long press detector, gesture detector and listener. 284 * This is used for testing only. 285 * @param longPressDetector The new LongPressDetector to be assigned. 286 * @param gestureDetector The new GestureDetector to be assigned. 287 * @param listener The new onGestureListener to be assigned. 288 */ 289 void setTestDependencies( 290 LongPressDetector longPressDetector, GestureDetector gestureDetector, 291 OnGestureListener listener) { 292 if (longPressDetector != null) mLongPressDetector = longPressDetector; 293 if (gestureDetector != null) mGestureDetector = gestureDetector; 294 if (listener != null) mListener = listener; 295 } 296 297 private void initGestureDetectors(final Context context) { 298 final int scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 299 mScaledTouchSlopSquare = scaledTouchSlop * scaledTouchSlop; 300 try { 301 TraceEvent.begin(); 302 GestureDetector.SimpleOnGestureListener listener = 303 new GestureDetector.SimpleOnGestureListener() { 304 @Override 305 public boolean onDown(MotionEvent e) { 306 mShowPressIsCalled = false; 307 mIgnoreSingleTap = false; 308 mTouchScrolling = false; 309 mSeenFirstScrollEvent = false; 310 mSnapScrollController.resetSnapScrollMode(); 311 mLastRawX = e.getRawX(); 312 mLastRawY = e.getRawY(); 313 mAccumulatedScrollErrorX = 0; 314 mAccumulatedScrollErrorY = 0; 315 mNeedsTapEndingEvent = false; 316 if (sendMotionEventAsGesture(GESTURE_TAP_DOWN, e, null)) { 317 mNeedsTapEndingEvent = true; 318 } 319 // Return true to indicate that we want to handle touch 320 return true; 321 } 322 323 @Override 324 public boolean onScroll(MotionEvent e1, MotionEvent e2, 325 float distanceX, float distanceY) { 326 assert e1.getEventTime() <= e2.getEventTime(); 327 if (!mSeenFirstScrollEvent) { 328 // Remove the touch slop region from the first scroll event to avoid a 329 // jump. 330 mSeenFirstScrollEvent = true; 331 double distance = Math.sqrt( 332 distanceX * distanceX + distanceY * distanceY); 333 double epsilon = 1e-3; 334 if (distance > epsilon) { 335 double ratio = Math.max(0, distance - scaledTouchSlop) / distance; 336 distanceX *= ratio; 337 distanceY *= ratio; 338 } 339 } 340 mSnapScrollController.updateSnapScrollMode(distanceX, distanceY); 341 if (mSnapScrollController.isSnappingScrolls()) { 342 if (mSnapScrollController.isSnapHorizontal()) { 343 distanceY = 0; 344 } else { 345 distanceX = 0; 346 } 347 } 348 349 mLastRawX = e2.getRawX(); 350 mLastRawY = e2.getRawY(); 351 if (!mTouchScrolling) { 352 sendTapCancelIfNecessary(e1); 353 endFlingIfNecessary(e2.getEventTime()); 354 if (sendGesture(GESTURE_SCROLL_START, e2.getEventTime(), 355 (int) e1.getX(), (int) e1.getY(), null)) { 356 mTouchScrolling = true; 357 } 358 } 359 // distanceX and distanceY is the scrolling offset since last onScroll. 360 // Because we are passing integers to webkit, this could introduce 361 // rounding errors. The rounding errors will accumulate overtime. 362 // To solve this, we should be adding back the rounding errors each time 363 // when we calculate the new offset. 364 int x = (int) e2.getX(); 365 int y = (int) e2.getY(); 366 int dx = (int) (distanceX + mAccumulatedScrollErrorX); 367 int dy = (int) (distanceY + mAccumulatedScrollErrorY); 368 mAccumulatedScrollErrorX = distanceX + mAccumulatedScrollErrorX - dx; 369 mAccumulatedScrollErrorY = distanceY + mAccumulatedScrollErrorY - dy; 370 371 mExtraParamBundleScroll.putInt(DISTANCE_X, dx); 372 mExtraParamBundleScroll.putInt(DISTANCE_Y, dy); 373 assert mExtraParamBundleScroll.size() == 2; 374 375 if ((dx | dy) != 0) { 376 sendGesture(GESTURE_SCROLL_BY, 377 e2.getEventTime(), x, y, mExtraParamBundleScroll); 378 } 379 380 mMotionEventDelegate.invokeZoomPicker(); 381 382 return true; 383 } 384 385 @Override 386 public boolean onFling(MotionEvent e1, MotionEvent e2, 387 float velocityX, float velocityY) { 388 assert e1.getEventTime() <= e2.getEventTime(); 389 if (mSnapScrollController.isSnappingScrolls()) { 390 if (mSnapScrollController.isSnapHorizontal()) { 391 velocityY = 0; 392 } else { 393 velocityX = 0; 394 } 395 } 396 397 fling(e2.getEventTime(), (int) e1.getX(0), (int) e1.getY(0), 398 (int) velocityX, (int) velocityY); 399 return true; 400 } 401 402 @Override 403 public void onShowPress(MotionEvent e) { 404 mShowPressIsCalled = true; 405 sendMotionEventAsGesture(GESTURE_SHOW_PRESSED_STATE, e, null); 406 } 407 408 @Override 409 public boolean onSingleTapUp(MotionEvent e) { 410 if (isDistanceBetweenDownAndUpTooLong(e.getRawX(), e.getRawY())) { 411 sendTapCancelIfNecessary(e); 412 mIgnoreSingleTap = true; 413 return true; 414 } 415 // This is a hack to address the issue where user hovers 416 // over a link for longer than DOUBLE_TAP_TIMEOUT, then 417 // onSingleTapConfirmed() is not triggered. But we still 418 // want to trigger the tap event at UP. So we override 419 // onSingleTapUp() in this case. This assumes singleTapUp 420 // gets always called before singleTapConfirmed. 421 if (!mIgnoreSingleTap && !mLongPressDetector.isInLongPress()) { 422 if (e.getEventTime() - e.getDownTime() > DOUBLE_TAP_TIMEOUT) { 423 float x = e.getX(); 424 float y = e.getY(); 425 if (sendTapEndingEventAsGesture(GESTURE_SINGLE_TAP_UP, e, null)) { 426 mIgnoreSingleTap = true; 427 } 428 setClickXAndY((int) x, (int) y); 429 430 mMotionEventDelegate.sendSingleTapUMA( 431 isDoubleTapDisabled() ? 432 ContentViewCore.UMASingleTapType.UNDELAYED_TAP : 433 ContentViewCore.UMASingleTapType.DELAYED_TAP); 434 435 return true; 436 } else if (isDoubleTapDisabled() || mDisableClickDelay) { 437 // If double tap has been disabled, there is no need to wait 438 // for the double tap timeout. 439 return onSingleTapConfirmed(e); 440 } else { 441 // Notify Blink about this tapUp event anyway, 442 // when none of the above conditions applied. 443 sendMotionEventAsGesture(GESTURE_SINGLE_TAP_UNCONFIRMED, e, null); 444 } 445 } 446 447 return triggerLongTapIfNeeded(e); 448 } 449 450 @Override 451 public boolean onSingleTapConfirmed(MotionEvent e) { 452 // Long taps in the edges of the screen have their events delayed by 453 // ContentViewHolder for tab swipe operations. As a consequence of the delay 454 // this method might be called after receiving the up event. 455 // These corner cases should be ignored. 456 if (mLongPressDetector.isInLongPress() || mIgnoreSingleTap) return true; 457 458 mMotionEventDelegate.sendSingleTapUMA( 459 isDoubleTapDisabled() ? 460 ContentViewCore.UMASingleTapType.UNDELAYED_TAP : 461 ContentViewCore.UMASingleTapType.DELAYED_TAP); 462 463 int x = (int) e.getX(); 464 int y = (int) e.getY(); 465 mExtraParamBundleSingleTap.putBoolean(SHOW_PRESS, mShowPressIsCalled); 466 assert mExtraParamBundleSingleTap.size() == 1; 467 if (sendTapEndingEventAsGesture(GESTURE_SINGLE_TAP_CONFIRMED, e, 468 mExtraParamBundleSingleTap)) { 469 mIgnoreSingleTap = true; 470 } 471 472 setClickXAndY(x, y); 473 return true; 474 } 475 476 @Override 477 public boolean onDoubleTapEvent(MotionEvent e) { 478 switch (e.getActionMasked()) { 479 case MotionEvent.ACTION_DOWN: 480 sendTapCancelIfNecessary(e); 481 mDoubleTapDragZoomAnchorX = e.getX(); 482 mDoubleTapDragZoomAnchorY = e.getY(); 483 mDoubleTapMode = DOUBLE_TAP_MODE_DRAG_DETECTION_IN_PROGRESS; 484 break; 485 case MotionEvent.ACTION_MOVE: 486 if (mDoubleTapMode 487 == DOUBLE_TAP_MODE_DRAG_DETECTION_IN_PROGRESS) { 488 float distanceX = mDoubleTapDragZoomAnchorX - e.getX(); 489 float distanceY = mDoubleTapDragZoomAnchorY - e.getY(); 490 491 // Begin double tap drag zoom mode if the move distance is 492 // further than the threshold. 493 if (distanceX * distanceX + distanceY * distanceY > 494 mScaledTouchSlopSquare) { 495 sendTapCancelIfNecessary(e); 496 sendGesture(GESTURE_SCROLL_START, e.getEventTime(), 497 (int) e.getX(), (int) e.getY(), null); 498 pinchBegin(e.getEventTime(), 499 Math.round(mDoubleTapDragZoomAnchorX), 500 Math.round(mDoubleTapDragZoomAnchorY)); 501 mDoubleTapMode = DOUBLE_TAP_MODE_DRAG_ZOOM; 502 } 503 } else if (mDoubleTapMode == DOUBLE_TAP_MODE_DRAG_ZOOM) { 504 assert mExtraParamBundleDoubleTapDragZoom.isEmpty(); 505 sendGesture(GESTURE_SCROLL_BY, e.getEventTime(), 506 (int) e.getX(), (int) e.getY(), 507 mExtraParamBundleDoubleTapDragZoom); 508 509 float dy = mDoubleTapY - e.getY(); 510 pinchBy(e.getEventTime(), 511 Math.round(mDoubleTapDragZoomAnchorX), 512 Math.round(mDoubleTapDragZoomAnchorY), 513 (float) Math.pow(dy > 0 ? 514 1.0f - DOUBLE_TAP_DRAG_ZOOM_SPEED : 515 1.0f + DOUBLE_TAP_DRAG_ZOOM_SPEED, 516 Math.abs(dy * mPxToDp))); 517 } 518 break; 519 case MotionEvent.ACTION_UP: 520 if (mDoubleTapMode != DOUBLE_TAP_MODE_DRAG_ZOOM) { 521 // Normal double tap gesture. 522 sendTapEndingEventAsGesture(GESTURE_DOUBLE_TAP, e, null); 523 } 524 endDoubleTapDragIfNecessary(e); 525 break; 526 case MotionEvent.ACTION_CANCEL: 527 sendTapCancelIfNecessary(e); 528 endDoubleTapDragIfNecessary(e); 529 break; 530 default: 531 break; 532 } 533 mDoubleTapY = e.getY(); 534 return true; 535 } 536 537 @Override 538 public void onLongPress(MotionEvent e) { 539 if (!mZoomManager.isScaleGestureDetectionInProgress() && 540 (mDoubleTapMode == DOUBLE_TAP_MODE_NONE || 541 isDoubleTapDisabled())) { 542 mLastLongPressEvent = e; 543 sendMotionEventAsGesture(GESTURE_LONG_PRESS, e, null); 544 } 545 } 546 547 /** 548 * This method inspects the distance between where the user started touching 549 * the surface, and where she released. If the points are too far apart, we 550 * should assume that the web page has consumed the scroll-events in-between, 551 * and as such, this should not be considered a single-tap. 552 * 553 * We use the Android frameworks notion of how far a touch can wander before 554 * we think the user is scrolling. 555 * 556 * @param x the new x coordinate 557 * @param y the new y coordinate 558 * @return true if the distance is too long to be considered a single tap 559 */ 560 private boolean isDistanceBetweenDownAndUpTooLong(float x, float y) { 561 double deltaX = mLastRawX - x; 562 double deltaY = mLastRawY - y; 563 return deltaX * deltaX + deltaY * deltaY > mScaledTouchSlopSquare; 564 } 565 }; 566 mListener = listener; 567 mDoubleTapListener = listener; 568 mGestureDetector = new GestureDetector(context, listener); 569 mGestureDetector.setIsLongpressEnabled(false); 570 } finally { 571 TraceEvent.end(); 572 } 573 } 574 575 /** 576 * @return LongPressDetector handling setting up timers for and canceling LongPress gestures. 577 */ 578 LongPressDetector getLongPressDetector() { 579 return mLongPressDetector; 580 } 581 582 /** 583 * @param event Start a LongPress gesture event from the listener. 584 */ 585 @Override 586 public void onLongPress(MotionEvent event) { 587 mListener.onLongPress(event); 588 } 589 590 /** 591 * Cancels any ongoing LongPress timers. 592 */ 593 void cancelLongPress() { 594 mLongPressDetector.cancelLongPress(); 595 } 596 597 /** 598 * Fling the ContentView from the current position. 599 * @param x Fling touch starting position 600 * @param y Fling touch starting position 601 * @param velocityX Initial velocity of the fling (X) measured in pixels per second. 602 * @param velocityY Initial velocity of the fling (Y) measured in pixels per second. 603 */ 604 void fling(long timeMs, int x, int y, int velocityX, int velocityY) { 605 endFlingIfNecessary(timeMs); 606 607 if (velocityX == 0 && velocityY == 0) { 608 endTouchScrollIfNecessary(timeMs, true); 609 return; 610 } 611 612 if (!mTouchScrolling) { 613 // The native side needs a GESTURE_SCROLL_BEGIN before GESTURE_FLING_START 614 // to send the fling to the correct target. Send if it has not sent. 615 sendGesture(GESTURE_SCROLL_START, timeMs, x, y, null); 616 } 617 endTouchScrollIfNecessary(timeMs, false); 618 619 mFlingMayBeActive = true; 620 621 mExtraParamBundleFling.putInt(VELOCITY_X, velocityX); 622 mExtraParamBundleFling.putInt(VELOCITY_Y, velocityY); 623 assert mExtraParamBundleFling.size() == 2; 624 sendGesture(GESTURE_FLING_START, timeMs, x, y, mExtraParamBundleFling); 625 } 626 627 /** 628 * Send a GESTURE_FLING_CANCEL event if necessary. 629 * @param timeMs The time in ms for the event initiating this gesture. 630 */ 631 void endFlingIfNecessary(long timeMs) { 632 if (!mFlingMayBeActive) return; 633 mFlingMayBeActive = false; 634 sendGesture(GESTURE_FLING_CANCEL, timeMs, 0, 0, null); 635 } 636 637 /** 638 * End DOUBLE_TAP_MODE_DRAG_ZOOM by sending GESTURE_SCROLL_END and GESTURE_PINCH_END events. 639 * @param event A hint event that its x, y, and eventTime will be used for the ending events 640 * to send. This argument is an optional and can be null. 641 */ 642 void endDoubleTapDragIfNecessary(MotionEvent event) { 643 if (!isDoubleTapActive()) return; 644 if (mDoubleTapMode == DOUBLE_TAP_MODE_DRAG_ZOOM) { 645 if (event == null) event = obtainActionCancelMotionEvent(); 646 pinchEnd(event.getEventTime()); 647 sendGesture(GESTURE_SCROLL_END, event.getEventTime(), 648 (int) event.getX(), (int) event.getY(), null); 649 } 650 mDoubleTapMode = DOUBLE_TAP_MODE_NONE; 651 updateDoubleTapListener(); 652 } 653 654 /** 655 * Reset touch scroll flag and optionally send a GESTURE_SCROLL_END event if necessary. 656 * @param timeMs The time in ms for the event initiating this gesture. 657 * @param sendScrollEndEvent Whether to send GESTURE_SCROLL_END event. 658 */ 659 private void endTouchScrollIfNecessary(long timeMs, boolean sendScrollEndEvent) { 660 if (!mTouchScrolling) return; 661 mTouchScrolling = false; 662 if (sendScrollEndEvent) { 663 sendGesture(GESTURE_SCROLL_END, timeMs, 0, 0, null); 664 } 665 } 666 667 /** 668 * @return Whether native is tracking a scroll. 669 */ 670 boolean isNativeScrolling() { 671 // TODO(wangxianzhu): Also return true when fling is active once the UI knows exactly when 672 // the fling ends. 673 return mTouchScrolling; 674 } 675 676 /** 677 * @return Whether native is tracking a pinch (i.e. between sending GESTURE_PINCH_BEGIN and 678 * GESTURE_PINCH_END). 679 */ 680 boolean isNativePinching() { 681 return mPinchInProgress; 682 } 683 684 /** 685 * Starts a pinch gesture. 686 * @param timeMs The time in ms for the event initiating this gesture. 687 * @param x The x coordinate for the event initiating this gesture. 688 * @param y The x coordinate for the event initiating this gesture. 689 */ 690 void pinchBegin(long timeMs, int x, int y) { 691 sendGesture(GESTURE_PINCH_BEGIN, timeMs, x, y, null); 692 } 693 694 /** 695 * Pinch by a given percentage. 696 * @param timeMs The time in ms for the event initiating this gesture. 697 * @param anchorX The x coordinate for the anchor point to be used in pinch. 698 * @param anchorY The y coordinate for the anchor point to be used in pinch. 699 * @param delta The percentage to pinch by. 700 */ 701 void pinchBy(long timeMs, int anchorX, int anchorY, float delta) { 702 mExtraParamBundlePinchBy.putFloat(DELTA, delta); 703 assert mExtraParamBundlePinchBy.size() == 1; 704 sendGesture(GESTURE_PINCH_BY, timeMs, anchorX, anchorY, mExtraParamBundlePinchBy); 705 mPinchInProgress = true; 706 } 707 708 /** 709 * End a pinch gesture. 710 * @param timeMs The time in ms for the event initiating this gesture. 711 */ 712 void pinchEnd(long timeMs) { 713 sendGesture(GESTURE_PINCH_END, timeMs, 0, 0, null); 714 mPinchInProgress = false; 715 } 716 717 /** 718 * Ignore singleTap gestures. 719 */ 720 void setIgnoreSingleTap(boolean value) { 721 mIgnoreSingleTap = value; 722 } 723 724 private void setClickXAndY(int x, int y) { 725 mSingleTapX = x; 726 mSingleTapY = y; 727 } 728 729 /** 730 * @return The x coordinate for the last point that a singleTap gesture was initiated from. 731 */ 732 public int getSingleTapX() { 733 return mSingleTapX; 734 } 735 736 /** 737 * @return The y coordinate for the last point that a singleTap gesture was initiated from. 738 */ 739 public int getSingleTapY() { 740 return mSingleTapY; 741 } 742 743 /** 744 * Cancel the current touch event sequence by sending ACTION_CANCEL and ignore all the 745 * subsequent events until the next ACTION_DOWN. 746 * 747 * One example usecase is stop processing the touch events when showing context popup menu. 748 */ 749 public void setIgnoreRemainingTouchEvents() { 750 onTouchEvent(obtainActionCancelMotionEvent()); 751 mIgnoreRemainingTouchEvents = true; 752 } 753 754 /** 755 * Handle the incoming MotionEvent. 756 * @return Whether the event was handled. 757 */ 758 boolean onTouchEvent(MotionEvent event) { 759 try { 760 TraceEvent.begin("onTouchEvent"); 761 762 if (mIgnoreRemainingTouchEvents) { 763 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 764 mIgnoreRemainingTouchEvents = false; 765 } else { 766 return false; 767 } 768 } 769 770 mLongPressDetector.cancelLongPressIfNeeded(event); 771 mSnapScrollController.setSnapScrollingMode(event); 772 // Notify native that scrolling has stopped whenever a down action is processed prior to 773 // passing the event to native as it will drop them as an optimization if scrolling is 774 // enabled. Ending the fling ensures scrolling has stopped as well as terminating the 775 // current fling if applicable. 776 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 777 endFlingIfNecessary(event.getEventTime()); 778 } else if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { 779 endDoubleTapDragIfNecessary(null); 780 } 781 782 if (offerTouchEventToJavaScript(event)) { 783 // offerTouchEventToJavaScript returns true to indicate the event was sent 784 // to the render process. If it is not subsequently handled, it will 785 // be returned via confirmTouchEvent(false) and eventually passed to 786 // processTouchEvent asynchronously. 787 return true; 788 } 789 return processTouchEvent(event); 790 } finally { 791 TraceEvent.end("onTouchEvent"); 792 } 793 } 794 795 /** 796 * Handle content view losing focus -- ensure that any remaining active state is removed. 797 */ 798 void onWindowFocusLost() { 799 if (mLongPressDetector.isInLongPress() && mLastLongPressEvent != null) { 800 sendTapCancelIfNecessary(mLastLongPressEvent); 801 } 802 } 803 804 private MotionEvent obtainActionCancelMotionEvent() { 805 return MotionEvent.obtain( 806 SystemClock.uptimeMillis(), 807 SystemClock.uptimeMillis(), 808 MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); 809 } 810 811 /** 812 * Resets gesture handlers state; called on didStartLoading(). 813 * Note that this does NOT clear the pending motion events queue; 814 * it gets cleared in hasTouchEventHandlers() called from WebKit 815 * FrameLoader::transitionToCommitted iff the page ever had touch handlers. 816 */ 817 void resetGestureHandlers() { 818 MotionEvent me = obtainActionCancelMotionEvent(); 819 me.setSource(InputDevice.SOURCE_CLASS_POINTER); 820 mGestureDetector.onTouchEvent(me); 821 mZoomManager.processTouchEvent(me); 822 me.recycle(); 823 mLongPressDetector.cancelLongPress(); 824 } 825 826 /** 827 * Sets the flag indicating that the content has registered listeners for touch events. 828 */ 829 void hasTouchEventHandlers(boolean hasTouchHandlers) { 830 if (hasTouchHandlers) { 831 // If no touch handler was previously registered, ensure that we 832 // don't send a partial gesture to Javascript. 833 if (mTouchHandlingState == NO_TOUCH_HANDLER) 834 mTouchHandlingState = NO_TOUCH_HANDLER_FOR_GESTURE; 835 } else { 836 // When mainframe is loading, FrameLoader::transitionToCommitted will 837 // call this method with |hasTouchHandlers| of false. We use this as 838 // an indicator to clear the pending motion events so that events from 839 // the previous page will not be carried over to the new page. 840 mTouchHandlingState = NO_TOUCH_HANDLER; 841 mPendingMotionEvents.clear(); 842 } 843 } 844 845 private boolean offerTouchEventToJavaScript(MotionEvent event) { 846 if (mTouchHandlingState == NO_TOUCH_HANDLER) return false; 847 848 if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { 849 // Avoid flooding the renderer process with move events: if the previous pending 850 // command is also a move (common case) that has not yet been forwarded, skip sending 851 // this event to the webkit side and collapse it into the pending event. 852 MotionEvent previousEvent = mPendingMotionEvents.peekLast(); 853 if (previousEvent != null 854 && previousEvent != mPendingMotionEvents.peekFirst() 855 && previousEvent.getActionMasked() == MotionEvent.ACTION_MOVE 856 && previousEvent.getPointerCount() == event.getPointerCount()) { 857 TraceEvent.instant("offerTouchEventToJavaScript:EventCoalesced", 858 "QueueSize = " + mPendingMotionEvents.size()); 859 MotionEvent.PointerCoords[] coords = 860 new MotionEvent.PointerCoords[event.getPointerCount()]; 861 for (int i = 0; i < coords.length; ++i) { 862 coords[i] = new MotionEvent.PointerCoords(); 863 event.getPointerCoords(i, coords[i]); 864 } 865 previousEvent.addBatch(event.getEventTime(), coords, event.getMetaState()); 866 return true; 867 } 868 } 869 if (mPendingMotionEvents.isEmpty()) { 870 // Add the event to the pending queue prior to calling sendPendingEventToNative. 871 // When sending an event to native, the callback to confirmTouchEvent can be 872 // synchronous or asynchronous and confirmTouchEvent expects the event to be 873 // in the queue when it is called. 874 MotionEvent clone = MotionEvent.obtain(event); 875 mPendingMotionEvents.add(clone); 876 877 int forward = sendPendingEventToNative(); 878 if (forward != EVENT_FORWARDED_TO_NATIVE) mPendingMotionEvents.remove(clone); 879 return forward != EVENT_NOT_FORWARDED; 880 } else { 881 TraceEvent.instant("offerTouchEventToJavaScript:EventQueued", 882 "QueueSize = " + mPendingMotionEvents.size()); 883 // Copy the event, as the original may get mutated after this method returns. 884 MotionEvent clone = MotionEvent.obtain(event); 885 mPendingMotionEvents.add(clone); 886 return true; 887 } 888 } 889 890 private int sendPendingEventToNative() { 891 MotionEvent event = mPendingMotionEvents.peekFirst(); 892 if (event == null) { 893 assert false : "Cannot send from an empty pending event queue"; 894 return EVENT_NOT_FORWARDED; 895 } 896 assert mTouchHandlingState != NO_TOUCH_HANDLER; 897 898 // The start of a new (multi)touch sequence will reset the touch handling state, and 899 // should always be offered to Javascript (when there is any touch handler). 900 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 901 mTouchHandlingState = HAS_TOUCH_HANDLER; 902 if (mCurrentDownEvent != null) recycleEvent(mCurrentDownEvent); 903 mCurrentDownEvent = null; 904 } 905 906 mLongPressDetector.onOfferTouchEventToJavaScript(event); 907 908 if (mTouchHandlingState == NO_TOUCH_HANDLER_FOR_GESTURE) return EVENT_NOT_FORWARDED; 909 910 if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { 911 // If javascript has not yet prevent-defaulted the touch sequence, 912 // only send move events if the move has exceeded the slop threshold. 913 boolean moveEventConfirmed = 914 mLongPressDetector.confirmOfferMoveEventToJavaScript(event); 915 if (mTouchHandlingState != JAVASCRIPT_CONSUMING_GESTURE 916 && !moveEventConfirmed) { 917 return EVENT_DROPPED; 918 } 919 } 920 921 if (mTouchScrolling || mPinchInProgress) return EVENT_NOT_FORWARDED; 922 923 TouchPoint[] pts = new TouchPoint[event.getPointerCount()]; 924 int type = TouchPoint.createTouchPoints(event, pts); 925 926 if (type == TouchPoint.CONVERSION_ERROR) return EVENT_NOT_FORWARDED; 927 928 if (mMotionEventDelegate.sendTouchEvent(event.getEventTime(), type, pts)) { 929 return EVENT_FORWARDED_TO_NATIVE; 930 } 931 return EVENT_NOT_FORWARDED; 932 } 933 934 private boolean processTouchEvent(MotionEvent event) { 935 boolean handled = false; 936 // The last "finger up" is an end to scrolling but may not be 937 // an end to movement (e.g. fling scroll). We do not tell 938 // native code to end scrolling until we are sure we did not 939 // fling. 940 boolean possiblyEndMovement = false; 941 // "Last finger raised" could be an end to movement. However, 942 // give the mSimpleTouchDetector a chance to continue 943 // scrolling with a fling. 944 if (event.getAction() == MotionEvent.ACTION_UP 945 || event.getAction() == MotionEvent.ACTION_CANCEL) { 946 if (mTouchScrolling) { 947 possiblyEndMovement = true; 948 } 949 } 950 951 mLongPressDetector.cancelLongPressIfNeeded(event); 952 mLongPressDetector.startLongPressTimerIfNeeded(event); 953 954 // Use the framework's GestureDetector to detect pans and zooms not already 955 // handled by the WebKit touch events gesture manager. 956 if (canHandle(event)) { 957 handled |= mGestureDetector.onTouchEvent(event); 958 if (event.getAction() == MotionEvent.ACTION_DOWN) { 959 mCurrentDownEvent = MotionEvent.obtain(event); 960 } 961 } 962 963 handled |= mZoomManager.processTouchEvent(event); 964 965 if (possiblyEndMovement && !handled) { 966 endTouchScrollIfNecessary(event.getEventTime(), true); 967 } 968 969 return handled; 970 } 971 972 /** 973 * Respond to a MotionEvent being returned from the native side. 974 * @param ackResult The status acknowledgment code. 975 */ 976 void confirmTouchEvent(int ackResult) { 977 try { 978 TraceEvent.begin("confirmTouchEvent"); 979 980 if (mPendingMotionEvents.isEmpty()) { 981 Log.w(TAG, "confirmTouchEvent with Empty pending list!"); 982 return; 983 } 984 assert mTouchHandlingState != NO_TOUCH_HANDLER; 985 assert mTouchHandlingState != NO_TOUCH_HANDLER_FOR_GESTURE; 986 987 MotionEvent ackedEvent = mPendingMotionEvents.removeFirst(); 988 switch (ackResult) { 989 case INPUT_EVENT_ACK_STATE_UNKNOWN: 990 // This should never get sent. 991 assert (false); 992 break; 993 case INPUT_EVENT_ACK_STATE_CONSUMED: 994 case INPUT_EVENT_ACK_STATE_IGNORED: 995 if (mTouchHandlingState != JAVASCRIPT_CONSUMING_GESTURE 996 && ackedEvent.getActionMasked() != MotionEvent.ACTION_DOWN) { 997 sendTapCancelIfNecessary(ackedEvent); 998 resetGestureHandlers(); 999 } else { 1000 mZoomManager.passTouchEventThrough(ackedEvent); 1001 } 1002 mTouchHandlingState = JAVASCRIPT_CONSUMING_GESTURE; 1003 trySendPendingEventsToNative(); 1004 break; 1005 case INPUT_EVENT_ACK_STATE_NOT_CONSUMED: 1006 if (mTouchHandlingState != JAVASCRIPT_CONSUMING_GESTURE) { 1007 processTouchEvent(ackedEvent); 1008 } 1009 trySendPendingEventsToNative(); 1010 break; 1011 case INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS: 1012 if (mTouchHandlingState != JAVASCRIPT_CONSUMING_GESTURE) { 1013 processTouchEvent(ackedEvent); 1014 } 1015 if (ackedEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { 1016 drainAllPendingEventsUntilNextDown(); 1017 } else { 1018 trySendPendingEventsToNative(); 1019 } 1020 break; 1021 default: 1022 break; 1023 } 1024 1025 mLongPressDetector.cancelLongPressIfNeeded(mPendingMotionEvents.iterator()); 1026 recycleEvent(ackedEvent); 1027 } finally { 1028 TraceEvent.end("confirmTouchEvent"); 1029 } 1030 } 1031 1032 private void trySendPendingEventsToNative() { 1033 while (!mPendingMotionEvents.isEmpty()) { 1034 int forward = sendPendingEventToNative(); 1035 if (forward == EVENT_FORWARDED_TO_NATIVE) break; 1036 1037 // Even though we missed sending one event to native, as long as we haven't 1038 // received INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, we should keep sending 1039 // events on the queue to native. 1040 MotionEvent event = mPendingMotionEvents.removeFirst(); 1041 if (mTouchHandlingState != JAVASCRIPT_CONSUMING_GESTURE 1042 && forward != EVENT_DROPPED) { 1043 processTouchEvent(event); 1044 } 1045 recycleEvent(event); 1046 } 1047 } 1048 1049 private void drainAllPendingEventsUntilNextDown() { 1050 assert mTouchHandlingState == HAS_TOUCH_HANDLER; 1051 mTouchHandlingState = NO_TOUCH_HANDLER_FOR_GESTURE; 1052 1053 // Now process all events that are in the queue until the next down event. 1054 MotionEvent nextEvent = mPendingMotionEvents.peekFirst(); 1055 while (nextEvent != null && nextEvent.getActionMasked() != MotionEvent.ACTION_DOWN) { 1056 processTouchEvent(nextEvent); 1057 mPendingMotionEvents.removeFirst(); 1058 recycleEvent(nextEvent); 1059 nextEvent = mPendingMotionEvents.peekFirst(); 1060 } 1061 1062 trySendPendingEventsToNative(); 1063 } 1064 1065 private void recycleEvent(MotionEvent event) { 1066 event.recycle(); 1067 } 1068 1069 private boolean sendMotionEventAsGesture( 1070 int type, MotionEvent event, Bundle extraParams) { 1071 return sendGesture(type, event.getEventTime(), 1072 (int) event.getX(), (int) event.getY(), extraParams); 1073 } 1074 1075 private boolean sendGesture( 1076 int type, long timeMs, int x, int y, Bundle extraParams) { 1077 assert timeMs != 0; 1078 updateDoubleTapUmaTimer(); 1079 1080 if (type == GESTURE_DOUBLE_TAP) reportDoubleTap(); 1081 1082 return mMotionEventDelegate.sendGesture(type, timeMs, x, y, extraParams); 1083 } 1084 1085 private boolean sendTapEndingEventAsGesture(int type, MotionEvent e, Bundle extraParams) { 1086 if (!sendMotionEventAsGesture(type, e, extraParams)) return false; 1087 mNeedsTapEndingEvent = false; 1088 return true; 1089 } 1090 1091 private void sendTapCancelIfNecessary(MotionEvent e) { 1092 if (!mNeedsTapEndingEvent) return; 1093 if (!sendTapEndingEventAsGesture(GESTURE_TAP_CANCEL, e, null)) return; 1094 mLastLongPressEvent = null; 1095 } 1096 1097 /** 1098 * @return Whether the ContentViewGestureHandler can handle a MotionEvent right now. True only 1099 * if it's the start of a new stream (ACTION_DOWN), or a continuation of the current stream. 1100 */ 1101 boolean canHandle(MotionEvent ev) { 1102 return ev.getAction() == MotionEvent.ACTION_DOWN || 1103 (mCurrentDownEvent != null && mCurrentDownEvent.getDownTime() == ev.getDownTime()); 1104 } 1105 1106 /** 1107 * @return Whether the event can trigger a LONG_TAP gesture. True when it can and the event 1108 * will be consumed. 1109 */ 1110 boolean triggerLongTapIfNeeded(MotionEvent ev) { 1111 if (mLongPressDetector.isInLongPress() && ev.getAction() == MotionEvent.ACTION_UP && 1112 !mZoomManager.isScaleGestureDetectionInProgress()) { 1113 sendTapCancelIfNecessary(ev); 1114 sendMotionEventAsGesture(GESTURE_LONG_TAP, ev, null); 1115 return true; 1116 } 1117 return false; 1118 } 1119 1120 /** 1121 * This is for testing only. 1122 * @return The first motion event on the pending motion events queue. 1123 */ 1124 MotionEvent peekFirstInPendingMotionEventsForTesting() { 1125 return mPendingMotionEvents.peekFirst(); 1126 } 1127 1128 /** 1129 * This is for testing only. 1130 * @return The number of motion events on the pending motion events queue. 1131 */ 1132 int getNumberOfPendingMotionEventsForTesting() { 1133 return mPendingMotionEvents.size(); 1134 } 1135 1136 /** 1137 * This is for testing only. 1138 * Sends a show pressed state gesture through mListener. This should always be called after 1139 * a down event; 1140 */ 1141 void sendShowPressedStateGestureForTesting() { 1142 if (mCurrentDownEvent == null) return; 1143 mListener.onShowPress(mCurrentDownEvent); 1144 } 1145 1146 /** 1147 * This is for testing only. 1148 * @return Whether a sent TapDown event has been accompanied by a tap-ending event. 1149 */ 1150 boolean needsTapEndingEventForTesting() { 1151 return mNeedsTapEndingEvent; 1152 } 1153 1154 /** 1155 * Update whether double-tap gestures are supported. This allows 1156 * double-tap gesture suppression independent of whether or not the page's 1157 * viewport and scale would normally prevent double-tap. 1158 * Note: This should never be called while a double-tap gesture is in progress. 1159 * @param supportDoubleTap Whether double-tap gestures are supported. 1160 */ 1161 public void updateDoubleTapSupport(boolean supportDoubleTap) { 1162 assert !isDoubleTapActive(); 1163 int doubleTapMode = supportDoubleTap ? 1164 DOUBLE_TAP_MODE_NONE : DOUBLE_TAP_MODE_DISABLED; 1165 if (mDoubleTapMode == doubleTapMode) return; 1166 mDoubleTapMode = doubleTapMode; 1167 updateDoubleTapListener(); 1168 } 1169 1170 /** 1171 * Update whether double-tap gesture detection should be suppressed due to 1172 * the viewport or scale of the current page. Suppressing double-tap gesture 1173 * detection allows for rapid and responsive single-tap gestures. 1174 * @param shouldDisableDoubleTap Whether double-tap should be suppressed. 1175 */ 1176 public void updateShouldDisableDoubleTap(boolean shouldDisableDoubleTap) { 1177 if (mShouldDisableDoubleTap == shouldDisableDoubleTap) return; 1178 mShouldDisableDoubleTap = shouldDisableDoubleTap; 1179 updateDoubleTapListener(); 1180 } 1181 1182 private boolean isDoubleTapDisabled() { 1183 return mDoubleTapMode == DOUBLE_TAP_MODE_DISABLED || mShouldDisableDoubleTap; 1184 } 1185 1186 private boolean isDoubleTapActive() { 1187 return mDoubleTapMode != DOUBLE_TAP_MODE_DISABLED && 1188 mDoubleTapMode != DOUBLE_TAP_MODE_NONE; 1189 } 1190 1191 private void updateDoubleTapListener() { 1192 if (isDoubleTapDisabled()) { 1193 // Defer nulling the DoubleTapListener until the double tap gesture is complete. 1194 if (isDoubleTapActive()) return; 1195 mGestureDetector.setOnDoubleTapListener(null); 1196 } else { 1197 mGestureDetector.setOnDoubleTapListener(mDoubleTapListener); 1198 } 1199 1200 } 1201 1202 private void reportDoubleTap() { 1203 // Make sure repeated double taps don't get silently dropped from 1204 // the statistics. 1205 if (mLastDoubleTapTimeMs > 0) { 1206 mMotionEventDelegate.sendActionAfterDoubleTapUMA( 1207 ContentViewCore.UMAActionAfterDoubleTap.NO_ACTION, !mDisableClickDelay); 1208 } 1209 1210 mLastDoubleTapTimeMs = SystemClock.uptimeMillis(); 1211 } 1212 1213 /** 1214 * Update the UMA stat tracking accidental double tap navigations with a user action. 1215 * @param type The action the user performed, one of the UMAActionAfterDoubleTap values 1216 * defined in ContentViewCore. 1217 */ 1218 public void reportActionAfterDoubleTapUMA(int type) { 1219 updateDoubleTapUmaTimer(); 1220 1221 if (mLastDoubleTapTimeMs == 0) return; 1222 1223 long nowMs = SystemClock.uptimeMillis(); 1224 if ((nowMs - mLastDoubleTapTimeMs) < ACTION_AFTER_DOUBLE_TAP_WINDOW_MS) { 1225 mMotionEventDelegate.sendActionAfterDoubleTapUMA(type, !mDisableClickDelay); 1226 mLastDoubleTapTimeMs = 0; 1227 } 1228 } 1229 1230 // Watch for the UMA "action after double tap" timer expiring and reset 1231 // the timer if necessary. 1232 private void updateDoubleTapUmaTimer() { 1233 if (mLastDoubleTapTimeMs == 0) return; 1234 1235 long nowMs = SystemClock.uptimeMillis(); 1236 if ((nowMs - mLastDoubleTapTimeMs) >= ACTION_AFTER_DOUBLE_TAP_WINDOW_MS) { 1237 // Time expired, user took no action (that we care about). 1238 mMotionEventDelegate.sendActionAfterDoubleTapUMA( 1239 ContentViewCore.UMAActionAfterDoubleTap.NO_ACTION, !mDisableClickDelay); 1240 mLastDoubleTapTimeMs = 0; 1241 } 1242 } 1243} 1244