| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.accessibilityservice; |
| |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.graphics.Path; |
| import android.graphics.PathMeasure; |
| import android.graphics.RectF; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.view.Display; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Accessibility services with the |
| * {@link android.R.styleable#AccessibilityService_canPerformGestures} property can dispatch |
| * gestures. This class describes those gestures. Gestures are made up of one or more strokes. |
| * Gestures are immutable once built and will be dispatched to the specified display. |
| * <p> |
| * Spatial dimensions throughout are in screen pixels. Time is measured in milliseconds. |
| */ |
| public final class GestureDescription { |
| /** Gestures may contain no more than this many strokes */ |
| private static final int MAX_STROKE_COUNT = 10; |
| |
| /** |
| * Upper bound on total gesture duration. Nearly all gestures will be much shorter. |
| */ |
| private static final long MAX_GESTURE_DURATION_MS = 60 * 1000; |
| |
| private final List<StrokeDescription> mStrokes = new ArrayList<>(); |
| private final float[] mTempPos = new float[2]; |
| private final int mDisplayId; |
| |
| /** |
| * Get the upper limit for the number of strokes a gesture may contain. |
| * |
| * @return The maximum number of strokes. |
| */ |
| public static int getMaxStrokeCount() { |
| return MAX_STROKE_COUNT; |
| } |
| |
| /** |
| * Get the upper limit on a gesture's duration. |
| * |
| * @return The maximum duration in milliseconds. |
| */ |
| public static long getMaxGestureDuration() { |
| return MAX_GESTURE_DURATION_MS; |
| } |
| |
| private GestureDescription() { |
| this(new ArrayList<>()); |
| } |
| |
| private GestureDescription(List<StrokeDescription> strokes) { |
| this(strokes, Display.DEFAULT_DISPLAY); |
| } |
| |
| private GestureDescription(List<StrokeDescription> strokes, int displayId) { |
| mStrokes.addAll(strokes); |
| mDisplayId = displayId; |
| } |
| |
| /** |
| * Get the number of stroke in the gesture. |
| * |
| * @return the number of strokes in this gesture |
| */ |
| public int getStrokeCount() { |
| return mStrokes.size(); |
| } |
| |
| /** |
| * Read a stroke from the gesture |
| * |
| * @param index the index of the stroke |
| * |
| * @return A description of the stroke. |
| */ |
| public StrokeDescription getStroke(@IntRange(from = 0) int index) { |
| return mStrokes.get(index); |
| } |
| |
| /** |
| * Returns the ID of the display this gesture is sent on, for use with |
| * {@link android.hardware.display.DisplayManager#getDisplay(int)}. |
| * |
| * @return The logical display id. |
| */ |
| public int getDisplayId() { |
| return mDisplayId; |
| } |
| |
| /** |
| * Return the smallest key point (where a path starts or ends) that is at least a specified |
| * offset |
| * @param offset the minimum start time |
| * @return The next key time that is at least the offset or -1 if one can't be found |
| */ |
| private long getNextKeyPointAtLeast(long offset) { |
| long nextKeyPoint = Long.MAX_VALUE; |
| for (int i = 0; i < mStrokes.size(); i++) { |
| long thisStartTime = mStrokes.get(i).mStartTime; |
| if ((thisStartTime < nextKeyPoint) && (thisStartTime >= offset)) { |
| nextKeyPoint = thisStartTime; |
| } |
| long thisEndTime = mStrokes.get(i).mEndTime; |
| if ((thisEndTime < nextKeyPoint) && (thisEndTime >= offset)) { |
| nextKeyPoint = thisEndTime; |
| } |
| } |
| return (nextKeyPoint == Long.MAX_VALUE) ? -1L : nextKeyPoint; |
| } |
| |
| /** |
| * Get the points that correspond to a particular moment in time. |
| * @param time The time of interest |
| * @param touchPoints An array to hold the current touch points. Must be preallocated to at |
| * least the number of paths in the gesture to prevent going out of bounds |
| * @return The number of points found, and thus the number of elements set in each array |
| */ |
| private int getPointsForTime(long time, TouchPoint[] touchPoints) { |
| int numPointsFound = 0; |
| for (int i = 0; i < mStrokes.size(); i++) { |
| StrokeDescription strokeDescription = mStrokes.get(i); |
| if (strokeDescription.hasPointForTime(time)) { |
| touchPoints[numPointsFound].mStrokeId = strokeDescription.getId(); |
| touchPoints[numPointsFound].mContinuedStrokeId = |
| strokeDescription.getContinuedStrokeId(); |
| touchPoints[numPointsFound].mIsStartOfPath = |
| (strokeDescription.getContinuedStrokeId() < 0) |
| && (time == strokeDescription.mStartTime); |
| touchPoints[numPointsFound].mIsEndOfPath = !strokeDescription.willContinue() |
| && (time == strokeDescription.mEndTime); |
| strokeDescription.getPosForTime(time, mTempPos); |
| touchPoints[numPointsFound].mX = Math.round(mTempPos[0]); |
| touchPoints[numPointsFound].mY = Math.round(mTempPos[1]); |
| numPointsFound++; |
| } |
| } |
| return numPointsFound; |
| } |
| |
| // Total duration assumes that the gesture starts at 0; waiting around to start a gesture |
| // counts against total duration |
| private static long getTotalDuration(List<StrokeDescription> paths) { |
| long latestEnd = Long.MIN_VALUE; |
| for (int i = 0; i < paths.size(); i++) { |
| StrokeDescription path = paths.get(i); |
| latestEnd = Math.max(latestEnd, path.mEndTime); |
| } |
| return Math.max(latestEnd, 0); |
| } |
| |
| /** |
| * Builder for a {@code GestureDescription} |
| */ |
| public static class Builder { |
| |
| private final List<StrokeDescription> mStrokes = new ArrayList<>(); |
| private int mDisplayId = Display.DEFAULT_DISPLAY; |
| |
| /** |
| * Adds a stroke to the gesture description. Up to |
| * {@link GestureDescription#getMaxStrokeCount()} paths may be |
| * added to a gesture, and the total gesture duration (earliest path start time to latest |
| * path end time) may not exceed {@link GestureDescription#getMaxGestureDuration()}. |
| * |
| * @param strokeDescription the stroke to add. |
| * |
| * @return this |
| */ |
| public Builder addStroke(@NonNull StrokeDescription strokeDescription) { |
| if (mStrokes.size() >= MAX_STROKE_COUNT) { |
| throw new IllegalStateException( |
| "Attempting to add too many strokes to a gesture"); |
| } |
| |
| mStrokes.add(strokeDescription); |
| |
| if (getTotalDuration(mStrokes) > MAX_GESTURE_DURATION_MS) { |
| mStrokes.remove(strokeDescription); |
| throw new IllegalStateException( |
| "Gesture would exceed maximum duration with new stroke"); |
| } |
| return this; |
| } |
| |
| /** |
| * Sets the id of the display to dispatch gestures. |
| * |
| * @param displayId The logical display id |
| * |
| * @return this |
| */ |
| public @NonNull Builder setDisplayId(int displayId) { |
| mDisplayId = displayId; |
| return this; |
| } |
| |
| public GestureDescription build() { |
| if (mStrokes.size() == 0) { |
| throw new IllegalStateException("Gestures must have at least one stroke"); |
| } |
| return new GestureDescription(mStrokes, mDisplayId); |
| } |
| } |
| |
| /** |
| * Immutable description of stroke that can be part of a gesture. |
| */ |
| public static class StrokeDescription { |
| private static final int INVALID_STROKE_ID = -1; |
| |
| static int sIdCounter; |
| |
| Path mPath; |
| long mStartTime; |
| long mEndTime; |
| private float mTimeToLengthConversion; |
| private PathMeasure mPathMeasure; |
| // The tap location is only set for zero-length paths |
| float[] mTapLocation; |
| int mId; |
| boolean mContinued; |
| int mContinuedStrokeId = INVALID_STROKE_ID; |
| |
| /** |
| * @param path The path to follow. Must have exactly one contour. The bounds of the path |
| * must not be negative. The path must not be empty. If the path has zero length |
| * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move. |
| * @param startTime The time, in milliseconds, from the time the gesture starts to the |
| * time the stroke should start. Must not be negative. |
| * @param duration The duration, in milliseconds, the stroke takes to traverse the path. |
| * Must be positive. |
| */ |
| public StrokeDescription(@NonNull Path path, |
| @IntRange(from = 0) long startTime, |
| @IntRange(from = 0) long duration) { |
| this(path, startTime, duration, false); |
| } |
| |
| /** |
| * @param path The path to follow. Must have exactly one contour. The bounds of the path |
| * must not be negative. The path must not be empty. If the path has zero length |
| * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move. |
| * @param startTime The time, in milliseconds, from the time the gesture starts to the |
| * time the stroke should start. Must not be negative. |
| * @param duration The duration, in milliseconds, the stroke takes to traverse the path. |
| * Must be positive. |
| * @param willContinue {@code true} if this stroke will be continued by one in the |
| * next gesture {@code false} otherwise. Continued strokes keep their pointers down when |
| * the gesture completes. |
| */ |
| public StrokeDescription(@NonNull Path path, |
| @IntRange(from = 0) long startTime, |
| @IntRange(from = 0) long duration, |
| boolean willContinue) { |
| mContinued = willContinue; |
| Preconditions.checkArgument(duration > 0, "Duration must be positive"); |
| Preconditions.checkArgument(startTime >= 0, "Start time must not be negative"); |
| Preconditions.checkArgument(!path.isEmpty(), "Path is empty"); |
| RectF bounds = new RectF(); |
| path.computeBounds(bounds, false /* unused */); |
| Preconditions.checkArgument((bounds.bottom >= 0) && (bounds.top >= 0) |
| && (bounds.right >= 0) && (bounds.left >= 0), |
| "Path bounds must not be negative"); |
| mPath = new Path(path); |
| mPathMeasure = new PathMeasure(path, false); |
| if (mPathMeasure.getLength() == 0) { |
| // Treat zero-length paths as taps |
| Path tempPath = new Path(path); |
| tempPath.lineTo(-1, -1); |
| mTapLocation = new float[2]; |
| PathMeasure pathMeasure = new PathMeasure(tempPath, false); |
| pathMeasure.getPosTan(0, mTapLocation, null); |
| } |
| if (mPathMeasure.nextContour()) { |
| throw new IllegalArgumentException("Path has more than one contour"); |
| } |
| /* |
| * Calling nextContour has moved mPathMeasure off the first contour, which is the only |
| * one we care about. Set the path again to go back to the first contour. |
| */ |
| mPathMeasure.setPath(mPath, false); |
| mStartTime = startTime; |
| mEndTime = startTime + duration; |
| mTimeToLengthConversion = getLength() / duration; |
| mId = sIdCounter++; |
| } |
| |
| /** |
| * Retrieve a copy of the path for this stroke |
| * |
| * @return A copy of the path |
| */ |
| public Path getPath() { |
| return new Path(mPath); |
| } |
| |
| /** |
| * Get the stroke's start time |
| * |
| * @return the start time for this stroke. |
| */ |
| public long getStartTime() { |
| return mStartTime; |
| } |
| |
| /** |
| * Get the stroke's duration |
| * |
| * @return the duration for this stroke |
| */ |
| public long getDuration() { |
| return mEndTime - mStartTime; |
| } |
| |
| /** |
| * Get the stroke's ID. The ID is used when a stroke is to be continued by another |
| * stroke in a future gesture. |
| * |
| * @return the ID of this stroke |
| * @hide |
| */ |
| public int getId() { |
| return mId; |
| } |
| |
| /** |
| * Create a new stroke that will continue this one. This is only possible if this stroke |
| * will continue. |
| * |
| * @param path The path for the stroke that continues this one. The starting point of |
| * this path must match the ending point of the stroke it continues. |
| * @param startTime The time, in milliseconds, from the time the gesture starts to the |
| * time this stroke should start. Must not be negative. This time is from |
| * the start of the new gesture, not the one being continued. |
| * @param duration The duration for the new stroke. Must not be negative. |
| * @param willContinue {@code true} if this stroke will be continued by one in the |
| * next gesture {@code false} otherwise. |
| * @return |
| */ |
| public StrokeDescription continueStroke(Path path, long startTime, long duration, |
| boolean willContinue) { |
| if (!mContinued) { |
| throw new IllegalStateException( |
| "Only strokes marked willContinue can be continued"); |
| } |
| StrokeDescription strokeDescription = |
| new StrokeDescription(path, startTime, duration, willContinue); |
| strokeDescription.mContinuedStrokeId = mId; |
| return strokeDescription; |
| } |
| |
| /** |
| * Check if this stroke is marked to continue in the next gesture. |
| * |
| * @return {@code true} if the stroke is to be continued. |
| */ |
| public boolean willContinue() { |
| return mContinued; |
| } |
| |
| /** |
| * Get the ID of the stroke that this one will continue. |
| * |
| * @return The ID of the stroke that this stroke continues, or 0 if no such stroke exists. |
| * @hide |
| */ |
| public int getContinuedStrokeId() { |
| return mContinuedStrokeId; |
| } |
| |
| float getLength() { |
| return mPathMeasure.getLength(); |
| } |
| |
| /* Assumes hasPointForTime returns true */ |
| boolean getPosForTime(long time, float[] pos) { |
| if (mTapLocation != null) { |
| pos[0] = mTapLocation[0]; |
| pos[1] = mTapLocation[1]; |
| return true; |
| } |
| if (time == mEndTime) { |
| // Close to the end time, roundoff can be a problem |
| return mPathMeasure.getPosTan(getLength(), pos, null); |
| } |
| float length = mTimeToLengthConversion * ((float) (time - mStartTime)); |
| return mPathMeasure.getPosTan(length, pos, null); |
| } |
| |
| boolean hasPointForTime(long time) { |
| return ((time >= mStartTime) && (time <= mEndTime)); |
| } |
| } |
| |
| /** |
| * The location of a finger for gesture dispatch |
| * |
| * @hide |
| */ |
| public static class TouchPoint implements Parcelable { |
| private static final int FLAG_IS_START_OF_PATH = 0x01; |
| private static final int FLAG_IS_END_OF_PATH = 0x02; |
| |
| public int mStrokeId; |
| public int mContinuedStrokeId; |
| public boolean mIsStartOfPath; |
| public boolean mIsEndOfPath; |
| public float mX; |
| public float mY; |
| |
| public TouchPoint() { |
| } |
| |
| public TouchPoint(TouchPoint pointToCopy) { |
| copyFrom(pointToCopy); |
| } |
| |
| public TouchPoint(Parcel parcel) { |
| mStrokeId = parcel.readInt(); |
| mContinuedStrokeId = parcel.readInt(); |
| int startEnd = parcel.readInt(); |
| mIsStartOfPath = (startEnd & FLAG_IS_START_OF_PATH) != 0; |
| mIsEndOfPath = (startEnd & FLAG_IS_END_OF_PATH) != 0; |
| mX = parcel.readFloat(); |
| mY = parcel.readFloat(); |
| } |
| |
| public void copyFrom(TouchPoint other) { |
| mStrokeId = other.mStrokeId; |
| mContinuedStrokeId = other.mContinuedStrokeId; |
| mIsStartOfPath = other.mIsStartOfPath; |
| mIsEndOfPath = other.mIsEndOfPath; |
| mX = other.mX; |
| mY = other.mY; |
| } |
| |
| @Override |
| public String toString() { |
| return "TouchPoint{" |
| + "mStrokeId=" + mStrokeId |
| + ", mContinuedStrokeId=" + mContinuedStrokeId |
| + ", mIsStartOfPath=" + mIsStartOfPath |
| + ", mIsEndOfPath=" + mIsEndOfPath |
| + ", mX=" + mX |
| + ", mY=" + mY |
| + '}'; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeInt(mStrokeId); |
| dest.writeInt(mContinuedStrokeId); |
| int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0; |
| startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0; |
| dest.writeInt(startEnd); |
| dest.writeFloat(mX); |
| dest.writeFloat(mY); |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<TouchPoint> CREATOR |
| = new Parcelable.Creator<TouchPoint>() { |
| public TouchPoint createFromParcel(Parcel in) { |
| return new TouchPoint(in); |
| } |
| |
| public TouchPoint[] newArray(int size) { |
| return new TouchPoint[size]; |
| } |
| }; |
| } |
| |
| /** |
| * A step along a gesture. Contains all of the touch points at a particular time |
| * |
| * @hide |
| */ |
| public static class GestureStep implements Parcelable { |
| public long timeSinceGestureStart; |
| public int numTouchPoints; |
| public TouchPoint[] touchPoints; |
| |
| public GestureStep(long timeSinceGestureStart, int numTouchPoints, |
| TouchPoint[] touchPointsToCopy) { |
| this.timeSinceGestureStart = timeSinceGestureStart; |
| this.numTouchPoints = numTouchPoints; |
| this.touchPoints = new TouchPoint[numTouchPoints]; |
| for (int i = 0; i < numTouchPoints; i++) { |
| this.touchPoints[i] = new TouchPoint(touchPointsToCopy[i]); |
| } |
| } |
| |
| public GestureStep(Parcel parcel) { |
| timeSinceGestureStart = parcel.readLong(); |
| Parcelable[] parcelables = |
| parcel.readParcelableArray(TouchPoint.class.getClassLoader()); |
| numTouchPoints = (parcelables == null) ? 0 : parcelables.length; |
| touchPoints = new TouchPoint[numTouchPoints]; |
| for (int i = 0; i < numTouchPoints; i++) { |
| touchPoints[i] = (TouchPoint) parcelables[i]; |
| } |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeLong(timeSinceGestureStart); |
| dest.writeParcelableArray(touchPoints, flags); |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<GestureStep> CREATOR |
| = new Parcelable.Creator<GestureStep>() { |
| public GestureStep createFromParcel(Parcel in) { |
| return new GestureStep(in); |
| } |
| |
| public GestureStep[] newArray(int size) { |
| return new GestureStep[size]; |
| } |
| }; |
| } |
| |
| /** |
| * Class to convert a GestureDescription to a series of GestureSteps. |
| * |
| * @hide |
| */ |
| public static class MotionEventGenerator { |
| /* Lazily-created scratch memory for processing touches */ |
| private static TouchPoint[] sCurrentTouchPoints; |
| |
| public static List<GestureStep> getGestureStepsFromGestureDescription( |
| GestureDescription description, int sampleTimeMs) { |
| final List<GestureStep> gestureSteps = new ArrayList<>(); |
| |
| // Point data at each time we generate an event for |
| final TouchPoint[] currentTouchPoints = |
| getCurrentTouchPoints(description.getStrokeCount()); |
| int currentTouchPointSize = 0; |
| /* Loop through each time slice where there are touch points */ |
| long timeSinceGestureStart = 0; |
| long nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart); |
| while (nextKeyPointTime >= 0) { |
| timeSinceGestureStart = (currentTouchPointSize == 0) ? nextKeyPointTime |
| : Math.min(nextKeyPointTime, timeSinceGestureStart + sampleTimeMs); |
| currentTouchPointSize = description.getPointsForTime(timeSinceGestureStart, |
| currentTouchPoints); |
| gestureSteps.add(new GestureStep(timeSinceGestureStart, currentTouchPointSize, |
| currentTouchPoints)); |
| |
| /* Move to next time slice */ |
| nextKeyPointTime = description.getNextKeyPointAtLeast(timeSinceGestureStart + 1); |
| } |
| return gestureSteps; |
| } |
| |
| private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) { |
| if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) { |
| sCurrentTouchPoints = new TouchPoint[requiredCapacity]; |
| for (int i = 0; i < requiredCapacity; i++) { |
| sCurrentTouchPoints[i] = new TouchPoint(); |
| } |
| } |
| return sCurrentTouchPoints; |
| } |
| } |
| } |