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 
17 package android.accessibilityservice;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.graphics.Path;
22 import android.graphics.PathMeasure;
23 import android.graphics.RectF;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 
27 import com.android.internal.util.Preconditions;
28 
29 import java.util.ArrayList;
30 import 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  */
40 public 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      */
getMaxStrokeCount()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      */
getMaxGestureDuration()66     public static long getMaxGestureDuration() {
67         return MAX_GESTURE_DURATION_MS;
68     }
69 
GestureDescription()70     private GestureDescription() {}
71 
GestureDescription(List<StrokeDescription> strokes)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      */
getStrokeCount()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      */
getStroke(@ntRangefrom = 0) int index)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      */
getNextKeyPointAtLeast(long offset)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      */
getPointsForTime(long time, TouchPoint[] touchPoints)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
getTotalDuration(List<StrokeDescription> paths)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          */
addStroke(@onNull StrokeDescription strokeDescription)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 
build()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          */
StrokeDescription(@onNull Path path, @IntRange(from = 0) long startTime, @IntRange(from = 0) long duration)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          */
StrokeDescription(@onNull Path path, @IntRange(from = 0) long startTime, @IntRange(from = 0) long duration, boolean willContinue)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          */
getPath()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          */
getStartTime()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          */
getDuration()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          */
getId()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          */
continueStroke(Path path, long startTime, long duration, boolean willContinue)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          */
willContinue()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          */
getContinuedStrokeId()360         public int getContinuedStrokeId() {
361             return mContinuedStrokeId;
362         }
363 
getLength()364         float getLength() {
365             return mPathMeasure.getLength();
366         }
367 
368         /* Assumes hasPointForTime returns true */
getPosForTime(long time, float[] pos)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 
hasPointForTime(long time)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 
TouchPoint()404         public TouchPoint() {
405         }
406 
TouchPoint(TouchPoint pointToCopy)407         public TouchPoint(TouchPoint pointToCopy) {
408             copyFrom(pointToCopy);
409         }
410 
TouchPoint(Parcel parcel)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 
copyFrom(TouchPoint other)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
toString()431         public String toString() {
432             return "TouchPoint{"
433                     + "mStrokeId=" + mStrokeId
434                     + ", mContinuedStrokeId=" + mContinuedStrokeId
435                     + ", mIsStartOfPath=" + mIsStartOfPath
436                     + ", mIsEndOfPath=" + mIsEndOfPath
437                     + ", mX=" + mX
438                     + ", mY=" + mY
439                     + '}';
440         }
441 
442         @Override
describeContents()443         public int describeContents() {
444             return 0;
445         }
446 
447         @Override
writeToParcel(Parcel dest, int flags)448         public void writeToParcel(Parcel dest, int flags) {
449             dest.writeInt(mStrokeId);
450             dest.writeInt(mContinuedStrokeId);
451             int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0;
452             startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0;
453             dest.writeInt(startEnd);
454             dest.writeFloat(mX);
455             dest.writeFloat(mY);
456         }
457 
458         public static final @android.annotation.NonNull Parcelable.Creator<TouchPoint> CREATOR
459                 = new Parcelable.Creator<TouchPoint>() {
460             public TouchPoint createFromParcel(Parcel in) {
461                 return new TouchPoint(in);
462             }
463 
464             public TouchPoint[] newArray(int size) {
465                 return new TouchPoint[size];
466             }
467         };
468     }
469 
470     /**
471      * A step along a gesture. Contains all of the touch points at a particular time
472      *
473      * @hide
474      */
475     public static class GestureStep implements Parcelable {
476         public long timeSinceGestureStart;
477         public int numTouchPoints;
478         public TouchPoint[] touchPoints;
479 
GestureStep(long timeSinceGestureStart, int numTouchPoints, TouchPoint[] touchPointsToCopy)480         public GestureStep(long timeSinceGestureStart, int numTouchPoints,
481                 TouchPoint[] touchPointsToCopy) {
482             this.timeSinceGestureStart = timeSinceGestureStart;
483             this.numTouchPoints = numTouchPoints;
484             this.touchPoints = new TouchPoint[numTouchPoints];
485             for (int i = 0; i < numTouchPoints; i++) {
486                 this.touchPoints[i] = new TouchPoint(touchPointsToCopy[i]);
487             }
488         }
489 
GestureStep(Parcel parcel)490         public GestureStep(Parcel parcel) {
491             timeSinceGestureStart = parcel.readLong();
492             Parcelable[] parcelables =
493                     parcel.readParcelableArray(TouchPoint.class.getClassLoader());
494             numTouchPoints = (parcelables == null) ? 0 : parcelables.length;
495             touchPoints = new TouchPoint[numTouchPoints];
496             for (int i = 0; i < numTouchPoints; i++) {
497                 touchPoints[i] = (TouchPoint) parcelables[i];
498             }
499         }
500 
501         @Override
describeContents()502         public int describeContents() {
503             return 0;
504         }
505 
506         @Override
writeToParcel(Parcel dest, int flags)507         public void writeToParcel(Parcel dest, int flags) {
508             dest.writeLong(timeSinceGestureStart);
509             dest.writeParcelableArray(touchPoints, flags);
510         }
511 
512         public static final @android.annotation.NonNull Parcelable.Creator<GestureStep> CREATOR
513                 = new Parcelable.Creator<GestureStep>() {
514             public GestureStep createFromParcel(Parcel in) {
515                 return new GestureStep(in);
516             }
517 
518             public GestureStep[] newArray(int size) {
519                 return new GestureStep[size];
520             }
521         };
522     }
523 
524     /**
525      * Class to convert a GestureDescription to a series of GestureSteps.
526      *
527      * @hide
528      */
529     public static class MotionEventGenerator {
530         /* Lazily-created scratch memory for processing touches */
531         private static TouchPoint[] sCurrentTouchPoints;
532 
getGestureStepsFromGestureDescription( GestureDescription description, int sampleTimeMs)533         public static List<GestureStep> getGestureStepsFromGestureDescription(
534                 GestureDescription description, int sampleTimeMs) {
535             final List<GestureStep> gestureSteps = new ArrayList<>();
536 
537             // Point data at each time we generate an event for
538             final TouchPoint[] currentTouchPoints =
539                     getCurrentTouchPoints(description.getStrokeCount());
540             int currentTouchPointSize = 0;
541             /* Loop through each time slice where there are touch points */
542             long timeSinceGestureStart = 0;
543             long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart);
544             while (nextKeyPointTime >= 0) {
545                 timeSinceGestureStart = (currentTouchPointSize == 0) ? nextKeyPointTime
546                         : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs);
547                 currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart,
548                         currentTouchPoints);
549                 gestureSteps.add(new GestureStep(timeSinceGestureStart, currentTouchPointSize,
550                         currentTouchPoints));
551 
552                 /* Move to next time slice */
553                 nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1);
554             }
555             return gestureSteps;
556         }
557 
getCurrentTouchPoints(int requiredCapacity)558         private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
559             if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
560                 sCurrentTouchPoints = new TouchPoint[requiredCapacity];
561                 for (int i = 0; i < requiredCapacity; i++) {
562                     sCurrentTouchPoints[i] = new TouchPoint();
563                 }
564             }
565             return sCurrentTouchPoints;
566         }
567     }
568 }
569