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