Merge "Support continuing dispatched a11y gestures."
diff --git a/api/current.txt b/api/current.txt
index 0148bed..5020a425 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2804,9 +2804,14 @@
 
   public static class GestureDescription.StrokeDescription {
     ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long);
+    ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long, int, boolean);
+    method public int getContinuedStrokeId();
     method public long getDuration();
+    method public int getId();
     method public android.graphics.Path getPath();
     method public long getStartTime();
+    method public boolean isContinued();
+    field public static final int INVALID_STROKE_ID = -1; // 0xffffffff
   }
 
 }
diff --git a/api/system-current.txt b/api/system-current.txt
index 73106b9..f23c1fc 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2918,9 +2918,14 @@
 
   public static class GestureDescription.StrokeDescription {
     ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long);
+    ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long, int, boolean);
+    method public int getContinuedStrokeId();
     method public long getDuration();
+    method public int getId();
     method public android.graphics.Path getPath();
     method public long getStartTime();
+    method public boolean isContinued();
+    field public static final int INVALID_STROKE_ID = -1; // 0xffffffff
   }
 
 }
diff --git a/api/test-current.txt b/api/test-current.txt
index f103eaf..810680d 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -2804,9 +2804,14 @@
 
   public static class GestureDescription.StrokeDescription {
     ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long);
+    ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long, int, boolean);
+    method public int getContinuedStrokeId();
     method public long getDuration();
+    method public int getId();
     method public android.graphics.Path getPath();
     method public long getStartTime();
+    method public boolean isContinued();
+    field public static final int INVALID_STROKE_ID = -1; // 0xffffffff
   }
 
 }
diff --git a/core/java/android/accessibilityservice/GestureDescription.java b/core/java/android/accessibilityservice/GestureDescription.java
index d9b03fa..c9da152 100644
--- a/core/java/android/accessibilityservice/GestureDescription.java
+++ b/core/java/android/accessibilityservice/GestureDescription.java
@@ -23,10 +23,6 @@
 import android.graphics.RectF;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import android.view.MotionEvent.PointerCoords;
-import android.view.MotionEvent.PointerProperties;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -128,9 +124,14 @@
         for (int i = 0; i < mStrokes.size(); i++) {
             StrokeDescription strokeDescription = mStrokes.get(i);
             if (strokeDescription.hasPointForTime(time)) {
-                touchPoints[numPointsFound].mPathIndex = i;
-                touchPoints[numPointsFound].mIsStartOfPath = (time == strokeDescription.mStartTime);
-                touchPoints[numPointsFound].mIsEndOfPath = (time == strokeDescription.mEndTime);
+                touchPoints[numPointsFound].mStrokeId = strokeDescription.getId();
+                touchPoints[numPointsFound].mContinuedStrokeId =
+                        strokeDescription.getContinuedStrokeId();
+                touchPoints[numPointsFound].mIsStartOfPath =
+                        (strokeDescription.getContinuedStrokeId() < 0)
+                                && (time == strokeDescription.mStartTime);
+                touchPoints[numPointsFound].mIsEndOfPath = !strokeDescription.isContinued()
+                        && (time == strokeDescription.mEndTime);
                 strokeDescription.getPosForTime(time, mTempPos);
                 touchPoints[numPointsFound].mX = Math.round(mTempPos[0]);
                 touchPoints[numPointsFound].mY = Math.round(mTempPos[1]);
@@ -196,6 +197,10 @@
      * Immutable description of stroke that can be part of a gesture.
      */
     public static class StrokeDescription {
+        public static final int INVALID_STROKE_ID = -1;
+
+        static int sIdCounter;
+
         Path mPath;
         long mStartTime;
         long mEndTime;
@@ -203,6 +208,9 @@
         private PathMeasure mPathMeasure;
         // The tap location is only set for zero-length paths
         float[] mTapLocation;
+        int mId;
+        boolean mContinued;
+        int mContinuedStrokeId;
 
         /**
          * @param path The path to follow. Must have exactly one contour. The bounds of the path
@@ -216,6 +224,32 @@
         public StrokeDescription(@NonNull Path path,
                 @IntRange(from = 0) long startTime,
                 @IntRange(from = 0) long duration) {
+            this(path, startTime, duration, INVALID_STROKE_ID, 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 continuedStrokeId The ID of the stroke that this stroke continues, or
+         * {@link #INVALID_STROKE_ID} if it continues no stroke. The stroke it
+         * continues must have its isContinued flag set to {@code true} and must be in the
+         * gesture dispatched immediately before the one containing this stroke.
+         * @param isContinued {@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,
+                @IntRange(from = 0) int continuedStrokeId,
+                boolean isContinued) {
+            mContinued = isContinued;
+            mContinuedStrokeId = continuedStrokeId;
             if (duration <= 0) {
                 throw new IllegalArgumentException("Duration must be positive");
             }
@@ -252,6 +286,7 @@
             mStartTime = startTime;
             mEndTime = startTime + duration;
             mTimeToLengthConversion = getLength() / duration;
+            mId = sIdCounter++;
         }
 
         /**
@@ -281,6 +316,34 @@
             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
+         */
+        public int getId() {
+            return mId;
+        }
+
+        /**
+         * Check if this stroke is marked to continue in the next gesture.
+         *
+         * @return {@code true} if the stroke is to be continued.
+         */
+        public boolean isContinued() {
+            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.
+         */
+        public int getContinuedStrokeId() {
+            return mContinuedStrokeId;
+        }
+
         float getLength() {
             return mPathMeasure.getLength();
         }
@@ -314,11 +377,12 @@
         private static final int FLAG_IS_START_OF_PATH = 0x01;
         private static final int FLAG_IS_END_OF_PATH = 0x02;
 
-        int mPathIndex;
-        boolean mIsStartOfPath;
-        boolean mIsEndOfPath;
-        float mX;
-        float mY;
+        public int mStrokeId;
+        public int mContinuedStrokeId;
+        public boolean mIsStartOfPath;
+        public boolean mIsEndOfPath;
+        public float mX;
+        public float mY;
 
         public TouchPoint() {
         }
@@ -328,7 +392,8 @@
         }
 
         public TouchPoint(Parcel parcel) {
-            mPathIndex = parcel.readInt();
+            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;
@@ -336,8 +401,9 @@
             mY = parcel.readFloat();
         }
 
-        void copyFrom(TouchPoint other) {
-            mPathIndex = other.mPathIndex;
+        public void copyFrom(TouchPoint other) {
+            mStrokeId = other.mStrokeId;
+            mContinuedStrokeId = other.mContinuedStrokeId;
             mIsStartOfPath = other.mIsStartOfPath;
             mIsEndOfPath = other.mIsEndOfPath;
             mX = other.mX;
@@ -351,7 +417,8 @@
 
         @Override
         public void writeToParcel(Parcel dest, int flags) {
-            dest.writeInt(mPathIndex);
+            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);
@@ -426,30 +493,15 @@
     }
 
     /**
-     * Class to convert a GestureDescription to a series of MotionEvents.
+     * Class to convert a GestureDescription to a series of GestureSteps.
      *
      * @hide
      */
     public static class MotionEventGenerator {
-        /**
-         * Constants used to initialize all MotionEvents
-         */
-        private static final int EVENT_META_STATE = 0;
-        private static final int EVENT_BUTTON_STATE = 0;
-        private static final int EVENT_DEVICE_ID = 0;
-        private static final int EVENT_EDGE_FLAGS = 0;
-        private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
-        private static final int EVENT_FLAGS = 0;
-        private static final float EVENT_X_PRECISION = 1;
-        private static final float EVENT_Y_PRECISION = 1;
-
         /* Lazily-created scratch memory for processing touches */
         private static TouchPoint[] sCurrentTouchPoints;
-        private static TouchPoint[] sLastTouchPoints;
-        private static PointerCoords[] sPointerCoords;
-        private static PointerProperties[] sPointerProps;
 
-        static List<GestureStep> getGestureStepsFromGestureDescription(
+        public static List<GestureStep> getGestureStepsFromGestureDescription(
                 GestureDescription description, int sampleTimeMs) {
             final List<GestureStep> gestureSteps = new ArrayList<>();
 
@@ -474,31 +526,6 @@
             return gestureSteps;
         }
 
-        public static List<MotionEvent> getMotionEventsFromGestureSteps(List<GestureStep> steps) {
-            final List<MotionEvent> motionEvents = new ArrayList<>();
-
-            // Number of points in last touch event
-            int lastTouchPointSize = 0;
-            TouchPoint[] lastTouchPoints;
-
-            for (int i = 0; i < steps.size(); i++) {
-                GestureStep step = steps.get(i);
-                int currentTouchPointSize = step.numTouchPoints;
-                lastTouchPoints = getLastTouchPoints(
-                        Math.max(lastTouchPointSize, currentTouchPointSize));
-
-                appendMoveEventIfNeeded(motionEvents, lastTouchPoints, lastTouchPointSize,
-                        step.touchPoints, currentTouchPointSize, step.timeSinceGestureStart);
-                lastTouchPointSize = appendUpEvents(motionEvents, lastTouchPoints,
-                        lastTouchPointSize, step.touchPoints, currentTouchPointSize,
-                        step.timeSinceGestureStart);
-                lastTouchPointSize = appendDownEvents(motionEvents, lastTouchPoints,
-                        lastTouchPointSize, step.touchPoints, currentTouchPointSize,
-                        step.timeSinceGestureStart);
-            }
-            return motionEvents;
-        }
-
         private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
             if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
                 sCurrentTouchPoints = new TouchPoint[requiredCapacity];
@@ -508,133 +535,5 @@
             }
             return sCurrentTouchPoints;
         }
-
-        private static TouchPoint[] getLastTouchPoints(int requiredCapacity) {
-            if ((sLastTouchPoints == null) || (sLastTouchPoints.length < requiredCapacity)) {
-                sLastTouchPoints = new TouchPoint[requiredCapacity];
-                for (int i = 0; i < requiredCapacity; i++) {
-                    sLastTouchPoints[i] = new TouchPoint();
-                }
-            }
-            return sLastTouchPoints;
-        }
-
-        private static PointerCoords[] getPointerCoords(int requiredCapacity) {
-            if ((sPointerCoords == null) || (sPointerCoords.length < requiredCapacity)) {
-                sPointerCoords = new PointerCoords[requiredCapacity];
-                for (int i = 0; i < requiredCapacity; i++) {
-                    sPointerCoords[i] = new PointerCoords();
-                }
-            }
-            return sPointerCoords;
-        }
-
-        private static PointerProperties[] getPointerProps(int requiredCapacity) {
-            if ((sPointerProps == null) || (sPointerProps.length < requiredCapacity)) {
-                sPointerProps = new PointerProperties[requiredCapacity];
-                for (int i = 0; i < requiredCapacity; i++) {
-                    sPointerProps[i] = new PointerProperties();
-                }
-            }
-            return sPointerProps;
-        }
-
-        private static void appendMoveEventIfNeeded(List<MotionEvent> motionEvents,
-                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
-                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
-            /* Look for pointers that have moved */
-            boolean moveFound = false;
-            for (int i = 0; i < currentTouchPointsSize; i++) {
-                int lastPointsIndex = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
-                        currentTouchPoints[i].mPathIndex);
-                if (lastPointsIndex >= 0) {
-                    moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX)
-                            || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY);
-                    lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]);
-                }
-            }
-
-            if (moveFound) {
-                long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
-                motionEvents.add(obtainMotionEvent(downTime, currentTime, MotionEvent.ACTION_MOVE,
-                        lastTouchPoints, lastTouchPointsSize));
-            }
-        }
-
-        private static int appendUpEvents(List<MotionEvent> motionEvents,
-                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
-                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
-            /* Look for a pointer at the end of its path */
-            for (int i = 0; i < currentTouchPointsSize; i++) {
-                if (currentTouchPoints[i].mIsEndOfPath) {
-                    int indexOfUpEvent = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
-                            currentTouchPoints[i].mPathIndex);
-                    if (indexOfUpEvent < 0) {
-                        continue; // Should not happen
-                    }
-                    long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
-                    int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_UP
-                            : MotionEvent.ACTION_POINTER_UP;
-                    action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-                    motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
-                            lastTouchPoints, lastTouchPointsSize));
-                    /* Remove this point from lastTouchPoints */
-                    for (int j = indexOfUpEvent; j < lastTouchPointsSize - 1; j++) {
-                        lastTouchPoints[j].copyFrom(lastTouchPoints[j+1]);
-                    }
-                    lastTouchPointsSize--;
-                }
-            }
-            return lastTouchPointsSize;
-        }
-
-        private static int appendDownEvents(List<MotionEvent> motionEvents,
-                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
-                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
-            /* Look for a pointer that is just starting */
-            for (int i = 0; i < currentTouchPointsSize; i++) {
-                if (currentTouchPoints[i].mIsStartOfPath) {
-                    /* Add the point to last coords and use the new array to generate the event */
-                    lastTouchPoints[lastTouchPointsSize++].copyFrom(currentTouchPoints[i]);
-                    int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_DOWN
-                            : MotionEvent.ACTION_POINTER_DOWN;
-                    long downTime = (action == MotionEvent.ACTION_DOWN) ? currentTime :
-                            motionEvents.get(motionEvents.size() - 1).getDownTime();
-                    action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-                    motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
-                            lastTouchPoints, lastTouchPointsSize));
-                }
-            }
-            return lastTouchPointsSize;
-        }
-
-        private static MotionEvent obtainMotionEvent(long downTime, long eventTime, int action,
-                TouchPoint[] touchPoints, int touchPointsSize) {
-            PointerCoords[] pointerCoords = getPointerCoords(touchPointsSize);
-            PointerProperties[] pointerProperties = getPointerProps(touchPointsSize);
-            for (int i = 0; i < touchPointsSize; i++) {
-                pointerProperties[i].id = touchPoints[i].mPathIndex;
-                pointerProperties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
-                pointerCoords[i].clear();
-                pointerCoords[i].pressure = 1.0f;
-                pointerCoords[i].size = 1.0f;
-                pointerCoords[i].x = touchPoints[i].mX;
-                pointerCoords[i].y = touchPoints[i].mY;
-            }
-            return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize,
-                    pointerProperties, pointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE,
-                    EVENT_X_PRECISION, EVENT_Y_PRECISION, EVENT_DEVICE_ID, EVENT_EDGE_FLAGS,
-                    EVENT_SOURCE, EVENT_FLAGS);
-        }
-
-        private static int findPointByPathIndex(TouchPoint[] touchPoints, int touchPointsSize,
-                int pathIndex) {
-            for (int i = 0; i < touchPointsSize; i++) {
-                if (touchPoints[i].mPathIndex == pathIndex) {
-                    return i;
-                }
-            }
-            return -1;
-        }
     }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index ab111a0..df71ced 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2843,15 +2843,8 @@
                     }
                     if (mMotionEventInjector != null) {
                         List<GestureDescription.GestureStep> steps = gestureSteps.getList();
-                        List<MotionEvent> events = GestureDescription.MotionEventGenerator
-                                .getMotionEventsFromGestureSteps(steps);
-                        // Confirm that the motion events end with an UP event.
-                        if (events.get(events.size() - 1).getAction() == MotionEvent.ACTION_UP) {
-                            mMotionEventInjector.injectEvents(events, mServiceInterface, sequence);
-                            return;
-                        } else {
-                            Slog.e(LOG_TAG, "Gesture is not well-formed");
-                        }
+                         mMotionEventInjector.injectEvents(steps, mServiceInterface, sequence);
+                         return;
                     } else {
                         Slog.e(LOG_TAG, "MotionEventInjector installation timed out");
                     }
