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