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