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 17package android.accessibilityservice; 18 19import android.annotation.IntRange; 20import android.annotation.NonNull; 21import android.graphics.Path; 22import android.graphics.PathMeasure; 23import android.graphics.RectF; 24import android.view.InputDevice; 25import android.view.MotionEvent; 26import android.view.MotionEvent.PointerCoords; 27import android.view.MotionEvent.PointerProperties; 28 29import java.util.ArrayList; 30import 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 */ 40public 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 */ 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 */ 66 public static long getMaxGestureDuration() { 67 return MAX_GESTURE_DURATION_MS; 68 } 69 70 private GestureDescription() {} 71 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 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 */ 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 */ 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 */ 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 */ 278 public long getDuration() { 279 return mEndTime - mStartTime; 280 } 281 282 float getLength() { 283 return mPathMeasure.getLength(); 284 } 285 286 /* Assumes hasPointForTime returns true */ 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 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 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 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 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 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 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 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 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 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 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 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 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