diff --git a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
index 8042ddb..48041ad 100644
--- a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
+++ b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
@@ -16,49 +16,67 @@
 
 package com.android.server.accessibility;
 
+import android.accessibilityservice.GestureDescription;
+import android.accessibilityservice.GestureDescription.GestureStep;
+import android.accessibilityservice.GestureDescription.TouchPoint;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.util.IntArray;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.WindowManagerPolicy;
 import android.view.accessibility.AccessibilityEvent;
 import com.android.internal.os.SomeArgs;
-import com.android.server.accessibility.AccessibilityManagerService.Service;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
  * Injects MotionEvents to permit {@code AccessibilityService}s to touch the screen on behalf of
  * users.
- *
+ * <p>
  * All methods except {@code injectEvents} must be called only from the main thread.
  */
 public class MotionEventInjector implements EventStreamTransformation, Handler.Callback {
     private static final String LOG_TAG = "MotionEventInjector";
     private static final int MESSAGE_SEND_MOTION_EVENT = 1;
     private static final int MESSAGE_INJECT_EVENTS = 2;
-    private static final int MAX_POINTERS = 11; // Non-binding maximum
+
+    /**
+     * Constants used to initialize all MotionEvents
+     */
+    private static final int EVENT_META_STATE = 0;
+    private static final int EVENT_BUTTON_STATE = 0;
+    private static final int EVENT_DEVICE_ID = 0;
+    private static final int EVENT_EDGE_FLAGS = 0;
+    private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
+    private static final int EVENT_FLAGS = 0;
+    private static final float EVENT_X_PRECISION = 1;
+    private static final float EVENT_Y_PRECISION = 1;
+
+    private static MotionEvent.PointerCoords[] sPointerCoords;
+    private static MotionEvent.PointerProperties[] sPointerProps;
 
     private final Handler mHandler;
     private final SparseArray<Boolean> mOpenGesturesInProgress = new SparseArray<>();
 
-    // These two arrays must be the same length
-    private MotionEvent.PointerProperties[] mPointerProperties =
-            new MotionEvent.PointerProperties[MAX_POINTERS];
-    private MotionEvent.PointerCoords[] mPointerCoords =
-            new MotionEvent.PointerCoords[MAX_POINTERS];
     private EventStreamTransformation mNext;
     private IAccessibilityServiceClient mServiceInterfaceForCurrentGesture;
-    private int mSequenceForCurrentGesture;
-    private int mSourceOfInjectedGesture = InputDevice.SOURCE_UNKNOWN;
+    private IntArray mSequencesInProgress = new IntArray(5);
     private boolean mIsDestroyed = false;
+    private TouchPoint[] mLastTouchPoints;
+    private int mNumLastTouchPoints;
+    private long mDownTime;
+    private long mLastScheduledEventTime;
+    private SparseIntArray mStrokeIdToPointerId = new SparseIntArray(5);
 
     /**
      * @param looper A looper on the main thread to use for dispatching new events
@@ -75,18 +93,18 @@
     }
 
     /**
-     * Schedule a series of events for injection. These events must comprise a complete, valid
-     * sequence. All gestures currently in progress will be cancelled, and all {@code downTime}
-     * and {@code eventTime} fields will be offset by the current time.
+     * Schedule a gesture for injection. The gesture is defined by a set of {@code GestureStep}s,
+     * from which {@code MotionEvent}s will be derived. All gestures currently in progress will be
+     * cancelled.
      *
-     * @param events The events to inject. Must all be from the same source.
+     * @param gestureSteps The gesture steps to inject.
      * @param serviceInterface The interface to call back with a result when the gesture is
      * either complete or cancelled.
      */
-    public void injectEvents(List<MotionEvent> events,
+    public void injectEvents(List<GestureStep> gestureSteps,
             IAccessibilityServiceClient serviceInterface, int sequence) {
         SomeArgs args = SomeArgs.obtain();
-        args.arg1 = events;
+        args.arg1 = gestureSteps;
         args.arg2 = serviceInterface;
         args.argi1 = sequence;
         mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_INJECT_EVENTS, args));
@@ -138,7 +156,7 @@
     public boolean handleMessage(Message message) {
         if (message.what == MESSAGE_INJECT_EVENTS) {
             SomeArgs args = (SomeArgs) message.obj;
-            injectEventsMainThread((List<MotionEvent>) args.arg1,
+            injectEventsMainThread((List<GestureStep>) args.arg1,
                     (IAccessibilityServiceClient) args.arg2, args.argi1);
             args.recycle();
             return true;
@@ -148,16 +166,16 @@
             return false;
         }
         MotionEvent motionEvent = (MotionEvent) message.obj;
-        sendMotionEventToNext(motionEvent, motionEvent,
-                WindowManagerPolicy.FLAG_PASS_TO_USER);
-        // If the message queue is now empty, then this gesture is complete
-        if (!mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
-            notifyService(true);
+        sendMotionEventToNext(motionEvent, motionEvent, WindowManagerPolicy.FLAG_PASS_TO_USER);
+        boolean isEndOfSequence = message.arg1 != 0;
+        if (isEndOfSequence) {
+            notifyService(mServiceInterfaceForCurrentGesture, mSequencesInProgress.get(0), true);
+            mSequencesInProgress.remove(0);
         }
         return true;
     }
 
-    private void injectEventsMainThread(List<MotionEvent> events,
+    private void injectEventsMainThread(List<GestureStep> gestureSteps,
             IAccessibilityServiceClient serviceInterface, int sequence) {
         if (mIsDestroyed) {
             try {
@@ -168,48 +186,110 @@
             }
             return;
         }
-        cancelAnyPendingInjectedEvents();
-        mSourceOfInjectedGesture = events.get(0).getSource();
-        cancelAnyGestureInProgress(mSourceOfInjectedGesture);
-        mServiceInterfaceForCurrentGesture = serviceInterface;
-        mSequenceForCurrentGesture = sequence;
+
         if (mNext == null) {
-            notifyService(false);
+            notifyService(serviceInterface, sequence, false);
             return;
         }
 
-        long startTime = SystemClock.uptimeMillis();
+        boolean continuingGesture = newGestureTriesToContinueOldOne(gestureSteps);
+
+        if (continuingGesture) {
+            if ((serviceInterface != mServiceInterfaceForCurrentGesture)
+                    || !prepareToContinueOldGesture(gestureSteps)) {
+                cancelAnyPendingInjectedEvents();
+                notifyService(serviceInterface, sequence, false);
+                return;
+            }
+        }
+        if (!continuingGesture) {
+            cancelAnyPendingInjectedEvents();
+            // Injected gestures have been canceled, but real gestures still need cancelling
+            cancelAnyGestureInProgress(EVENT_SOURCE);
+        }
+        mServiceInterfaceForCurrentGesture = serviceInterface;
+
+        long currentTime = SystemClock.uptimeMillis();
+        List<MotionEvent> events = getMotionEventsFromGestureSteps(gestureSteps,
+                (mSequencesInProgress.size() == 0) ? currentTime : mLastScheduledEventTime);
+        if (events.isEmpty()) {
+            notifyService(serviceInterface, sequence, false);
+            return;
+        }
+        mSequencesInProgress.add(sequence);
+
         for (int i = 0; i < events.size(); i++) {
             MotionEvent event = events.get(i);
-            int numPointers = event.getPointerCount();
-            if (numPointers > mPointerCoords.length) {
-                mPointerCoords = new MotionEvent.PointerCoords[numPointers];
-                mPointerProperties = new MotionEvent.PointerProperties[numPointers];
-            }
-            for (int j = 0; j < numPointers; j++) {
-                if (mPointerCoords[j] == null) {
-                    mPointerCoords[j] = new MotionEvent.PointerCoords();
-                    mPointerProperties[j] = new MotionEvent.PointerProperties();
-                }
-                event.getPointerCoords(j, mPointerCoords[j]);
-                event.getPointerProperties(j, mPointerProperties[j]);
-            }
-
-            /*
-             * MotionEvent doesn't have a setEventTime() method (it carries around history data,
-             * which could become inconsistent), so we need to obtain a new one.
-             */
-            MotionEvent offsetEvent = MotionEvent.obtain(startTime + event.getDownTime(),
-                    startTime + event.getEventTime(), event.getAction(), numPointers,
-                    mPointerProperties, mPointerCoords, event.getMetaState(),
-                    event.getButtonState(), event.getXPrecision(), event.getYPrecision(),
-                    event.getDeviceId(), event.getEdgeFlags(), event.getSource(),
-                    event.getFlags());
-            Message message = mHandler.obtainMessage(MESSAGE_SEND_MOTION_EVENT, offsetEvent);
-            mHandler.sendMessageDelayed(message, event.getEventTime());
+            int isEndOfSequence = (i == events.size() - 1) ? 1 : 0;
+            Message message = mHandler.obtainMessage(
+                    MESSAGE_SEND_MOTION_EVENT, isEndOfSequence, 0, event);
+            mLastScheduledEventTime = event.getEventTime();
+            mHandler.sendMessageDelayed(message, Math.max(0, event.getEventTime() - currentTime));
         }
     }
 
+    private boolean newGestureTriesToContinueOldOne(List<GestureStep> gestureSteps) {
+        if (gestureSteps.isEmpty()) {
+            return false;
+        }
+        GestureStep firstStep = gestureSteps.get(0);
+        for (int i = 0; i < firstStep.numTouchPoints; i++) {
+            if (!firstStep.touchPoints[i].mIsStartOfPath) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * A gesture can only continue a gesture if it contains intermediate points that continue
+     * each continued stroke of the last gesture, and no extra points.
+     *
+     * @param gestureSteps The steps of the new gesture
+     * @return {@code true} if the new gesture could continue the last one dispatched. {@code false}
+     * otherwise.
+     */
+    private boolean prepareToContinueOldGesture(List<GestureStep> gestureSteps) {
+        if (gestureSteps.isEmpty() || (mLastTouchPoints == null) || (mNumLastTouchPoints == 0)) {
+            return false;
+        }
+        GestureStep firstStep = gestureSteps.get(0);
+        // Make sure all of the continuing paths match up
+        int numContinuedStrokes = 0;
+        for (int i = 0; i < firstStep.numTouchPoints; i++) {
+            TouchPoint touchPoint = firstStep.touchPoints[i];
+            if (!touchPoint.mIsStartOfPath) {
+                int continuedPointerId = mStrokeIdToPointerId
+                        .get(touchPoint.mContinuedStrokeId, -1);
+                if (continuedPointerId == -1) {
+                    return false;
+                }
+                mStrokeIdToPointerId.put(touchPoint.mStrokeId, continuedPointerId);
+                int lastPointIndex = findPointByStrokeId(
+                        mLastTouchPoints, mNumLastTouchPoints, touchPoint.mContinuedStrokeId);
+                if (lastPointIndex < 0) {
+                    return false;
+                }
+                if (mLastTouchPoints[lastPointIndex].mIsEndOfPath
+                        || (mLastTouchPoints[lastPointIndex].mX != touchPoint.mX)
+                        || (mLastTouchPoints[lastPointIndex].mY != touchPoint.mY)) {
+                    return false;
+                }
+                // Update the last touch point to match the continuation, so the gestures will
+                // line up
+                mLastTouchPoints[lastPointIndex].mStrokeId = touchPoint.mStrokeId;
+            }
+            numContinuedStrokes++;
+        }
+        // Make sure we didn't miss any paths
+        for (int i = 0; i < mNumLastTouchPoints; i++) {
+            if (!mLastTouchPoints[i].mIsEndOfPath) {
+                numContinuedStrokes--;
+            }
+        }
+        return numContinuedStrokes == 0;
+    }
+
     private void sendMotionEventToNext(MotionEvent event, MotionEvent rawEvent,
             int policyFlags) {
         if (mNext != null) {
@@ -228,7 +308,7 @@
         if ((mNext != null) && mOpenGesturesInProgress.get(source, false)) {
             long now = SystemClock.uptimeMillis();
             MotionEvent cancelEvent =
-                    MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+                    obtainMotionEvent(now, now, MotionEvent.ACTION_CANCEL, getLastTouchPoints(), 1);
             sendMotionEventToNext(cancelEvent, cancelEvent,
                     WindowManagerPolicy.FLAG_PASS_TO_USER);
             mOpenGesturesInProgress.put(source, false);
@@ -237,19 +317,187 @@
 
     private void cancelAnyPendingInjectedEvents() {
         if (mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
-            cancelAnyGestureInProgress(mSourceOfInjectedGesture);
             mHandler.removeMessages(MESSAGE_SEND_MOTION_EVENT);
-            notifyService(false);
+            cancelAnyGestureInProgress(EVENT_SOURCE);
+            for (int i = mSequencesInProgress.size() - 1; i >= 0; i--) {
+                notifyService(mServiceInterfaceForCurrentGesture,
+                        mSequencesInProgress.get(i), false);
+                mSequencesInProgress.remove(i);
+            }
+        } else if (mNumLastTouchPoints != 0) {
+            // An injected gesture is in progress and waiting for a continuation. Cancel it.
+            cancelAnyGestureInProgress(EVENT_SOURCE);
         }
+        mNumLastTouchPoints = 0;
+        mStrokeIdToPointerId.clear();
     }
 
-    private void notifyService(boolean success) {
+    private void notifyService(IAccessibilityServiceClient service, int sequence, boolean success) {
         try {
-            mServiceInterfaceForCurrentGesture.onPerformGestureResult(
-                    mSequenceForCurrentGesture, success);
+            service.onPerformGestureResult(sequence, success);
         } catch (RemoteException re) {
             Slog.e(LOG_TAG, "Error sending motion event injection status to "
                     + mServiceInterfaceForCurrentGesture, re);
         }
     }
+
+    private List<MotionEvent> getMotionEventsFromGestureSteps(
+            List<GestureStep> steps, long startTime) {
+        final List<MotionEvent> motionEvents = new ArrayList<>();
+
+        TouchPoint[] lastTouchPoints = getLastTouchPoints();
+
+        for (int i = 0; i < steps.size(); i++) {
+            GestureDescription.GestureStep step = steps.get(i);
+            int currentTouchPointSize = step.numTouchPoints;
+            if (currentTouchPointSize > lastTouchPoints.length) {
+                mNumLastTouchPoints = 0;
+                motionEvents.clear();
+                return motionEvents;
+            }
+
+            appendMoveEventIfNeeded(motionEvents, step.touchPoints, currentTouchPointSize,
+                    startTime + step.timeSinceGestureStart);
+            appendUpEvents(motionEvents, step.touchPoints, currentTouchPointSize,
+                    startTime + step.timeSinceGestureStart);
+            appendDownEvents(motionEvents, step.touchPoints, currentTouchPointSize,
+                    startTime + step.timeSinceGestureStart);
+        }
+        return motionEvents;
+    }
+
+    private TouchPoint[] getLastTouchPoints() {
+        if (mLastTouchPoints == null) {
+            int capacity = GestureDescription.getMaxStrokeCount();
+            mLastTouchPoints = new TouchPoint[capacity];
+            for (int i = 0; i < capacity; i++) {
+                mLastTouchPoints[i] = new GestureDescription.TouchPoint();
+            }
+        }
+        return mLastTouchPoints;
+    }
+
+    private void appendMoveEventIfNeeded(List<MotionEvent> motionEvents,
+            TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+            /* Look for pointers that have moved */
+        boolean moveFound = false;
+        TouchPoint[] lastTouchPoints = getLastTouchPoints();
+        for (int i = 0; i < currentTouchPointsSize; i++) {
+            int lastPointsIndex = findPointByStrokeId(lastTouchPoints, mNumLastTouchPoints,
+                    currentTouchPoints[i].mStrokeId);
+            if (lastPointsIndex >= 0) {
+                moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX)
+                        || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY);
+                lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]);
+            }
+        }
+
+        if (moveFound) {
+            motionEvents.add(obtainMotionEvent(mDownTime, currentTime, MotionEvent.ACTION_MOVE,
+                    lastTouchPoints, mNumLastTouchPoints));
+        }
+    }
+
+    private void appendUpEvents(List<MotionEvent> motionEvents,
+            TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+        /* Look for a pointer at the end of its path */
+        TouchPoint[] lastTouchPoints = getLastTouchPoints();
+        for (int i = 0; i < currentTouchPointsSize; i++) {
+            if (currentTouchPoints[i].mIsEndOfPath) {
+                int indexOfUpEvent = findPointByStrokeId(lastTouchPoints, mNumLastTouchPoints,
+                        currentTouchPoints[i].mStrokeId);
+                if (indexOfUpEvent < 0) {
+                    continue; // Should not happen
+                }
+                int action = (mNumLastTouchPoints == 1) ? MotionEvent.ACTION_UP
+                        : MotionEvent.ACTION_POINTER_UP;
+                action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+                motionEvents.add(obtainMotionEvent(mDownTime, currentTime, action,
+                        lastTouchPoints, mNumLastTouchPoints));
+                    /* Remove this point from lastTouchPoints */
+                for (int j = indexOfUpEvent; j < mNumLastTouchPoints - 1; j++) {
+                    lastTouchPoints[j].copyFrom(mLastTouchPoints[j + 1]);
+                }
+                mNumLastTouchPoints--;
+                if (mNumLastTouchPoints == 0) {
+                    mStrokeIdToPointerId.clear();
+                }
+            }
+        }
+    }
+
+    private void appendDownEvents(List<MotionEvent> motionEvents,
+            TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+        /* Look for a pointer that is just starting */
+        TouchPoint[] lastTouchPoints = getLastTouchPoints();
+        for (int i = 0; i < currentTouchPointsSize; i++) {
+            if (currentTouchPoints[i].mIsStartOfPath) {
+                /* Add the point to last coords and use the new array to generate the event */
+                lastTouchPoints[mNumLastTouchPoints++].copyFrom(currentTouchPoints[i]);
+                int action = (mNumLastTouchPoints == 1) ? MotionEvent.ACTION_DOWN
+                        : MotionEvent.ACTION_POINTER_DOWN;
+                if (action == MotionEvent.ACTION_DOWN) {
+                    mDownTime = currentTime;
+                }
+                action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+                motionEvents.add(obtainMotionEvent(mDownTime, currentTime, action,
+                        lastTouchPoints, mNumLastTouchPoints));
+            }
+        }
+    }
+
+    private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action,
+            TouchPoint[] touchPoints, int touchPointsSize) {
+        if ((sPointerCoords == null) || (sPointerCoords.length < touchPointsSize)) {
+            sPointerCoords = new MotionEvent.PointerCoords[touchPointsSize];
+            for (int i = 0; i < touchPointsSize; i++) {
+                sPointerCoords[i] = new MotionEvent.PointerCoords();
+            }
+        }
+        if ((sPointerProps == null) || (sPointerProps.length < touchPointsSize)) {
+            sPointerProps = new MotionEvent.PointerProperties[touchPointsSize];
+            for (int i = 0; i < touchPointsSize; i++) {
+                sPointerProps[i] = new MotionEvent.PointerProperties();
+            }
+        }
+        for (int i = 0; i < touchPointsSize; i++) {
+            int pointerId = mStrokeIdToPointerId.get(touchPoints[i].mStrokeId, -1);
+            if (pointerId == -1) {
+                pointerId = getUnusedPointerId();
+                mStrokeIdToPointerId.put(touchPoints[i].mStrokeId, pointerId);
+            }
+            sPointerProps[i].id = pointerId;
+            sPointerProps[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+            sPointerCoords[i].clear();
+            sPointerCoords[i].pressure = 1.0f;
+            sPointerCoords[i].size = 1.0f;
+            sPointerCoords[i].x = touchPoints[i].mX;
+            sPointerCoords[i].y = touchPoints[i].mY;
+        }
+        return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize,
+                sPointerProps, sPointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE,
+                EVENT_X_PRECISION, EVENT_Y_PRECISION, EVENT_DEVICE_ID, EVENT_EDGE_FLAGS,
+                EVENT_SOURCE, EVENT_FLAGS);
+    }
+
+    private static int findPointByStrokeId(TouchPoint[] touchPoints, int touchPointsSize,
+            int strokeId) {
+        for (int i = 0; i < touchPointsSize; i++) {
+            if (touchPoints[i].mStrokeId == strokeId) {
+                return i;
+            }
+        }
+        return -1;
+    }
+    private int getUnusedPointerId() {
+        int MAX_POINTER_ID = 10;
+        int pointerId = 0;
+        while (mStrokeIdToPointerId.indexOfValue(pointerId) >= 0) {
+            pointerId++;
+            if (pointerId >= MAX_POINTER_ID) {
+                return MAX_POINTER_ID;
+            }
+        }
+        return pointerId;
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/GestureDescriptionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/GestureDescriptionTest.java
new file mode 100644
index 0000000..b876a5f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/GestureDescriptionTest.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2016 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 com.android.server.accessibility;
+
+import static android.accessibilityservice.GestureDescription.StrokeDescription.INVALID_STROKE_ID;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.everyItem;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.accessibilityservice.GestureDescription;
+import android.accessibilityservice.GestureDescription.GestureStep;
+import android.accessibilityservice.GestureDescription.MotionEventGenerator;
+import android.accessibilityservice.GestureDescription.StrokeDescription;
+import android.graphics.Path;
+import android.graphics.PointF;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Test;
+
+import java.util.List;
+
+import static junit.framework.TestCase.assertEquals;
+
+/**
+ * Tests for GestureDescription
+ */
+public class GestureDescriptionTest {
+    @Test
+    public void testGestureShorterThanSampleRate_producesStartAndEnd() {
+        PointF click = new PointF(10, 20);
+        Path clickPath = new Path();
+        clickPath.moveTo(click.x, click.y);
+        StrokeDescription clickStroke = new StrokeDescription(clickPath, 0, 10);
+        GestureDescription.Builder clickBuilder = new GestureDescription.Builder();
+        clickBuilder.addStroke(clickStroke);
+        GestureDescription clickGesture = clickBuilder.build();
+
+        List<GestureStep> clickGestureSteps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(clickGesture, 100);
+
+        assertEquals(2, clickGestureSteps.size());
+        assertThat(clickGestureSteps.get(0), allOf(numTouchPointsIs(1), numStartsOfStroke(1),
+                numEndsOfStroke(0), hasPoint(click)));
+        assertThat(clickGestureSteps.get(1), allOf(numTouchPointsIs(1), numStartsOfStroke(0),
+                numEndsOfStroke(1), hasPoint(click)));
+    }
+
+    @Test
+    public void testSwipe_shouldContainEvenlySpacedPoints() {
+        int samplePeriod = 10;
+        int numSamples = 5;
+        float stepX = 2;
+        float stepY = 3;
+        PointF start = new PointF(10, 20);
+        PointF end = new PointF(10 + numSamples * stepX, 20 + numSamples * stepY);
+
+        GestureDescription swipe =
+                createSwipe(start.x, start.y, end.x, end.y, numSamples * samplePeriod);
+        List<GestureStep> swipeGestureSteps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(swipe, samplePeriod);
+        assertEquals(numSamples + 1, swipeGestureSteps.size());
+
+        assertThat(swipeGestureSteps.get(0), allOf(numTouchPointsIs(1), numStartsOfStroke(1),
+                numEndsOfStroke(0), hasPoint(start)));
+        assertThat(swipeGestureSteps.get(numSamples), allOf(numTouchPointsIs(1),
+                numStartsOfStroke(0), numEndsOfStroke(1), hasPoint(end)));
+
+        for (int i = 1; i < numSamples; ++i) {
+            PointF interpPoint = new PointF(start.x + stepX * i, start.y + stepY * i);
+            assertThat(swipeGestureSteps.get(i), allOf(numTouchPointsIs(1),
+                    numStartsOfStroke(0), numEndsOfStroke(0), hasPoint(interpPoint)));
+        }
+    }
+
+    @Test
+    public void testSwipeWithNonIntegerValues_shouldRound() {
+        int strokeTime = 10;
+
+        GestureDescription swipe = createSwipe(10.1f, 20.6f, 11.9f, 22.1f, strokeTime);
+        List<GestureStep> swipeGestureSteps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(swipe, strokeTime);
+        assertEquals(2, swipeGestureSteps.size());
+        assertThat(swipeGestureSteps.get(0), hasPoint(new PointF(10, 21)));
+        assertThat(swipeGestureSteps.get(1), hasPoint(new PointF(12, 22)));
+    }
+
+    @Test
+    public void testPathsWithOverlappingTiming_produceCorrectSteps() {
+        // There are 4 paths
+        // 0: an L-shaped path that starts first
+        // 1: a swipe that starts in the middle of the L-shaped path and ends when the L ends
+        // 2: a swipe that starts at the same time as #1 but extends past the end of the L
+        // 3: a swipe that starts when #3 ends
+        PointF path0Start = new PointF(100, 150);
+        PointF path0Turn = new PointF(100, 200);
+        PointF path0End = new PointF(250, 200);
+        int path0StartTime = 0;
+        int path0EndTime = 100;
+        int path0Duration = path0EndTime - path0StartTime;
+        Path path0 = new Path();
+        path0.moveTo(path0Start.x, path0Start.y);
+        path0.lineTo(path0Turn.x, path0Turn.y);
+        path0.lineTo(path0End.x, path0End.y);
+        StrokeDescription path0Stroke = new StrokeDescription(path0, path0StartTime, path0Duration);
+
+        PointF path1Start = new PointF(300, 350);
+        PointF path1End = new PointF(300, 400);
+        int path1StartTime = 50;
+        int path1EndTime = path0EndTime;
+        StrokeDescription path1Stroke = createSwipeStroke(
+                path1Start.x, path1Start.y, path1End.x, path1End.y, path1StartTime, path1EndTime);
+
+        PointF path2Start = new PointF(400, 450);
+        PointF path2End = new PointF(400, 500);
+        int path2StartTime = 50;
+        int path2EndTime = 150;
+        StrokeDescription path2Stroke = createSwipeStroke(
+                path2Start.x, path2Start.y, path2End.x, path2End.y, path2StartTime, path2EndTime);
+
+        PointF path3Start = new PointF(500, 550);
+        PointF path3End = new PointF(500, 600);
+        int path3StartTime = path2EndTime;
+        int path3EndTime = 200;
+        StrokeDescription path3Stroke = createSwipeStroke(
+                path3Start.x, path3Start.y, path3End.x, path3End.y, path3StartTime, path3EndTime);
+
+        int deltaT = 12; // Force samples to happen on extra boundaries
+        GestureDescription.Builder builder = new GestureDescription.Builder();
+        builder.addStroke(path0Stroke);
+        builder.addStroke(path1Stroke);
+        builder.addStroke(path2Stroke);
+        builder.addStroke(path3Stroke);
+        List<GestureStep> steps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(builder.build(), deltaT);
+
+        long start = 0;
+        assertThat(steps.get(0), allOf(numStartsOfStroke(1), numEndsOfStroke(0), isAtTime(start),
+                numTouchPointsIs(1), hasPoint(path0Start)));
+        assertThat(steps.get(1), allOf(numTouchPointsIs(1), noStartsOrEnds(),
+                isAtTime(start + deltaT)));
+        assertThat(steps.get(2), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 2)));
+        assertThat(steps.get(3), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 3)));
+        assertThat(steps.get(4), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 4)));
+
+        assertThat(steps.get(5), allOf(numTouchPointsIs(3), numStartsOfStroke(2),
+                numEndsOfStroke(0), isAtTime(path1StartTime), hasPoint(path1Start),
+                hasPoint(path2Start)));
+
+        start = path1StartTime;
+        assertThat(steps.get(6), allOf(numTouchPointsIs(3), isAtTime(start + deltaT * 1)));
+        assertThat(steps.get(7), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 2)));
+        assertThat(steps.get(8), allOf(numTouchPointsIs(3), isAtTime(start + deltaT * 3)));
+        assertThat(steps.get(9), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 4)));
+
+        assertThat(steps.get(10), allOf(numTouchPointsIs(3), numStartsOfStroke(0),
+                numEndsOfStroke(2), isAtTime(path0EndTime), hasPoint(path0End),
+                hasPoint(path1End)));
+
+        start = path0EndTime;
+        assertThat(steps.get(11), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 1)));
+        assertThat(steps.get(12), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 2)));
+        assertThat(steps.get(13), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 3)));
+        assertThat(steps.get(14), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 4)));
+
+        assertThat(steps.get(15), allOf(numTouchPointsIs(2), numStartsOfStroke(1),
+                numEndsOfStroke(1), isAtTime(path2EndTime), hasPoint(path2End),
+                hasPoint(path3Start)));
+
+        start = path2EndTime;
+        assertThat(steps.get(16), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 1)));
+        assertThat(steps.get(17), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 2)));
+        assertThat(steps.get(18), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 3)));
+        assertThat(steps.get(19), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 4)));
+
+        assertThat(steps.get(20), allOf(numTouchPointsIs(1), numStartsOfStroke(0),
+                numEndsOfStroke(1), isAtTime(path3EndTime), hasPoint(path3End)));
+    }
+
+    @Test
+    public void testMaxTouchpoints_shouldHaveValidCoords() {
+        GestureDescription.Builder maxPointBuilder = new GestureDescription.Builder();
+        PointF baseStartPoint = new PointF(100, 100);
+        PointF baseEndPoint = new PointF(100, 200);
+        int xStep = 10;
+        int samplePeriod = 15;
+        int numSamples = 2;
+        int numPoints = GestureDescription.getMaxStrokeCount();
+        for (int i = 0; i < numPoints; i++) {
+            Path path = new Path();
+            path.moveTo(baseStartPoint.x + i * xStep, baseStartPoint.y);
+            path.lineTo(baseEndPoint.x + i * xStep, baseEndPoint.y);
+            maxPointBuilder.addStroke(new StrokeDescription(path, 0, samplePeriod * numSamples));
+        }
+
+        List<GestureStep> steps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(maxPointBuilder.build(), samplePeriod);
+        assertEquals(3, steps.size());
+
+        assertThat(steps.get(0), allOf(numTouchPointsIs(numPoints), numStartsOfStroke(numPoints),
+                numEndsOfStroke(0), isAtTime(0)));
+        assertThat(steps.get(1), allOf(numTouchPointsIs(numPoints), numStartsOfStroke(0),
+                numEndsOfStroke(0), isAtTime(samplePeriod)));
+        assertThat(steps.get(2), allOf(numTouchPointsIs(numPoints), numStartsOfStroke(0),
+                numEndsOfStroke(numPoints), isAtTime(samplePeriod * 2)));
+
+        PointF baseMidPoint = new PointF((baseStartPoint.x + baseEndPoint.x) / 2,
+                (baseStartPoint.y + baseEndPoint.y) / 2);
+        for (int i = 0; i < numPoints; i++) {
+            assertThat(steps.get(0),
+                    hasPoint(new PointF(baseStartPoint.x + i * xStep, baseStartPoint.y)));
+            assertThat(steps.get(1),
+                    hasPoint(new PointF(baseMidPoint.x + i * xStep, baseMidPoint.y)));
+            assertThat(steps.get(2),
+                    hasPoint(new PointF(baseEndPoint.x + i * xStep, baseEndPoint.y)));
+        }
+    }
+
+    @Test
+    public void testGetGestureSteps_touchPointsHaveStrokeId() {
+        StrokeDescription swipeStroke = createSwipeStroke(10, 20, 30, 40, 0, 100);
+        GestureDescription swipe = new GestureDescription.Builder().addStroke(swipeStroke).build();
+        List<GestureStep> swipeGestureSteps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(swipe, 10);
+
+        assertThat(swipeGestureSteps, everyItem(hasStrokeId(swipeStroke.getId())));
+    }
+
+    @Test
+    public void testGetGestureSteps_continuedStroke_hasNoEndPoint() {
+        Path swipePath = new Path();
+        swipePath.moveTo(10, 20);
+        swipePath.lineTo(30, 40);
+        StrokeDescription stroke1 =
+                new StrokeDescription(swipePath, 0, 100, 0, true);
+        GestureDescription gesture = new GestureDescription.Builder().addStroke(stroke1).build();
+        List<GestureStep> steps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(gesture, 10);
+
+        assertThat(steps, everyItem(numEndsOfStroke(0)));
+    }
+
+    @Test
+    public void testGetGestureSteps_continuingStroke_hasNoStartPointAndHasContinuedId() {
+        Path swipePath = new Path();
+        swipePath.moveTo(10, 20);
+        swipePath.lineTo(30, 40);
+        StrokeDescription stroke1 =
+                new StrokeDescription(swipePath, 0, 100, INVALID_STROKE_ID, true);
+        StrokeDescription stroke2 =
+                new StrokeDescription(swipePath, 0, 100, stroke1.getId(), false);
+        GestureDescription gesture = new GestureDescription.Builder().addStroke(stroke2).build();
+        List<GestureStep> steps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(gesture, 10);
+
+        assertThat(steps, everyItem(
+                allOf(continuesStrokeId(stroke1.getId()), numStartsOfStroke(0))));
+    }
+
+    private GestureDescription createSwipe(
+            float startX, float startY, float endX, float endY, long duration) {
+        GestureDescription.Builder swipeBuilder = new GestureDescription.Builder();
+        swipeBuilder.addStroke(createSwipeStroke(startX, startY, endX, endY, 0, duration));
+        return swipeBuilder.build();
+    }
+
+    private StrokeDescription createSwipeStroke(
+            float startX, float startY, float endX, float endY, long startTime, long endTime) {
+        Path swipePath = new Path();
+        swipePath.moveTo(startX, startY);
+        swipePath.lineTo(endX, endY);
+        StrokeDescription swipeStroke =
+                new StrokeDescription(swipePath, startTime, endTime - startTime);
+        return swipeStroke;
+    }
+
+    Matcher<GestureStep> numTouchPointsIs(final int numTouchPoints) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                return gestureStep.numTouchPoints == numTouchPoints;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Has " + numTouchPoints + " touch point(s)");
+            }
+        };
+    }
+
+    Matcher<GestureStep> numStartsOfStroke(final int numStarts) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                int numStartsFound = 0;
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if (gestureStep.touchPoints[i].mIsStartOfPath) {
+                        numStartsFound++;
+                    }
+                }
+                return numStartsFound == numStarts;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Starts " + numStarts + " stroke(s)");
+            }
+        };
+    }
+
+    Matcher<GestureStep> numEndsOfStroke(final int numEnds) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                int numEndsFound = 0;
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if (gestureStep.touchPoints[i].mIsEndOfPath) {
+                        numEndsFound++;
+                    }
+                }
+                return numEndsFound == numEnds;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Ends " + numEnds + " stroke(s)");
+            }
+        };
+    }
+
+    Matcher<GestureStep> hasPoint(final PointF point) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if ((gestureStep.touchPoints[i].mX == point.x)
+                            && (gestureStep.touchPoints[i].mY == point.y)) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Has at least one point at " + point);
+            }
+        };
+    }
+
+    Matcher<GestureStep> hasStrokeId(final int strokeId) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if (gestureStep.touchPoints[i].mStrokeId == strokeId) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Has at least one point with stroke id " + strokeId);
+            }
+        };
+    }
+
+    Matcher<GestureStep> continuesStrokeId(final int strokeId) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if (gestureStep.touchPoints[i].mContinuedStrokeId == strokeId) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Continues stroke id " + strokeId);
+            }
+        };
+    }
+
+    Matcher<GestureStep> isAtTime(final long time) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                return gestureStep.timeSinceGestureStart == time;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Is at time " + time);
+            }
+        };
+    }
+
+    Matcher<GestureStep> noStartsOrEnds() {
+        return allOf(numStartsOfStroke(0), numEndsOfStroke(0));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
index d5305d9..73344e0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
@@ -16,9 +16,17 @@
 
 package com.android.server.accessibility;
 
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+import static android.view.WindowManagerPolicy.FLAG_PASS_TO_USER;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.everyItem;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.argThat;
 import static org.mockito.Matchers.eq;
@@ -29,7 +37,10 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
+import android.accessibilityservice.GestureDescription.GestureStep;
+import android.accessibilityservice.GestureDescription.TouchPoint;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.graphics.Point;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -40,11 +51,15 @@
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.WindowManagerPolicy;
+
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import android.view.accessibility.AccessibilityEvent;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -58,30 +73,59 @@
 @RunWith(AndroidJUnit4.class)
 public class MotionEventInjectorTest {
     private static final String LOG_TAG = "MotionEventInjectorTest";
-    private static final int CLICK_X = 100;
-    private static final int CLICK_Y_START = 200;
-    private static final int CLICK_Y_END = 201;
-    private static final int CLICK_DURATION = 10;
-    private static final int SEQUENCE = 50;
+    private static final Matcher<MotionEvent> IS_ACTION_DOWN =
+            new MotionEventActionMatcher(ACTION_DOWN);
+    private static final Matcher<MotionEvent> IS_ACTION_POINTER_DOWN =
+            new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_DOWN);
+    private static final Matcher<MotionEvent> IS_ACTION_UP =
+            new MotionEventActionMatcher(ACTION_UP);
+    private static final Matcher<MotionEvent> IS_ACTION_POINTER_UP =
+            new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_UP);
+    private static final Matcher<MotionEvent> IS_ACTION_CANCEL =
+            new MotionEventActionMatcher(MotionEvent.ACTION_CANCEL);
+    private static final Matcher<MotionEvent> IS_ACTION_MOVE =
+            new MotionEventActionMatcher(MotionEvent.ACTION_MOVE);
 
