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