- add sources.
[platform/framework/web/crosswalk.git] / src / content / public / android / java / src / org / chromium / content / browser / LongPressDetector.java
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
5 package org.chromium.content.browser;
6
7 import android.content.Context;
8 import android.os.Handler;
9 import android.os.Message;
10 import android.view.MotionEvent;
11 import android.view.ViewConfiguration;
12
13 import java.util.Iterator;
14
15 /**
16  * This class controls long press timers and is owned by a ContentViewGestureHandler.
17  *
18  * For instance, we may receive a DOWN then UP, so we may need to cancel the
19  * timer before the UP completes its roundtrip from WebKit.
20  */
21 class LongPressDetector {
22     private MotionEvent mCurrentDownEvent;
23     private final LongPressDelegate mLongPressDelegate;
24     private final Handler mLongPressHandler;
25     private final int mTouchSlopSquare;
26     private boolean mInLongPress;
27
28     // The following are used when touch events are offered to native, and not for
29     // anything relating to the GestureDetector.
30     // True iff a touch_move has exceeded the touch slop distance.
31     private boolean mMoveConfirmed;
32     // Coordinates of the start of a touch event (i.e. the touch_down).
33     private int mTouchInitialX;
34     private int mTouchInitialY;
35
36     private static final int LONG_PRESS = 2;
37
38     private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
39     private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
40
41     LongPressDetector(Context context, LongPressDelegate delegate) {
42         mLongPressDelegate = delegate;
43         mLongPressHandler = new LongPressHandler();
44         final ViewConfiguration configuration = ViewConfiguration.get(context);
45         int touchSlop = configuration.getScaledTouchSlop();
46         mTouchSlopSquare = touchSlop * touchSlop;
47     }
48
49     private class LongPressHandler extends Handler {
50         LongPressHandler() {
51             super();
52         }
53
54         @Override
55         public void handleMessage(Message msg) {
56             switch (msg.what) {
57             case LONG_PRESS:
58                 dispatchLongPress();
59                 break;
60             default:
61                 throw new RuntimeException("Unknown message " + msg); //never
62             }
63         }
64     }
65
66     /**
67      * This is an interface to execute the LongPress when it receives the onLongPress message.
68      */
69     interface LongPressDelegate {
70         /**
71          * @param event The event will be recycled after this call has returned.
72          */
73         public void onLongPress(MotionEvent event);
74     }
75
76     private static long calculateLongPressTimeoutTime(MotionEvent ev) {
77         // Using getEventTime instead of getDownTime since for Android WebView,
78         // event stream can be arbitrarily delayed.
79         return ev.getEventTime() + TAP_TIMEOUT + LONGPRESS_TIMEOUT;
80     }
81
82     /**
83      * Initiates a LONG_PRESS gesture timer if needed.
84      */
85     void startLongPressTimerIfNeeded(MotionEvent ev) {
86         if (ev.getAction() != MotionEvent.ACTION_DOWN) return;
87
88         // If there is a current down, we do not expect another down event before
89         // receiving an up event
90         if (mCurrentDownEvent != null) return;
91
92         mCurrentDownEvent = MotionEvent.obtain(ev);
93         mLongPressHandler.sendEmptyMessageAtTime(LONG_PRESS,
94                 calculateLongPressTimeoutTime(mCurrentDownEvent));
95         mInLongPress = false;
96     }
97
98     // Cancel LONG_PRESS timers.
99     void cancelLongPressIfNeeded(MotionEvent ev) {
100         if (!hasPendingMessage() ||
101             mCurrentDownEvent == null || ev.getDownTime() != mCurrentDownEvent.getDownTime()) {
102             return;
103         }
104         final int action = ev.getAction();
105         final float y = ev.getY();
106         final float x = ev.getX();
107         switch (action & MotionEvent.ACTION_MASK) {
108             case MotionEvent.ACTION_MOVE:
109                 final int deltaX = (int) (x - mCurrentDownEvent.getX());
110                 final int deltaY = (int) (y - mCurrentDownEvent.getY());
111                 int distance = (deltaX * deltaX) + (deltaY * deltaY);
112                 if (distance > mTouchSlopSquare) {
113                     cancelLongPress();
114                 }
115                 break;
116             case MotionEvent.ACTION_UP:
117             case MotionEvent.ACTION_CANCEL:
118                 if (calculateLongPressTimeoutTime(mCurrentDownEvent) > ev.getEventTime()) {
119                     cancelLongPress();
120                 }
121                 break;
122             default:
123                 break;
124         }
125     }
126
127     // Given a stream of pending events, cancel the LONG_PRESS timer if appropriate.
128     void cancelLongPressIfNeeded(Iterator<MotionEvent> pendingEvents) {
129         if (mCurrentDownEvent == null)
130             return;
131         long currentDownTime = mCurrentDownEvent.getDownTime();
132         while (pendingEvents.hasNext()) {
133             MotionEvent pending = pendingEvents.next();
134             if (pending.getDownTime() != currentDownTime) {
135                 break;
136             }
137             cancelLongPressIfNeeded(pending);
138         }
139     }
140
141     void cancelLongPress() {
142         mInLongPress = false;
143         if (hasPendingMessage()) {
144             mLongPressHandler.removeMessages(LONG_PRESS);
145             mCurrentDownEvent.recycle();
146             mCurrentDownEvent = null;
147         }
148     }
149
150     // Used this to check if a onSingleTapUp is part of a long press event.
151     boolean isInLongPress() {
152         return mInLongPress;
153     }
154
155     private void dispatchLongPress() {
156         mInLongPress = true;
157         mLongPressDelegate.onLongPress(mCurrentDownEvent);
158         mCurrentDownEvent.recycle();
159         mCurrentDownEvent = null;
160     }
161
162     boolean hasPendingMessage() {
163         return mCurrentDownEvent != null;
164     }
165
166     void onOfferTouchEventToJavaScript(MotionEvent event) {
167         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
168             mMoveConfirmed = false;
169             mTouchInitialX = Math.round(event.getX());
170             mTouchInitialY = Math.round(event.getY());
171         }
172     }
173
174     boolean confirmOfferMoveEventToJavaScript(MotionEvent event) {
175         if (!mMoveConfirmed) {
176             int deltaX = Math.round(event.getX()) - mTouchInitialX;
177             int deltaY = Math.round(event.getY()) - mTouchInitialY;
178             if (deltaX * deltaX + deltaY * deltaY >= mTouchSlopSquare) {
179                 mMoveConfirmed = true;
180             }
181         }
182         return mMoveConfirmed;
183     }
184
185     /**
186      * This is for testing only.
187      * Sends a LongPress gesture. This should always be called after a down event.
188      */
189     void sendLongPressGestureForTest() {
190         if (mCurrentDownEvent == null) return;
191         dispatchLongPress();
192     }
193 }