GestureDescription.java revision 2f165944ce6109134e7285a71da32d1a1647960b
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.os.Parcel; 25import android.os.Parcelable; 26 27import java.util.ArrayList; 28import java.util.List; 29 30/** 31 * Accessibility services with the 32 * {@link android.R.styleable#AccessibilityService_canPerformGestures} property can dispatch 33 * gestures. This class describes those gestures. Gestures are made up of one or more strokes. 34 * Gestures are immutable once built. 35 * <p> 36 * Spatial dimensions throughout are in screen pixels. Time is measured in milliseconds. 37 */ 38public final class GestureDescription { 39 /** Gestures may contain no more than this many strokes */ 40 private static final int MAX_STROKE_COUNT = 10; 41 42 /** 43 * Upper bound on total gesture duration. Nearly all gestures will be much shorter. 44 */ 45 private static final long MAX_GESTURE_DURATION_MS = 60 * 1000; 46 47 private final List<StrokeDescription> mStrokes = new ArrayList<>(); 48 private final float[] mTempPos = new float[2]; 49 50 /** 51 * Get the upper limit for the number of strokes a gesture may contain. 52 * 53 * @return The maximum number of strokes. 54 */ 55 public static int getMaxStrokeCount() { 56 return MAX_STROKE_COUNT; 57 } 58 59 /** 60 * Get the upper limit on a gesture's duration. 61 * 62 * @return The maximum duration in milliseconds. 63 */ 64 public static long getMaxGestureDuration() { 65 return MAX_GESTURE_DURATION_MS; 66 } 67 68 private GestureDescription() {} 69 70 private GestureDescription(List<StrokeDescription> strokes) { 71 mStrokes.addAll(strokes); 72 } 73 74 /** 75 * Get the number of stroke in the gesture. 76 * 77 * @return the number of strokes in this gesture 78 */ 79 public int getStrokeCount() { 80 return mStrokes.size(); 81 } 82 83 /** 84 * Read a stroke from the gesture 85 * 86 * @param index the index of the stroke 87 * 88 * @return A description of the stroke. 89 */ 90 public StrokeDescription getStroke(@IntRange(from = 0) int index) { 91 return mStrokes.get(index); 92 } 93 94 /** 95 * Return the smallest key point (where a path starts or ends) that is at least a specified 96 * offset 97 * @param offset the minimum start time 98 * @return The next key time that is at least the offset or -1 if one can't be found 99 */ 100 private long getNextKeyPointAtLeast(long offset) { 101 long nextKeyPoint = Long.MAX_VALUE; 102 for (int i = 0; i < mStrokes.size(); i++) { 103 long thisStartTime = mStrokes.get(i).mStartTime; 104 if ((thisStartTime < nextKeyPoint) && (thisStartTime >= offset)) { 105 nextKeyPoint = thisStartTime; 106 } 107 long thisEndTime = mStrokes.get(i).mEndTime; 108 if ((thisEndTime < nextKeyPoint) && (thisEndTime >= offset)) { 109 nextKeyPoint = thisEndTime; 110 } 111 } 112 return (nextKeyPoint == Long.MAX_VALUE) ? -1L : nextKeyPoint; 113 } 114 115 /** 116 * Get the points that correspond to a particular moment in time. 117 * @param time The time of interest 118 * @param touchPoints An array to hold the current touch points. Must be preallocated to at 119 * least the number of paths in the gesture to prevent going out of bounds 120 * @return The number of points found, and thus the number of elements set in each array 121 */ 122 private int getPointsForTime(long time, TouchPoint[] touchPoints) { 123 int numPointsFound = 0; 124 for (int i = 0; i < mStrokes.size(); i++) { 125 StrokeDescription strokeDescription = mStrokes.get(i); 126 if (strokeDescription.hasPointForTime(time)) { 127 touchPoints[numPointsFound].mStrokeId = strokeDescription.getId(); 128 touchPoints[numPointsFound].mContinuedStrokeId = 129 strokeDescription.getContinuedStrokeId(); 130 touchPoints[numPointsFound].mIsStartOfPath = 131 (strokeDescription.getContinuedStrokeId() < 0) 132 && (time == strokeDescription.mStartTime); 133 touchPoints[numPointsFound].mIsEndOfPath = !strokeDescription.isContinued() 134 && (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 164 * {@link GestureDescription#getMaxStrokeCount()} paths may be 165 * added to a gesture, and the total gesture duration (earliest path start time to latest 166 * path end time) may not exceed {@link GestureDescription#getMaxGestureDuration()}. 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 public static final int INVALID_STROKE_ID = -1; 201 202 static int sIdCounter; 203 204 Path mPath; 205 long mStartTime; 206 long mEndTime; 207 private float mTimeToLengthConversion; 208 private PathMeasure mPathMeasure; 209 // The tap location is only set for zero-length paths 210 float[] mTapLocation; 211 int mId; 212 boolean mContinued; 213 int mContinuedStrokeId; 214 215 /** 216 * @param path The path to follow. Must have exactly one contour. The bounds of the path 217 * must not be negative. The path must not be empty. If the path has zero length 218 * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move. 219 * @param startTime The time, in milliseconds, from the time the gesture starts to the 220 * time the stroke should start. Must not be negative. 221 * @param duration The duration, in milliseconds, the stroke takes to traverse the path. 222 * Must not be negative. 223 */ 224 public StrokeDescription(@NonNull Path path, 225 @IntRange(from = 0) long startTime, 226 @IntRange(from = 0) long duration) { 227 this(path, startTime, duration, INVALID_STROKE_ID, false); 228 } 229 230 /** 231 * @param path The path to follow. Must have exactly one contour. The bounds of the path 232 * must not be negative. The path must not be empty. If the path has zero length 233 * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move. 234 * @param startTime The time, in milliseconds, from the time the gesture starts to the 235 * time the stroke should start. Must not be negative. 236 * @param duration The duration, in milliseconds, the stroke takes to traverse the path. 237 * Must be positive. 238 * @param continuedStrokeId The ID of the stroke that this stroke continues, or 239 * {@link #INVALID_STROKE_ID} if it continues no stroke. The stroke it 240 * continues must have its isContinued flag set to {@code true} and must be in the 241 * gesture dispatched immediately before the one containing this stroke. 242 * @param isContinued {@code true} if this stroke will be continued by one in the 243 * next gesture {@code false} otherwise. Continued strokes keep their pointers down when 244 * the gesture completes. 245 */ 246 public StrokeDescription(@NonNull Path path, 247 @IntRange(from = 0) long startTime, 248 @IntRange(from = 0) long duration, 249 @IntRange(from = 0) int continuedStrokeId, 250 boolean isContinued) { 251 mContinued = isContinued; 252 mContinuedStrokeId = continuedStrokeId; 253 if (duration <= 0) { 254 throw new IllegalArgumentException("Duration must be positive"); 255 } 256 if (startTime < 0) { 257 throw new IllegalArgumentException("Start time must not be negative"); 258 } 259 RectF bounds = new RectF(); 260 path.computeBounds(bounds, false /* unused */); 261 if ((bounds.bottom < 0) || (bounds.top < 0) || (bounds.right < 0) 262 || (bounds.left < 0)) { 263 throw new IllegalArgumentException("Path bounds must not be negative"); 264 } 265 if (path.isEmpty()) { 266 throw new IllegalArgumentException("Path is empty"); 267 } 268 mPath = new Path(path); 269 mPathMeasure = new PathMeasure(path, false); 270 if (mPathMeasure.getLength() == 0) { 271 // Treat zero-length paths as taps 272 Path tempPath = new Path(path); 273 tempPath.lineTo(-1, -1); 274 mTapLocation = new float[2]; 275 PathMeasure pathMeasure = new PathMeasure(tempPath, false); 276 pathMeasure.getPosTan(0, mTapLocation, null); 277 } 278 if (mPathMeasure.nextContour()) { 279 throw new IllegalArgumentException("Path has more than one contour"); 280 } 281 /* 282 * Calling nextContour has moved mPathMeasure off the first contour, which is the only 283 * one we care about. Set the path again to go back to the first contour. 284 */ 285 mPathMeasure.setPath(mPath, false); 286 mStartTime = startTime; 287 mEndTime = startTime + duration; 288 mTimeToLengthConversion = getLength() / duration; 289 mId = sIdCounter++; 290 } 291 292 /** 293 * Retrieve a copy of the path for this stroke 294 * 295 * @return A copy of the path 296 */ 297 public Path getPath() { 298 return new Path(mPath); 299 } 300 301 /** 302 * Get the stroke's start time 303 * 304 * @return the start time for this stroke. 305 */ 306 public long getStartTime() { 307 return mStartTime; 308 } 309 310 /** 311 * Get the stroke's duration 312 * 313 * @return the duration for this stroke 314 */ 315 public long getDuration() { 316 return mEndTime - mStartTime; 317 } 318 319 /** 320 * Get the stroke's ID. The ID is used when a stroke is to be continued by another 321 * stroke in a future gesture. 322 * 323 * @return the ID of this stroke 324 */ 325 public int getId() { 326 return mId; 327 } 328 329 /** 330 * Check if this stroke is marked to continue in the next gesture. 331 * 332 * @return {@code true} if the stroke is to be continued. 333 */ 334 public boolean isContinued() { 335 return mContinued; 336 } 337 338 /** 339 * Get the ID of the stroke that this one will continue. 340 * 341 * @return The ID of the stroke that this stroke continues, or 0 if no such stroke exists. 342 */ 343 public int getContinuedStrokeId() { 344 return mContinuedStrokeId; 345 } 346 347 float getLength() { 348 return mPathMeasure.getLength(); 349 } 350 351 /* Assumes hasPointForTime returns true */ 352 boolean getPosForTime(long time, float[] pos) { 353 if (mTapLocation != null) { 354 pos[0] = mTapLocation[0]; 355 pos[1] = mTapLocation[1]; 356 return true; 357 } 358 if (time == mEndTime) { 359 // Close to the end time, roundoff can be a problem 360 return mPathMeasure.getPosTan(getLength(), pos, null); 361 } 362 float length = mTimeToLengthConversion * ((float) (time - mStartTime)); 363 return mPathMeasure.getPosTan(length, pos, null); 364 } 365 366 boolean hasPointForTime(long time) { 367 return ((time >= mStartTime) && (time <= mEndTime)); 368 } 369 } 370 371 /** 372 * The location of a finger for gesture dispatch 373 * 374 * @hide 375 */ 376 public static class TouchPoint implements Parcelable { 377 private static final int FLAG_IS_START_OF_PATH = 0x01; 378 private static final int FLAG_IS_END_OF_PATH = 0x02; 379 380 public int mStrokeId; 381 public int mContinuedStrokeId; 382 public boolean mIsStartOfPath; 383 public boolean mIsEndOfPath; 384 public float mX; 385 public float mY; 386 387 public TouchPoint() { 388 } 389 390 public TouchPoint(TouchPoint pointToCopy) { 391 copyFrom(pointToCopy); 392 } 393 394 public TouchPoint(Parcel parcel) { 395 mStrokeId = parcel.readInt(); 396 mContinuedStrokeId = parcel.readInt(); 397 int startEnd = parcel.readInt(); 398 mIsStartOfPath = (startEnd & FLAG_IS_START_OF_PATH) != 0; 399 mIsEndOfPath = (startEnd & FLAG_IS_END_OF_PATH) != 0; 400 mX = parcel.readFloat(); 401 mY = parcel.readFloat(); 402 } 403 404 public void copyFrom(TouchPoint other) { 405 mStrokeId = other.mStrokeId; 406 mContinuedStrokeId = other.mContinuedStrokeId; 407 mIsStartOfPath = other.mIsStartOfPath; 408 mIsEndOfPath = other.mIsEndOfPath; 409 mX = other.mX; 410 mY = other.mY; 411 } 412 413 @Override 414 public int describeContents() { 415 return 0; 416 } 417 418 @Override 419 public void writeToParcel(Parcel dest, int flags) { 420 dest.writeInt(mStrokeId); 421 dest.writeInt(mContinuedStrokeId); 422 int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0; 423 startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0; 424 dest.writeInt(startEnd); 425 dest.writeFloat(mX); 426 dest.writeFloat(mY); 427 } 428 429 public static final Parcelable.Creator<TouchPoint> CREATOR 430 = new Parcelable.Creator<TouchPoint>() { 431 public TouchPoint createFromParcel(Parcel in) { 432 return new TouchPoint(in); 433 } 434 435 public TouchPoint[] newArray(int size) { 436 return new TouchPoint[size]; 437 } 438 }; 439 } 440 441 /** 442 * A step along a gesture. Contains all of the touch points at a particular time 443 * 444 * @hide 445 */ 446 public static class GestureStep implements Parcelable { 447 public long timeSinceGestureStart; 448 public int numTouchPoints; 449 public TouchPoint[] touchPoints; 450 451 public GestureStep(long timeSinceGestureStart, int numTouchPoints, 452 TouchPoint[] touchPointsToCopy) { 453 this.timeSinceGestureStart = timeSinceGestureStart; 454 this.numTouchPoints = numTouchPoints; 455 this.touchPoints = new TouchPoint[numTouchPoints]; 456 for (int i = 0; i < numTouchPoints; i++) { 457 this.touchPoints[i] = new TouchPoint(touchPointsToCopy[i]); 458 } 459 } 460 461 public GestureStep(Parcel parcel) { 462 timeSinceGestureStart = parcel.readLong(); 463 Parcelable[] parcelables = 464 parcel.readParcelableArray(TouchPoint.class.getClassLoader()); 465 numTouchPoints = (parcelables == null) ? 0 : parcelables.length; 466 touchPoints = new TouchPoint[numTouchPoints]; 467 for (int i = 0; i < numTouchPoints; i++) { 468 touchPoints[i] = (TouchPoint) parcelables[i]; 469 } 470 } 471 472 @Override 473 public int describeContents() { 474 return 0; 475 } 476 477 @Override 478 public void writeToParcel(Parcel dest, int flags) { 479 dest.writeLong(timeSinceGestureStart); 480 dest.writeParcelableArray(touchPoints, flags); 481 } 482 483 public static final Parcelable.Creator<GestureStep> CREATOR 484 = new Parcelable.Creator<GestureStep>() { 485 public GestureStep createFromParcel(Parcel in) { 486 return new GestureStep(in); 487 } 488 489 public GestureStep[] newArray(int size) { 490 return new GestureStep[size]; 491 } 492 }; 493 } 494 495 /** 496 * Class to convert a GestureDescription to a series of GestureSteps. 497 * 498 * @hide 499 */ 500 public static class MotionEventGenerator { 501 /* Lazily-created scratch memory for processing touches */ 502 private static TouchPoint[] sCurrentTouchPoints; 503 504 public static List<GestureStep> getGestureStepsFromGestureDescription( 505 GestureDescription description, int sampleTimeMs) { 506 final List<GestureStep> gestureSteps = new ArrayList<>(); 507 508 // Point data at each time we generate an event for 509 final TouchPoint[] currentTouchPoints = 510 getCurrentTouchPoints(description.getStrokeCount()); 511 int currentTouchPointSize = 0; 512 /* Loop through each time slice where there are touch points */ 513 long timeSinceGestureStart = 0; 514 long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart); 515 while (nextKeyPointTime >= 0) { 516 timeSinceGestureStart = (currentTouchPointSize == 0) ? nextKeyPointTime 517 : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs); 518 currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart, 519 currentTouchPoints); 520 gestureSteps.add(new GestureStep(timeSinceGestureStart, currentTouchPointSize, 521 currentTouchPoints)); 522 523 /* Move to next time slice */ 524 nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1); 525 } 526 return gestureSteps; 527 } 528 529 private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) { 530 if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) { 531 sCurrentTouchPoints = new TouchPoint[requiredCapacity]; 532 for (int i = 0; i < requiredCapacity; i++) { 533 sCurrentTouchPoints[i] = new TouchPoint(); 534 } 535 } 536 return sCurrentTouchPoints; 537 } 538 } 539} 540