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