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