blob: e18a34de3f915915402b7a93245c33fb1e06c79a [file] [log] [blame]
Phil Weavera6b64f52015-12-04 15:21:35 -08001/*
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.Matrix;
22import android.graphics.Path;
23import android.graphics.PathMeasure;
24import android.graphics.RectF;
25import android.view.InputDevice;
26import android.view.MotionEvent;
27import android.view.MotionEvent.PointerCoords;
28import android.view.MotionEvent.PointerProperties;
29import android.view.ViewConfiguration;
30
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.List;
34
35/**
36 * Accessibility services with the
37 * {@link android.R.styleable#AccessibilityService_canPerformGestures} property can dispatch
38 * gestures. This class describes those gestures. Gestures are made up of one or more strokes.
Phil Weaver83138812016-03-29 18:15:28 -070039 * Gestures are immutable once built.
Phil Weavera6b64f52015-12-04 15:21:35 -080040 * <p>
41 * Spatial dimensions throughout are in screen pixels. Time is measured in milliseconds.
42 */
43public final class GestureDescription {
44 /** Gestures may contain no more than this many strokes */
Phil Weaver4503fcf2016-03-08 16:29:44 -080045 private static final int MAX_STROKE_COUNT = 10;
Phil Weavera6b64f52015-12-04 15:21:35 -080046
47 /**
48 * Upper bound on total gesture duration. Nearly all gestures will be much shorter.
49 */
Phil Weaver4503fcf2016-03-08 16:29:44 -080050 private static final long MAX_GESTURE_DURATION_MS = 60 * 1000;
Phil Weavera6b64f52015-12-04 15:21:35 -080051
52 private final List<StrokeDescription> mStrokes = new ArrayList<>();
53 private final float[] mTempPos = new float[2];
54
55 /**
Phil Weaver4503fcf2016-03-08 16:29:44 -080056 * Get the upper limit for the number of strokes a gesture may contain.
Phil Weavera6b64f52015-12-04 15:21:35 -080057 *
Phil Weaver4503fcf2016-03-08 16:29:44 -080058 * @return The maximum number of strokes.
Phil Weavera6b64f52015-12-04 15:21:35 -080059 */
Phil Weaver4503fcf2016-03-08 16:29:44 -080060 public static int getMaxStrokeCount() {
61 return MAX_STROKE_COUNT;
Phil Weavera6b64f52015-12-04 15:21:35 -080062 }
63
64 /**
Phil Weaver4503fcf2016-03-08 16:29:44 -080065 * Get the upper limit on a gesture's duration.
Phil Weavera6b64f52015-12-04 15:21:35 -080066 *
Phil Weaver4503fcf2016-03-08 16:29:44 -080067 * @return The maximum duration in milliseconds.
Phil Weavera6b64f52015-12-04 15:21:35 -080068 */
Phil Weaver4503fcf2016-03-08 16:29:44 -080069 public static long getMaxGestureDuration() {
70 return MAX_GESTURE_DURATION_MS;
Phil Weavera6b64f52015-12-04 15:21:35 -080071 }
72
73 private GestureDescription() {}
74
75 private GestureDescription(List<StrokeDescription> strokes) {
76 mStrokes.addAll(strokes);
77 }
78
Phil Weavera6b64f52015-12-04 15:21:35 -080079 /**
80 * Get the number of stroke in the gesture.
81 *
82 * @return the number of strokes in this gesture
83 */
84 public int getStrokeCount() {
85 return mStrokes.size();
86 }
87
88 /**
89 * Read a stroke from the gesture
90 *
91 * @param index the index of the stroke
92 *
93 * @return A description of the stroke.
94 */
95 public StrokeDescription getStroke(@IntRange(from = 0) int index) {
96 return mStrokes.get(index);
97 }
98
99 /**
100 * Return the smallest key point (where a path starts or ends) that is at least a specified
101 * offset
102 * @param offset the minimum start time
103 * @return The next key time that is at least the offset or -1 if one can't be found
104 */
105 private long getNextKeyPointAtLeast(long offset) {
106 long nextKeyPoint = Long.MAX_VALUE;
107 for (int i = 0; i < mStrokes.size(); i++) {
108 long thisStartTime = mStrokes.get(i).mStartTime;
109 if ((thisStartTime < nextKeyPoint) && (thisStartTime >= offset)) {
110 nextKeyPoint = thisStartTime;
111 }
112 long thisEndTime = mStrokes.get(i).mEndTime;
113 if ((thisEndTime < nextKeyPoint) && (thisEndTime >= offset)) {
114 nextKeyPoint = thisEndTime;
115 }
116 }
117 return (nextKeyPoint == Long.MAX_VALUE) ? -1L : nextKeyPoint;
118 }
119
120 /**
121 * Get the points that correspond to a particular moment in time.
122 * @param time The time of interest
123 * @param touchPoints An array to hold the current touch points. Must be preallocated to at
124 * least the number of paths in the gesture to prevent going out of bounds
125 * @return The number of points found, and thus the number of elements set in each array
126 */
127 private int getPointsForTime(long time, TouchPoint[] touchPoints) {
128 int numPointsFound = 0;
129 for (int i = 0; i < mStrokes.size(); i++) {
130 StrokeDescription strokeDescription = mStrokes.get(i);
131 if (strokeDescription.hasPointForTime(time)) {
132 touchPoints[numPointsFound].mPathIndex = i;
133 touchPoints[numPointsFound].mIsStartOfPath = (time == strokeDescription.mStartTime);
134 touchPoints[numPointsFound].mIsEndOfPath = (time == strokeDescription.mEndTime);
135 strokeDescription.getPosForTime(time, mTempPos);
136 touchPoints[numPointsFound].mX = Math.round(mTempPos[0]);
137 touchPoints[numPointsFound].mY = Math.round(mTempPos[1]);
138 numPointsFound++;
139 }
140 }
141 return numPointsFound;
142 }
143
144 // Total duration assumes that the gesture starts at 0; waiting around to start a gesture
145 // counts against total duration
146 private static long getTotalDuration(List<StrokeDescription> paths) {
147 long latestEnd = Long.MIN_VALUE;
148 for (int i = 0; i < paths.size(); i++) {
149 StrokeDescription path = paths.get(i);
150 latestEnd = Math.max(latestEnd, path.mEndTime);
151 }
152 return Math.max(latestEnd, 0);
153 }
154
155 /**
156 * Builder for a {@code GestureDescription}
157 */
158 public static class Builder {
159
160 private final List<StrokeDescription> mStrokes = new ArrayList<>();
161
162 /**
163 * Add a stroke to the gesture description. Up to {@code MAX_STROKE_COUNT} paths may be
164 * added to a gesture, and the total gesture duration (earliest path start time to latest path
165 * end time) may not exceed {@code MAX_GESTURE_DURATION_MS}.
166 *
167 * @param strokeDescription the stroke to add.
168 *
169 * @return this
170 */
171 public Builder addStroke(@NonNull StrokeDescription strokeDescription) {
172 if (mStrokes.size() >= MAX_STROKE_COUNT) {
Phil Weaver4503fcf2016-03-08 16:29:44 -0800173 throw new IllegalStateException(
174 "Attempting to add too many strokes to a gesture");
Phil Weavera6b64f52015-12-04 15:21:35 -0800175 }
176
177 mStrokes.add(strokeDescription);
178
179 if (getTotalDuration(mStrokes) > MAX_GESTURE_DURATION_MS) {
180 mStrokes.remove(strokeDescription);
Phil Weaver4503fcf2016-03-08 16:29:44 -0800181 throw new IllegalStateException(
182 "Gesture would exceed maximum duration with new stroke");
Phil Weavera6b64f52015-12-04 15:21:35 -0800183 }
184 return this;
185 }
186
187 public GestureDescription build() {
188 if (mStrokes.size() == 0) {
Phil Weaver4503fcf2016-03-08 16:29:44 -0800189 throw new IllegalStateException("Gestures must have at least one stroke");
Phil Weavera6b64f52015-12-04 15:21:35 -0800190 }
191 return new GestureDescription(mStrokes);
192 }
193 }
194
195 /**
196 * Immutable description of stroke that can be part of a gesture.
197 */
198 public static class StrokeDescription {
199 Path mPath;
200 long mStartTime;
201 long mEndTime;
202 private float mTimeToLengthConversion;
203 private PathMeasure mPathMeasure;
204
205 /**
206 * @param path The path to follow. Must have exactly one contour, and that contour must
207 * have nonzero length. The bounds of the path must not be negative.
208 * @param startTime The time, in milliseconds, from the time the gesture starts to the
209 * time the stroke should start. Must not be negative.
210 * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
211 * Must not be negative.
212 */
213 public StrokeDescription(@NonNull Path path,
Phil Weaver4503fcf2016-03-08 16:29:44 -0800214 @IntRange(from = 0) long startTime,
215 @IntRange(from = 0) long duration) {
Phil Weavera6b64f52015-12-04 15:21:35 -0800216 if (duration <= 0) {
217 throw new IllegalArgumentException("Duration must be positive");
218 }
219 if (startTime < 0) {
220 throw new IllegalArgumentException("Start time must not be negative");
221 }
222 RectF bounds = new RectF();
223 path.computeBounds(bounds, false /* unused */);
224 if ((bounds.bottom < 0) || (bounds.top < 0) || (bounds.right < 0)
225 || (bounds.left < 0)) {
226 throw new IllegalArgumentException("Path bounds must not be negative");
227 }
228 mPath = new Path(path);
229 mPathMeasure = new PathMeasure(path, false);
230 if (mPathMeasure.getLength() == 0) {
231 throw new IllegalArgumentException("Path has zero length");
232 }
233 if (mPathMeasure.nextContour()) {
234 throw new IllegalArgumentException("Path has more than one contour");
235 }
236 /*
237 * Calling nextContour has moved mPathMeasure off the first contour, which is the only
238 * one we care about. Set the path again to go back to the first contour.
239 */
240 mPathMeasure.setPath(path, false);
241 mStartTime = startTime;
242 mEndTime = startTime + duration;
243 if (duration > 0) {
244 mTimeToLengthConversion = getLength() / duration;
245 }
246 }
247
248 /**
249 * Retrieve a copy of the path for this stroke
250 *
251 * @return A copy of the path
252 */
253 public Path getPath() {
254 return new Path(mPath);
255 }
256
257 /**
258 * Get the stroke's start time
259 *
260 * @return the start time for this stroke.
261 */
262 public long getStartTime() {
263 return mStartTime;
264 }
265
266 /**
267 * Get the stroke's duration
268 *
269 * @return the duration for this stroke
270 */
271 public long getDuration() {
272 return mEndTime - mStartTime;
273 }
274
275 float getLength() {
276 return mPathMeasure.getLength();
277 }
278
279 /* Assumes hasPointForTime returns true */
280 boolean getPosForTime(long time, float[] pos) {
281 if (time == mEndTime) {
282 // Close to the end time, roundoff can be a problem
283 return mPathMeasure.getPosTan(getLength(), pos, null);
284 }
285 float length = mTimeToLengthConversion * ((float) (time - mStartTime));
286 return mPathMeasure.getPosTan(length, pos, null);
287 }
288
289 boolean hasPointForTime(long time) {
290 return ((time >= mStartTime) && (time <= mEndTime));
291 }
292 }
293
294 private static class TouchPoint {
295 int mPathIndex;
296 boolean mIsStartOfPath;
297 boolean mIsEndOfPath;
298 float mX;
299 float mY;
300
301 void copyFrom(TouchPoint other) {
302 mPathIndex = other.mPathIndex;
303 mIsStartOfPath = other.mIsStartOfPath;
304 mIsEndOfPath = other.mIsEndOfPath;
305 mX = other.mX;
306 mY = other.mY;
307 }
308 }
309
310 /**
311 * Class to convert a GestureDescription to a series of MotionEvents.
312 */
313 static class MotionEventGenerator {
314 /**
315 * Constants used to initialize all MotionEvents
316 */
317 private static final int EVENT_META_STATE = 0;
318 private static final int EVENT_BUTTON_STATE = 0;
319 private static final int EVENT_DEVICE_ID = 0;
320 private static final int EVENT_EDGE_FLAGS = 0;
321 private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
322 private static final int EVENT_FLAGS = 0;
323 private static final float EVENT_X_PRECISION = 1;
324 private static final float EVENT_Y_PRECISION = 1;
325
326 /* Lazily-created scratch memory for processing touches */
327 private static TouchPoint[] sCurrentTouchPoints;
328 private static TouchPoint[] sLastTouchPoints;
329 private static PointerCoords[] sPointerCoords;
330 private static PointerProperties[] sPointerProps;
331
332 static List<MotionEvent> getMotionEventsFromGestureDescription(
333 GestureDescription description, int sampleTimeMs) {
334 final List<MotionEvent> motionEvents = new ArrayList<>();
335
336 // Point data at each time we generate an event for
337 final TouchPoint[] currentTouchPoints =
338 getCurrentTouchPoints(description.getStrokeCount());
339 // Point data sent in last touch event
340 int lastTouchPointSize = 0;
341 final TouchPoint[] lastTouchPoints =
342 getLastTouchPoints(description.getStrokeCount());
343
344 /* Loop through each time slice where there are touch points */
345 long timeSinceGestureStart = 0;
346 long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart);
347 while (nextKeyPointTime >= 0) {
348 timeSinceGestureStart = (lastTouchPointSize == 0) ? nextKeyPointTime
349 : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs);
350 int currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart,
351 currentTouchPoints);
352
353 appendMoveEventIfNeeded(motionEvents, lastTouchPoints, lastTouchPointSize,
354 currentTouchPoints, currentTouchPointSize, timeSinceGestureStart);
355 lastTouchPointSize = appendUpEvents(motionEvents, lastTouchPoints,
356 lastTouchPointSize, currentTouchPoints, currentTouchPointSize,
357 timeSinceGestureStart);
358 lastTouchPointSize = appendDownEvents(motionEvents, lastTouchPoints,
359 lastTouchPointSize, currentTouchPoints, currentTouchPointSize,
360 timeSinceGestureStart);
361
362 /* Move to next time slice */
363 nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1);
364 }
365 return motionEvents;
366 }
367
368 private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
369 if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
370 sCurrentTouchPoints = new TouchPoint[requiredCapacity];
371 for (int i = 0; i < requiredCapacity; i++) {
372 sCurrentTouchPoints[i] = new TouchPoint();
373 }
374 }
375 return sCurrentTouchPoints;
376 }
377
378 private static TouchPoint[] getLastTouchPoints(int requiredCapacity) {
379 if ((sLastTouchPoints == null) || (sLastTouchPoints.length < requiredCapacity)) {
380 sLastTouchPoints = new TouchPoint[requiredCapacity];
381 for (int i = 0; i < requiredCapacity; i++) {
382 sLastTouchPoints[i] = new TouchPoint();
383 }
384 }
385 return sLastTouchPoints;
386 }
387
388 private static PointerCoords[] getPointerCoords(int requiredCapacity) {
389 if ((sPointerCoords == null) || (sPointerCoords.length < requiredCapacity)) {
390 sPointerCoords = new PointerCoords[requiredCapacity];
391 for (int i = 0; i < requiredCapacity; i++) {
392 sPointerCoords[i] = new PointerCoords();
393 }
394 }
395 return sPointerCoords;
396 }
397
398 private static PointerProperties[] getPointerProps(int requiredCapacity) {
399 if ((sPointerProps == null) || (sPointerProps.length < requiredCapacity)) {
400 sPointerProps = new PointerProperties[requiredCapacity];
401 for (int i = 0; i < requiredCapacity; i++) {
402 sPointerProps[i] = new PointerProperties();
403 }
404 }
405 return sPointerProps;
406 }
407
408 private static void appendMoveEventIfNeeded(List<MotionEvent> motionEvents,
409 TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
410 TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
411 /* Look for pointers that have moved */
412 boolean moveFound = false;
413 for (int i = 0; i < currentTouchPointsSize; i++) {
414 int lastPointsIndex = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
415 currentTouchPoints[i].mPathIndex);
416 if (lastPointsIndex >= 0) {
417 moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX)
418 || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY);
419 lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]);
420 }
421 }
422
423 if (moveFound) {
424 long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
425 motionEvents.add(obtainMotionEvent(downTime, currentTime, MotionEvent.ACTION_MOVE,
426 lastTouchPoints, lastTouchPointsSize));
427 }
428 }
429
430 private static int appendUpEvents(List<MotionEvent> motionEvents,
431 TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
432 TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
433 /* Look for a pointer at the end of its path */
434 for (int i = 0; i < currentTouchPointsSize; i++) {
435 if (currentTouchPoints[i].mIsEndOfPath) {
436 int indexOfUpEvent = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
437 currentTouchPoints[i].mPathIndex);
438 if (indexOfUpEvent < 0) {
439 continue; // Should not happen
440 }
441 long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
442 int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_UP
443 : MotionEvent.ACTION_POINTER_UP;
444 action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
445 motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
446 lastTouchPoints, lastTouchPointsSize));
447 /* Remove this point from lastTouchPoints */
448 for (int j = indexOfUpEvent; j < lastTouchPointsSize - 1; j++) {
449 lastTouchPoints[j].copyFrom(lastTouchPoints[j+1]);
450 }
451 lastTouchPointsSize--;
452 }
453 }
454 return lastTouchPointsSize;
455 }
456
457 private static int appendDownEvents(List<MotionEvent> motionEvents,
458 TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
459 TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
460 /* Look for a pointer that is just starting */
461 for (int i = 0; i < currentTouchPointsSize; i++) {
462 if (currentTouchPoints[i].mIsStartOfPath) {
463 /* Add the point to last coords and use the new array to generate the event */
464 lastTouchPoints[lastTouchPointsSize++].copyFrom(currentTouchPoints[i]);
465 int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_DOWN
466 : MotionEvent.ACTION_POINTER_DOWN;
467 long downTime = (action == MotionEvent.ACTION_DOWN) ? currentTime :
468 motionEvents.get(motionEvents.size() - 1).getDownTime();
469 action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
470 motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
471 lastTouchPoints, lastTouchPointsSize));
472 }
473 }
474 return lastTouchPointsSize;
475 }
476
477 private static MotionEvent obtainMotionEvent(long downTime, long eventTime, int action,
478 TouchPoint[] touchPoints, int touchPointsSize) {
479 PointerCoords[] pointerCoords = getPointerCoords(touchPointsSize);
480 PointerProperties[] pointerProperties = getPointerProps(touchPointsSize);
481 for (int i = 0; i < touchPointsSize; i++) {
482 pointerProperties[i].id = touchPoints[i].mPathIndex;
483 pointerProperties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
484 pointerCoords[i].clear();
485 pointerCoords[i].pressure = 1.0f;
486 pointerCoords[i].size = 1.0f;
487 pointerCoords[i].x = touchPoints[i].mX;
488 pointerCoords[i].y = touchPoints[i].mY;
489 }
490 return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize,
491 pointerProperties, pointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE,
492 EVENT_X_PRECISION, EVENT_Y_PRECISION, EVENT_DEVICE_ID, EVENT_EDGE_FLAGS,
493 EVENT_SOURCE, EVENT_FLAGS);
494 }
495
496 private static int findPointByPathIndex(TouchPoint[] touchPoints, int touchPointsSize,
497 int pathIndex) {
498 for (int i = 0; i < touchPointsSize; i++) {
499 if (touchPoints[i].mPathIndex == pathIndex) {
500 return i;
501 }
502 }
503 return -1;
504 }
505 }
506}