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