GestureDescription.java revision 2f165944ce6109134e7285a71da32d1a1647960b
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;
26
27import java.util.ArrayList;
28import java.util.List;
29
30/**
31 * Accessibility services with the
32 * {@link android.R.styleable#AccessibilityService_canPerformGestures} property can dispatch
33 * gestures. This class describes those gestures. Gestures are made up of one or more strokes.
34 * Gestures are immutable once built.
35 * <p>
36 * Spatial dimensions throughout are in screen pixels. Time is measured in milliseconds.
37 */
38public final class GestureDescription {
39    /** Gestures may contain no more than this many strokes */
40    private static final int MAX_STROKE_COUNT = 10;
41
42    /**
43     * Upper bound on total gesture duration. Nearly all gestures will be much shorter.
44     */
45    private static final long MAX_GESTURE_DURATION_MS = 60 * 1000;
46
47    private final List<StrokeDescription> mStrokes = new ArrayList<>();
48    private final float[] mTempPos = new float[2];
49
50    /**
51     * Get the upper limit for the number of strokes a gesture may contain.
52     *
53     * @return The maximum number of strokes.
54     */
55    public static int getMaxStrokeCount() {
56        return MAX_STROKE_COUNT;
57    }
58
59    /**
60     * Get the upper limit on a gesture's duration.
61     *
62     * @return The maximum duration in milliseconds.
63     */
64    public static long getMaxGestureDuration() {
65        return MAX_GESTURE_DURATION_MS;
66    }
67
68    private GestureDescription() {}
69
70    private GestureDescription(List<StrokeDescription> strokes) {
71        mStrokes.addAll(strokes);
72    }
73
74    /**
75     * Get the number of stroke in the gesture.
76     *
77     * @return the number of strokes in this gesture
78     */
79    public int getStrokeCount() {
80        return mStrokes.size();
81    }
82
83    /**
84     * Read a stroke from the gesture
85     *
86     * @param index the index of the stroke
87     *
88     * @return A description of the stroke.
89     */
90    public StrokeDescription getStroke(@IntRange(from = 0) int index) {
91        return mStrokes.get(index);
92    }
93
94    /**
95     * Return the smallest key point (where a path starts or ends) that is at least a specified
96     * offset
97     * @param offset the minimum start time
98     * @return The next key time that is at least the offset or -1 if one can't be found
99     */
100    private long getNextKeyPointAtLeast(long offset) {
101        long nextKeyPoint = Long.MAX_VALUE;
102        for (int i = 0; i < mStrokes.size(); i++) {
103            long thisStartTime = mStrokes.get(i).mStartTime;
104            if ((thisStartTime < nextKeyPoint) && (thisStartTime >= offset)) {
105                nextKeyPoint = thisStartTime;
106            }
107            long thisEndTime = mStrokes.get(i).mEndTime;
108            if ((thisEndTime < nextKeyPoint) && (thisEndTime >= offset)) {
109                nextKeyPoint = thisEndTime;
110            }
111        }
112        return (nextKeyPoint == Long.MAX_VALUE) ? -1L : nextKeyPoint;
113    }
114
115    /**
116     * Get the points that correspond to a particular moment in time.
117     * @param time The time of interest
118     * @param touchPoints An array to hold the current touch points. Must be preallocated to at
119     * least the number of paths in the gesture to prevent going out of bounds
120     * @return The number of points found, and thus the number of elements set in each array
121     */
122    private int getPointsForTime(long time, TouchPoint[] touchPoints) {
123        int numPointsFound = 0;
124        for (int i = 0; i < mStrokes.size(); i++) {
125            StrokeDescription strokeDescription = mStrokes.get(i);
126            if (strokeDescription.hasPointForTime(time)) {
127                touchPoints[numPointsFound].mStrokeId = strokeDescription.getId();
128                touchPoints[numPointsFound].mContinuedStrokeId =
129                        strokeDescription.getContinuedStrokeId();
130                touchPoints[numPointsFound].mIsStartOfPath =
131                        (strokeDescription.getContinuedStrokeId() < 0)
132                                && (time == strokeDescription.mStartTime);
133                touchPoints[numPointsFound].mIsEndOfPath = !strokeDescription.isContinued()
134                        && (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
164         * {@link GestureDescription#getMaxStrokeCount()} paths may be
165         * added to a gesture, and the total gesture duration (earliest path start time to latest
166         * path end time) may not exceed {@link GestureDescription#getMaxGestureDuration()}.
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        public static final int INVALID_STROKE_ID = -1;
201
202        static int sIdCounter;
203
204        Path mPath;
205        long mStartTime;
206        long mEndTime;
207        private float mTimeToLengthConversion;
208        private PathMeasure mPathMeasure;
209        // The tap location is only set for zero-length paths
210        float[] mTapLocation;
211        int mId;
212        boolean mContinued;
213        int mContinuedStrokeId;
214
215        /**
216         * @param path The path to follow. Must have exactly one contour. The bounds of the path
217         * must not be negative. The path must not be empty. If the path has zero length
218         * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move.
219         * @param startTime The time, in milliseconds, from the time the gesture starts to the
220         * time the stroke should start. Must not be negative.
221         * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
222         * Must not be negative.
223         */
224        public StrokeDescription(@NonNull Path path,
225                @IntRange(from = 0) long startTime,
226                @IntRange(from = 0) long duration) {
227            this(path, startTime, duration, INVALID_STROKE_ID, false);
228        }
229
230        /**
231         * @param path The path to follow. Must have exactly one contour. The bounds of the path
232         * must not be negative. The path must not be empty. If the path has zero length
233         * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move.
234         * @param startTime The time, in milliseconds, from the time the gesture starts to the
235         * time the stroke should start. Must not be negative.
236         * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
237         * Must be positive.
238         * @param continuedStrokeId The ID of the stroke that this stroke continues, or
239         * {@link #INVALID_STROKE_ID} if it continues no stroke. The stroke it
240         * continues must have its isContinued flag set to {@code true} and must be in the
241         * gesture dispatched immediately before the one containing this stroke.
242         * @param isContinued {@code true} if this stroke will be continued by one in the
243         * next gesture {@code false} otherwise. Continued strokes keep their pointers down when
244         * the gesture completes.
245         */
246        public StrokeDescription(@NonNull Path path,
247                @IntRange(from = 0) long startTime,
248                @IntRange(from = 0) long duration,
249                @IntRange(from = 0) int continuedStrokeId,
250                boolean isContinued) {
251            mContinued = isContinued;
252            mContinuedStrokeId = continuedStrokeId;
253            if (duration <= 0) {
254                throw new IllegalArgumentException("Duration must be positive");
255            }
256            if (startTime < 0) {
257                throw new IllegalArgumentException("Start time must not be negative");
258            }
259            RectF bounds = new RectF();
260            path.computeBounds(bounds, false /* unused */);
261            if ((bounds.bottom < 0) || (bounds.top < 0) || (bounds.right < 0)
262                    || (bounds.left < 0)) {
263                throw new IllegalArgumentException("Path bounds must not be negative");
264            }
265            if (path.isEmpty()) {
266                throw new IllegalArgumentException("Path is empty");
267            }
268            mPath = new Path(path);
269            mPathMeasure = new PathMeasure(path, false);
270            if (mPathMeasure.getLength() == 0) {
271                // Treat zero-length paths as taps
272                Path tempPath = new Path(path);
273                tempPath.lineTo(-1, -1);
274                mTapLocation = new float[2];
275                PathMeasure pathMeasure = new PathMeasure(tempPath, false);
276                pathMeasure.getPosTan(0, mTapLocation, null);
277            }
278            if (mPathMeasure.nextContour()) {
279                throw new IllegalArgumentException("Path has more than one contour");
280            }
281            /*
282             * Calling nextContour has moved mPathMeasure off the first contour, which is the only
283             * one we care about. Set the path again to go back to the first contour.
284             */
285            mPathMeasure.setPath(mPath, false);
286            mStartTime = startTime;
287            mEndTime = startTime + duration;
288            mTimeToLengthConversion = getLength() / duration;
289            mId = sIdCounter++;
290        }
291
292        /**
293         * Retrieve a copy of the path for this stroke
294         *
295         * @return A copy of the path
296         */
297        public Path getPath() {
298            return new Path(mPath);
299        }
300
301        /**
302         * Get the stroke's start time
303         *
304         * @return the start time for this stroke.
305         */
306        public long getStartTime() {
307            return mStartTime;
308        }
309
310        /**
311         * Get the stroke's duration
312         *
313         * @return the duration for this stroke
314         */
315        public long getDuration() {
316            return mEndTime - mStartTime;
317        }
318
319        /**
320         * Get the stroke's ID. The ID is used when a stroke is to be continued by another
321         * stroke in a future gesture.
322         *
323         * @return the ID of this stroke
324         */
325        public int getId() {
326            return mId;
327        }
328
329        /**
330         * Check if this stroke is marked to continue in the next gesture.
331         *
332         * @return {@code true} if the stroke is to be continued.
333         */
334        public boolean isContinued() {
335            return mContinued;
336        }
337
338        /**
339         * Get the ID of the stroke that this one will continue.
340         *
341         * @return The ID of the stroke that this stroke continues, or 0 if no such stroke exists.
342         */
343        public int getContinuedStrokeId() {
344            return mContinuedStrokeId;
345        }
346
347        float getLength() {
348            return mPathMeasure.getLength();
349        }
350
351        /* Assumes hasPointForTime returns true */
352        boolean getPosForTime(long time, float[] pos) {
353            if (mTapLocation != null) {
354                pos[0] = mTapLocation[0];
355                pos[1] = mTapLocation[1];
356                return true;
357            }
358            if (time == mEndTime) {
359                // Close to the end time, roundoff can be a problem
360                return mPathMeasure.getPosTan(getLength(), pos, null);
361            }
362            float length = mTimeToLengthConversion * ((float) (time - mStartTime));
363            return mPathMeasure.getPosTan(length, pos, null);
364        }
365
366        boolean hasPointForTime(long time) {
367            return ((time >= mStartTime) && (time <= mEndTime));
368        }
369    }
370
371    /**
372     * The location of a finger for gesture dispatch
373     *
374     * @hide
375     */
376    public static class TouchPoint implements Parcelable {
377        private static final int FLAG_IS_START_OF_PATH = 0x01;
378        private static final int FLAG_IS_END_OF_PATH = 0x02;
379
380        public int mStrokeId;
381        public int mContinuedStrokeId;
382        public boolean mIsStartOfPath;
383        public boolean mIsEndOfPath;
384        public float mX;
385        public float mY;
386
387        public TouchPoint() {
388        }
389
390        public TouchPoint(TouchPoint pointToCopy) {
391            copyFrom(pointToCopy);
392        }
393
394        public TouchPoint(Parcel parcel) {
395            mStrokeId = parcel.readInt();
396            mContinuedStrokeId = parcel.readInt();
397            int startEnd = parcel.readInt();
398            mIsStartOfPath = (startEnd & FLAG_IS_START_OF_PATH) != 0;
399            mIsEndOfPath = (startEnd & FLAG_IS_END_OF_PATH) != 0;
400            mX = parcel.readFloat();
401            mY = parcel.readFloat();
402        }
403
404        public void copyFrom(TouchPoint other) {
405            mStrokeId = other.mStrokeId;
406            mContinuedStrokeId = other.mContinuedStrokeId;
407            mIsStartOfPath = other.mIsStartOfPath;
408            mIsEndOfPath = other.mIsEndOfPath;
409            mX = other.mX;
410            mY = other.mY;
411        }
412
413        @Override
414        public int describeContents() {
415            return 0;
416        }
417
418        @Override
419        public void writeToParcel(Parcel dest, int flags) {
420            dest.writeInt(mStrokeId);
421            dest.writeInt(mContinuedStrokeId);
422            int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0;
423            startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0;
424            dest.writeInt(startEnd);
425            dest.writeFloat(mX);
426            dest.writeFloat(mY);
427        }
428
429        public static final Parcelable.Creator<TouchPoint> CREATOR
430                = new Parcelable.Creator<TouchPoint>() {
431            public TouchPoint createFromParcel(Parcel in) {
432                return new TouchPoint(in);
433            }
434
435            public TouchPoint[] newArray(int size) {
436                return new TouchPoint[size];
437            }
438        };
439    }
440
441    /**
442     * A step along a gesture. Contains all of the touch points at a particular time
443     *
444     * @hide
445     */
446    public static class GestureStep implements Parcelable {
447        public long timeSinceGestureStart;
448        public int numTouchPoints;
449        public TouchPoint[] touchPoints;
450
451        public GestureStep(long timeSinceGestureStart, int numTouchPoints,
452                TouchPoint[] touchPointsToCopy) {
453            this.timeSinceGestureStart = timeSinceGestureStart;
454            this.numTouchPoints = numTouchPoints;
455            this.touchPoints = new TouchPoint[numTouchPoints];
456            for (int i = 0; i < numTouchPoints; i++) {
457                this.touchPoints[i] = new TouchPoint(touchPointsToCopy[i]);
458            }
459        }
460
461        public GestureStep(Parcel parcel) {
462            timeSinceGestureStart = parcel.readLong();
463            Parcelable[] parcelables =
464                    parcel.readParcelableArray(TouchPoint.class.getClassLoader());
465            numTouchPoints = (parcelables == null) ? 0 : parcelables.length;
466            touchPoints = new TouchPoint[numTouchPoints];
467            for (int i = 0; i < numTouchPoints; i++) {
468                touchPoints[i] = (TouchPoint) parcelables[i];
469            }
470        }
471
472        @Override
473        public int describeContents() {
474            return 0;
475        }
476
477        @Override
478        public void writeToParcel(Parcel dest, int flags) {
479            dest.writeLong(timeSinceGestureStart);
480            dest.writeParcelableArray(touchPoints, flags);
481        }
482
483        public static final Parcelable.Creator<GestureStep> CREATOR
484                = new Parcelable.Creator<GestureStep>() {
485            public GestureStep createFromParcel(Parcel in) {
486                return new GestureStep(in);
487            }
488
489            public GestureStep[] newArray(int size) {
490                return new GestureStep[size];
491            }
492        };
493    }
494
495    /**
496     * Class to convert a GestureDescription to a series of GestureSteps.
497     *
498     * @hide
499     */
500    public static class MotionEventGenerator {
501        /* Lazily-created scratch memory for processing touches */
502        private static TouchPoint[] sCurrentTouchPoints;
503
504        public static List<GestureStep> getGestureStepsFromGestureDescription(
505                GestureDescription description, int sampleTimeMs) {
506            final List<GestureStep> gestureSteps = new ArrayList<>();
507
508            // Point data at each time we generate an event for
509            final TouchPoint[] currentTouchPoints =
510                    getCurrentTouchPoints(description.getStrokeCount());
511            int currentTouchPointSize = 0;
512            /* Loop through each time slice where there are touch points */
513            long timeSinceGestureStart = 0;
514            long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart);
515            while (nextKeyPointTime >= 0) {
516                timeSinceGestureStart = (currentTouchPointSize == 0) ? nextKeyPointTime
517                        : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs);
518                currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart,
519                        currentTouchPoints);
520                gestureSteps.add(new GestureStep(timeSinceGestureStart, currentTouchPointSize,
521                        currentTouchPoints));
522
523                /* Move to next time slice */
524                nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1);
525            }
526            return gestureSteps;
527        }
528
529        private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
530            if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
531                sCurrentTouchPoints = new TouchPoint[requiredCapacity];
532                for (int i = 0; i < requiredCapacity; i++) {
533                    sCurrentTouchPoints[i] = new TouchPoint();
534                }
535            }
536            return sCurrentTouchPoints;
537        }
538    }
539}
540