-    private static final int SECOND_CLICK_X = 1000;
-    private static final int SECOND_CLICK_Y = 2000;
-    private static final int SECOND_SEQUENCE = 51;
+    private static final Point LINE_START = new Point(100, 200);
+    private static final Point LINE_END = new Point(100, 300);
+    private static final int LINE_DURATION = 100;
+    private static final int LINE_SEQUENCE = 50;
+
+    private static final Point CLICK_POINT = new Point(1000, 2000);
+    private static final int CLICK_DURATION = 10;
+    private static final int CLICK_SEQUENCE = 51;
 
     private static final int MOTION_EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
     private static final int OTHER_EVENT_SOURCE = InputDevice.SOURCE_MOUSE;
 
+    private static final Point CONTINUED_LINE_START = new Point(500, 300);
+    private static final Point CONTINUED_LINE_MID1 = new Point(500, 400);
+    private static final Point CONTINUED_LINE_MID2 = new Point(600, 300);
+    private static final Point CONTINUED_LINE_END = new Point(600, 400);
+    private static final int CONTINUED_LINE_STROKE_ID_1 = 100;
+    private static final int CONTINUED_LINE_STROKE_ID_2 = 101;
+    private static final int CONTINUED_LINE_INTERVAL = 100;
+    private static final int CONTINUED_LINE_SEQUENCE_1 = 52;
+    private static final int CONTINUED_LINE_SEQUENCE_2 = 53;
+
     MotionEventInjector mMotionEventInjector;
     IAccessibilityServiceClient mServiceInterface;
