1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS;
20 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP;
21 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
22 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL;
23 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
24 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.annotation.UiContext;
29 import android.compat.annotation.UnsupportedAppUsage;
30 import android.content.Context;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.Message;
35 import android.os.StrictMode;
36 import android.os.SystemClock;
37 
38 import com.android.internal.util.FrameworkStatsLog;
39 
40 /**
41  * Detects various gestures and events using the supplied {@link MotionEvent}s.
42  * The {@link OnGestureListener} callback will notify users when a particular
43  * motion event has occurred. This class should only be used with {@link MotionEvent}s
44  * reported via touch (don't use for trackball events).
45  *
46  * To use this class:
47  * <ul>
48  *  <li>Create an instance of the {@code GestureDetector} for your {@link View}
49  *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
50  *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
51  *          will be executed when the events occur.
52  *  <li>If listening for {@link OnContextClickListener#onContextClick(MotionEvent)}
53  *          you must call {@link #onGenericMotionEvent(MotionEvent)}
54  *          in {@link View#onGenericMotionEvent(MotionEvent)}.
55  * </ul>
56  */
57 public class GestureDetector {
58     /**
59      * The listener that is used to notify when gestures occur.
60      * If you want to listen for all the different gestures then implement
61      * this interface. If you only want to listen for a subset it might
62      * be easier to extend {@link SimpleOnGestureListener}.
63      */
64     public interface OnGestureListener {
65 
66         /**
67          * Notified when a tap occurs with the down {@link MotionEvent}
68          * that triggered it. This will be triggered immediately for
69          * every down event. All other events should be preceded by this.
70          *
71          * @param e The down motion event.
72          */
onDown(@onNull MotionEvent e)73         boolean onDown(@NonNull MotionEvent e);
74 
75         /**
76          * The user has performed a down {@link MotionEvent} and not performed
77          * a move or up yet. This event is commonly used to provide visual
78          * feedback to the user to let them know that their action has been
79          * recognized i.e. highlight an element.
80          *
81          * @param e The down motion event
82          */
onShowPress(@onNull MotionEvent e)83         void onShowPress(@NonNull MotionEvent e);
84 
85         /**
86          * Notified when a tap occurs with the up {@link MotionEvent}
87          * that triggered it.
88          *
89          * @param e The up motion event that completed the first tap
90          * @return true if the event is consumed, else false
91          */
onSingleTapUp(@onNull MotionEvent e)92         boolean onSingleTapUp(@NonNull MotionEvent e);
93 
94         /**
95          * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
96          * current move {@link MotionEvent}. The distance in x and y is also supplied for
97          * convenience.
98          *
99          * @param e1 The first down motion event that started the scrolling. A {@code null} event
100          *           indicates an incomplete event stream or error state.
101          * @param e2 The move motion event that triggered the current onScroll.
102          * @param distanceX The distance along the X axis that has been scrolled since the last
103          *              call to onScroll. This is NOT the distance between {@code e1}
104          *              and {@code e2}.
105          * @param distanceY The distance along the Y axis that has been scrolled since the last
106          *              call to onScroll. This is NOT the distance between {@code e1}
107          *              and {@code e2}.
108          * @return true if the event is consumed, else false
109          */
onScroll(@ullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY)110         boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
111                 float distanceY);
112 
113         /**
114          * Notified when a long press occurs with the initial on down {@link MotionEvent}
115          * that trigged it.
116          *
117          * @param e The initial on down motion event that started the longpress.
118          */
onLongPress(@onNull MotionEvent e)119         void onLongPress(@NonNull MotionEvent e);
120 
121         /**
122          * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
123          * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
124          * the x and y axis in pixels per second.
125          *
126          * @param e1 The first down motion event that started the fling. A {@code null} event
127          *           indicates an incomplete event stream or error state.
128          * @param e2 The move motion event that triggered the current onFling.
129          * @param velocityX The velocity of this fling measured in pixels per second
130          *              along the x axis.
131          * @param velocityY The velocity of this fling measured in pixels per second
132          *              along the y axis.
133          * @return true if the event is consumed, else false
134          */
onFling(@ullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY)135         boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
136                 float velocityY);
137     }
138 
139     /**
140      * The listener that is used to notify when a double-tap or a confirmed
141      * single-tap occur.
142      */
143     public interface OnDoubleTapListener {
144         /**
145          * Notified when a single-tap occurs.
146          * <p>
147          * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
148          * will only be called after the detector is confident that the user's
149          * first tap is not followed by a second tap leading to a double-tap
150          * gesture.
151          *
152          * @param e The down motion event of the single-tap.
153          * @return true if the event is consumed, else false
154          */
onSingleTapConfirmed(@onNull MotionEvent e)155         boolean onSingleTapConfirmed(@NonNull MotionEvent e);
156 
157         /**
158          * Notified when a double-tap occurs. Triggered on the down event of second tap.
159          *
160          * @param e The down motion event of the first tap of the double-tap.
161          * @return true if the event is consumed, else false
162          */
onDoubleTap(@onNull MotionEvent e)163         boolean onDoubleTap(@NonNull MotionEvent e);
164 
165         /**
166          * Notified when an event within a double-tap gesture occurs, including
167          * the down, move, and up events.
168          *
169          * @param e The motion event that occurred during the double-tap gesture.
170          * @return true if the event is consumed, else false
171          */
onDoubleTapEvent(@onNull MotionEvent e)172         boolean onDoubleTapEvent(@NonNull MotionEvent e);
173     }
174 
175     /**
176      * The listener that is used to notify when a context click occurs. When listening for a
177      * context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
178      * {@link View#onGenericMotionEvent(MotionEvent)}.
179      */
180     public interface OnContextClickListener {
181         /**
182          * Notified when a context click occurs.
183          *
184          * @param e The motion event that occurred during the context click.
185          * @return true if the event is consumed, else false
186          */
onContextClick(@onNull MotionEvent e)187         boolean onContextClick(@NonNull MotionEvent e);
188     }
189 
190     /**
191      * A convenience class to extend when you only want to listen for a subset
192      * of all the gestures. This implements all methods in the
193      * {@link OnGestureListener}, {@link OnDoubleTapListener}, and {@link OnContextClickListener}
194      * but does nothing and return {@code false} for all applicable methods.
195      */
196     public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
197             OnContextClickListener {
198 
onSingleTapUp(@onNull MotionEvent e)199         public boolean onSingleTapUp(@NonNull MotionEvent e) {
200             return false;
201         }
202 
onLongPress(@onNull MotionEvent e)203         public void onLongPress(@NonNull MotionEvent e) {
204         }
205 
onScroll(@ullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY)206         public boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2,
207                 float distanceX, float distanceY) {
208             return false;
209         }
210 
onFling(@ullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY)211         public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
212                 float velocityY) {
213             return false;
214         }
215 
onShowPress(@onNull MotionEvent e)216         public void onShowPress(@NonNull MotionEvent e) {
217         }
218 
onDown(@onNull MotionEvent e)219         public boolean onDown(@NonNull MotionEvent e) {
220             return false;
221         }
222 
onDoubleTap(@onNull MotionEvent e)223         public boolean onDoubleTap(@NonNull MotionEvent e) {
224             return false;
225         }
226 
onDoubleTapEvent(@onNull MotionEvent e)227         public boolean onDoubleTapEvent(@NonNull MotionEvent e) {
228             return false;
229         }
230 
onSingleTapConfirmed(@onNull MotionEvent e)231         public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
232             return false;
233         }
234 
onContextClick(@onNull MotionEvent e)235         public boolean onContextClick(@NonNull MotionEvent e) {
236             return false;
237         }
238     }
239 
240     private static final String TAG = GestureDetector.class.getSimpleName();
241     @UnsupportedAppUsage
242     private int mTouchSlopSquare;
243     private int mDoubleTapTouchSlopSquare;
244     private int mDoubleTapSlopSquare;
245     private float mAmbiguousGestureMultiplier;
246     @UnsupportedAppUsage
247     private int mMinimumFlingVelocity;
248     private int mMaximumFlingVelocity;
249 
250     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
251     private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
252     private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
253     private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
254     private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();
255 
256     // constants for Message.what used by GestureHandler below
257     private static final int SHOW_PRESS = 1;
258     private static final int LONG_PRESS = 2;
259     private static final int TAP = 3;
260 
261     private final Handler mHandler;
262     @UnsupportedAppUsage
263     private final OnGestureListener mListener;
264     private OnDoubleTapListener mDoubleTapListener;
265     private OnContextClickListener mContextClickListener;
266 
267     private boolean mStillDown;
268     private boolean mDeferConfirmSingleTap;
269     private boolean mInLongPress;
270     private boolean mInContextClick;
271     @UnsupportedAppUsage
272     private boolean mAlwaysInTapRegion;
273     private boolean mAlwaysInBiggerTapRegion;
274     private boolean mIgnoreNextUpEvent;
275     // Whether a classification has been recorded by statsd for the current event stream. Reset on
276     // ACTION_DOWN.
277     private boolean mHasRecordedClassification;
278 
279     private MotionEvent mCurrentDownEvent;
280     private MotionEvent mCurrentMotionEvent;
281     private MotionEvent mPreviousUpEvent;
282 
283     /**
284      * True when the user is still touching for the second tap (down, move, and
285      * up events). Can only be true if there is a double tap listener attached.
286      */
287     private boolean mIsDoubleTapping;
288 
289     private float mLastFocusX;
290     private float mLastFocusY;
291     private float mDownFocusX;
292     private float mDownFocusY;
293 
294     private boolean mIsLongpressEnabled;
295 
296     /**
297      * Determines speed during touch scrolling
298      */
299     private VelocityTracker mVelocityTracker;
300 
301     /**
302      * Consistency verifier for debugging purposes.
303      */
304     private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
305             InputEventConsistencyVerifier.isInstrumentationEnabled() ?
306                     new InputEventConsistencyVerifier(this, 0) : null;
307 
308     private class GestureHandler extends Handler {
GestureHandler()309         GestureHandler() {
310             super();
311         }
312 
GestureHandler(Handler handler)313         GestureHandler(Handler handler) {
314             super(handler.getLooper());
315         }
316 
317         @Override
handleMessage(Message msg)318         public void handleMessage(Message msg) {
319             switch (msg.what) {
320                 case SHOW_PRESS:
321                     mListener.onShowPress(mCurrentDownEvent);
322                     break;
323 
324                 case LONG_PRESS:
325                     recordGestureClassification(msg.arg1);
326                     dispatchLongPress();
327                     break;
328 
329                 case TAP:
330                     // If the user's finger is still down, do not count it as a tap
331                     if (mDoubleTapListener != null) {
332                         if (!mStillDown) {
333                             recordGestureClassification(
334                                     TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
335                             mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
336                         } else {
337                             mDeferConfirmSingleTap = true;
338                         }
339                     }
340                     break;
341 
342                 default:
343                     throw new RuntimeException("Unknown message " + msg); //never
344             }
345         }
346     }
347 
348     /**
349      * Creates a GestureDetector with the supplied listener.
350      * This variant of the constructor should be used from a non-UI thread
351      * (as it allows specifying the Handler).
352      *
353      * @param listener the listener invoked for all the callbacks, this must
354      * not be null.
355      * @param handler the handler to use
356      *
357      * @throws NullPointerException if {@code listener} is null.
358      *
359      * @deprecated Use {@link #GestureDetector(android.content.Context,
360      *      android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
361      */
362     @Deprecated
GestureDetector(@onNull OnGestureListener listener, @Nullable Handler handler)363     public GestureDetector(@NonNull OnGestureListener listener, @Nullable Handler handler) {
364         this(null, listener, handler);
365     }
366 
367     /**
368      * Creates a GestureDetector with the supplied listener.
369      * You may only use this constructor from a UI thread (this is the usual situation).
370      * @see android.os.Handler#Handler()
371      *
372      * @param listener the listener invoked for all the callbacks, this must
373      * not be null.
374      *
375      * @throws NullPointerException if {@code listener} is null.
376      *
377      * @deprecated Use {@link #GestureDetector(android.content.Context,
378      *      android.view.GestureDetector.OnGestureListener)} instead.
379      */
380     @Deprecated
GestureDetector(@onNull OnGestureListener listener)381     public GestureDetector(@NonNull OnGestureListener listener) {
382         this(null, listener, null);
383     }
384 
385     /**
386      * Creates a GestureDetector with the supplied listener.
387      * You may only use this constructor from a {@link android.os.Looper} thread.
388      * @see android.os.Handler#Handler()
389      *
390      * @param context An {@link android.app.Activity} or a {@link Context} created from
391      * {@link Context#createWindowContext(int, Bundle)}
392      * @param listener the listener invoked for all the callbacks, this must
393      * not be null. If the listener implements the {@link OnDoubleTapListener} or
394      * {@link OnContextClickListener} then it will also be set as the listener for
395      * these callbacks (for example when using the {@link SimpleOnGestureListener}).
396      *
397      * @throws NullPointerException if {@code listener} is null.
398      */
399     // TODO(b/182007470): Use @ConfigurationContext instead
GestureDetector(@ullable @iContext Context context, @NonNull OnGestureListener listener)400     public GestureDetector(@Nullable @UiContext Context context,
401             @NonNull OnGestureListener listener) {
402         this(context, listener, null);
403     }
404 
405     /**
406      * Creates a GestureDetector with the supplied listener that runs deferred events on the
407      * thread associated with the supplied {@link android.os.Handler}.
408      * @see android.os.Handler#Handler()
409      *
410      * @param context An {@link android.app.Activity} or a {@link Context} created from
411      * {@link Context#createWindowContext(int, Bundle)}
412      * @param listener the listener invoked for all the callbacks, this must
413      * not be null. If the listener implements the {@link OnDoubleTapListener} or
414      * {@link OnContextClickListener} then it will also be set as the listener for
415      * these callbacks (for example when using the {@link SimpleOnGestureListener}).
416      * @param handler the handler to use for running deferred listener events.
417      *
418      * @throws NullPointerException if {@code listener} is null.
419      */
GestureDetector(@ullable @iContext Context context, @NonNull OnGestureListener listener, @Nullable Handler handler)420     public GestureDetector(@Nullable @UiContext Context context,
421             @NonNull OnGestureListener listener, @Nullable Handler handler) {
422         if (handler != null) {
423             mHandler = new GestureHandler(handler);
424         } else {
425             mHandler = new GestureHandler();
426         }
427         mListener = listener;
428         if (listener instanceof OnDoubleTapListener) {
429             setOnDoubleTapListener((OnDoubleTapListener) listener);
430         }
431         if (listener instanceof OnContextClickListener) {
432             setContextClickListener((OnContextClickListener) listener);
433         }
434         init(context);
435     }
436 
437     /**
438      * Creates a GestureDetector with the supplied listener that runs deferred events on the
439      * thread associated with the supplied {@link android.os.Handler}.
440      * @see android.os.Handler#Handler()
441      *
442      * @param context An {@link android.app.Activity} or a {@link Context} created from
443      * {@link Context#createWindowContext(int, Bundle)}
444      * @param listener the listener invoked for all the callbacks, this must
445      * not be null.
446      * @param handler the handler to use for running deferred listener events.
447      * @param unused currently not used.
448      *
449      * @throws NullPointerException if {@code listener} is null.
450      */
GestureDetector(@ullable @iContext Context context, @NonNull OnGestureListener listener, @Nullable Handler handler, boolean unused)451     public GestureDetector(@Nullable @UiContext Context context,
452             @NonNull OnGestureListener listener, @Nullable Handler handler, boolean unused) {
453         this(context, listener, handler);
454     }
455 
init(@iContext Context context)456     private void init(@UiContext Context context) {
457         if (mListener == null) {
458             throw new NullPointerException("OnGestureListener must not be null");
459         }
460         mIsLongpressEnabled = true;
461 
462         // Fallback to support pre-donuts releases
463         int touchSlop, doubleTapSlop, doubleTapTouchSlop;
464         if (context == null) {
465             //noinspection deprecation
466             touchSlop = ViewConfiguration.getTouchSlop();
467             doubleTapTouchSlop = touchSlop; // Hack rather than adding a hidden method for this
468             doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
469             //noinspection deprecation
470             mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
471             mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
472             mAmbiguousGestureMultiplier = ViewConfiguration.getAmbiguousGestureMultiplier();
473         } else {
474             StrictMode.assertConfigurationContext(context, "GestureDetector#init");
475             final ViewConfiguration configuration = ViewConfiguration.get(context);
476             touchSlop = configuration.getScaledTouchSlop();
477             doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
478             doubleTapSlop = configuration.getScaledDoubleTapSlop();
479             mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
480             mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
481             mAmbiguousGestureMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
482         }
483         mTouchSlopSquare = touchSlop * touchSlop;
484         mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
485         mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
486     }
487 
488     /**
489      * Sets the listener which will be called for double-tap and related
490      * gestures.
491      *
492      * @param onDoubleTapListener the listener invoked for all the callbacks, or
493      *        null to stop listening for double-tap gestures.
494      */
setOnDoubleTapListener(@ullable OnDoubleTapListener onDoubleTapListener)495     public void setOnDoubleTapListener(@Nullable OnDoubleTapListener onDoubleTapListener) {
496         mDoubleTapListener = onDoubleTapListener;
497     }
498 
499     /**
500      * Sets the listener which will be called for context clicks.
501      *
502      * @param onContextClickListener the listener invoked for all the callbacks, or null to stop
503      *            listening for context clicks.
504      */
setContextClickListener(@ullable OnContextClickListener onContextClickListener)505     public void setContextClickListener(@Nullable OnContextClickListener onContextClickListener) {
506         mContextClickListener = onContextClickListener;
507     }
508 
509     /**
510      * Set whether longpress is enabled, if this is enabled when a user
511      * presses and holds down you get a longpress event and nothing further.
512      * If it's disabled the user can press and hold down and then later
513      * moved their finger and you will get scroll events. By default
514      * longpress is enabled.
515      *
516      * @param isLongpressEnabled whether longpress should be enabled.
517      */
setIsLongpressEnabled(boolean isLongpressEnabled)518     public void setIsLongpressEnabled(boolean isLongpressEnabled) {
519         mIsLongpressEnabled = isLongpressEnabled;
520     }
521 
522     /**
523      * @return true if longpress is enabled, else false.
524      */
isLongpressEnabled()525     public boolean isLongpressEnabled() {
526         return mIsLongpressEnabled;
527     }
528 
529     /**
530      * Analyzes the given motion event and if applicable triggers the
531      * appropriate callbacks on the {@link OnGestureListener} supplied.
532      *
533      * @param ev The current motion event.
534      * @return true if the {@link OnGestureListener} consumed the event,
535      *              else false.
536      */
onTouchEvent(@onNull MotionEvent ev)537     public boolean onTouchEvent(@NonNull MotionEvent ev) {
538         if (mInputEventConsistencyVerifier != null) {
539             mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
540         }
541 
542         final int action = ev.getAction();
543 
544         if (mCurrentMotionEvent != null) {
545             mCurrentMotionEvent.recycle();
546         }
547         mCurrentMotionEvent = MotionEvent.obtain(ev);
548 
549         if (mVelocityTracker == null) {
550             mVelocityTracker = VelocityTracker.obtain();
551         }
552         mVelocityTracker.addMovement(ev);
553 
554         final boolean pointerUp =
555                 (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
556         final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
557         final boolean isGeneratedGesture =
558                 (ev.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
559 
560         // Determine focal point
561         float sumX = 0, sumY = 0;
562         final int count = ev.getPointerCount();
563         for (int i = 0; i < count; i++) {
564             if (skipIndex == i) continue;
565             sumX += ev.getX(i);
566             sumY += ev.getY(i);
567         }
568         final int div = pointerUp ? count - 1 : count;
569         final float focusX = sumX / div;
570         final float focusY = sumY / div;
571 
572         boolean handled = false;
573 
574         switch (action & MotionEvent.ACTION_MASK) {
575             case MotionEvent.ACTION_POINTER_DOWN:
576                 mDownFocusX = mLastFocusX = focusX;
577                 mDownFocusY = mLastFocusY = focusY;
578                 // Cancel long press and taps
579                 cancelTaps();
580                 break;
581 
582             case MotionEvent.ACTION_POINTER_UP:
583                 mDownFocusX = mLastFocusX = focusX;
584                 mDownFocusY = mLastFocusY = focusY;
585 
586                 // Check the dot product of current velocities.
587                 // If the pointer that left was opposing another velocity vector, clear.
588                 mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
589                 final int upIndex = ev.getActionIndex();
590                 final int id1 = ev.getPointerId(upIndex);
591                 final float x1 = mVelocityTracker.getXVelocity(id1);
592                 final float y1 = mVelocityTracker.getYVelocity(id1);
593                 for (int i = 0; i < count; i++) {
594                     if (i == upIndex) continue;
595 
596                     final int id2 = ev.getPointerId(i);
597                     final float x = x1 * mVelocityTracker.getXVelocity(id2);
598                     final float y = y1 * mVelocityTracker.getYVelocity(id2);
599 
600                     final float dot = x + y;
601                     if (dot < 0) {
602                         mVelocityTracker.clear();
603                         break;
604                     }
605                 }
606                 break;
607 
608             case MotionEvent.ACTION_DOWN:
609                 if (mDoubleTapListener != null) {
610                     boolean hadTapMessage = mHandler.hasMessages(TAP);
611                     if (hadTapMessage) mHandler.removeMessages(TAP);
612                     if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null)
613                             && hadTapMessage
614                             && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
615                         // This is a second tap
616                         mIsDoubleTapping = true;
617                         recordGestureClassification(
618                                 TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
619                         // Give a callback with the first tap of the double-tap
620                         handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
621                         // Give a callback with down event of the double-tap
622                         handled |= mDoubleTapListener.onDoubleTapEvent(ev);
623                     } else {
624                         // This is a first tap
625                         mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
626                     }
627                 }
628 
629                 mDownFocusX = mLastFocusX = focusX;
630                 mDownFocusY = mLastFocusY = focusY;
631                 if (mCurrentDownEvent != null) {
632                     mCurrentDownEvent.recycle();
633                 }
634                 mCurrentDownEvent = MotionEvent.obtain(ev);
635                 mAlwaysInTapRegion = true;
636                 mAlwaysInBiggerTapRegion = true;
637                 mStillDown = true;
638                 mInLongPress = false;
639                 mDeferConfirmSingleTap = false;
640                 mHasRecordedClassification = false;
641 
642                 if (mIsLongpressEnabled) {
643                     mHandler.removeMessages(LONG_PRESS);
644                     mHandler.sendMessageAtTime(
645                             mHandler.obtainMessage(
646                                     LONG_PRESS,
647                                     TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
648                                     0 /* arg2 */),
649                             mCurrentDownEvent.getDownTime()
650                                     + ViewConfiguration.getLongPressTimeout());
651                 }
652                 mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
653                         mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
654                 handled |= mListener.onDown(ev);
655                 break;
656 
657             case MotionEvent.ACTION_MOVE:
658                 if (mInLongPress || mInContextClick) {
659                     break;
660                 }
661 
662                 final int motionClassification = ev.getClassification();
663                 final boolean hasPendingLongPress = mHandler.hasMessages(LONG_PRESS);
664 
665                 final float scrollX = mLastFocusX - focusX;
666                 final float scrollY = mLastFocusY - focusY;
667                 if (mIsDoubleTapping) {
668                     // Give the move events of the double-tap
669                     recordGestureClassification(
670                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
671                     handled |= mDoubleTapListener.onDoubleTapEvent(ev);
672                 } else if (mAlwaysInTapRegion) {
673                     final int deltaX = (int) (focusX - mDownFocusX);
674                     final int deltaY = (int) (focusY - mDownFocusY);
675                     int distance = (deltaX * deltaX) + (deltaY * deltaY);
676                     int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
677 
678                     final boolean ambiguousGesture =
679                             motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
680                     final boolean shouldInhibitDefaultAction =
681                             hasPendingLongPress && ambiguousGesture;
682                     if (shouldInhibitDefaultAction) {
683                         // Inhibit default long press
684                         if (distance > slopSquare) {
685                             // The default action here is to remove long press. But if the touch
686                             // slop below gets increased, and we never exceed the modified touch
687                             // slop while still receiving AMBIGUOUS_GESTURE, we risk that *nothing*
688                             // will happen in response to user input. To prevent this,
689                             // reschedule long press with a modified timeout.
690                             mHandler.removeMessages(LONG_PRESS);
691                             final long longPressTimeout = ViewConfiguration.getLongPressTimeout();
692                             mHandler.sendMessageAtTime(
693                                     mHandler.obtainMessage(
694                                             LONG_PRESS,
695                                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
696                                             0 /* arg2 */),
697                                     ev.getDownTime()
698                                         + (long) (longPressTimeout * mAmbiguousGestureMultiplier));
699                         }
700                         // Inhibit default scroll. If a gesture is ambiguous, we prevent scroll
701                         // until the gesture is resolved.
702                         // However, for safety, simply increase the touch slop in case the
703                         // classification is erroneous. Since the value is squared, multiply twice.
704                         slopSquare *= mAmbiguousGestureMultiplier * mAmbiguousGestureMultiplier;
705                     }
706 
707                     if (distance > slopSquare) {
708                         recordGestureClassification(
709                                 TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL);
710                         handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
711                         mLastFocusX = focusX;
712                         mLastFocusY = focusY;
713                         mAlwaysInTapRegion = false;
714                         mHandler.removeMessages(TAP);
715                         mHandler.removeMessages(SHOW_PRESS);
716                         mHandler.removeMessages(LONG_PRESS);
717                     }
718                     int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
719                     if (distance > doubleTapSlopSquare) {
720                         mAlwaysInBiggerTapRegion = false;
721                     }
722                 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
723                     recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL);
724                     handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
725                     mLastFocusX = focusX;
726                     mLastFocusY = focusY;
727                 }
728                 final boolean deepPress =
729                         motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
730                 if (deepPress && hasPendingLongPress) {
731                     mHandler.removeMessages(LONG_PRESS);
732                     mHandler.sendMessage(
733                             mHandler.obtainMessage(
734                                   LONG_PRESS,
735                                   TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS,
736                                   0 /* arg2 */));
737                 }
738                 break;
739 
740             case MotionEvent.ACTION_UP:
741                 mStillDown = false;
742                 MotionEvent currentUpEvent = MotionEvent.obtain(ev);
743                 if (mIsDoubleTapping) {
744                     // Finally, give the up event of the double-tap
745                     recordGestureClassification(
746                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
747                     handled |= mDoubleTapListener.onDoubleTapEvent(ev);
748                 } else if (mInLongPress) {
749                     mHandler.removeMessages(TAP);
750                     mInLongPress = false;
751                 } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
752                     recordGestureClassification(
753                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
754                     handled = mListener.onSingleTapUp(ev);
755                     if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
756                         mDoubleTapListener.onSingleTapConfirmed(ev);
757                     }
758                 } else if (!mIgnoreNextUpEvent) {
759 
760                     // A fling must travel the minimum tap distance
761                     final VelocityTracker velocityTracker = mVelocityTracker;
762                     final int pointerId = ev.getPointerId(0);
763                     velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
764                     final float velocityY = velocityTracker.getYVelocity(pointerId);
765                     final float velocityX = velocityTracker.getXVelocity(pointerId);
766 
767                     if ((Math.abs(velocityY) > mMinimumFlingVelocity)
768                             || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
769                         handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
770                     }
771                 }
772                 if (mPreviousUpEvent != null) {
773                     mPreviousUpEvent.recycle();
774                 }
775                 // Hold the event we obtained above - listeners may have changed the original.
776                 mPreviousUpEvent = currentUpEvent;
777                 if (mVelocityTracker != null) {
778                     // This may have been cleared when we called out to the
779                     // application above.
780                     mVelocityTracker.recycle();
781                     mVelocityTracker = null;
782                 }
783                 mIsDoubleTapping = false;
784                 mDeferConfirmSingleTap = false;
785                 mIgnoreNextUpEvent = false;
786                 mHandler.removeMessages(SHOW_PRESS);
787                 mHandler.removeMessages(LONG_PRESS);
788                 break;
789 
790             case MotionEvent.ACTION_CANCEL:
791                 cancel();
792                 break;
793         }
794 
795         if (!handled && mInputEventConsistencyVerifier != null) {
796             mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
797         }
798         return handled;
799     }
800 
801     /**
802      * Analyzes the given generic motion event and if applicable triggers the
803      * appropriate callbacks on the {@link OnGestureListener} supplied.
804      *
805      * @param ev The current motion event.
806      * @return true if the {@link OnGestureListener} consumed the event,
807      *              else false.
808      */
onGenericMotionEvent(@onNull MotionEvent ev)809     public boolean onGenericMotionEvent(@NonNull MotionEvent ev) {
810         if (mInputEventConsistencyVerifier != null) {
811             mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0);
812         }
813 
814         final int actionButton = ev.getActionButton();
815         switch (ev.getActionMasked()) {
816             case MotionEvent.ACTION_BUTTON_PRESS:
817                 if (mContextClickListener != null && !mInContextClick && !mInLongPress
818                         && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
819                         || actionButton == MotionEvent.BUTTON_SECONDARY)) {
820                     if (mContextClickListener.onContextClick(ev)) {
821                         mInContextClick = true;
822                         mHandler.removeMessages(LONG_PRESS);
823                         mHandler.removeMessages(TAP);
824                         return true;
825                     }
826                 }
827                 break;
828 
829             case MotionEvent.ACTION_BUTTON_RELEASE:
830                 if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
831                         || actionButton == MotionEvent.BUTTON_SECONDARY)) {
832                     mInContextClick = false;
833                     mIgnoreNextUpEvent = true;
834                 }
835                 break;
836         }
837         return false;
838     }
839 
cancel()840     private void cancel() {
841         mHandler.removeMessages(SHOW_PRESS);
842         mHandler.removeMessages(LONG_PRESS);
843         mHandler.removeMessages(TAP);
844         mVelocityTracker.recycle();
845         mVelocityTracker = null;
846         mIsDoubleTapping = false;
847         mStillDown = false;
848         mAlwaysInTapRegion = false;
849         mAlwaysInBiggerTapRegion = false;
850         mDeferConfirmSingleTap = false;
851         mInLongPress = false;
852         mInContextClick = false;
853         mIgnoreNextUpEvent = false;
854     }
855 
cancelTaps()856     private void cancelTaps() {
857         mHandler.removeMessages(SHOW_PRESS);
858         mHandler.removeMessages(LONG_PRESS);
859         mHandler.removeMessages(TAP);
860         mIsDoubleTapping = false;
861         mAlwaysInTapRegion = false;
862         mAlwaysInBiggerTapRegion = false;
863         mDeferConfirmSingleTap = false;
864         mInLongPress = false;
865         mInContextClick = false;
866         mIgnoreNextUpEvent = false;
867     }
868 
isConsideredDoubleTap(@onNull MotionEvent firstDown, @NonNull MotionEvent firstUp, @NonNull MotionEvent secondDown)869     private boolean isConsideredDoubleTap(@NonNull MotionEvent firstDown,
870             @NonNull MotionEvent firstUp, @NonNull MotionEvent secondDown) {
871         if (!mAlwaysInBiggerTapRegion) {
872             return false;
873         }
874 
875         final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
876         if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
877             return false;
878         }
879 
880         int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
881         int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
882         final boolean isGeneratedGesture =
883                 (firstDown.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
884         int slopSquare = isGeneratedGesture ? 0 : mDoubleTapSlopSquare;
885         return (deltaX * deltaX + deltaY * deltaY < slopSquare);
886     }
887 
dispatchLongPress()888     private void dispatchLongPress() {
889         mHandler.removeMessages(TAP);
890         mDeferConfirmSingleTap = false;
891         mInLongPress = true;
892         mListener.onLongPress(mCurrentDownEvent);
893     }
894 
recordGestureClassification(int classification)895     private void recordGestureClassification(int classification) {
896         if (mHasRecordedClassification
897                 || classification
898                     == TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION) {
899             // Only record the first classification for an event stream.
900             return;
901         }
902         if (mCurrentDownEvent == null || mCurrentMotionEvent == null) {
903             // If the complete event stream wasn't seen, don't record anything.
904             mHasRecordedClassification = true;
905             return;
906         }
907         FrameworkStatsLog.write(
908                 FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED,
909                 getClass().getName(),
910                 classification,
911                 (int) (SystemClock.uptimeMillis() - mCurrentMotionEvent.getDownTime()),
912                 (float) Math.hypot(mCurrentMotionEvent.getRawX() - mCurrentDownEvent.getRawX(),
913                                    mCurrentMotionEvent.getRawY() - mCurrentDownEvent.getRawY()));
914         mHasRecordedClassification = true;
915     }
916 }
917