summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Weaver <pweaver@google.com>2016-07-20 16:45:46 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2016-07-20 16:45:47 +0000
commit140df85b137e620bdc98652a08a81fc03b51cc1c (patch)
tree237dc116646577c99a896d4d1f712695ad1352bb
parent625939907dd475ddaa7d78b7f08c8779081714cd (diff)
parenta7dcedc65fbdae3625f25e1a40756d251a5a586b (diff)
Merge "Add tests for accessibility motion event injector."
-rw-r--r--services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java60
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/KeyEventDispatcherTest.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java548
3 files changed, 587 insertions, 29 deletions
diff --git a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
index 800f0e10cc5e..8042ddb56ead 100644
--- a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
+++ b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
@@ -40,7 +40,7 @@ import java.util.List;
*
* All methods except {@code injectEvents} must be called only from the main thread.
*/
-public class MotionEventInjector implements EventStreamTransformation {
+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;
@@ -64,7 +64,14 @@ public class MotionEventInjector implements EventStreamTransformation {
* @param looper A looper on the main thread to use for dispatching new events
*/
public MotionEventInjector(Looper looper) {
- mHandler = new Handler(looper, new Callback());
+ mHandler = new Handler(looper, this);
+ }
+
+ /**
+ * @param handler A handler to post messages. Exposes internal state for testing only.
+ */
+ public MotionEventInjector(Handler handler) {
+ mHandler = handler;
}
/**
@@ -127,6 +134,29 @@ public class MotionEventInjector implements EventStreamTransformation {
mIsDestroyed = true;
}
+ @Override
+ public boolean handleMessage(Message message) {
+ if (message.what == MESSAGE_INJECT_EVENTS) {
+ SomeArgs args = (SomeArgs) message.obj;
+ injectEventsMainThread((List<MotionEvent>) args.arg1,
+ (IAccessibilityServiceClient) args.arg2, args.argi1);
+ args.recycle();
+ return true;
+ }
+ if (message.what != MESSAGE_SEND_MOTION_EVENT) {
+ Slog.e(LOG_TAG, "Unknown message: " + message.what);
+ 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);
+ }
+ return true;
+ }
+
private void injectEventsMainThread(List<MotionEvent> events,
IAccessibilityServiceClient serviceInterface, int sequence) {
if (mIsDestroyed) {
@@ -201,6 +231,7 @@ public class MotionEventInjector implements EventStreamTransformation {
MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
sendMotionEventToNext(cancelEvent, cancelEvent,
WindowManagerPolicy.FLAG_PASS_TO_USER);
+ mOpenGesturesInProgress.put(source, false);
}
}
@@ -210,7 +241,6 @@ public class MotionEventInjector implements EventStreamTransformation {
mHandler.removeMessages(MESSAGE_SEND_MOTION_EVENT);
notifyService(false);
}
-
}
private void notifyService(boolean success) {
@@ -222,28 +252,4 @@ public class MotionEventInjector implements EventStreamTransformation {
+ mServiceInterfaceForCurrentGesture, re);
}
}
-
- private class Callback implements Handler.Callback {
- @Override
- public boolean handleMessage(Message message) {
- if (message.what == MESSAGE_INJECT_EVENTS) {
- SomeArgs args = (SomeArgs) message.obj;
- injectEventsMainThread((List<MotionEvent>) args.arg1,
- (IAccessibilityServiceClient) args.arg2, args.argi1);
- args.recycle();
- return true;
- }
- if (message.what != MESSAGE_SEND_MOTION_EVENT) {
- throw new IllegalArgumentException("Unknown message: " + message.what);
- }
- 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);
- }
- return true;
- }
- }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/KeyEventDispatcherTest.java b/services/tests/servicestests/src/com/android/server/accessibility/KeyEventDispatcherTest.java
index 6eb76b77a510..0ab91a15e348 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/KeyEventDispatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/KeyEventDispatcherTest.java
@@ -47,6 +47,7 @@ import android.view.KeyEvent;
import android.view.WindowManagerPolicy;
import com.android.server.accessibility.KeyEventDispatcher.KeyEventFilter;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -71,8 +72,11 @@ public class KeyEventDispatcherTest {
private ArgumentCaptor<Integer> mFilter1SequenceCaptor = ArgumentCaptor.forClass(Integer.class);
private ArgumentCaptor<Integer> mFilter2SequenceCaptor = ArgumentCaptor.forClass(Integer.class);
- static {
- Looper.prepare();
+ @BeforeClass
+ public static void oneTimeInitialization() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
}
@Before
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
new file mode 100644
index 000000000000..5920fef1dff5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
@@ -0,0 +1,548 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.accessibilityservice.IAccessibilityServiceClient;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+import android.util.Pair;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.WindowManagerPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.view.accessibility.AccessibilityEvent;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+
+/**
+ * Tests for MotionEventInjector
+ */
+@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 int SECOND_CLICK_X = 1000;
+ private static final int SECOND_CLICK_Y = 2000;
+ private static final int SECOND_SEQUENCE = 51;
+
+ private static final int MOTION_EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
+ private static final int OTHER_EVENT_SOURCE = InputDevice.SOURCE_MOUSE;
+
+ MotionEventInjector mMotionEventInjector;
+ IAccessibilityServiceClient mServiceInterface;
+ List<MotionEvent> mClickList = new ArrayList<>();
+ List<MotionEvent> mSecondClickList = new ArrayList<>();
+ ArgumentCaptor<MotionEvent> mCaptor1 = ArgumentCaptor.forClass(MotionEvent.class);
+ ArgumentCaptor<MotionEvent> mCaptor2 = ArgumentCaptor.forClass(MotionEvent.class);
+ MessageCapturingHandler mMessageCapturingHandler;
+ MotionEventMatcher mClickEvent0Matcher;
+ MotionEventMatcher mClickEvent1Matcher;
+ MotionEventMatcher mClickEvent2Matcher;
+ MotionEventMatcher mSecondClickEvent0Matcher;
+
+ @BeforeClass
+ public static void oneTimeInitialization() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ }
+
+ @Before
+ public void setUp() {
+ mMessageCapturingHandler = new MessageCapturingHandler();
+ 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);
+ }
+
+ @Test
+ public void testInjectEvents_shouldEmergeInOrderWithCorrectTiming() throws RemoteException {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
+ mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+ 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));
+ verifyNoMoreInteractions(next);
+ reset(next);
+
+ mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+ verify(next).onMotionEvent(argThat(mClickEvent1Matcher), argThat(mClickEvent1Matcher),
+ eq(WindowManagerPolicy.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));
+ verifyNoMoreInteractions(next);
+ reset(next);
+
+ verify(mServiceInterface).onPerformGestureResult(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;
+ }
+ 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);
+ 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());
+ }
+
+ @Test
+ public void testRegularEvent_afterGestureComplete_shouldPassToNext() {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
+ mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+ 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));
+ }
+
+ @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
+
+ 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());
+ 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));
+ }
+
+ @Test
+ public void testInjectEvents_withRealMouseGestureUnderway_shouldContinueRealAndPassInjected() {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ MotionEvent mouseEvent = MotionEvent.obtain(mClickList.get(0));
+ mouseEvent.setSource(InputDevice.SOURCE_MOUSE);
+ MotionEventMatcher mouseEventMatcher = new MotionEventMatcher(mouseEvent);
+ mMotionEventInjector.onMotionEvent(mouseEvent, mouseEvent, 0);
+ mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
+ mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+
+ 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)));
+ }
+
+ @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.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)));
+ 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));
+ }
+
+ @Test
+ public void testOnMotionEvents_openInjectedGestureInProgress_shouldCancelAndNotifyAndPassReal()
+ throws RemoteException {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
+ mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+
+ mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+ mMotionEventInjector.onMotionEvent(mSecondClickList.get(0), mSecondClickList.get(0), 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);
+ }
+
+ @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
+
+ // 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);
+
+ 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);
+ assertFalse(mMessageCapturingHandler.hasMessages());
+ }
+
+ @Test
+ public void testInjectEvents_openInjectedGestureInProgress_shouldCancelAndNotifyAndPassNew()
+ throws RemoteException {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
+ mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+ mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+
+ mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SECOND_SEQUENCE);
+ mMessageCapturingHandler.sendLastMessage(); // Process the second event injection
+ mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+
+ verify(mServiceInterface, times(1)).onPerformGestureResult(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)));
+ }
+
+ @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
+
+ // 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
+ mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+
+ verify(mServiceInterface).onPerformGestureResult(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)));
+ }
+
+ @Test
+ public void testClearEvents_realGestureInProgress_shouldForgetAboutGesture() {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+ mMotionEventInjector.clearEvents(MOTION_EVENT_SOURCE);
+ mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
+ mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+ 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)));
+ }
+
+ @Test
+ public void testClearEventsOnOtherSource_realGestureInProgress_shouldNotForgetAboutGesture() {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+ mMotionEventInjector.clearEvents(OTHER_EVENT_SOURCE);
+ mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SECOND_SEQUENCE);
+ mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+ 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)));
+ }
+
+ @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);
+ }
+
+ @Test
+ public void testInjectEvents_withNoNext_shouldCancel() throws RemoteException {
+ mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
+ mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+ verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+ }
+
+ @Test
+ public void testOnMotionEvent_withNoNext_shouldNotCrash() {
+ mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+ }
+
+ @Test
+ public void testOnKeyEvent_shouldPassToNext() {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ KeyEvent event = new KeyEvent(0, 0);
+ mMotionEventInjector.onKeyEvent(event, 0);
+ verify(next).onKeyEvent(event, 0);
+ }
+
+ @Test
+ public void testOnKeyEvent_withNoNext_shouldNotCrash() {
+ KeyEvent event = new KeyEvent(0, 0);
+ mMotionEventInjector.onKeyEvent(event, 0);
+ }
+
+ @Test
+ public void testOnAccessibilityEvent_shouldPassToNext() {
+ EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ mMotionEventInjector.onAccessibilityEvent(event);
+ verify(next).onAccessibilityEvent(event);
+ }
+
+ @Test
+ public void testOnAccessibilityEvent_withNoNext_shouldNotCrash() {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ mMotionEventInjector.onAccessibilityEvent(event);
+ }
+
+ private EventStreamTransformation attachMockNext(MotionEventInjector motionEventInjector) {
+ EventStreamTransformation next = mock(EventStreamTransformation.class);
+ motionEventInjector.setNext(next);
+ return next;
+ }
+
+ static class MotionEventMatcher extends ArgumentMatcher<MotionEvent> {
+ long mDownTime;
+ long mEventTime;
+ long mActionMasked;
+ int mX;
+ int mY;
+
+ MotionEventMatcher(long downTime, long eventTime, int actionMasked, int x, int y) {
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mActionMasked = actionMasked;
+ mX = x;
+ mY = y;
+ }
+
+ MotionEventMatcher(MotionEvent event) {
+ this(event.getDownTime(), event.getEventTime(), event.getActionMasked(),
+ (int) event.getX(), (int) event.getY());
+ }
+
+ void offsetTimesBy(long timeOffset) {
+ mDownTime += timeOffset;
+ mEventTime += timeOffset;
+ }
+
+ @Override
+ public boolean matches(Object o) {
+ MotionEvent event = (MotionEvent) o;
+ if ((event.getDownTime() == mDownTime) && (event.getEventTime() == mEventTime)
+ && (event.getActionMasked() == mActionMasked) && ((int) event.getX() == mX)
+ && ((int) event.getY() == mY)) {
+ return true;
+ }
+ Log.e(LOG_TAG, "MotionEvent match failed");
+ Log.e(LOG_TAG, "event.getDownTime() = " + event.getDownTime()
+ + ", expected " + mDownTime);
+ Log.e(LOG_TAG, "event.getEventTime() = " + event.getEventTime()
+ + ", expected " + mEventTime);
+ Log.e(LOG_TAG, "event.getActionMasked() = " + event.getActionMasked()
+ + ", expected " + mActionMasked);
+ Log.e(LOG_TAG, "event.getX() = " + event.getX() + ", expected " + mX);
+ Log.e(LOG_TAG, "event.getY() = " + event.getY() + ", expected " + mY);
+ return false;
+ }
+ }
+
+ private class MessageCapturingHandler extends Handler {
+ List<Pair<Message, Long>> timedMessages = new ArrayList<>();
+
+ @Override
+ public boolean sendMessageAtTime(Message message, long uptimeMillis) {
+ timedMessages.add(new Pair<>(Message.obtain(message), uptimeMillis));
+ return super.sendMessageAtTime(message, uptimeMillis);
+ }
+
+ void sendOneMessage() {
+ Message message = timedMessages.remove(0).first;
+ removeMessages(message.what, message.obj);
+ mMotionEventInjector.handleMessage(message);
+ removeStaleMessages();
+ }
+
+ void sendAllMessages() {
+ while (!timedMessages.isEmpty()) {
+ sendOneMessage();
+ }
+ }
+
+ void sendLastMessage() {
+ Message message = timedMessages.remove(timedMessages.size() - 1).first;
+ removeMessages(message.what, message.obj);
+ mMotionEventInjector.handleMessage(message);
+ removeStaleMessages();
+ }
+
+ boolean hasMessages() {
+ removeStaleMessages();
+ return !timedMessages.isEmpty();
+ }
+
+ private void removeStaleMessages() {
+ for (int i = 0; i < timedMessages.size(); i++) {
+ Message message = timedMessages.get(i).first;
+ if (!hasMessages(message.what, message.obj)) {
+ timedMessages.remove(i--);
+ }
+ }
+ }
+ }
+}