GestureDescription.java revision 4503fcfa2d26091c2a02d610da591e8b7c7b5a9f
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.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 */ 46 private static final int MAX_STROKE_COUNT = 10; 47 48 /** 49 * Upper bound on total gesture duration. Nearly all gestures will be much shorter. 50 */ 51 private static final long MAX_GESTURE_DURATION_MS = 60 * 1000; 52 53 private final List<StrokeDescription> mStrokes = new ArrayList<>(); 54 private final float[] mTempPos = new float[2]; 55 56 /** 57 * Get the upper limit for the number of strokes a gesture may contain. 58 * 59 * @return The maximum number of strokes. 60 */ 61 public static int getMaxStrokeCount() { 62 return MAX_STROKE_COUNT; 63 } 64 65 /** 66 * Get the upper limit on a gesture's duration. 67 * 68 * @return The maximum duration in milliseconds. 69 */ 70 public static long getMaxGestureDuration() { 71 return MAX_GESTURE_DURATION_MS; 72 } 73 74 private GestureDescription() {} 75 76 private GestureDescription(List<StrokeDescription> strokes) { 77 mStrokes.addAll(strokes); 78 } 79 80 /** 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) { 174 throw new IllegalStateException( 175 "Attempting to add too many strokes to a gesture"); 176 } 177 178 mStrokes.add(strokeDescription); 179 180 if (getTotalDuration(mStrokes) > MAX_GESTURE_DURATION_MS) { 181 mStrokes.remove(strokeDescription); 182 throw new IllegalStateException( 183 "Gesture would exceed maximum duration with new stroke"); 184 } 185 return this; 186 } 187 188 public GestureDescription build() { 189 if (mStrokes.size() == 0) { 190 throw new IllegalStateException("Gestures must have at least one stroke"); 191 } 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, 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 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} 508