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