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