• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 com.android.server.accessibility.gestures;
18 
19 import static android.view.MotionEvent.INVALID_POINTER_ID;
20 
21 import static com.android.server.accessibility.gestures.GestureUtils.getActionIndex;
22 import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG;
23 
24 import android.content.Context;
25 import android.graphics.PointF;
26 import android.os.Handler;
27 import android.util.DisplayMetrics;
28 import android.util.Slog;
29 import android.view.MotionEvent;
30 import android.view.ViewConfiguration;
31 
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 
35 /**
36  * This class is responsible for matching one-finger swipe gestures. Each instance matches one swipe
37  * gesture. A swipe is specified as a series of one or more directions e.g. left, left and up, etc.
38  * At this time swipes with more than two directions are not supported.
39  */
40 class MultiFingerSwipe extends GestureMatcher {
41 
42     // Direction constants.
43     public static final int LEFT = 0;
44     public static final int RIGHT = 1;
45     public static final int UP = 2;
46     public static final int DOWN = 3;
47 
48     // Buffer for storing points for gesture detection.
49     private final ArrayList<PointF>[] mStrokeBuffers;
50 
51     // The swipe direction for this matcher.
52     private int mDirection;
53     private int[] mPointerIds;
54     // The starting point of each finger's path in the gesture.
55     private PointF[] mBase;
56     // The most recent entry in each finger's gesture path.
57     private PointF[] mPreviousGesturePoint;
58     private int mTargetFingerCount;
59     private int mCurrentFingerCount;
60     // Whether the appropriate number of fingers have gone down at some point. This is reset only on
61     // clear.
62     private boolean mTargetFingerCountReached = false;
63     // Constants for sampling motion event points.
64     // We sample based on a minimum distance between points, primarily to improve accuracy by
65     // reducing noisy minor changes in direction.
66     private static final float MIN_CM_BETWEEN_SAMPLES = 0.25f;
67     private final float mMinPixelsBetweenSamplesX;
68     private final float mMinPixelsBetweenSamplesY;
69     // The minmimum distance the finger must travel before we evaluate the initial direction of the
70     // swipe.
71     // Anything less is still considered a touch.
72     private int mTouchSlop;
73 
MultiFingerSwipe( Context context, int fingerCount, int direction, int gesture, GestureMatcher.StateChangeListener listener)74     MultiFingerSwipe(
75             Context context,
76             int fingerCount,
77             int direction,
78             int gesture,
79             GestureMatcher.StateChangeListener listener) {
80         super(gesture, new Handler(context.getMainLooper()), listener);
81         mTargetFingerCount = fingerCount;
82         mPointerIds = new int[mTargetFingerCount];
83         mBase = new PointF[mTargetFingerCount];
84         mPreviousGesturePoint = new PointF[mTargetFingerCount];
85         mStrokeBuffers = new ArrayList[mTargetFingerCount];
86         mDirection = direction;
87         DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
88         // Calculate gesture sampling interval.
89         final float pixelsPerCmX = displayMetrics.xdpi / GestureUtils.CM_PER_INCH;
90         final float pixelsPerCmY = displayMetrics.ydpi / GestureUtils.CM_PER_INCH;
91         mMinPixelsBetweenSamplesX = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmX;
92         mMinPixelsBetweenSamplesY = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmY;
93         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
94         clear();
95     }
96 
97     @Override
clear()98     public void clear() {
99         mTargetFingerCountReached = false;
100         mCurrentFingerCount = 0;
101         for (int i = 0; i < mTargetFingerCount; ++i) {
102             mPointerIds[i] = INVALID_POINTER_ID;
103             if (mBase[i] == null) {
104                 mBase[i] = new PointF();
105             }
106             mBase[i].x = Float.NaN;
107             mBase[i].y = Float.NaN;
108             if (mPreviousGesturePoint[i] == null) {
109                 mPreviousGesturePoint[i] = new PointF();
110             }
111             mPreviousGesturePoint[i].x = Float.NaN;
112             mPreviousGesturePoint[i].y = Float.NaN;
113             if (mStrokeBuffers[i] == null) {
114                 mStrokeBuffers[i] = new ArrayList<>(100);
115             }
116             mStrokeBuffers[i].clear();
117         }
118         super.clear();
119     }
120 
121     @Override
onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags)122     protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
123         if (mCurrentFingerCount > 0) {
124             cancelGesture(event, rawEvent, policyFlags);
125             return;
126         }
127         mCurrentFingerCount = 1;
128         final int actionIndex = getActionIndex(rawEvent);
129         final int pointerId = rawEvent.getPointerId(actionIndex);
130         int pointerIndex = rawEvent.getPointerCount() - 1;
131         if (pointerId < 0) {
132             // Nonsensical pointer id.
133             cancelGesture(event, rawEvent, policyFlags);
134             return;
135         }
136         if (mPointerIds[pointerIndex] != INVALID_POINTER_ID) {
137             // Inconsistent event stream.
138             cancelGesture(event, rawEvent, policyFlags);
139             return;
140         }
141         mPointerIds[pointerIndex] = pointerId;
142         if (Float.isNaN(mBase[pointerIndex].x) && Float.isNaN(mBase[pointerIndex].y)) {
143             final float x = rawEvent.getX(actionIndex);
144             final float y = rawEvent.getY(actionIndex);
145             if (x < 0f || y < 0f) {
146                 cancelGesture(event, rawEvent, policyFlags);
147                 return;
148             }
149             mBase[pointerIndex].x = x;
150             mBase[pointerIndex].y = y;
151             mPreviousGesturePoint[pointerIndex].x = x;
152             mPreviousGesturePoint[pointerIndex].y = y;
153         } else {
154             // This  event doesn't make sense in the middle of a gesture.
155             cancelGesture(event, rawEvent, policyFlags);
156             return;
157         }
158     }
159 
160     @Override
onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags)161     protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
162         if (event.getPointerCount() > mTargetFingerCount) {
163             cancelGesture(event, rawEvent, policyFlags);
164             return;
165         }
166         mCurrentFingerCount += 1;
167         if (mCurrentFingerCount != rawEvent.getPointerCount()) {
168             cancelGesture(event, rawEvent, policyFlags);
169             return;
170         }
171         if (mCurrentFingerCount == mTargetFingerCount) {
172             mTargetFingerCountReached = true;
173         }
174         final int actionIndex = getActionIndex(rawEvent);
175         final int pointerId = rawEvent.getPointerId(actionIndex);
176         if (pointerId < 0) {
177             // Nonsensical pointer id.
178             cancelGesture(event, rawEvent, policyFlags);
179             return;
180         }
181         int pointerIndex = mCurrentFingerCount - 1;
182         if (mPointerIds[pointerIndex] != INVALID_POINTER_ID) {
183             // Inconsistent event stream.
184             cancelGesture(event, rawEvent, policyFlags);
185             return;
186         }
187         mPointerIds[pointerIndex] = pointerId;
188         if (Float.isNaN(mBase[pointerIndex].x) && Float.isNaN(mBase[pointerIndex].y)) {
189             final float x = rawEvent.getX(actionIndex);
190             final float y = rawEvent.getY(actionIndex);
191             if (x < 0f || y < 0f) {
192                 cancelGesture(event, rawEvent, policyFlags);
193                 return;
194             }
195             mBase[pointerIndex].x = x;
196             mBase[pointerIndex].y = y;
197             mPreviousGesturePoint[pointerIndex].x = x;
198             mPreviousGesturePoint[pointerIndex].y = y;
199         } else {
200             cancelGesture(event, rawEvent, policyFlags);
201             return;
202         }
203     }
204 
205     @Override
onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags)206     protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
207         if (!mTargetFingerCountReached) {
208             cancelGesture(event, rawEvent, policyFlags);
209             return;
210         }
211         mCurrentFingerCount -= 1;
212         final int actionIndex = getActionIndex(event);
213         final int pointerId = event.getPointerId(actionIndex);
214         if (pointerId < 0) {
215             // Nonsensical pointer id.
216             cancelGesture(event, rawEvent, policyFlags);
217             return;
218         }
219         final int pointerIndex = Arrays.binarySearch(mPointerIds, pointerId);
220         if (pointerIndex < 0) {
221             cancelGesture(event, rawEvent, policyFlags);
222             return;
223         }
224         final float x = rawEvent.getX(actionIndex);
225         final float y = rawEvent.getY(actionIndex);
226         if (x < 0f || y < 0f) {
227             cancelGesture(event, rawEvent, policyFlags);
228             return;
229         }
230         final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x);
231         final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y);
232         if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
233             mStrokeBuffers[pointerIndex].add(new PointF(x, y));
234         }
235         // We will evaluate all the paths on ACTION_UP.
236     }
237 
238     @Override
onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags)239     protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
240         for (int pointerIndex = 0; pointerIndex < mTargetFingerCount; ++pointerIndex) {
241             if (mPointerIds[pointerIndex] == INVALID_POINTER_ID) {
242                 // Fingers have started to move before the required number of fingers are down.
243                 // However, they can still move less than the touch slop and still be considered
244                 // touching, not moving.
245                 // So we just ignore fingers that haven't been assigned a pointer id and process
246                 // those who have.
247                 continue;
248             }
249             if (DEBUG) {
250                 Slog.d(getGestureName(), "Processing move on finger " + pointerIndex);
251             }
252             int index = rawEvent.findPointerIndex(mPointerIds[pointerIndex]);
253             if (index < 0) {
254                 // This finger is not present in this event. It could have gone up just before this
255                 // movement.
256                 if (DEBUG) {
257                     Slog.d(
258                             getGestureName(),
259                             "Finger " + pointerIndex + " not found in this event. skipping.");
260                 }
261                 continue;
262             }
263             final float x = rawEvent.getX(index);
264             final float y = rawEvent.getY(index);
265             if (x < 0f || y < 0f) {
266                 cancelGesture(event, rawEvent, policyFlags);
267                 return;
268             }
269             final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x);
270             final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y);
271             final double moveDelta =
272                     Math.hypot(
273                             Math.abs(x - mBase[pointerIndex].x),
274                             Math.abs(y - mBase[pointerIndex].y));
275             if (DEBUG) {
276                 Slog.d(getGestureName(), "moveDelta:" + moveDelta);
277             }
278             if (getState() == STATE_CLEAR) {
279                 if (moveDelta < (mTargetFingerCount * mTouchSlop)) {
280                     // This still counts as a touch not a swipe.
281                     continue;
282                 }
283                 // First, make sure we have the right number of fingers down.
284                 if (mCurrentFingerCount != mTargetFingerCount) {
285                     cancelGesture(event, rawEvent, policyFlags);
286                     return;
287                 }
288                 // Then, make sure the pointer is going in the right direction.
289                 int direction = toDirection(x - mBase[pointerIndex].x, y - mBase[pointerIndex].y);
290                 if (direction != mDirection) {
291                     cancelGesture(event, rawEvent, policyFlags);
292                     return;
293                 }
294                 // This is confirmed to be some kind of swipe so start tracking points.
295                 startGesture(event, rawEvent, policyFlags);
296                 for (int i = 0; i < mTargetFingerCount; ++i) {
297                     mStrokeBuffers[i].add(new PointF(mBase[i]));
298                 }
299             } else if (getState() == STATE_GESTURE_STARTED) {
300                 // Cancel if the finger starts to go the wrong way.
301                 // Note that this only works because this matcher assumes one direction.
302                 int direction = toDirection(x - mBase[pointerIndex].x, y - mBase[pointerIndex].y);
303                 if (direction != mDirection) {
304                     cancelGesture(event, rawEvent, policyFlags);
305                     return;
306                 }
307                 if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
308                     // Sample every 2.5 MM in order to guard against minor variations in path.
309                     mPreviousGesturePoint[pointerIndex].x = x;
310                     mPreviousGesturePoint[pointerIndex].y = y;
311                     mStrokeBuffers[pointerIndex].add(new PointF(x, y));
312                 }
313             }
314         }
315     }
316 
317     @Override
onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags)318     protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
319         if (getState() != STATE_GESTURE_STARTED) {
320             cancelGesture(event, rawEvent, policyFlags);
321             return;
322         }
323         mCurrentFingerCount = 0;
324         final int actionIndex = getActionIndex(event);
325         final int pointerId = event.getPointerId(actionIndex);
326         final int pointerIndex = Arrays.binarySearch(mPointerIds, pointerId);
327         if (pointerIndex < 0) {
328             cancelGesture(event, rawEvent, policyFlags);
329             return;
330         }
331         final float x = rawEvent.getX(actionIndex);
332         final float y = rawEvent.getY(actionIndex);
333         if (x < 0f || y < 0f) {
334             cancelGesture(event, rawEvent, policyFlags);
335             return;
336         }
337         final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x);
338         final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y);
339         if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
340             mStrokeBuffers[pointerIndex].add(new PointF(x, y));
341         }
342         recognizeGesture(event, rawEvent, policyFlags);
343     }
344 
345     /**
346      * Looks at the sequence of motions in mStrokeBuffer, classifies the gesture, then transitions
347      * to the complete or cancel state depending on the result.
348      */
recognizeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags)349     private void recognizeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
350         // Check the path of each finger against the specified direction.
351         // Note that we sample every 2.5 MMm, and the direction matching is extremely tolerant (each
352         // direction has a 90-degree arch of tolerance) meaning that minor perpendicular movements
353         // should not create false negatives.
354         for (int i = 0; i < mTargetFingerCount; ++i) {
355             if (DEBUG) {
356                 Slog.d(getGestureName(), "Recognizing finger: " + i);
357             }
358             if (mStrokeBuffers[i].size() < 2) {
359                 Slog.d(getGestureName(), "Too few points.");
360                 cancelGesture(event, rawEvent, policyFlags);
361                 return;
362             }
363             ArrayList<PointF> path = mStrokeBuffers[i];
364 
365             if (DEBUG) {
366                 Slog.d(getGestureName(), "path=" + path.toString());
367             }
368             // Classify line segments, and call Listener callbacks.
369             if (!recognizeGesturePath(event, rawEvent, policyFlags, path)) {
370                 cancelGesture(event, rawEvent, policyFlags);
371                 return;
372             }
373         }
374         // If we reach this point then all paths match.
375         completeGesture(event, rawEvent, policyFlags);
376     }
377 
378     /**
379      * Tests the path of a given finger against the direction specified in this matcher.
380      *
381      * @return True if the path matches the specified direction for this matcher, otherwise false.
382      */
recognizeGesturePath( MotionEvent event, MotionEvent rawEvent, int policyFlags, ArrayList<PointF> path)383     private boolean recognizeGesturePath(
384             MotionEvent event, MotionEvent rawEvent, int policyFlags, ArrayList<PointF> path) {
385 
386         final int displayId = event.getDisplayId();
387         for (int i = 0; i < path.size() - 1; ++i) {
388             PointF start = path.get(i);
389             PointF end = path.get(i + 1);
390 
391             float dX = end.x - start.x;
392             float dY = end.y - start.y;
393             int direction = toDirection(dX, dY);
394             if (direction != mDirection) {
395                 if (DEBUG) {
396                     Slog.d(
397                             getGestureName(),
398                             "Found direction "
399                                     + directionToString(direction)
400                                     + " when expecting "
401                                     + directionToString(mDirection));
402                 }
403                 return false;
404             }
405         }
406         if (DEBUG) {
407             Slog.d(getGestureName(), "Completed.");
408         }
409         return true;
410     }
411 
toDirection(float dX, float dY)412     private static int toDirection(float dX, float dY) {
413         if (Math.abs(dX) > Math.abs(dY)) {
414             // Horizontal
415             return (dX < 0) ? LEFT : RIGHT;
416         } else {
417             // Vertical
418             return (dY < 0) ? UP : DOWN;
419         }
420     }
421 
directionToString(int direction)422     public static String directionToString(int direction) {
423         switch (direction) {
424             case LEFT:
425                 return "left";
426             case RIGHT:
427                 return "right";
428             case UP:
429                 return "up";
430             case DOWN:
431                 return "down";
432             default:
433                 return "Unknown Direction";
434         }
435     }
436 
437     @Override
getGestureName()438     protected String getGestureName() {
439         StringBuilder builder = new StringBuilder();
440         builder.append(mTargetFingerCount).append("-finger ");
441         builder.append("Swipe ").append(directionToString(mDirection));
442         return builder.toString();
443     }
444 
445     @Override
toString()446     public String toString() {
447         StringBuilder builder = new StringBuilder(super.toString());
448         if (getState() != STATE_GESTURE_CANCELED) {
449             builder.append(", mBase: ")
450                     .append(Arrays.toString(mBase))
451                     .append(", mMinPixelsBetweenSamplesX:")
452                     .append(mMinPixelsBetweenSamplesX)
453                     .append(", mMinPixelsBetweenSamplesY:")
454                     .append(mMinPixelsBetweenSamplesY);
455         }
456         return builder.toString();
457     }
458 }
459