-    List<MotionEvent> mClickList = new ArrayList<>();
-    List<MotionEvent> mSecondClickList = new ArrayList<>();
+    List<GestureStep> mLineList = new ArrayList<>();
+    List<GestureStep> mClickList = new ArrayList<>();
+    List<GestureStep> mContinuedLineList1 = new ArrayList<>();
+    List<GestureStep> mContinuedLineList2 = new ArrayList<>();
+
+    MotionEvent mClickDownEvent;
+    MotionEvent mClickUpEvent;
+
     ArgumentCaptor<MotionEvent> mCaptor1 = ArgumentCaptor.forClass(MotionEvent.class);
     ArgumentCaptor<MotionEvent> mCaptor2 = ArgumentCaptor.forClass(MotionEvent.class);
     MessageCapturingHandler mMessageCapturingHandler;
-    MotionEventMatcher mClickEvent0Matcher;
-    MotionEventMatcher mClickEvent1Matcher;
-    MotionEventMatcher mClickEvent2Matcher;
-    MotionEventMatcher mSecondClickEvent0Matcher;
+    Matcher<MotionEvent> mIsLineStart;
+    Matcher<MotionEvent> mIsLineMiddle;
+    Matcher<MotionEvent> mIsLineEnd;
+    Matcher<MotionEvent> mIsClickDown;
+    Matcher<MotionEvent> mIsClickUp;
 
     @BeforeClass
     public static void oneTimeInitialization() {
@@ -99,226 +143,191 @@
             }
         });
         mMotionEventInjector = new MotionEventInjector(mMessageCapturingHandler);
