CTS test for accessibility gesture recognizer.
Change-Id: I5b3efdfde2c703a6b90d066cb8a02e1ea9d9319c
Test: Checked that test passes on nexus6p, pixel, and pixelC tablet.
diff --git a/tests/accessibilityservice/AndroidManifest.xml b/tests/accessibilityservice/AndroidManifest.xml
index a036819..cf6b774 100644
--- a/tests/accessibilityservice/AndroidManifest.xml
+++ b/tests/accessibilityservice/AndroidManifest.xml
@@ -64,7 +64,19 @@
<meta-data
android:name="android.accessibilityservice"
- android:resource="@xml/stub_gesture_a11y_service" />
+ android:resource="@xml/stub_gesture_dispatch_a11y_service" />
+ </service>
+
+ <service
+ android:name=".AccessibilityGestureDetectorTest$StubService"
+ android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+ <intent-filter>
+ <action android:name="android.accessibilityservice.AccessibilityService" />
+ <category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
+ </intent-filter>
+ <meta-data
+ android:name="android.accessibilityservice"
+ android:resource="@xml/stub_gesture_detect_a11y_service" />
</service>
<activity
diff --git a/tests/accessibilityservice/res/values/strings.xml b/tests/accessibilityservice/res/values/strings.xml
index 3a6b855..2e3d5dd 100644
--- a/tests/accessibilityservice/res/values/strings.xml
+++ b/tests/accessibilityservice/res/values/strings.xml
@@ -139,7 +139,9 @@
<string name="foo_bar_baz">Foo bar baz.</string>
- <string name="stub_gesture_a11y_service_description">com.android.accessibilityservice.cts.StubGestureAccessibilityService</string>
+ <string name="stub_gesture_dispatch_a11y_service_description">com.android.accessibilityservice.cts.StubGestureAccessibilityService</string>
+
+ <string name="stub_gesture_detector_a11y_service_description">com.android.accessibilityservice.cts.StubService</string>
<string name="stub_fprint_a11y_service_description">com.android.accessibilityservice.cts.StubFingerprintGestureAccessibilityService</string>
diff --git a/tests/accessibilityservice/res/xml/stub_gesture_detect_a11y_service.xml b/tests/accessibilityservice/res/xml/stub_gesture_detect_a11y_service.xml
new file mode 100644
index 0000000..ca3eab2
--- /dev/null
+++ b/tests/accessibilityservice/res/xml/stub_gesture_detect_a11y_service.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2017 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.
+-->
+
+<accessibility-service
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:description="@string/stub_gesture_detector_a11y_service_description"
+ android:accessibilityEventTypes="typeAllMask"
+ android:accessibilityFeedbackType="feedbackGeneric"
+ android:accessibilityFlags="flagDefault|flagRequestTouchExplorationMode|flagReportViewIds"
+ android:canRequestTouchExplorationMode="true"
+ android:canRetrieveWindowContent="true"
+ android:canPerformGestures="true" />
diff --git a/tests/accessibilityservice/res/xml/stub_gesture_a11y_service.xml b/tests/accessibilityservice/res/xml/stub_gesture_dispatch_a11y_service.xml
similarity index 96%
rename from tests/accessibilityservice/res/xml/stub_gesture_a11y_service.xml
rename to tests/accessibilityservice/res/xml/stub_gesture_dispatch_a11y_service.xml
index 25a55fc..912c9c4 100644
--- a/tests/accessibilityservice/res/xml/stub_gesture_a11y_service.xml
+++ b/tests/accessibilityservice/res/xml/stub_gesture_dispatch_a11y_service.xml
@@ -16,7 +16,7 @@
-->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
- android:description="@string/stub_gesture_a11y_service_description"
+ android:description="@string/stub_gesture_dispatch_a11y_service_description"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityGestureDetectorTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityGestureDetectorTest.java
new file mode 100644
index 0000000..f1560aa
--- /dev/null
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityGestureDetectorTest.java
@@ -0,0 +1,238 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accessibilityservice.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.accessibilityservice.AccessibilityService;
+import android.accessibilityservice.GestureDescription;
+import android.accessibilityservice.GestureDescription.StrokeDescription;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import java.util.ArrayList;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Verify that motion events are recognized as accessibility gestures.
+ */
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityGestureDetectorTest {
+
+ // Constants
+ static final float GESTURE_LENGTH_INCHES = 1.0f;
+ static final long STROKE_MS = 400;
+ static final long GESTURE_DISPATCH_TIMEOUT_MS = 3000;
+ static final long GESTURE_RECOGNIZE_TIMEOUT_MS = 3000;
+
+ // Member variables
+ StubService mService; // Test AccessibilityService that collects gestures.
+ boolean mHasTouchScreen;
+ boolean mScreenBigEnough;
+ int mStrokeLenPxX; // Gesture stroke size, in pixels
+ int mStrokeLenPxY;
+ Point mCenter; // Center of screen. Gestures all start from this point.
+ @Mock AccessibilityService.GestureResultCallback mGestureDispatchCallback;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ // Check that device has a touch screen.
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ PackageManager pm = instrumentation.getContext().getPackageManager();
+ mHasTouchScreen = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
+ || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH);
+ if (!mHasTouchScreen) {
+ return;
+ }
+
+ // Find screen size, check that it is big enough for gestures.
+ // Gestures will start in the center of the screen, so we need enough horiz/vert space.
+ WindowManager windowManager = (WindowManager) instrumentation.getContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+ final DisplayMetrics metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(metrics);
+ mCenter = new Point((int) metrics.widthPixels / 2, (int) metrics.heightPixels / 2);
+ mStrokeLenPxX = (int)(GESTURE_LENGTH_INCHES * metrics.xdpi);
+ mStrokeLenPxY = (int)(GESTURE_LENGTH_INCHES * metrics.ydpi);
+ mScreenBigEnough = (metrics.widthPixels / (2 * metrics.xdpi) > GESTURE_LENGTH_INCHES)
+ && (metrics.heightPixels / (2 * metrics.ydpi) > GESTURE_LENGTH_INCHES);
+ if (!mScreenBigEnough) {
+ return;
+ }
+
+ // Start stub accessibility service.
+ mService = StubService.enableSelf(instrumentation);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (!mHasTouchScreen || !mScreenBigEnough) {
+ return;
+ }
+ mService.runOnServiceSync(() -> mService.disableSelf());
+ }
+
+ @Test
+ public void testRecognizeGesturePath() {
+ if (!mHasTouchScreen || !mScreenBigEnough) {
+ return;
+ }
+
+ // Compute gesture stroke lengths, in pixels.
+ final int dx = mStrokeLenPxX;
+ final int dy = mStrokeLenPxY;
+
+ // Test recognizing various gestures.
+ testPath(p(-dx, +0), AccessibilityService.GESTURE_SWIPE_LEFT);
+ testPath(p(+dx, +0), AccessibilityService.GESTURE_SWIPE_RIGHT);
+ testPath(p(+0, -dy), AccessibilityService.GESTURE_SWIPE_UP);
+ testPath(p(+0, +dy), AccessibilityService.GESTURE_SWIPE_DOWN);
+
+ testPath(p(-dx, +0), p(+0, +0), AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT);
+ testPath(p(-dx, +0), p(-dx, -dy), AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP);
+ testPath(p(-dx, +0), p(-dx, +dy), AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN);
+
+ testPath(p(+dx, +0), p(+0, +0), AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT);
+ testPath(p(+dx, +0), p(+dx, -dy), AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP);
+ testPath(p(+dx, +0), p(+dx, +dy), AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN);
+
+ testPath(p(+0, -dy), p(-dx, -dy), AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT);
+ testPath(p(+0, -dy), p(+dx, -dy), AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT);
+ testPath(p(+0, -dy), p(+0, +0), AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN);
+
+ testPath(p(+0, +dy), p(-dx, +dy), AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT);
+ testPath(p(+0, +dy), p(+dx, +dy), AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT);
+ testPath(p(+0, +dy), p(+0, +0), AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP);
+ }
+
+ /** Convenient short alias to make a Point. */
+ private static Point p(int x, int y) {
+ return new Point(x, y);
+ }
+
+ /** Test recognizing path from PATH_START to PATH_START+delta. */
+ private void testPath(Point delta, int gestureId) {
+ testPath(delta, null, gestureId);
+ }
+
+ /** Test recognizing path from PATH_START to PATH_START+delta1 to PATH_START+delta2. */
+ private void testPath(Point delta1, Point delta2, int gestureId) {
+ // Create gesture motions.
+ int numPathSegments = (delta2 == null) ? 1 : 2;
+ long pathDurationMs = numPathSegments * STROKE_MS;
+ GestureDescription gesture = new GestureDescription.Builder()
+ .addStroke(new StrokeDescription(
+ linePath(mCenter, delta1, delta2), 0, pathDurationMs, false))
+ .build();
+
+ // Dispatch gesture motions.
+ // Use AccessibilityService.dispatchGesture() instead of Instrumentation.sendPointerSync()
+ // because accessibility services read gesture events upstream from the point where
+ // sendPointerSync() injects events.
+ mService.clearGestures();
+ mService.runOnServiceSync(() ->
+ mService.dispatchGesture(gesture, mGestureDispatchCallback, null));
+ verify(mGestureDispatchCallback,
+ timeout(GESTURE_DISPATCH_TIMEOUT_MS).atLeastOnce())
+ .onCompleted(any());
+
+ // Wait for gesture recognizer, and check recognized gesture.
+ mService.waitUntilGesture();
+ assertEquals(1, mService.getGesturesSize());
+ assertEquals(gestureId, mService.getGesture(0));
+ }
+
+ /** Create a path from startPoint, moving by delta1, then delta2. (delta2 may be null.) */
+ Path linePath(Point startPoint, Point delta1, Point delta2) {
+ Path path = new Path();
+ path.moveTo(startPoint.x, startPoint.y);
+ path.lineTo(startPoint.x + delta1.x, startPoint.y + delta1.y);
+ if (delta2 != null) {
+ path.lineTo(startPoint.x + delta2.x, startPoint.y + delta2.y);
+ }
+ return path;
+ }
+
+ /** Acessibility service stub, which will collect recognized gestures. */
+ public static class StubService extends InstrumentedAccessibilityService {
+
+ private ArrayList<Integer> mCollectedGestures = new ArrayList();
+
+ public static StubService enableSelf(Instrumentation instrumentation) {
+ return InstrumentedAccessibilityService.enableService(
+ instrumentation, StubService.class);
+ }
+
+ @Override
+ protected boolean onGesture(int gestureId) {
+ synchronized (mCollectedGestures) {
+ mCollectedGestures.add(gestureId);
+ mCollectedGestures.notifyAll(); // Stop waiting for gesture.
+ }
+ return true;
+ }
+
+ public void clearGestures() {
+ synchronized (mCollectedGestures) {
+ mCollectedGestures.clear();
+ }
+ }
+
+ public int getGesturesSize() {
+ synchronized (mCollectedGestures) {
+ return mCollectedGestures.size();
+ }
+ }
+
+ public int getGesture(int index) {
+ synchronized (mCollectedGestures) {
+ return mCollectedGestures.get(index);
+ }
+ }
+
+ /** Wait for onGesture() to collect next gesture. */
+ public void waitUntilGesture() {
+ synchronized (mCollectedGestures) {
+ if (mCollectedGestures.size() > 0) {
+ return;
+ }
+ try {
+ mCollectedGestures.wait(GESTURE_RECOGNIZE_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+}
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/InstrumentedAccessibilityService.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/InstrumentedAccessibilityService.java
index 324cf33..2b6d2dd 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/InstrumentedAccessibilityService.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/InstrumentedAccessibilityService.java
@@ -7,6 +7,7 @@
import android.os.Handler;
import android.os.SystemClock;
import android.provider.Settings;
+import android.support.annotation.CallSuper;
import android.test.InstrumentationTestCase;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
@@ -36,6 +37,7 @@
@Override
+ @CallSuper
protected void onServiceConnected() {
synchronized (sInstances) {
sInstances.put(getClass(), new WeakReference<>(this));