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 17 package android.accessibilityservice; 18 19 import android.annotation.IntRange; 20 import android.annotation.NonNull; 21 import android.graphics.Path; 22 import android.graphics.PathMeasure; 23 import android.graphics.RectF; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 27 import com.android.internal.util.Preconditions; 28 29 import java.util.ArrayList; 30 import 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 */ 40 public 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 */ getMaxStrokeCount()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 */ getMaxGestureDuration()66 public static long getMaxGestureDuration() { 67 return MAX_GESTURE_DURATION_MS; 68 } 69 GestureDescription()70 private GestureDescription() {} 71 GestureDescription(List<StrokeDescription> strokes)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 */ getStrokeCount()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 */ getStroke(@ntRangefrom = 0) int index)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 */ getNextKeyPointAtLeast(long offset)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 */ getPointsForTime(long time, TouchPoint[] touchPoints)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].mStrokeId = strokeDescription.getId(); 130 touchPoints[numPointsFound].mContinuedStrokeId = 131 strokeDescription.getContinuedStrokeId(); 132 touchPoints[numPointsFound].mIsStartOfPath = 133 (strokeDescription.getContinuedStrokeId() < 0) 134 && (time == strokeDescription.mStartTime); 135 touchPoints[numPointsFound].mIsEndOfPath = !strokeDescription.willContinue() 136 && (time == strokeDescription.mEndTime); 137 strokeDescription.getPosForTime(time, mTempPos); 138 touchPoints[numPointsFound].mX = Math.round(mTempPos[0]); 139 touchPoints[numPointsFound].mY = Math.round(mTempPos[1]); 140 numPointsFound++; 141 } 142 } 143 return numPointsFound; 144 } 145 146 // Total duration assumes that the gesture starts at 0; waiting around to start a gesture 147 // counts against total duration getTotalDuration(List<StrokeDescription> paths)148 private static long getTotalDuration(List<StrokeDescription> paths) { 149 long latestEnd = Long.MIN_VALUE; 150 for (int i = 0; i < paths.size(); i++) { 151 StrokeDescription path = paths.get(i); 152 latestEnd = Math.max(latestEnd, path.mEndTime); 153 } 154 return Math.max(latestEnd, 0); 155 } 156 157 /** 158 * Builder for a {@code GestureDescription} 159 */ 160 public static class Builder { 161 162 private final List<StrokeDescription> mStrokes = new ArrayList<>(); 163 164 /** 165 * Add a stroke to the gesture description. Up to 166 * {@link GestureDescription#getMaxStrokeCount()} paths may be 167 * added to a gesture, and the total gesture duration (earliest path start time to latest 168 * path end time) may not exceed {@link GestureDescription#getMaxGestureDuration()}. 169 * 170 * @param strokeDescription the stroke to add. 171 * 172 * @return this 173 */ addStroke(@onNull StrokeDescription strokeDescription)174 public Builder addStroke(@NonNull StrokeDescription strokeDescription) { 175 if (mStrokes.size() >= MAX_STROKE_COUNT) { 176 throw new IllegalStateException( 177 "Attempting to add too many strokes to a gesture"); 178 } 179 180 mStrokes.add(strokeDescription); 181 182 if (getTotalDuration(mStrokes) > MAX_GESTURE_DURATION_MS) { 183 mStrokes.remove(strokeDescription); 184 throw new IllegalStateException( 185 "Gesture would exceed maximum duration with new stroke"); 186 } 187 return this; 188 } 189 build()190 public GestureDescription build() { 191 if (mStrokes.size() == 0) { 192 throw new IllegalStateException("Gestures must have at least one stroke"); 193 } 194 return new GestureDescription(mStrokes); 195 } 196 } 197 198 /** 199 * Immutable description of stroke that can be part of a gesture. 200 */ 201 public static class StrokeDescription { 202 private static final int INVALID_STROKE_ID = -1; 203 204 static int sIdCounter; 205 206 Path mPath; 207 long mStartTime; 208 long mEndTime; 209 private float mTimeToLengthConversion; 210 private PathMeasure mPathMeasure; 211 // The tap location is only set for zero-length paths 212 float[] mTapLocation; 213 int mId; 214 boolean mContinued; 215 int mContinuedStrokeId = INVALID_STROKE_ID; 216 217 /** 218 * @param path The path to follow. Must have exactly one contour. The bounds of the path 219 * must not be negative. The path must not be empty. If the path has zero length 220 * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move. 221 * @param startTime The time, in milliseconds, from the time the gesture starts to the 222 * time the stroke should start. Must not be negative. 223 * @param duration The duration, in milliseconds, the stroke takes to traverse the path. 224 * Must be positive. 225 */ StrokeDescription(@onNull Path path, @IntRange(from = 0) long startTime, @IntRange(from = 0) long duration)226 public StrokeDescription(@NonNull Path path, 227 @IntRange(from = 0) long startTime, 228 @IntRange(from = 0) long duration) { 229 this(path, startTime, duration, false); 230 } 231 232 /** 233 * @param path The path to follow. Must have exactly one contour. The bounds of the path 234 * must not be negative. The path must not be empty. If the path has zero length 235 * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move. 236 * @param startTime The time, in milliseconds, from the time the gesture starts to the 237 * time the stroke should start. Must not be negative. 238 * @param duration The duration, in milliseconds, the stroke takes to traverse the path. 239 * Must be positive. 240 * @param willContinue {@code true} if this stroke will be continued by one in the 241 * next gesture {@code false} otherwise. Continued strokes keep their pointers down when 242 * the gesture completes. 243 */ StrokeDescription(@onNull Path path, @IntRange(from = 0) long startTime, @IntRange(from = 0) long duration, boolean willContinue)244 public StrokeDescription(@NonNull Path path, 245 @IntRange(from = 0) long startTime, 246 @IntRange(from = 0) long duration, 247 boolean willContinue) { 248 mContinued = willContinue; 249 Preconditions.checkArgument(duration > 0, "Duration must be positive"); 250 Preconditions.checkArgument(startTime >= 0, "Start time must not be negative"); 251 Preconditions.checkArgument(!path.isEmpty(), "Path is empty"); 252 RectF bounds = new RectF(); 253 path.computeBounds(bounds, false /* unused */); 254 Preconditions.checkArgument((bounds.bottom >= 0) && (bounds.top >= 0) 255 && (bounds.right >= 0) && (bounds.left >= 0), 256 "Path bounds must not be negative"); 257 mPath = new Path(path); 258 mPathMeasure = new PathMeasure(path, false); 259 if (mPathMeasure.getLength() == 0) { 260 // Treat zero-length paths as taps 261 Path tempPath = new Path(path); 262 tempPath.lineTo(-1, -1); 263 mTapLocation = new float[2]; 264 PathMeasure pathMeasure = new PathMeasure(tempPath, false); 265 pathMeasure.getPosTan(0, mTapLocation, null); 266 } 267 if (mPathMeasure.nextContour()) { 268 throw new IllegalArgumentException("Path has more than one contour"); 269 } 270 /* 271 * Calling nextContour has moved mPathMeasure off the first contour, which is the only 272 * one we care about. Set the path again to go back to the first contour. 273 */ 274 mPathMeasure.setPath(mPath, false); 275 mStartTime = startTime; 276 mEndTime = startTime + duration; 277 mTimeToLengthConversion = getLength() / duration; 278 mId = sIdCounter++; 279 } 280 281 /** 282 * Retrieve a copy of the path for this stroke 283 * 284 * @return A copy of the path 285 */ getPath()286 public Path getPath() { 287 return new Path(mPath); 288 } 289 290 /** 291 * Get the stroke's start time 292 * 293 * @return the start time for this stroke. 294 */ getStartTime()295 public long getStartTime() { 296 return mStartTime; 297 } 298 299 /** 300 * Get the stroke's duration 301 * 302 * @return the duration for this stroke 303 */ getDuration()304 public long getDuration() { 305 return mEndTime - mStartTime; 306 } 307 308 /** 309 * Get the stroke's ID. The ID is used when a stroke is to be continued by another 310 * stroke in a future gesture. 311 * 312 * @return the ID of this stroke 313 * @hide 314 */ getId()315 public int getId() { 316 return mId; 317 } 318 319 /** 320 * Create a new stroke that will continue this one. This is only possible if this stroke 321 * will continue. 322 * 323 * @param path The path for the stroke that continues this one. The starting point of 324 * this path must match the ending point of the stroke it continues. 325 * @param startTime The time, in milliseconds, from the time the gesture starts to the 326 * time this stroke should start. Must not be negative. This time is from 327 * the start of the new gesture, not the one being continued. 328 * @param duration The duration for the new stroke. Must not be negative. 329 * @param willContinue {@code true} if this stroke will be continued by one in the 330 * next gesture {@code false} otherwise. 331 * @return 332 */ continueStroke(Path path, long startTime, long duration, boolean willContinue)333 public StrokeDescription continueStroke(Path path, long startTime, long duration, 334 boolean willContinue) { 335 if (!mContinued) { 336 throw new IllegalStateException( 337 "Only strokes marked willContinue can be continued"); 338 } 339 StrokeDescription strokeDescription = 340 new StrokeDescription(path, startTime, duration, willContinue); 341 strokeDescription.mContinuedStrokeId = mId; 342 return strokeDescription; 343 } 344 345 /** 346 * Check if this stroke is marked to continue in the next gesture. 347 * 348 * @return {@code true} if the stroke is to be continued. 349 */ willContinue()350 public boolean willContinue() { 351 return mContinued; 352 } 353 354 /** 355 * Get the ID of the stroke that this one will continue. 356 * 357 * @return The ID of the stroke that this stroke continues, or 0 if no such stroke exists. 358 * @hide 359 */ getContinuedStrokeId()360 public int getContinuedStrokeId() { 361 return mContinuedStrokeId; 362 } 363 getLength()364 float getLength() { 365 return mPathMeasure.getLength(); 366 } 367 368 /* Assumes hasPointForTime returns true */ getPosForTime(long time, float[] pos)369 boolean getPosForTime(long time, float[] pos) { 370 if (mTapLocation != null) { 371 pos[0] = mTapLocation[0]; 372 pos[1] = mTapLocation[1]; 373 return true; 374 } 375 if (time == mEndTime) { 376 // Close to the end time, roundoff can be a problem 377 return mPathMeasure.getPosTan(getLength(), pos, null); 378 } 379 float length = mTimeToLengthConversion * ((float) (time - mStartTime)); 380 return mPathMeasure.getPosTan(length, pos, null); 381 } 382 hasPointForTime(long time)383 boolean hasPointForTime(long time) { 384 return ((time >= mStartTime) && (time <= mEndTime)); 385 } 386 } 387 388 /** 389 * The location of a finger for gesture dispatch 390 * 391 * @hide 392 */ 393 public static class TouchPoint implements Parcelable { 394 private static final int FLAG_IS_START_OF_PATH = 0x01; 395 private static final int FLAG_IS_END_OF_PATH = 0x02; 396 397 public int mStrokeId; 398 public int mContinuedStrokeId; 399 public boolean mIsStartOfPath; 400 public boolean mIsEndOfPath; 401 public float mX; 402 public float mY; 403 TouchPoint()404 public TouchPoint() { 405 } 406 TouchPoint(TouchPoint pointToCopy)407 public TouchPoint(TouchPoint pointToCopy) { 408 copyFrom(pointToCopy); 409 } 410 TouchPoint(Parcel parcel)411 public TouchPoint(Parcel parcel) { 412 mStrokeId = parcel.readInt(); 413 mContinuedStrokeId = parcel.readInt(); 414 int startEnd = parcel.readInt(); 415 mIsStartOfPath = (startEnd & FLAG_IS_START_OF_PATH) != 0; 416 mIsEndOfPath = (startEnd & FLAG_IS_END_OF_PATH) != 0; 417 mX = parcel.readFloat(); 418 mY = parcel.readFloat(); 419 } 420 copyFrom(TouchPoint other)421 public void copyFrom(TouchPoint other) { 422 mStrokeId = other.mStrokeId; 423 mContinuedStrokeId = other.mContinuedStrokeId; 424 mIsStartOfPath = other.mIsStartOfPath; 425 mIsEndOfPath = other.mIsEndOfPath; 426 mX = other.mX; 427 mY = other.mY; 428 } 429 430 @Override toString()431 public String toString() { 432 return "TouchPoint{" 433 + "mStrokeId=" + mStrokeId 434 + ", mContinuedStrokeId=" + mContinuedStrokeId 435 + ", mIsStartOfPath=" + mIsStartOfPath 436 + ", mIsEndOfPath=" + mIsEndOfPath 437 + ", mX=" + mX 438 + ", mY=" + mY 439 + '}'; 440 } 441 442 @Override describeContents()443 public int describeContents() { 444 return 0; 445 } 446 447 @Override writeToParcel(Parcel dest, int flags)448 public void writeToParcel(Parcel dest, int flags) { 449 dest.writeInt(mStrokeId); 450 dest.writeInt(mContinuedStrokeId); 451 int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0; 452 startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0; 453 dest.writeInt(startEnd); 454 dest.writeFloat(mX); 455 dest.writeFloat(mY); 456 } 457 458 public static final @android.annotation.NonNull Parcelable.Creator<TouchPoint> CREATOR 459 = new Parcelable.Creator<TouchPoint>() { 460 public TouchPoint createFromParcel(Parcel in) { 461 return new TouchPoint(in); 462 } 463 464 public TouchPoint[] newArray(int size) { 465 return new TouchPoint[size]; 466 } 467 }; 468 } 469 470 /** 471 * A step along a gesture. Contains all of the touch points at a particular time 472 * 473 * @hide 474 */ 475 public static class GestureStep implements Parcelable { 476 public long timeSinceGestureStart; 477 public int numTouchPoints; 478 public TouchPoint[] touchPoints; 479 GestureStep(long timeSinceGestureStart, int numTouchPoints, TouchPoint[] touchPointsToCopy)480 public GestureStep(long timeSinceGestureStart, int numTouchPoints, 481 TouchPoint[] touchPointsToCopy) { 482 this.timeSinceGestureStart = timeSinceGestureStart; 483 this.numTouchPoints = numTouchPoints; 484 this.touchPoints = new TouchPoint[numTouchPoints]; 485 for (int i = 0; i < numTouchPoints; i++) { 486 this.touchPoints[i] = new TouchPoint(touchPointsToCopy[i]); 487 } 488 } 489 GestureStep(Parcel parcel)490 public GestureStep(Parcel parcel) { 491 timeSinceGestureStart = parcel.readLong(); 492 Parcelable[] parcelables = 493 parcel.readParcelableArray(TouchPoint.class.getClassLoader()); 494 numTouchPoints = (parcelables == null) ? 0 : parcelables.length; 495 touchPoints = new TouchPoint[numTouchPoints]; 496 for (int i = 0; i < numTouchPoints; i++) { 497 touchPoints[i] = (TouchPoint) parcelables[i]; 498 } 499 } 500 501 @Override describeContents()502 public int describeContents() { 503 return 0; 504 } 505 506 @Override writeToParcel(Parcel dest, int flags)507 public void writeToParcel(Parcel dest, int flags) { 508 dest.writeLong(timeSinceGestureStart); 509 dest.writeParcelableArray(touchPoints, flags); 510 } 511 512 public static final @android.annotation.NonNull Parcelable.Creator<GestureStep> CREATOR 513 = new Parcelable.Creator<GestureStep>() { 514 public GestureStep createFromParcel(Parcel in) { 515 return new GestureStep(in); 516 } 517 518 public GestureStep[] newArray(int size) { 519 return new GestureStep[size]; 520 } 521 }; 522 } 523 524 /** 525 * Class to convert a GestureDescription to a series of GestureSteps. 526 * 527 * @hide 528 */ 529 public static class MotionEventGenerator { 530 /* Lazily-created scratch memory for processing touches */ 531 private static TouchPoint[] sCurrentTouchPoints; 532 getGestureStepsFromGestureDescription( GestureDescription description, int sampleTimeMs)533 public static List<GestureStep> getGestureStepsFromGestureDescription( 534 GestureDescription description, int sampleTimeMs) { 535 final List<GestureStep> gestureSteps = new ArrayList<>(); 536 537 // Point data at each time we generate an event for 538 final TouchPoint[] currentTouchPoints = 539 getCurrentTouchPoints(description.getStrokeCount()); 540 int currentTouchPointSize = 0; 541 /* Loop through each time slice where there are touch points */ 542 long timeSinceGestureStart = 0; 543 long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart); 544 while (nextKeyPointTime >= 0) { 545 timeSinceGestureStart = (currentTouchPointSize == 0) ? nextKeyPointTime 546 : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs); 547 currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart, 548 currentTouchPoints); 549 gestureSteps.add(new GestureStep(timeSinceGestureStart, currentTouchPointSize, 550 currentTouchPoints)); 551 552 /* Move to next time slice */ 553 nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1); 554 } 555 return gestureSteps; 556 } 557 getCurrentTouchPoints(int requiredCapacity)558 private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) { 559 if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) { 560 sCurrentTouchPoints = new TouchPoint[requiredCapacity]; 561 for (int i = 0; i < requiredCapacity; i++) { 562 sCurrentTouchPoints[i] = new TouchPoint(); 563 } 564 } 565 return sCurrentTouchPoints; 566 } 567 } 568 } 569