-        mClickList.add(
-                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, CLICK_X, CLICK_Y_START, 0));
-        mClickList.add(MotionEvent.obtain(
-                0, CLICK_DURATION, MotionEvent.ACTION_MOVE, CLICK_X, CLICK_Y_END, 0));
-        mClickList.add(MotionEvent.obtain(
-                0, CLICK_DURATION, MotionEvent.ACTION_UP, CLICK_X, CLICK_Y_END, 0));
-        for (int i = 0; i < mClickList.size(); i++) {
-            mClickList.get(i).setSource(MOTION_EVENT_SOURCE);
-        }
-
-        mClickEvent0Matcher = new MotionEventMatcher(mClickList.get(0));
-        mClickEvent1Matcher = new MotionEventMatcher(mClickList.get(1));
-        mClickEvent2Matcher = new MotionEventMatcher(mClickList.get(2));
-
-        mSecondClickList.add(MotionEvent.obtain(
-                0, 0, MotionEvent.ACTION_DOWN, SECOND_CLICK_X, SECOND_CLICK_Y, 0));
-        mSecondClickList.add(MotionEvent.obtain(
-                0, CLICK_DURATION, MotionEvent.ACTION_MOVE, SECOND_CLICK_X, CLICK_Y_END, 0));
-        mSecondClickList.add(MotionEvent.obtain(
-                0, CLICK_DURATION, MotionEvent.ACTION_UP, SECOND_CLICK_X, CLICK_Y_END, 0));
-        for (int i = 0; i < mSecondClickList.size(); i++) {
-            mSecondClickList.get(i).setSource(MOTION_EVENT_SOURCE);
-        }
-
-        mSecondClickEvent0Matcher = new MotionEventMatcher(mSecondClickList.get(0));
-
         mServiceInterface = mock(IAccessibilityServiceClient.class);
