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