GestureDescription.java revision 8313881e2855b6057812458d37b2dcd804f54953
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 once built.
40 * <p>
41 * Spatial dimensions throughout are in screen pixels. Time is measured in milliseconds.
42 */
43public final class GestureDescription {
44    /** Gestures may contain no more than this many strokes */
45    private static final int MAX_STROKE_COUNT = 10;
46
47    /**
48     * Upper bound on total gesture duration. Nearly all gestures will be much shorter.
49     */
50    private static final long MAX_GESTURE_DURATION_MS = 60 * 1000;
51
52    private final List<StrokeDescription> mStrokes = new ArrayList<>();
53    private final float[] mTempPos = new float[2];
54
55    /**
56     * Get the upper limit for the number of strokes a gesture may contain.
57     *
58     * @return The maximum number of strokes.
59     */
60    public static int getMaxStrokeCount() {
61        return MAX_STROKE_COUNT;
62    }
63
64    /**
65     * Get the upper limit on a gesture's duration.
66     *
67     * @return The maximum duration in milliseconds.
68     */
69    public static long getMaxGestureDuration() {
70        return MAX_GESTURE_DURATION_MS;
71    }
72
73    private GestureDescription() {}
74
75    private GestureDescription(List<StrokeDescription> strokes) {
76        mStrokes.addAll(strokes);
77    }
78
79    /**
80     * Get the number of stroke in the gesture.
81     *
82     * @return the number of strokes in this gesture
83     */
84    public int getStrokeCount() {
85        return mStrokes.size();
86    }
87
88    /**
89     * Read a stroke from the gesture
90     *
91     * @param index the index of the stroke
92     *
93     * @return A description of the stroke.
94     */
95    public StrokeDescription getStroke(@IntRange(from = 0) int index) {
96        return mStrokes.get(index);
97    }
98
99    /**
100     * Return the smallest key point (where a path starts or ends) that is at least a specified
101     * offset
102     * @param offset the minimum start time
103     * @return The next key time that is at least the offset or -1 if one can't be found
104     */
105    private long getNextKeyPointAtLeast(long offset) {
106        long nextKeyPoint = Long.MAX_VALUE;
107        for (int i = 0; i < mStrokes.size(); i++) {
108            long thisStartTime = mStrokes.get(i).mStartTime;
109            if ((thisStartTime < nextKeyPoint) && (thisStartTime >= offset)) {
110                nextKeyPoint = thisStartTime;
111            }
112            long thisEndTime = mStrokes.get(i).mEndTime;
113            if ((thisEndTime < nextKeyPoint) && (thisEndTime >= offset)) {
114                nextKeyPoint = thisEndTime;
115            }
116        }
117        return (nextKeyPoint == Long.MAX_VALUE) ? -1L : nextKeyPoint;
118    }
119
120    /**
121     * Get the points that correspond to a particular moment in time.
122     * @param time The time of interest
123     * @param touchPoints An array to hold the current touch points. Must be preallocated to at
124     * least the number of paths in the gesture to prevent going out of bounds
125     * @return The number of points found, and thus the number of elements set in each array
126     */
127    private int getPointsForTime(long time, TouchPoint[] touchPoints) {
128        int numPointsFound = 0;
129        for (int i = 0; i < mStrokes.size(); i++) {
130            StrokeDescription strokeDescription = mStrokes.get(i);
131            if (strokeDescription.hasPointForTime(time)) {
132                touchPoints[numPointsFound].mPathIndex = i;
133                touchPoints[numPointsFound].mIsStartOfPath = (time == strokeDescription.mStartTime);
134                touchPoints[numPointsFound].mIsEndOfPath = (time == strokeDescription.mEndTime);
135                strokeDescription.getPosForTime(time, mTempPos);
136                touchPoints[numPointsFound].mX = Math.round(mTempPos[0]);
137                touchPoints[numPointsFound].mY = Math.round(mTempPos[1]);
138                numPointsFound++;
139            }
140        }
141        return numPointsFound;
142    }
143
144    // Total duration assumes that the gesture starts at 0; waiting around to start a gesture
145    // counts against total duration
146    private static long getTotalDuration(List<StrokeDescription> paths) {
147        long latestEnd = Long.MIN_VALUE;
148        for (int i = 0; i < paths.size(); i++) {
149            StrokeDescription path = paths.get(i);
150            latestEnd = Math.max(latestEnd, path.mEndTime);
151        }
152        return Math.max(latestEnd, 0);
153    }
154
155    /**
156     * Builder for a {@code GestureDescription}
157     */
158    public static class Builder {
159
160        private final List<StrokeDescription> mStrokes = new ArrayList<>();
161
162        /**
163         * Add a stroke to the gesture description. Up to {@code MAX_STROKE_COUNT} paths may be
164         * added to a gesture, and the total gesture duration (earliest path start time to latest path
165         * end time) may not exceed {@code MAX_GESTURE_DURATION_MS}.
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
205        /**
206         * @param path The path to follow. Must have exactly one contour, and that contour must
207         * have nonzero length. The bounds of the path must not be negative.
208         * @param startTime The time, in milliseconds, from the time the gesture starts to the
209         * time the stroke should start. Must not be negative.
210         * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
211         * Must not be negative.
212         */
213        public StrokeDescription(@NonNull Path path,
214                @IntRange(from = 0) long startTime,
215                @IntRange(from = 0) long duration) {
216            if (duration <= 0) {
217                throw new IllegalArgumentException("Duration must be positive");
218            }
219            if (startTime < 0) {
220                throw new IllegalArgumentException("Start time must not be negative");
221            }
222            RectF bounds = new RectF();
223            path.computeBounds(bounds, false /* unused */);
224            if ((bounds.bottom < 0) || (bounds.top < 0) || (bounds.right < 0)
225                    || (bounds.left < 0)) {
226                throw new IllegalArgumentException("Path bounds must not be negative");
227            }
228            mPath = new Path(path);
229            mPathMeasure = new PathMeasure(path, false);
230            if (mPathMeasure.getLength() == 0) {
231                throw new IllegalArgumentException("Path has zero length");
232            }
233            if (mPathMeasure.nextContour()) {
234                throw new IllegalArgumentException("Path has more than one contour");
235            }
236            /*
237             * Calling nextContour has moved mPathMeasure off the first contour, which is the only
238             * one we care about. Set the path again to go back to the first contour.
239             */
240            mPathMeasure.setPath(path, false);
241            mStartTime = startTime;
242            mEndTime = startTime + duration;
243            if (duration > 0) {
244                mTimeToLengthConversion = getLength() / duration;
245            }
246        }
247
248        /**
249         * Retrieve a copy of the path for this stroke
250         *
251         * @return A copy of the path
252         */
253        public Path getPath() {
254            return new Path(mPath);
255        }
256
257        /**
258         * Get the stroke's start time
259         *
260         * @return the start time for this stroke.
261         */
262        public long getStartTime() {
263            return mStartTime;
264        }
265
266        /**
267         * Get the stroke's duration
268         *
269         * @return the duration for this stroke
270         */
271        public long getDuration() {
272            return mEndTime - mStartTime;
273        }
274
275        float getLength() {
276            return mPathMeasure.getLength();
277        }
278
279        /* Assumes hasPointForTime returns true */
280        boolean getPosForTime(long time, float[] pos) {
281            if (time == mEndTime) {
282                // Close to the end time, roundoff can be a problem
283                return mPathMeasure.getPosTan(getLength(), pos, null);
284            }
285            float length = mTimeToLengthConversion * ((float) (time - mStartTime));
286            return mPathMeasure.getPosTan(length, pos, null);
287        }
288
289        boolean hasPointForTime(long time) {
290            return ((time >= mStartTime) && (time <= mEndTime));
291        }
292    }
293
294    private static class TouchPoint {
295        int mPathIndex;
296        boolean mIsStartOfPath;
297        boolean mIsEndOfPath;
298        float mX;
299        float mY;
300
301        void copyFrom(TouchPoint other) {
302            mPathIndex = other.mPathIndex;
303            mIsStartOfPath = other.mIsStartOfPath;
304            mIsEndOfPath = other.mIsEndOfPath;
305            mX = other.mX;
306            mY = other.mY;
307        }
308    }
309
310    /**
311     * Class to convert a GestureDescription to a series of MotionEvents.
312     */
313    static class MotionEventGenerator {
314        /**
315         * Constants used to initialize all MotionEvents
316         */
317        private static final int EVENT_META_STATE = 0;
318        private static final int EVENT_BUTTON_STATE = 0;
319        private static final int EVENT_DEVICE_ID = 0;
320        private static final int EVENT_EDGE_FLAGS = 0;
321        private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
322        private static final int EVENT_FLAGS = 0;
323        private static final float EVENT_X_PRECISION = 1;
324        private static final float EVENT_Y_PRECISION = 1;
325
326        /* Lazily-created scratch memory for processing touches */
327        private static TouchPoint[] sCurrentTouchPoints;
328        private static TouchPoint[] sLastTouchPoints;
329        private static PointerCoords[] sPointerCoords;
330        private static PointerProperties[] sPointerProps;
331
332        static List<MotionEvent> getMotionEventsFromGestureDescription(
333                GestureDescription description, int sampleTimeMs) {
334            final List<MotionEvent> motionEvents = new ArrayList<>();
335
336            // Point data at each time we generate an event for
337            final TouchPoint[] currentTouchPoints =
338                    getCurrentTouchPoints(description.getStrokeCount());
339            // Point data sent in last touch event
340            int lastTouchPointSize = 0;
341            final TouchPoint[] lastTouchPoints =
342                    getLastTouchPoints(description.getStrokeCount());
343
344            /* Loop through each time slice where there are touch points */
345            long timeSinceGestureStart = 0;
346            long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart);
347            while (nextKeyPointTime >= 0) {
348                timeSinceGestureStart = (lastTouchPointSize == 0) ? nextKeyPointTime
349                        : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs);
350                int currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart,
351                        currentTouchPoints);
352
353                appendMoveEventIfNeeded(motionEvents, lastTouchPoints, lastTouchPointSize,
354                        currentTouchPoints, currentTouchPointSize, timeSinceGestureStart);
355                lastTouchPointSize = appendUpEvents(motionEvents, lastTouchPoints,
356                        lastTouchPointSize, currentTouchPoints, currentTouchPointSize,
357                        timeSinceGestureStart);
358                lastTouchPointSize = appendDownEvents(motionEvents, lastTouchPoints,
359                        lastTouchPointSize, currentTouchPoints, currentTouchPointSize,
360                        timeSinceGestureStart);
361
362                /* Move to next time slice */
363                nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1);
364            }
365            return motionEvents;
366        }
367
368        private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
369            if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
370                sCurrentTouchPoints = new TouchPoint[requiredCapacity];
371                for (int i = 0; i < requiredCapacity; i++) {
372                    sCurrentTouchPoints[i] = new TouchPoint();
373                }
374            }
375            return sCurrentTouchPoints;
376        }
377
378        private static TouchPoint[] getLastTouchPoints(int requiredCapacity) {
379            if ((sLastTouchPoints == null) || (sLastTouchPoints.length < requiredCapacity)) {
380                sLastTouchPoints = new TouchPoint[requiredCapacity];
381                for (int i = 0; i < requiredCapacity; i++) {
382                    sLastTouchPoints[i] = new TouchPoint();
383                }
384            }
385            return sLastTouchPoints;
386        }
387
388        private static PointerCoords[] getPointerCoords(int requiredCapacity) {
389            if ((sPointerCoords == null) || (sPointerCoords.length < requiredCapacity)) {
390                sPointerCoords = new PointerCoords[requiredCapacity];
391                for (int i = 0; i < requiredCapacity; i++) {
392                    sPointerCoords[i] = new PointerCoords();
393                }
394            }
395            return sPointerCoords;
396        }
397
398        private static PointerProperties[] getPointerProps(int requiredCapacity) {
399            if ((sPointerProps == null) || (sPointerProps.length < requiredCapacity)) {
400                sPointerProps = new PointerProperties[requiredCapacity];
401                for (int i = 0; i < requiredCapacity; i++) {
402                    sPointerProps[i] = new PointerProperties();
403                }
404            }
405            return sPointerProps;
406        }
407
408        private static void appendMoveEventIfNeeded(List<MotionEvent> motionEvents,
409                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
410                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
411            /* Look for pointers that have moved */
412            boolean moveFound = false;
413            for (int i = 0; i < currentTouchPointsSize; i++) {
414                int lastPointsIndex = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
415                        currentTouchPoints[i].mPathIndex);
416                if (lastPointsIndex >= 0) {
417                    moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX)
418                            || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY);
419                    lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]);
420                }
421            }
422
423            if (moveFound) {
424                long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
425                motionEvents.add(obtainMotionEvent(downTime, currentTime, MotionEvent.ACTION_MOVE,
426                        lastTouchPoints, lastTouchPointsSize));
427            }
428        }
429
430        private static int appendUpEvents(List<MotionEvent> motionEvents,
431                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
432                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
433            /* Look for a pointer at the end of its path */
434            for (int i = 0; i < currentTouchPointsSize; i++) {
435                if (currentTouchPoints[i].mIsEndOfPath) {
436                    int indexOfUpEvent = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
437                            currentTouchPoints[i].mPathIndex);
438                    if (indexOfUpEvent < 0) {
439                        continue; // Should not happen
440                    }
441                    long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
442                    int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_UP
443                            : MotionEvent.ACTION_POINTER_UP;
444                    action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
445                    motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
446                            lastTouchPoints, lastTouchPointsSize));
447                    /* Remove this point from lastTouchPoints */
448                    for (int j = indexOfUpEvent; j < lastTouchPointsSize - 1; j++) {
449                        lastTouchPoints[j].copyFrom(lastTouchPoints[j+1]);
450                    }
451                    lastTouchPointsSize--;
452                }
453            }
454            return lastTouchPointsSize;
455        }
456
457        private static int appendDownEvents(List<MotionEvent> motionEvents,
458                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
459                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
460            /* Look for a pointer that is just starting */
461            for (int i = 0; i < currentTouchPointsSize; i++) {
462                if (currentTouchPoints[i].mIsStartOfPath) {
463                    /* Add the point to last coords and use the new array to generate the event */
464                    lastTouchPoints[lastTouchPointsSize++].copyFrom(currentTouchPoints[i]);
465                    int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_DOWN
466                            : MotionEvent.ACTION_POINTER_DOWN;
467                    long downTime = (action == MotionEvent.ACTION_DOWN) ? currentTime :
468                            motionEvents.get(motionEvents.size() - 1).getDownTime();
469                    action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
470                    motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
471                            lastTouchPoints, lastTouchPointsSize));
472                }
473            }
474            return lastTouchPointsSize;
475        }
476
477        private static MotionEvent obtainMotionEvent(long downTime, long eventTime, int action,
478                TouchPoint[] touchPoints, int touchPointsSize) {
479            PointerCoords[] pointerCoords = getPointerCoords(touchPointsSize);
480            PointerProperties[] pointerProperties = getPointerProps(touchPointsSize);
481            for (int i = 0; i < touchPointsSize; i++) {
482                pointerProperties[i].id = touchPoints[i].mPathIndex;
483                pointerProperties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
484                pointerCoords[i].clear();
485                pointerCoords[i].pressure = 1.0f;
486                pointerCoords[i].size = 1.0f;
487                pointerCoords[i].x = touchPoints[i].mX;
488                pointerCoords[i].y = touchPoints[i].mY;
489            }
490            return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize,
491                    pointerProperties, pointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE,
492                    EVENT_X_PRECISION, EVENT_Y_PRECISION, EVENT_DEVICE_ID, EVENT_EDGE_FLAGS,
493                    EVENT_SOURCE, EVENT_FLAGS);
494        }
495
496        private static int findPointByPathIndex(TouchPoint[] touchPoints, int touchPointsSize,
497                int pathIndex) {
498            for (int i = 0; i < touchPointsSize; i++) {
499                if (touchPoints[i].mPathIndex == pathIndex) {
500                    return i;
501                }
502            }
503            return -1;
504        }
505    }
506}
507