+
+        mLineList = createSimpleGestureFromPoints(0, 0, false, LINE_DURATION, LINE_START, LINE_END);
+        mClickList = createSimpleGestureFromPoints(
+                0, 0, false, CLICK_DURATION, CLICK_POINT, CLICK_POINT);
+        mContinuedLineList1 = createSimpleGestureFromPoints(CONTINUED_LINE_STROKE_ID_1, 0, true,
+                CONTINUED_LINE_INTERVAL, CONTINUED_LINE_START, CONTINUED_LINE_MID1);
+        mContinuedLineList2 = createSimpleGestureFromPoints(CONTINUED_LINE_STROKE_ID_2,
+                CONTINUED_LINE_STROKE_ID_1, false, CONTINUED_LINE_INTERVAL, CONTINUED_LINE_MID1,
+                CONTINUED_LINE_MID2, CONTINUED_LINE_END);
+
+        mClickDownEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, CLICK_POINT.x, CLICK_POINT.y, 0);
+        mClickDownEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+        mClickUpEvent = MotionEvent.obtain(0, CLICK_DURATION, ACTION_UP, CLICK_POINT.x,
+                CLICK_POINT.y, 0);
+        mClickUpEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+
+        mIsLineStart = allOf(IS_ACTION_DOWN, isAtPoint(LINE_START), hasStandardInitialization(),
+                hasTimeFromDown(0));
+        mIsLineMiddle = allOf(IS_ACTION_MOVE, isAtPoint(LINE_END), hasStandardInitialization(),
+                hasTimeFromDown(LINE_DURATION));
+        mIsLineEnd = allOf(IS_ACTION_UP, isAtPoint(LINE_END), hasStandardInitialization(),
+                hasTimeFromDown(LINE_DURATION));
+        mIsClickDown = allOf(IS_ACTION_DOWN, isAtPoint(CLICK_POINT), hasStandardInitialization(),
+                hasTimeFromDown(0));
+        mIsClickUp = allOf(IS_ACTION_UP, isAtPoint(CLICK_POINT), hasStandardInitialization(),
+                hasTimeFromDown(CLICK_DURATION));
     }
 
     @Test
     public void testInjectEvents_shouldEmergeInOrderWithCorrectTiming() throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         verifyNoMoreInteractions(next);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
-        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
-        long gestureStart = mCaptor1.getValue().getDownTime();
-        mClickEvent0Matcher.offsetTimesBy(gestureStart);
-        mClickEvent1Matcher.offsetTimesBy(gestureStart);
-        mClickEvent2Matcher.offsetTimesBy(gestureStart);
-
-        verify(next).onMotionEvent(argThat(mClickEvent0Matcher), argThat(mClickEvent0Matcher),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), eq(FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(argThat(mIsLineStart), argThat(mIsLineStart),
+                eq(FLAG_PASS_TO_USER));
         verifyNoMoreInteractions(next);
         reset(next);
 
+        Matcher<MotionEvent> hasRightDownTime = hasDownTime(mCaptor1.getValue().getDownTime());
+
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        verify(next).onMotionEvent(argThat(mClickEvent1Matcher), argThat(mClickEvent1Matcher),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(argThat(allOf(mIsLineMiddle, hasRightDownTime)),
+                argThat(allOf(mIsLineMiddle, hasRightDownTime)), eq(FLAG_PASS_TO_USER));
         verifyNoMoreInteractions(next);
         reset(next);
 
         verifyZeroInteractions(mServiceInterface);
 
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        verify(next).onMotionEvent(argThat(mClickEvent2Matcher), argThat(mClickEvent2Matcher),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(argThat(allOf(mIsLineEnd, hasRightDownTime)),
+                argThat(allOf(mIsLineEnd, hasRightDownTime)), eq(FLAG_PASS_TO_USER));
         verifyNoMoreInteractions(next);
-        reset(next);
 
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, true);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, true);
         verifyNoMoreInteractions(mServiceInterface);
     }
 
     @Test
-    public void testInjectEvents_eventWithManyPointers_shouldNotCrash() {
-        int manyPointersCount = 20;
-        MotionEvent.PointerCoords[] pointerCoords =
-                new MotionEvent.PointerCoords[manyPointersCount];
-        MotionEvent.PointerProperties[] pointerProperties =
-                new MotionEvent.PointerProperties[manyPointersCount];
-        for (int i = 0; i < manyPointersCount; i++) {
-            pointerProperties[i] = new MotionEvent.PointerProperties();
-            pointerProperties[i].id = i;
-            pointerProperties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
-            pointerCoords[i] = new MotionEvent.PointerCoords();
-            pointerCoords[i].clear();
-            pointerCoords[i].pressure = 1.0f;
-            pointerCoords[i].size = 1.0f;
-            pointerCoords[i].x = i;
-            pointerCoords[i].y = i;
+    public void testInjectEvents_gestureWithTooManyPoints_shouldNotCrash() throws  Exception {
+        int tooManyPointsCount = 20;
+        TouchPoint[] startTouchPoints = new TouchPoint[tooManyPointsCount];
+        TouchPoint[] endTouchPoints = new TouchPoint[tooManyPointsCount];
+        for (int i = 0; i < tooManyPointsCount; i++) {
+            startTouchPoints[i] = new TouchPoint();
+            startTouchPoints[i].mIsStartOfPath = true;
+            startTouchPoints[i].mX = i;
+            startTouchPoints[i].mY = i;
+            endTouchPoints[i] = new TouchPoint();
+            endTouchPoints[i].mIsEndOfPath = true;
+            endTouchPoints[i].mX = i;
+            endTouchPoints[i].mY = i;
         }
-        List<MotionEvent> events = new ArrayList<>();
-        events.add(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, manyPointersCount,
-                pointerProperties, pointerCoords, 0, 0,
-                1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0));
-        events.add(MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, manyPointersCount,
-                pointerProperties, pointerCoords, 0, 0,
-                1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0));
-        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(events, mServiceInterface, SEQUENCE);
+        List<GestureStep> events = Arrays.asList(
+                new GestureStep(0, tooManyPointsCount, startTouchPoints),
+                new GestureStep(CLICK_DURATION, tooManyPointsCount, endTouchPoints));
+        attachMockNext(mMotionEventInjector);
+        injectEventsSync(events, mServiceInterface, CLICK_SEQUENCE);
         mMessageCapturingHandler.sendAllMessages();
-        verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertEquals(MotionEvent.ACTION_DOWN, mCaptor1.getAllValues().get(0).getActionMasked());
-        assertEquals(MotionEvent.ACTION_UP, mCaptor1.getAllValues().get(1).getActionMasked());
+        verify(mServiceInterface).onPerformGestureResult(eq(CLICK_SEQUENCE), anyBoolean());
     }
 
     @Test
     public void testRegularEvent_afterGestureComplete_shouldPassToNext() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendAllMessages(); // Send all motion events
         reset(next);
-        mMotionEventInjector.onMotionEvent(mSecondClickList.get(0), mClickList.get(0), 0);
-        verify(next).onMotionEvent(argThat(mSecondClickEvent0Matcher),
-                argThat(mClickEvent0Matcher), eq(0));
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
+        verify(next).onMotionEvent(argThat(mIsClickDown), argThat(mIsClickDown), eq(0));
     }
 
     @Test
     public void testInjectEvents_withRealGestureUnderway_shouldCancelRealAndPassInjected() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
 
         verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertEquals(MotionEvent.ACTION_CANCEL, mCaptor1.getAllValues().get(1).getActionMasked());
+        assertThat(mCaptor1.getAllValues().get(0), mIsClickDown);
+        assertThat(mCaptor1.getAllValues().get(1), IS_ACTION_CANCEL);
         reset(next);
 
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
-        long gestureStart = mCaptor1.getValue().getDownTime();
-        mSecondClickEvent0Matcher.offsetTimesBy(gestureStart);
-
-        verify(next).onMotionEvent(argThat(mSecondClickEvent0Matcher),
-                argThat(mSecondClickEvent0Matcher), eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(
+                argThat(mIsLineStart), argThat(mIsLineStart), eq(FLAG_PASS_TO_USER));
     }
 
     @Test
     public void testInjectEvents_withRealMouseGestureUnderway_shouldContinueRealAndPassInjected() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        MotionEvent mouseEvent = MotionEvent.obtain(mClickList.get(0));
