GestureDescription.java revision a8918f23c712e97fa1dc4911f64827d64fc906e5
1/*
2 * Copyright (C) 2015 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
17package android.accessibilityservice;
18
19import android.annotation.IntRange;
20import android.annotation.NonNull;
21import android.graphics.Path;
22import android.graphics.PathMeasure;
23import android.graphics.RectF;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.view.InputDevice;
27import android.view.MotionEvent;
28import android.view.MotionEvent.PointerCoords;
29import android.view.MotionEvent.PointerProperties;
30
31import java.util.ArrayList;
32import java.util.List;
33
34/**
35 * Accessibility services with the
36 * {@link android.R.styleable#AccessibilityService_canPerformGestures} property can dispatch
37 * gestures. This class describes those gestures. Gestures are made up of one or more strokes.
38 * Gestures are immutable once built.
39 * <p>
40 * Spatial dimensions throughout are in screen pixels. Time is measured in milliseconds.
41 */
42public final class GestureDescription {
43    /** Gestures may contain no more than this many strokes */
44    private static final int MAX_STROKE_COUNT = 10;
45
46    /**
47     * Upper bound on total gesture duration. Nearly all gestures will be much shorter.
48     */
49    private static final long MAX_GESTURE_DURATION_MS = 60 * 1000;
50
51    private final List<StrokeDescription> mStrokes = new ArrayList<>();
52    private final float[] mTempPos = new float[2];
53
54    /**
55     * Get the upper limit for the number of strokes a gesture may contain.
56     *
57     * @return The maximum number of strokes.
58     */
59    public static int getMaxStrokeCount() {
60        return MAX_STROKE_COUNT;
61    }
62
63    /**
64     * Get the upper limit on a gesture's duration.
65     *
66     * @return The maximum duration in milliseconds.
67     */
68    public static long getMaxGestureDuration() {
69        return MAX_GESTURE_DURATION_MS;
70    }
71
72    private GestureDescription() {}
73
74    private GestureDescription(List<StrokeDescription> strokes) {
75        mStrokes.addAll(strokes);
76    }
77
78    /**
79     * Get the number of stroke in the gesture.
80     *
81     * @return the number of strokes in this gesture
82     */
83    public int getStrokeCount() {
84        return mStrokes.size();
85    }
86
87    /**
88     * Read a stroke from the gesture
89     *
90     * @param index the index of the stroke
91     *
92     * @return A description of the stroke.
93     */
94    public StrokeDescription getStroke(@IntRange(from = 0) int index) {
95        return mStrokes.get(index);
96    }
97
98    /**
99     * Return the smallest key point (where a path starts or ends) that is at least a specified
100     * offset
101     * @param offset the minimum start time
102     * @return The next key time that is at least the offset or -1 if one can't be found
103     */
104    private long getNextKeyPointAtLeast(long offset) {
105        long nextKeyPoint = Long.MAX_VALUE;
106        for (int i = 0; i < mStrokes.size(); i++) {
107            long thisStartTime = mStrokes.get(i).mStartTime;
108            if ((thisStartTime < nextKeyPoint) && (thisStartTime >= offset)) {
109                nextKeyPoint = thisStartTime;
110            }
111            long thisEndTime = mStrokes.get(i).mEndTime;
112            if ((thisEndTime < nextKeyPoint) && (thisEndTime >= offset)) {
113                nextKeyPoint = thisEndTime;
114            }
115        }
116        return (nextKeyPoint == Long.MAX_VALUE) ? -1L : nextKeyPoint;
117    }
118
119    /**
120     * Get the points that correspond to a particular moment in time.
121     * @param time The time of interest
122     * @param touchPoints An array to hold the current touch points. Must be preallocated to at
123     * least the number of paths in the gesture to prevent going out of bounds
124     * @return The number of points found, and thus the number of elements set in each array
125     */
126    private int getPointsForTime(long time, TouchPoint[] touchPoints) {
127        int numPointsFound = 0;
128        for (int i = 0; i < mStrokes.size(); i++) {
129            StrokeDescription strokeDescription = mStrokes.get(i);
130            if (strokeDescription.hasPointForTime(time)) {
131                touchPoints[numPointsFound].mPathIndex = i;
132                touchPoints[numPointsFound].mIsStartOfPath = (time == strokeDescription.mStartTime);
133                touchPoints[numPointsFound].mIsEndOfPath = (time == strokeDescription.mEndTime);
134                strokeDescription.getPosForTime(time, mTempPos);
135                touchPoints[numPointsFound].mX = Math.round(mTempPos[0]);
136                touchPoints[numPointsFound].mY = Math.round(mTempPos[1]);
137                numPointsFound++;
138            }
139        }
140        return numPointsFound;
141    }
142
143    // Total duration assumes that the gesture starts at 0; waiting around to start a gesture
144    // counts against total duration
145    private static long getTotalDuration(List<StrokeDescription> paths) {
146        long latestEnd = Long.MIN_VALUE;
147        for (int i = 0; i < paths.size(); i++) {
148            StrokeDescription path = paths.get(i);
149            latestEnd = Math.max(latestEnd, path.mEndTime);
150        }
151        return Math.max(latestEnd, 0);
152    }
153
154    /**
155     * Builder for a {@code GestureDescription}
156     */
157    public static class Builder {
158
159        private final List<StrokeDescription> mStrokes = new ArrayList<>();
160
161        /**
162         * Add a stroke to the gesture description. Up to
163         * {@link GestureDescription#getMaxStrokeCount()} paths may be
164         * added to a gesture, and the total gesture duration (earliest path start time to latest
165         * path end time) may not exceed {@link GestureDescription#getMaxGestureDuration()}.
166         *
167         * @param strokeDescription the stroke to add.
168         *
169         * @return this
170         */
171        public Builder addStroke(@NonNull StrokeDescription strokeDescription) {
172            if (mStrokes.size() >= MAX_STROKE_COUNT) {
173                throw new IllegalStateException(
174                        "Attempting to add too many strokes to a gesture");
175            }
176
177            mStrokes.add(strokeDescription);
178
179            if (getTotalDuration(mStrokes) > MAX_GESTURE_DURATION_MS) {
180                mStrokes.remove(strokeDescription);
181                throw new IllegalStateException(
182                        "Gesture would exceed maximum duration with new stroke");
183            }
184            return this;
185        }
186
187        public GestureDescription build() {
188            if (mStrokes.size() == 0) {
189                throw new IllegalStateException("Gestures must have at least one stroke");
190            }
191            return new GestureDescription(mStrokes);
192        }
193    }
194
195    /**
196     * Immutable description of stroke that can be part of a gesture.
197     */
198    public static class StrokeDescription {
199        Path mPath;
200        long mStartTime;
201        long mEndTime;
202        private float mTimeToLengthConversion;
203        private PathMeasure mPathMeasure;
204        // The tap location is only set for zero-length paths
205        float[] mTapLocation;
206
207        /**
208         * @param path The path to follow. Must have exactly one contour. The bounds of the path
209         * must not be negative. The path must not be empty. If the path has zero length
210         * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move.
211         * @param startTime The time, in milliseconds, from the time the gesture starts to the
212         * time the stroke should start. Must not be negative.
213         * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
214         * Must not be negative.
215         */
216        public StrokeDescription(@NonNull Path path,
217                @IntRange(from = 0) long startTime,
218                @IntRange(from = 0) long duration) {
219            if (duration <= 0) {
220                throw new IllegalArgumentException("Duration must be positive");
221            }
222            if (startTime < 0) {
223                throw new IllegalArgumentException("Start time must not be negative");
224            }
225            RectF bounds = new RectF();
226            path.computeBounds(bounds, false /* unused */);
227            if ((bounds.bottom < 0) || (bounds.top < 0) || (bounds.right < 0)
228                    || (bounds.left < 0)) {
229                throw new IllegalArgumentException("Path bounds must not be negative");
230            }
231            if (path.isEmpty()) {
232                throw new IllegalArgumentException("Path is empty");
233            }
234            mPath = new Path(path);
235            mPathMeasure = new PathMeasure(path, false);
236            if (mPathMeasure.getLength() == 0) {
237                // Treat zero-length paths as taps
238                Path tempPath = new Path(path);
239                tempPath.lineTo(-1, -1);
240                mTapLocation = new float[2];
241                PathMeasure pathMeasure = new PathMeasure(tempPath, false);
242                pathMeasure.getPosTan(0, mTapLocation, null);
243            }
244            if (mPathMeasure.nextContour()) {
245                throw new IllegalArgumentException("Path has more than one contour");
246            }
247            /*
248             * Calling nextContour has moved mPathMeasure off the first contour, which is the only
249             * one we care about. Set the path again to go back to the first contour.
250             */
251            mPathMeasure.setPath(mPath, false);
252            mStartTime = startTime;
253            mEndTime = startTime + duration;
254            mTimeToLengthConversion = getLength() / duration;
255        }
256
257        /**
258         * Retrieve a copy of the path for this stroke
259         *
260         * @return A copy of the path
261         */
262        public Path getPath() {
263            return new Path(mPath);
264        }
265
266        /**
267         * Get the stroke's start time
268         *
269         * @return the start time for this stroke.
270         */
271        public long getStartTime() {
272            return mStartTime;
273        }
274
275        /**
276         * Get the stroke's duration
277         *
278         * @return the duration for this stroke
279         */
280        public long getDuration() {
281            return mEndTime - mStartTime;
282        }
283
284        float getLength() {
285            return mPathMeasure.getLength();
286        }
287
288        /* Assumes hasPointForTime returns true */
289        boolean getPosForTime(long time, float[] pos) {
290            if (mTapLocation != null) {
291                pos[0] = mTapLocation[0];
292                pos[1] = mTapLocation[1];
293                return true;
294            }
295            if (time == mEndTime) {
296                // Close to the end time, roundoff can be a problem
297                return mPathMeasure.getPosTan(getLength(), pos, null);
298            }
299            float length = mTimeToLengthConversion * ((float) (time - mStartTime));
300            return mPathMeasure.getPosTan(length, pos, null);
301        }
302
303        boolean hasPointForTime(long time) {
304            return ((time >= mStartTime) && (time <= mEndTime));
305        }
306    }
307
308    /**
309     * The location of a finger for gesture dispatch
310     *
311     * @hide
312     */
313    public static class TouchPoint implements Parcelable {
314        private static final int FLAG_IS_START_OF_PATH = 0x01;
315        private static final int FLAG_IS_END_OF_PATH = 0x02;
316
317        int mPathIndex;
318        boolean mIsStartOfPath;
319        boolean mIsEndOfPath;
320        float mX;
321        float mY;
322
323        public TouchPoint() {
324        }
325
326        public TouchPoint(TouchPoint pointToCopy) {
327            copyFrom(pointToCopy);
328        }
329
330        public TouchPoint(Parcel parcel) {
331            mPathIndex = parcel.readInt();
332            int startEnd = parcel.readInt();
333            mIsStartOfPath = (startEnd & FLAG_IS_START_OF_PATH) != 0;
334            mIsEndOfPath = (startEnd & FLAG_IS_END_OF_PATH) != 0;
335            mX = parcel.readFloat();
336            mY = parcel.readFloat();
337        }
338
339        void copyFrom(TouchPoint other) {
340            mPathIndex = other.mPathIndex;
341            mIsStartOfPath = other.mIsStartOfPath;
342            mIsEndOfPath = other.mIsEndOfPath;
343            mX = other.mX;
344            mY = other.mY;
345        }
346
347        @Override
348        public int describeContents() {
349            return 0;
350        }
351
352        @Override
353        public void writeToParcel(Parcel dest, int flags) {
354            dest.writeInt(mPathIndex);
355            int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0;
356            startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0;
357            dest.writeInt(startEnd);
358            dest.writeFloat(mX);
359            dest.writeFloat(mY);
360        }
361
362        public static final Parcelable.Creator<TouchPoint> CREATOR
363                = new Parcelable.Creator<TouchPoint>() {
364            public TouchPoint createFromParcel(Parcel in) {
365                return new TouchPoint(in);
366            }
367
368            public TouchPoint[] newArray(int size) {
369                return new TouchPoint[size];
370            }
371        };
372    }
373
374    /**
375     * A step along a gesture. Contains all of the touch points at a particular time
376     *
377     * @hide
378     */
379    public static class GestureStep implements Parcelable {
380        public long timeSinceGestureStart;
381        public int numTouchPoints;
382        public TouchPoint[] touchPoints;
383
384        public GestureStep(long timeSinceGestureStart, int numTouchPoints,
385                TouchPoint[] touchPointsToCopy) {
386            this.timeSinceGestureStart = timeSinceGestureStart;
387            this.numTouchPoints = numTouchPoints;
388            this.touchPoints = new TouchPoint[numTouchPoints];
389            for (int i = 0; i < numTouchPoints; i++) {
390                this.touchPoints[i] = new TouchPoint(touchPointsToCopy[i]);
391            }
392        }
393
394        public GestureStep(Parcel parcel) {
395            timeSinceGestureStart = parcel.readLong();
396            Parcelable[] parcelables =
397                    parcel.readParcelableArray(TouchPoint.class.getClassLoader());
398            numTouchPoints = (parcelables == null) ? 0 : parcelables.length;
399            touchPoints = new TouchPoint[numTouchPoints];
400            for (int i = 0; i < numTouchPoints; i++) {
401                touchPoints[i] = (TouchPoint) parcelables[i];
402            }
403        }
404
405        @Override
406        public int describeContents() {
407            return 0;
408        }
409
410        @Override
411        public void writeToParcel(Parcel dest, int flags) {
412            dest.writeLong(timeSinceGestureStart);
413            dest.writeParcelableArray(touchPoints, flags);
414        }
415
416        public static final Parcelable.Creator<GestureStep> CREATOR
417                = new Parcelable.Creator<GestureStep>() {
418            public GestureStep createFromParcel(Parcel in) {
419                return new GestureStep(in);
420            }
421
422            public GestureStep[] newArray(int size) {
423                return new GestureStep[size];
424            }
425        };
426    }
427
428    /**
429     * Class to convert a GestureDescription to a series of MotionEvents.
430     *
431     * @hide
432     */
433    public static class MotionEventGenerator {
434        /**
435         * Constants used to initialize all MotionEvents
436         */
437        private static final int EVENT_META_STATE = 0;
438        private static final int EVENT_BUTTON_STATE = 0;
439        private static final int EVENT_DEVICE_ID = 0;
440        private static final int EVENT_EDGE_FLAGS = 0;
441        private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
442        private static final int EVENT_FLAGS = 0;
443        private static final float EVENT_X_PRECISION = 1;
444        private static final float EVENT_Y_PRECISION = 1;
445
446        /* Lazily-created scratch memory for processing touches */
447        private static TouchPoint[] sCurrentTouchPoints;
448        private static TouchPoint[] sLastTouchPoints;
449        private static PointerCoords[] sPointerCoords;
450        private static PointerProperties[] sPointerProps;
451
452        static List<GestureStep> getGestureStepsFromGestureDescription(
453                GestureDescription description, int sampleTimeMs) {
454            final List<GestureStep> gestureSteps = new ArrayList<>();
455
456            // Point data at each time we generate an event for
457            final TouchPoint[] currentTouchPoints =
458                    getCurrentTouchPoints(description.getStrokeCount());
459            int currentTouchPointSize = 0;
460            /* Loop through each time slice where there are touch points */
461            long timeSinceGestureStart = 0;
462            long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart);
463            while (nextKeyPointTime >= 0) {
464                timeSinceGestureStart = (currentTouchPointSize == 0) ? nextKeyPointTime
465                        : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs);
466                currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart,
467                        currentTouchPoints);
468                gestureSteps.add(new GestureStep(timeSinceGestureStart, currentTouchPointSize,
469                        currentTouchPoints));
470
471                /* Move to next time slice */
472                nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1);
473            }
474            return gestureSteps;
475        }
476
477        public static List<MotionEvent> getMotionEventsFromGestureSteps(List<GestureStep> steps) {
478            final List<MotionEvent> motionEvents = new ArrayList<>();
479
480            // Number of points in last touch event
481            int lastTouchPointSize = 0;
482            TouchPoint[] lastTouchPoints;
483
484            for (int i = 0; i < steps.size(); i++) {
485                GestureStep step = steps.get(i);
486                int currentTouchPointSize = step.numTouchPoints;
487                lastTouchPoints = getLastTouchPoints(
488                        Math.max(lastTouchPointSize, currentTouchPointSize));
489
490                appendMoveEventIfNeeded(motionEvents, lastTouchPoints, lastTouchPointSize,
491                        step.touchPoints, currentTouchPointSize, step.timeSinceGestureStart);
492                lastTouchPointSize = appendUpEvents(motionEvents, lastTouchPoints,
493                        lastTouchPointSize, step.touchPoints, currentTouchPointSize,
494                        step.timeSinceGestureStart);
495                lastTouchPointSize = appendDownEvents(motionEvents, lastTouchPoints,
496                        lastTouchPointSize, step.touchPoints, currentTouchPointSize,
497                        step.timeSinceGestureStart);
498            }
499            return motionEvents;
500        }
501
502        private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
503            if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
504                sCurrentTouchPoints = new TouchPoint[requiredCapacity];
505                for (int i = 0; i < requiredCapacity; i++) {
506                    sCurrentTouchPoints[i] = new TouchPoint();
507                }
508            }
509            return sCurrentTouchPoints;
510        }
511
512        private static TouchPoint[] getLastTouchPoints(int requiredCapacity) {
513            if ((sLastTouchPoints == null) || (sLastTouchPoints.length < requiredCapacity)) {
514                sLastTouchPoints = new TouchPoint[requiredCapacity];
515                for (int i = 0; i < requiredCapacity; i++) {
516                    sLastTouchPoints[i] = new TouchPoint();
517                }
518            }
519            return sLastTouchPoints;
520        }
521
522        private static PointerCoords[] getPointerCoords(int requiredCapacity) {
523            if ((sPointerCoords == null) || (sPointerCoords.length < requiredCapacity)) {
524                sPointerCoords = new PointerCoords[requiredCapacity];
525                for (int i = 0; i < requiredCapacity; i++) {
526                    sPointerCoords[i] = new PointerCoords();
527                }
528            }
529            return sPointerCoords;
530        }
531
532        private static PointerProperties[] getPointerProps(int requiredCapacity) {
533            if ((sPointerProps == null) || (sPointerProps.length < requiredCapacity)) {
534                sPointerProps = new PointerProperties[requiredCapacity];
535                for (int i = 0; i < requiredCapacity; i++) {
536                    sPointerProps[i] = new PointerProperties();
537                }
538            }
539            return sPointerProps;
540        }
541
542        private static void appendMoveEventIfNeeded(List<MotionEvent> motionEvents,
543                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
544                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
545            /* Look for pointers that have moved */
546            boolean moveFound = false;
547            for (int i = 0; i < currentTouchPointsSize; i++) {
548                int lastPointsIndex = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
549                        currentTouchPoints[i].mPathIndex);
550                if (lastPointsIndex >= 0) {
551                    moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX)
552                            || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY);
553                    lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]);
554                }
555            }
556
557            if (moveFound) {
558                long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
559                motionEvents.add(obtainMotionEvent(downTime, currentTime, MotionEvent.ACTION_MOVE,
560                        lastTouchPoints, lastTouchPointsSize));
561            }
562        }
563
564        private static int appendUpEvents(List<MotionEvent> motionEvents,
565                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
566                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
567            /* Look for a pointer at the end of its path */
568            for (int i = 0; i < currentTouchPointsSize; i++) {
569                if (currentTouchPoints[i].mIsEndOfPath) {
570                    int indexOfUpEvent = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
571                            currentTouchPoints[i].mPathIndex);
572                    if (indexOfUpEvent < 0) {
573                        continue; // Should not happen
574                    }
575                    long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
576                    int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_UP
577                            : MotionEvent.ACTION_POINTER_UP;
578                    action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
579                    motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
580                            lastTouchPoints, lastTouchPointsSize));
581                    /* Remove this point from lastTouchPoints */
582                    for (int j = indexOfUpEvent; j < lastTouchPointsSize - 1; j++) {
583                        lastTouchPoints[j].copyFrom(lastTouchPoints[j+1]);
584                    }
585                    lastTouchPointsSize--;
586                }
587            }
588            return lastTouchPointsSize;
589        }
590
591        private static int appendDownEvents(List<MotionEvent> motionEvents,
592                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
593                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
594            /* Look for a pointer that is just starting */
595            for (int i = 0; i < currentTouchPointsSize; i++) {
596                if (currentTouchPoints[i].mIsStartOfPath) {
597                    /* Add the point to last coords and use the new array to generate the event */
598                    lastTouchPoints[lastTouchPointsSize++].copyFrom(currentTouchPoints[i]);
599                    int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_DOWN
600                            : MotionEvent.ACTION_POINTER_DOWN;
601                    long downTime = (action == MotionEvent.ACTION_DOWN) ? currentTime :
602                            motionEvents.get(motionEvents.size() - 1).getDownTime();
603                    action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
604                    motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
605                            lastTouchPoints, lastTouchPointsSize));
606                }
607            }
608            return lastTouchPointsSize;
609        }
610
611        private static MotionEvent obtainMotionEvent(long downTime, long eventTime, int action,
612                TouchPoint[] touchPoints, int touchPointsSize) {
613            PointerCoords[] pointerCoords = getPointerCoords(touchPointsSize);
614            PointerProperties[] pointerProperties = getPointerProps(touchPointsSize);
615            for (int i = 0; i < touchPointsSize; i++) {
616                pointerProperties[i].id = touchPoints[i].mPathIndex;
617                pointerProperties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
618                pointerCoords[i].clear();
619                pointerCoords[i].pressure = 1.0f;
620                pointerCoords[i].size = 1.0f;
621                pointerCoords[i].x = touchPoints[i].mX;
622                pointerCoords[i].y = touchPoints[i].mY;
623            }
624            return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize,
625                    pointerProperties, pointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE,
626                    EVENT_X_PRECISION, EVENT_Y_PRECISION, EVENT_DEVICE_ID, EVENT_EDGE_FLAGS,
627                    EVENT_SOURCE, EVENT_FLAGS);
628        }
629
630        private static int findPointByPathIndex(TouchPoint[] touchPoints, int touchPointsSize,
631                int pathIndex) {
632            for (int i = 0; i < touchPointsSize; i++) {
633                if (touchPoints[i].mPathIndex == pathIndex) {
634                    return i;
635                }
636            }
637            return -1;
638        }
639    }
640}
641