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