+        MotionEvent mouseEvent = MotionEvent.obtain(mClickDownEvent);
         mouseEvent.setSource(InputDevice.SOURCE_MOUSE);
-        MotionEventMatcher mouseEventMatcher = new MotionEventMatcher(mouseEvent);
+        MotionEventMatcher isMouseEvent = new MotionEventMatcher(mouseEvent);
         mMotionEventInjector.onMotionEvent(mouseEvent, mouseEvent, 0);
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
 
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
         verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertTrue(mouseEventMatcher.matches(mCaptor1.getAllValues().get(0)));
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(1).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(1)));
+        assertThat(mCaptor1.getAllValues().get(0), isMouseEvent);
+        assertThat(mCaptor1.getAllValues().get(1), mIsLineStart);
     }
 
     @Test
     public void testInjectEvents_withRealGestureFinished_shouldJustPassInjected() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
-        mMotionEventInjector.onMotionEvent(mClickList.get(1), mClickList.get(1), 0);
-        mMotionEventInjector.onMotionEvent(mClickList.get(2), mClickList.get(2), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
+        mMotionEventInjector.onMotionEvent(mClickUpEvent, mClickUpEvent, 0);
 
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
-        verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertTrue(mClickEvent1Matcher.matches(mCaptor1.getAllValues().get(1)));
-        assertTrue(mClickEvent2Matcher.matches(mCaptor1.getAllValues().get(2)));
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        assertThat(mCaptor1.getAllValues().get(0), mIsClickDown);
+        assertThat(mCaptor1.getAllValues().get(1), mIsClickUp);
         reset(next);
 
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getValue().getDownTime());
-        verify(next).onMotionEvent(argThat(mSecondClickEvent0Matcher),
-                argThat(mSecondClickEvent0Matcher), eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), eq(FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(
+                argThat(mIsLineStart), argThat(mIsLineStart), eq(FLAG_PASS_TO_USER));
     }
 
     @Test
     public void testOnMotionEvents_openInjectedGestureInProgress_shouldCancelAndNotifyAndPassReal()
             throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
-
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        mMotionEventInjector.onMotionEvent(mSecondClickList.get(0), mSecondClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
 
         verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        mClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(0).getDownTime());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertEquals(MotionEvent.ACTION_CANCEL, mCaptor1.getAllValues().get(1).getActionMasked());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(2)));
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        assertThat(mCaptor1.getAllValues().get(0), mIsLineStart);
+        assertThat(mCaptor1.getAllValues().get(1), IS_ACTION_CANCEL);
+        assertThat(mCaptor1.getAllValues().get(2), mIsClickDown);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
     }
 
     @Test
     public void testOnMotionEvents_closedInjectedGestureInProgress_shouldOnlyNotifyAndPassReal()
             throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mClickList.add(MotionEvent.obtain(2 * CLICK_DURATION, 2 * CLICK_DURATION,
-                MotionEvent.ACTION_DOWN, CLICK_X, CLICK_Y_START, 0));
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        // Tack a click down to the end of the line
+        TouchPoint clickTouchPoint = new TouchPoint();
+        clickTouchPoint.mIsStartOfPath = true;
+        clickTouchPoint.mX = CLICK_POINT.x;
+        clickTouchPoint.mY = CLICK_POINT.y;
+        mLineList.add(new GestureStep(0, 1, new TouchPoint[] {clickTouchPoint}));
+
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
 
         // Send 3 motion events, leaving the extra down in the queue
         mMessageCapturingHandler.sendOneMessage();
         mMessageCapturingHandler.sendOneMessage();
         mMessageCapturingHandler.sendOneMessage();
 
-        mMotionEventInjector.onMotionEvent(mSecondClickList.get(0), mClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
 
         verify(next, times(4)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        long gestureStart = mCaptor1.getAllValues().get(0).getDownTime();
-        mClickEvent0Matcher.offsetTimesBy(gestureStart);
-        mClickEvent1Matcher.offsetTimesBy(gestureStart);
-        mClickEvent2Matcher.offsetTimesBy(gestureStart);
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertTrue(mClickEvent1Matcher.matches(mCaptor1.getAllValues().get(1)));
-        assertTrue(mClickEvent2Matcher.matches(mCaptor1.getAllValues().get(2)));
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(3)));
-
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        assertThat(mCaptor1.getAllValues().get(0), mIsLineStart);
+        assertThat(mCaptor1.getAllValues().get(1), mIsLineMiddle);
+        assertThat(mCaptor1.getAllValues().get(2), mIsLineEnd);
+        assertThat(mCaptor1.getAllValues().get(3), mIsClickDown);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
         assertFalse(mMessageCapturingHandler.hasMessages());
     }
 
@@ -326,105 +335,327 @@
     public void testInjectEvents_openInjectedGestureInProgress_shouldCancelAndNotifyAndPassNew()
             throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SECOND_SEQUENCE);
-        mMessageCapturingHandler.sendLastMessage(); // Process the second event injection
+        injectEventsSync(mClickList, mServiceInterface, CLICK_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
-        verify(mServiceInterface, times(1)).onPerformGestureResult(SEQUENCE, false);
+        verify(mServiceInterface, times(1)).onPerformGestureResult(LINE_SEQUENCE, false);
         verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        mClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(0).getDownTime());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertEquals(MotionEvent.ACTION_CANCEL, mCaptor1.getAllValues().get(1).getActionMasked());
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(2).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(2)));
+        assertThat(mCaptor1.getAllValues().get(0), mIsLineStart);
+        assertThat(mCaptor1.getAllValues().get(1), IS_ACTION_CANCEL);
+        assertThat(mCaptor1.getAllValues().get(2), mIsClickDown);
     }
 
     @Test
     public void testInjectEvents_closedInjectedGestureInProgress_shouldOnlyNotifyAndPassNew()
             throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        MotionEvent newEvent = MotionEvent.obtain(2 * CLICK_DURATION, 2 * CLICK_DURATION,
-                MotionEvent.ACTION_DOWN, CLICK_X, CLICK_Y_START, 0);
-        newEvent.setSource(mClickList.get(0).getSource());
-        mClickList.add(newEvent);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        // Tack a click down to the end of the line
+        TouchPoint clickTouchPoint = new TouchPoint();
+        clickTouchPoint.mIsStartOfPath = true;
+        clickTouchPoint.mX = CLICK_POINT.x;
+        clickTouchPoint.mY = CLICK_POINT.y;
+        mLineList.add(new GestureStep(0, 1, new TouchPoint[] {clickTouchPoint}));
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
 
         // Send 3 motion events, leaving newEvent in the queue
         mMessageCapturingHandler.sendOneMessage();
         mMessageCapturingHandler.sendOneMessage();
         mMessageCapturingHandler.sendOneMessage();
 
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SECOND_SEQUENCE);
-        mMessageCapturingHandler.sendLastMessage(); // Process the event injection
+        injectEventsSync(mClickList, mServiceInterface, CLICK_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
         verify(next, times(4)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        long gestureStart = mCaptor1.getAllValues().get(0).getDownTime();
-        mClickEvent0Matcher.offsetTimesBy(gestureStart);
-        mClickEvent1Matcher.offsetTimesBy(gestureStart);
-        mClickEvent2Matcher.offsetTimesBy(gestureStart);
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertTrue(mClickEvent1Matcher.matches(mCaptor1.getAllValues().get(1)));
-        assertTrue(mClickEvent2Matcher.matches(mCaptor1.getAllValues().get(2)));
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(3).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(3)));
+        assertThat(mCaptor1.getAllValues().get(0), mIsLineStart);
+        assertThat(mCaptor1.getAllValues().get(1), mIsLineMiddle);
+        assertThat(mCaptor1.getAllValues().get(2), mIsLineEnd);
+        assertThat(mCaptor1.getAllValues().get(3), mIsClickDown);
+    }
+
+    @Test
+    public void testContinuedGesture_continuationArrivesAfterDispatched_gestureCompletes()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        injectEventsSync(mContinuedLineList2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, true);
+        verify(next, times(5)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        long downTime = events.get(0).getDownTime();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN,
+                hasEventTime(downTime)));
+        assertThat(events, everyItem(hasDownTime(downTime)));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL)));
+        // Timing will restart when the gesture continues
+        long secondSequenceStart = events.get(2).getEventTime();
+        assertTrue(secondSequenceStart > events.get(1).getEventTime());
+        assertThat(events.get(2), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_MOVE));
+        assertThat(events.get(3), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_MOVE,
+                hasEventTime(secondSequenceStart + CONTINUED_LINE_INTERVAL)));
+        assertThat(events.get(4), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_UP,
+                hasEventTime(secondSequenceStart + CONTINUED_LINE_INTERVAL)));
+    }
+
+    @Test
+    public void testContinuedGesture_withTwoTouchPoints_gestureCompletes()
+            throws Exception {
+        // Run one point through the continued line backwards
+        int backLineId1 = 30;
+        int backLineId2 = 30;
+        List<GestureStep> continuedBackLineList1 = createSimpleGestureFromPoints(backLineId1, 0,
+                true, CONTINUED_LINE_INTERVAL, CONTINUED_LINE_END, CONTINUED_LINE_MID2);
+        List<GestureStep> continuedBackLineList2 = createSimpleGestureFromPoints(backLineId2,
+                backLineId1, false, CONTINUED_LINE_INTERVAL, CONTINUED_LINE_MID2,
+                CONTINUED_LINE_MID1, CONTINUED_LINE_START);
+        List<GestureStep> combinedLines1 = combineGestureSteps(
+                mContinuedLineList1, continuedBackLineList1);
+        List<GestureStep> combinedLines2 = combineGestureSteps(
+                mContinuedLineList2, continuedBackLineList2);
+
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(combinedLines1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        injectEventsSync(combinedLines2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, true);
+        verify(next, times(7)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        long downTime = events.get(0).getDownTime();
+        assertThat(events.get(0), allOf(
+                anyOf(isAtPoint(CONTINUED_LINE_END), isAtPoint(CONTINUED_LINE_START)),
+                IS_ACTION_DOWN, hasEventTime(downTime)));
+        assertThat(events, everyItem(hasDownTime(downTime)));
+        assertThat(events.get(1), allOf(containsPoints(CONTINUED_LINE_START, CONTINUED_LINE_END),
+                IS_ACTION_POINTER_DOWN, hasEventTime(downTime)));
+        assertThat(events.get(2), allOf(containsPoints(CONTINUED_LINE_MID1, CONTINUED_LINE_MID2),
+                IS_ACTION_MOVE, hasEventTime(downTime + CONTINUED_LINE_INTERVAL)));
+        assertThat(events.get(3), allOf(containsPoints(CONTINUED_LINE_MID1, CONTINUED_LINE_MID2),
+                IS_ACTION_MOVE, hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 2)));
+        assertThat(events.get(4), allOf(containsPoints(CONTINUED_LINE_START, CONTINUED_LINE_END),
+                IS_ACTION_MOVE, hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+        assertThat(events.get(5), allOf(containsPoints(CONTINUED_LINE_START, CONTINUED_LINE_END),
+                IS_ACTION_POINTER_UP, hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+        assertThat(events.get(6), allOf(
+                anyOf(isAtPoint(CONTINUED_LINE_END), isAtPoint(CONTINUED_LINE_START)),
+                IS_ACTION_UP, hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+    }
+
+
+    @Test
+    public void testContinuedGesture_continuationArrivesWhileDispatching_gestureCompletes()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+        injectEventsSync(mContinuedLineList2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, true);
+        verify(next, times(5)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        long downTime = events.get(0).getDownTime();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN,
+                hasEventTime(downTime)));
+        assertThat(events, everyItem(hasDownTime(downTime)));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL)));
+        assertThat(events.get(2), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 2)));
+        assertThat(events.get(3), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+        assertThat(events.get(4), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_UP,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+    }
+
+    @Test
+    public void testContinuedGesture_twoContinuationsArriveWhileDispatching_gestureCompletes()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        // Continue line again
+        List<GestureStep> continuedLineList2 = createSimpleGestureFromPoints(
+                CONTINUED_LINE_STROKE_ID_2, CONTINUED_LINE_STROKE_ID_1, true,
+                CONTINUED_LINE_INTERVAL, CONTINUED_LINE_MID1,
+                CONTINUED_LINE_MID2, CONTINUED_LINE_END);
+        // Finish line by backtracking
+        int strokeId3 = CONTINUED_LINE_STROKE_ID_2 + 1;
+        int sequence3 = CONTINUED_LINE_SEQUENCE_2 + 1;
+        List<GestureStep> continuedLineList3 = createSimpleGestureFromPoints(strokeId3,
+                CONTINUED_LINE_STROKE_ID_2, false, CONTINUED_LINE_INTERVAL, CONTINUED_LINE_END,
+                CONTINUED_LINE_MID2);
+
+        mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+        injectEventsSync(continuedLineList2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        injectEventsSync(continuedLineList3, mServiceInterface, sequence3);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, true);
+        verify(mServiceInterface).onPerformGestureResult(sequence3, true);
+        verify(next, times(6)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        long downTime = events.get(0).getDownTime();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN,
+                hasEventTime(downTime)));
+        assertThat(events, everyItem(hasDownTime(downTime)));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL)));
+        assertThat(events.get(2), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 2)));
+        assertThat(events.get(3), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+        assertThat(events.get(4), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 4)));
+        assertThat(events.get(5), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_UP,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 4)));
+    }
+
+    @Test
+    public void testContinuedGesture_nonContinuingGestureArrivesDuringDispatch_gestureCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, false);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, true);
+        verify(next, times(5)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), IS_ACTION_CANCEL);
+        assertThat(events.get(2), allOf(isAtPoint(LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(3), allOf(isAtPoint(LINE_END), IS_ACTION_MOVE));
+        assertThat(events.get(4), allOf(isAtPoint(LINE_END), IS_ACTION_UP));
+    }
+
+    @Test
+    public void testContinuedGesture_nonContinuingGestureArrivesAfterDispatch_gestureCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, true);
+        verify(next, times(6)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE));
+        assertThat(events.get(2), IS_ACTION_CANCEL);
+        assertThat(events.get(3), allOf(isAtPoint(LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(4), allOf(isAtPoint(LINE_END), IS_ACTION_MOVE));
+        assertThat(events.get(5), allOf(isAtPoint(LINE_END), IS_ACTION_UP));
+    }
+
+    @Test
+    public void testContinuedGesture_misMatchedContinuationArrives_bothGesturesCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        List<GestureStep> discontinuousGesture = mContinuedLineList2
+                .subList(1, mContinuedLineList2.size());
+        injectEventsSync(discontinuousGesture, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, false);
+        verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE));
+        assertThat(events.get(2), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_CANCEL));
+    }
+
+    @Test
+    public void testContinuedGesture_continuationArrivesFromOtherService_bothGesturesCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        IAccessibilityServiceClient otherService = mock(IAccessibilityServiceClient.class);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendOneMessage(); // Send a motion events
+        injectEventsSync(mContinuedLineList2, otherService, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, false);
+        verify(otherService).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, false);
+        verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), IS_ACTION_CANCEL);
+    }
+
+    @Test
+    public void testContinuedGesture_realGestureArrivesInBetween_getsCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
+
+        injectEventsSync(mContinuedLineList2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, false);
+        verify(next, times(4)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE));
+        assertThat(events.get(2), IS_ACTION_CANCEL);
+        assertThat(events.get(3), allOf(isAtPoint(CLICK_POINT), IS_ACTION_DOWN));
     }
 
     @Test
     public void testClearEvents_realGestureInProgress_shouldForgetAboutGesture() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
         mMotionEventInjector.clearEvents(MOTION_EVENT_SOURCE);
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
         verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(1).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(1)));
