Home | History | Annotate | Download | only in accessibilityservice
      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      */
     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      */
     66     public static long getMaxGestureDuration() {
     67         return MAX_GESTURE_DURATION_MS;
     68     }
     69 
     70     private GestureDescription() {}
     71 
     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      */
     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      */
     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      */
    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      */
    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
    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          */
    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 
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    360         public int getContinuedStrokeId() {
    361             return mContinuedStrokeId;
    362         }
    363 
    364         float getLength() {
    365             return mPathMeasure.getLength();
    366         }
    367 
    368         /* Assumes hasPointForTime returns true */
    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 
    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 
    404         public TouchPoint() {
    405         }
    406 
    407         public TouchPoint(TouchPoint pointToCopy) {
    408             copyFrom(pointToCopy);
    409         }
    410 
    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 
    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
    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
    443         public int describeContents() {
    444             return 0;
    445         }
    446 
    447         @Override
    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 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 
    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 
    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
    502         public int describeContents() {
    503             return 0;
    504         }
    505 
    506         @Override
    507         public void writeToParcel(Parcel dest, int flags) {
    508             dest.writeLong(timeSinceGestureStart);
    509             dest.writeParcelableArray(touchPoints, flags);
    510         }
    511 
    512         public static final 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 
    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 
    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