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");
+ }
+ };
+ }
}