+        assertThat(mCaptor1.getAllValues().get(0), mIsClickDown);
+        assertThat(mCaptor1.getAllValues().get(1), mIsLineStart);
     }
 
     @Test
     public void testClearEventsOnOtherSource_realGestureInProgress_shouldNotForgetAboutGesture() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
         mMotionEventInjector.clearEvents(OTHER_EVENT_SOURCE);
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SECOND_SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
         verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertEquals(MotionEvent.ACTION_CANCEL, mCaptor1.getAllValues().get(1).getActionMasked());
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(2).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(2)));
+        assertThat(mCaptor1.getAllValues().get(0), mIsClickDown);
+        assertThat(mCaptor1.getAllValues().get(1), IS_ACTION_CANCEL);
+        assertThat(mCaptor1.getAllValues().get(2), mIsLineStart);
     }
 
     @Test
     public void testOnDestroy_shouldCancelGestures() throws RemoteException {
         mMotionEventInjector.onDestroy();
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
     }
 
     @Test
     public void testInjectEvents_withNoNext_shouldCancel() throws RemoteException {
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
     }
 
     @Test
     public void testOnMotionEvent_withNoNext_shouldNotCrash() {
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
     }
 
     @Test
@@ -455,6 +686,52 @@
         mMotionEventInjector.onAccessibilityEvent(event);
     }
 
+    private void injectEventsSync(List<GestureStep> gestureSteps,
+            IAccessibilityServiceClient serviceInterface, int sequence) {
+        mMotionEventInjector.injectEvents(gestureSteps, serviceInterface, sequence);
+        // Dispatch the message sent by the injector. Our simple handler doesn't guarantee stuff
+        // happens in order.
+        mMessageCapturingHandler.sendLastMessage();
+    }
+
+    private List<GestureStep> createSimpleGestureFromPoints(int strokeId, int continuedStrokeId,
+            boolean continued, long interval, Point... points) {
+        List<GestureStep> gesture = new ArrayList<>(points.length);
+        TouchPoint[] touchPoints = new TouchPoint[1];
+        touchPoints[0] = new TouchPoint();
+        for (int i = 0; i < points.length; i++) {
+            touchPoints[0].mX = points[i].x;
+            touchPoints[0].mY = points[i].y;
+            touchPoints[0].mIsStartOfPath = ((i == 0) && (continuedStrokeId <= 0));
+            touchPoints[0].mContinuedStrokeId = continuedStrokeId;
+            touchPoints[0].mStrokeId = strokeId;
+            touchPoints[0].mIsEndOfPath = ((i == points.length - 1) && !continued);
+            gesture.add(new GestureStep(interval * i, 1, touchPoints));
+        }
+        return gesture;
+    }
+
+    List<GestureStep> combineGestureSteps(List<GestureStep> list1, List<GestureStep> list2) {
+        assertEquals(list1.size(), list2.size());
+        List<GestureStep> gesture = new ArrayList<>(list1.size());
+        for (int i = 0; i < list1.size(); i++) {
+            int numPoints1 = list1.get(i).numTouchPoints;
+            int numPoints2 = list2.get(i).numTouchPoints;
+            TouchPoint[] touchPoints = new TouchPoint[numPoints1 + numPoints2];
+            for (int j = 0; j < numPoints1; j++) {
+                touchPoints[j] = new TouchPoint();
+                touchPoints[j].copyFrom(list1.get(i).touchPoints[j]);
+            }
+            for (int j = 0; j < numPoints2; j++) {
+                touchPoints[numPoints1 + j] = new TouchPoint();
+                touchPoints[numPoints1 + j].copyFrom(list2.get(i).touchPoints[j]);
+            }
+            gesture.add(new GestureStep(list1.get(i).timeSinceGestureStart,
+                    numPoints1 + numPoints2, touchPoints));
+        }
+        return gesture;
+    }
+
     private EventStreamTransformation attachMockNext(MotionEventInjector motionEventInjector) {
         EventStreamTransformation next = mock(EventStreamTransformation.class);
         motionEventInjector.setNext(next);
@@ -506,4 +783,126 @@
             return false;
         }
     }
+
+    private static class MotionEventActionMatcher extends TypeSafeMatcher<MotionEvent> {
+        int mAction;
+
+        MotionEventActionMatcher(int action) {
+            super();
+            mAction = action;
+        }
+
+        @Override
+        protected boolean matchesSafely(MotionEvent motionEvent) {
+            return motionEvent.getActionMasked() == mAction;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("Matching to action " + mAction);
+        }
+    }
+
+    private static TypeSafeMatcher<MotionEvent> isAtPoint(final Point point) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return ((event.getX() == point.x) && (event.getY() == point.y));
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Is at point " + point);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> containsPoints(final Point... points) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+                for (int i = 0; i < points.length; i++) {
+                    boolean havePoint = false;
+                    for (int j = 0; j < points.length; j++) {
+                        event.getPointerCoords(j, coords);
+                        if ((points[i].x == coords.x) && (points[i].y == coords.y)) {
+                            havePoint = true;
+                        }
+                    }
+                    if (!havePoint) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Contains points " + points);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> hasDownTime(final long downTime) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return event.getDownTime() == downTime;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Down time = " + downTime);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> hasEventTime(final long eventTime) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return event.getEventTime() == eventTime;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Event time = " + eventTime);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> hasTimeFromDown(final long timeFromDown) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return (event.getEventTime() - event.getDownTime()) == timeFromDown;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Time from down to event times = " + timeFromDown);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> hasStandardInitialization() {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return (0 == event.getActionIndex()) && (0 == event.getDeviceId())
+                        && (0 == event.getEdgeFlags()) && (0 == event.getFlags())
+                        && (0 == event.getMetaState()) && (0F == event.getOrientation())
+                        && (0F == event.getTouchMajor()) && (0F == event.getTouchMinor())
+                        && (1F == event.getXPrecision()) && (1F == event.getYPrecision())
+                        && (1 == event.getPointerCount()) && (1F == event.getPressure())
+                        && (InputDevice.SOURCE_TOUCHSCREEN == event.getSource());
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Has standard values for all parameters");
+            }
+        };
+    }
 }