Home | History | Annotate | Download | only in cts
      1 /**
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
      5  * in compliance with the License. You may obtain a copy of the License at
      6  *
      7  * http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the
     10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
     11  * express or implied. See the License for the specific language governing permissions and
     12  * limitations under the License.
     13  */
     14 
     15 package android.accessibilityservice.cts;
     16 
     17 import static android.accessibilityservice.cts.utils.AsyncUtils.await;
     18 import static android.accessibilityservice.cts.utils.AsyncUtils.awaitCancellation;
     19 import static android.accessibilityservice.cts.utils.GestureUtils.add;
     20 import static android.accessibilityservice.cts.utils.GestureUtils.ceil;
     21 import static android.accessibilityservice.cts.utils.GestureUtils.click;
     22 import static android.accessibilityservice.cts.utils.GestureUtils.diff;
     23 import static android.accessibilityservice.cts.utils.GestureUtils.dispatchGesture;
     24 import static android.accessibilityservice.cts.utils.GestureUtils.longClick;
     25 import static android.accessibilityservice.cts.utils.GestureUtils.path;
     26 import static android.accessibilityservice.cts.utils.GestureUtils.times;
     27 
     28 import static org.hamcrest.CoreMatchers.allOf;
     29 import static org.hamcrest.CoreMatchers.any;
     30 import static org.hamcrest.CoreMatchers.both;
     31 import static org.hamcrest.CoreMatchers.everyItem;
     32 import static org.hamcrest.CoreMatchers.hasItem;
     33 import static org.hamcrest.MatcherAssert.assertThat;
     34 
     35 import static java.util.concurrent.TimeUnit.MILLISECONDS;
     36 
     37 import android.accessibilityservice.AccessibilityService;
     38 import android.accessibilityservice.GestureDescription;
     39 import android.accessibilityservice.GestureDescription.StrokeDescription;
     40 import android.accessibilityservice.cts.activities.AccessibilityTestActivity;
     41 import android.content.Context;
     42 import android.content.pm.PackageManager;
     43 import android.graphics.Matrix;
     44 import android.graphics.Path;
     45 import android.graphics.PointF;
     46 import android.os.Bundle;
     47 import android.os.SystemClock;
     48 import android.platform.test.annotations.AppModeFull;
     49 import android.test.ActivityInstrumentationTestCase2;
     50 import android.util.Log;
     51 import android.view.Display;
     52 import android.view.MotionEvent;
     53 import android.view.View;
     54 import android.view.ViewConfiguration;
     55 import android.view.WindowManager;
     56 import android.widget.TextView;
     57 
     58 import org.hamcrest.Description;
     59 import org.hamcrest.Matcher;
     60 import org.hamcrest.TypeSafeMatcher;
     61 
     62 import java.util.ArrayList;
     63 import java.util.List;
     64 import java.util.concurrent.atomic.AtomicBoolean;
     65 
     66 /**
     67  * Verify that gestures dispatched from an accessibility service show up in the current UI
     68  */
     69 @AppModeFull
     70 public class AccessibilityGestureDispatchTest extends
     71         ActivityInstrumentationTestCase2<AccessibilityGestureDispatchTest.GestureDispatchActivity> {
     72     private static final String TAG = AccessibilityGestureDispatchTest.class.getSimpleName();
     73 
     74     private static final int GESTURE_COMPLETION_TIMEOUT = 5000; // millis
     75     private static final int MOTION_EVENT_TIMEOUT = 1000; // millis
     76 
     77     private static final Matcher<MotionEvent> IS_ACTION_DOWN =
     78             new MotionEventActionMatcher(MotionEvent.ACTION_DOWN);
     79     private static final Matcher<MotionEvent> IS_ACTION_POINTER_DOWN =
     80             new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_DOWN);
     81     private static final Matcher<MotionEvent> IS_ACTION_UP =
     82             new MotionEventActionMatcher(MotionEvent.ACTION_UP);
     83     private static final Matcher<MotionEvent> IS_ACTION_POINTER_UP =
     84             new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_UP);
     85     private static final Matcher<MotionEvent> IS_ACTION_CANCEL =
     86             new MotionEventActionMatcher(MotionEvent.ACTION_CANCEL);
     87     private static final Matcher<MotionEvent> IS_ACTION_MOVE =
     88             new MotionEventActionMatcher(MotionEvent.ACTION_MOVE);
     89 
     90 
     91     final List<MotionEvent> mMotionEvents = new ArrayList<>();
     92     StubGestureAccessibilityService mService;
     93     MyTouchListener mMyTouchListener = new MyTouchListener();
     94     TextView mFullScreenTextView;
     95     int[] mViewLocation = new int[2];
     96     boolean mGotUpEvent;
     97     // Without a touch screen, there's no point in testing this feature
     98     boolean mHasTouchScreen;
     99     boolean mHasMultiTouch;
    100 
    101     public AccessibilityGestureDispatchTest() {
    102         super(GestureDispatchActivity.class);
    103     }
    104 
    105     @Override
    106     public void setUp() throws Exception {
    107         super.setUp();
    108         PackageManager pm = getInstrumentation().getContext().getPackageManager();
    109         mHasTouchScreen = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
    110                 || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH);
    111         if (!mHasTouchScreen) {
    112             return;
    113         }
    114 
    115         mHasMultiTouch = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
    116                 || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT);
    117 
    118         mFullScreenTextView =
    119                 (TextView) getActivity().findViewById(R.id.full_screen_text_view);
    120         getInstrumentation().runOnMainSync(() -> {
    121             mFullScreenTextView.getLocationOnScreen(mViewLocation);
    122             mFullScreenTextView.setOnTouchListener(mMyTouchListener);
    123         });
    124 
    125         mService = StubGestureAccessibilityService.enableSelf(getInstrumentation());
    126 
    127         mMotionEvents.clear();
    128         mGotUpEvent = false;
    129     }
    130 
    131     @Override
    132     public void tearDown() throws Exception {
    133         if (!mHasTouchScreen) {
    134             return;
    135         }
    136 
    137         mService.runOnServiceSync(() -> mService.disableSelf());
    138         super.tearDown();
    139     }
    140 
    141     public void testClickAt_producesDownThenUp() throws InterruptedException {
    142         if (!mHasTouchScreen) {
    143             return;
    144         }
    145 
    146         PointF clickPoint = new PointF(10, 20);
    147         dispatch(clickWithinView(clickPoint), GESTURE_COMPLETION_TIMEOUT);
    148         waitForMotionEvents(any(MotionEvent.class), 2);
    149 
    150         assertEquals(2, mMotionEvents.size());
    151         MotionEvent clickDown = mMotionEvents.get(0);
    152         MotionEvent clickUp = mMotionEvents.get(1);
    153         assertThat(clickDown, both(IS_ACTION_DOWN).and(isAtPoint(clickPoint)));
    154         assertThat(clickUp, both(IS_ACTION_UP).and(isAtPoint(clickPoint)));
    155 
    156         // Verify other MotionEvent fields in this test to make sure they get initialized.
    157         assertEquals(0, clickDown.getActionIndex());
    158         assertEquals(0, clickDown.getDeviceId());
    159         assertEquals(0, clickDown.getEdgeFlags());
    160         assertEquals(1F, clickDown.getXPrecision());
    161         assertEquals(1F, clickDown.getYPrecision());
    162         assertEquals(1, clickDown.getPointerCount());
    163         assertEquals(1F, clickDown.getPressure());
    164 
    165         // Verify timing matches click
    166         assertEquals(clickDown.getDownTime(), clickDown.getEventTime());
    167         assertEquals(clickDown.getDownTime(), clickUp.getDownTime());
    168         assertEquals(ViewConfiguration.getTapTimeout(),
    169                 clickUp.getEventTime() - clickUp.getDownTime());
    170         assertTrue(clickDown.getEventTime() + ViewConfiguration.getLongPressTimeout()
    171                 > clickUp.getEventTime());
    172     }
    173 
    174     public void testLongClickAt_producesEventsWithLongClickTiming() throws InterruptedException {
    175         if (!mHasTouchScreen) {
    176             return;
    177         }
    178 
    179         PointF clickPoint = new PointF(10, 20);
    180         dispatch(longClickWithinView(clickPoint),
    181                 ViewConfiguration.getLongPressTimeout() + GESTURE_COMPLETION_TIMEOUT);
    182 
    183         waitForMotionEvents(any(MotionEvent.class), 2);
    184         MotionEvent clickDown = mMotionEvents.get(0);
    185         MotionEvent clickUp = mMotionEvents.get(1);
    186         assertThat(clickDown, both(IS_ACTION_DOWN).and(isAtPoint(clickPoint)));
    187         assertThat(clickUp, both(IS_ACTION_UP).and(isAtPoint(clickPoint)));
    188 
    189         assertTrue(clickDown.getEventTime() + ViewConfiguration.getLongPressTimeout()
    190                 <= clickUp.getEventTime());
    191         assertEquals(clickDown.getDownTime(), clickUp.getDownTime());
    192     }
    193 
    194     public void testSwipe_shouldContainPointsInALine() throws InterruptedException {
    195         if (!mHasTouchScreen) {
    196             return;
    197         }
    198 
    199         PointF startPoint = new PointF(10, 20);
    200         PointF endPoint = new PointF(20, 40);
    201         int gestureTime = 500;
    202 
    203         dispatch(swipeWithinView(startPoint, endPoint, gestureTime),
    204                 gestureTime + GESTURE_COMPLETION_TIMEOUT);
    205         waitForMotionEvents(IS_ACTION_UP, 1);
    206 
    207         int numEvents = mMotionEvents.size();
    208 
    209         MotionEvent downEvent = mMotionEvents.get(0);
    210         MotionEvent upEvent = mMotionEvents.get(numEvents - 1);
    211         assertThat(downEvent, both(IS_ACTION_DOWN).and(isAtPoint(startPoint)));
    212         assertThat(upEvent, both(IS_ACTION_UP).and(isAtPoint(endPoint)));
    213         assertEquals(gestureTime, upEvent.getEventTime() - downEvent.getEventTime());
    214 
    215         long lastEventTime = downEvent.getEventTime();
    216         for (int i = 1; i < numEvents - 1; i++) {
    217             MotionEvent moveEvent = mMotionEvents.get(i);
    218             assertTrue(moveEvent.getEventTime() >= lastEventTime);
    219             float fractionOfSwipe =
    220                     ((float) (moveEvent.getEventTime() - downEvent.getEventTime())) / gestureTime;
    221             PointF intermediatePoint = add(startPoint,
    222                     ceil(times(fractionOfSwipe, diff(endPoint, startPoint))));
    223             assertThat(moveEvent, both(IS_ACTION_MOVE).and(isAtPoint(intermediatePoint)));
    224             lastEventTime = moveEvent.getEventTime();
    225         }
    226     }
    227 
    228     public void dispatch(GestureDescription gesture, int timeoutMs) {
    229         await(dispatchGesture(mService, gesture), timeoutMs, MILLISECONDS);
    230     }
    231 
    232     public void testSlowSwipe_shouldNotContainMovesForTinyMovement() throws InterruptedException {
    233         if (!mHasTouchScreen) {
    234             return;
    235         }
    236 
    237         PointF startPoint = new PointF(10, 20);
    238         PointF intermediatePoint1 = new PointF(10, 21);
    239         PointF intermediatePoint2 = new PointF(11, 21);
    240         PointF intermediatePoint3 = new PointF(11, 22);
    241         PointF endPoint = new PointF(11, 22);
    242         int gestureTime = 1000;
    243 
    244         dispatch(swipeWithinView(startPoint, endPoint, gestureTime),
    245                 gestureTime + GESTURE_COMPLETION_TIMEOUT);
    246         waitForMotionEvents(IS_ACTION_UP, 1);
    247 
    248         assertEquals(5, mMotionEvents.size());
    249         assertThat(mMotionEvents.get(0), both(IS_ACTION_DOWN).and(isAtPoint(startPoint)));
    250         assertThat(mMotionEvents.get(1), both(IS_ACTION_MOVE).and(isAtPoint(intermediatePoint1)));
    251         assertThat(mMotionEvents.get(2), both(IS_ACTION_MOVE).and(isAtPoint(intermediatePoint2)));
    252         assertThat(mMotionEvents.get(3), both(IS_ACTION_MOVE).and(isAtPoint(intermediatePoint3)));
    253         assertThat(mMotionEvents.get(4), both(IS_ACTION_UP).and(isAtPoint(endPoint)));
    254     }
    255 
    256     public void testAngledPinch_looksReasonable() throws InterruptedException {
    257         if (!(mHasTouchScreen && mHasMultiTouch)) {
    258             return;
    259         }
    260 
    261         PointF centerPoint = new PointF(50, 60);
    262         int startSpacing = 100;
    263         int endSpacing = 50;
    264         int gestureTime = 500;
    265         float pinchTolerance = 2.0f;
    266 
    267         dispatch(pinchWithinView(centerPoint, startSpacing, endSpacing, 45.0F, gestureTime),
    268                 gestureTime + GESTURE_COMPLETION_TIMEOUT);
    269         waitForMotionEvents(IS_ACTION_UP, 1);
    270         int numEvents = mMotionEvents.size();
    271 
    272         // First and last two events are the pointers going down and up
    273         assertThat(mMotionEvents.get(0), IS_ACTION_DOWN);
    274         assertThat(mMotionEvents.get(1), IS_ACTION_POINTER_DOWN);
    275         assertThat(mMotionEvents.get(numEvents - 2), IS_ACTION_POINTER_UP);
    276         assertThat(mMotionEvents.get(numEvents - 1), IS_ACTION_UP);
    277         // The rest of the events are all moves
    278         assertEquals(numEvents - 4, getEventsMatching(IS_ACTION_MOVE).size());
    279 
    280         // All but the first and last events have two pointers
    281         float lastSpacing = startSpacing;
    282         for (int i = 1; i < numEvents - 1; i++) {
    283             MotionEvent.PointerCoords coords0 = new MotionEvent.PointerCoords();
    284             MotionEvent.PointerCoords coords1 = new MotionEvent.PointerCoords();
    285             MotionEvent event = mMotionEvents.get(i);
    286             event.getPointerCoords(0, coords0);
    287             event.getPointerCoords(1, coords1);
    288             // Verify center point
    289             assertEquals((float) centerPoint.x, (coords0.x + coords1.x) / 2, pinchTolerance);
    290             assertEquals((float) centerPoint.y, (coords0.y + coords1.y) / 2, pinchTolerance);
    291             // Verify angle
    292             assertEquals(coords0.x - centerPoint.x, coords0.y - centerPoint.y,
    293                     pinchTolerance);
    294             assertEquals(coords1.x - centerPoint.x, coords1.y - centerPoint.y,
    295                     pinchTolerance);
    296             float spacing = distance(coords0, coords1);
    297             assertTrue(spacing <= lastSpacing + pinchTolerance);
    298             assertTrue(spacing >= endSpacing - pinchTolerance);
    299             lastSpacing = spacing;
    300         }
    301     }
    302 
    303     // This test assumes device's screen contains its center (W/2, H/2) with some surroundings
    304     // and should work for rectangular, round and round with chin screens.
    305     public void testClickWhenMagnified_matchesActualTouch() throws InterruptedException {
    306         final float POINT_TOL = 2.0f;
    307         final float CLICK_OFFSET_X = 10;
    308         final float CLICK_OFFSET_Y = 20;
    309         final float MAGNIFICATION_FACTOR = 2;
    310         if (!mHasTouchScreen) {
    311             return;
    312         }
    313 
    314         int displayId = getActivity().getWindow().getDecorView().getDisplay().getDisplayId();
    315         if (displayId != Display.DEFAULT_DISPLAY) {
    316             Log.i(TAG, "Magnification is not supported on virtual displays.");
    317             return;
    318         }
    319 
    320         final WindowManager wm = (WindowManager) getInstrumentation().getContext().getSystemService(
    321                 Context.WINDOW_SERVICE);
    322         final StubMagnificationAccessibilityService magnificationService =
    323                 StubMagnificationAccessibilityService.enableSelf(getInstrumentation());
    324         final AccessibilityService.MagnificationController
    325                 magnificationController = magnificationService.getMagnificationController();
    326 
    327         final PointF magRegionCenterPoint = new PointF();
    328         magnificationService.runOnServiceSync(() -> {
    329             magnificationController.reset(false);
    330             magRegionCenterPoint.set(magnificationController.getCenterX(),
    331                     magnificationController.getCenterY());
    332         });
    333         final PointF magRegionOffsetPoint
    334                 = add(magRegionCenterPoint, CLICK_OFFSET_X, CLICK_OFFSET_Y);
    335 
    336         final PointF magRegionOffsetClickPoint = add(magRegionCenterPoint,
    337                 CLICK_OFFSET_X * MAGNIFICATION_FACTOR, CLICK_OFFSET_Y * MAGNIFICATION_FACTOR);
    338 
    339         try {
    340             // Zoom in
    341             final AtomicBoolean setScale = new AtomicBoolean();
    342             magnificationService.runOnServiceSync(() -> {
    343                 setScale.set(magnificationController.setScale(MAGNIFICATION_FACTOR, false));
    344             });
    345             assertTrue("Failed to set scale", setScale.get());
    346 
    347             // Click in the center of the magnification region
    348             dispatch(new GestureDescription.Builder()
    349                     .addStroke(click(magRegionCenterPoint))
    350                     .build(),
    351                     GESTURE_COMPLETION_TIMEOUT);
    352 
    353             // Click at a slightly offset point
    354             dispatch(new GestureDescription.Builder()
    355                     .addStroke(click(magRegionOffsetClickPoint))
    356                     .build(),
    357                     GESTURE_COMPLETION_TIMEOUT);
    358             waitForMotionEvents(any(MotionEvent.class), 4);
    359         } finally {
    360             // Reset magnification
    361             final AtomicBoolean result = new AtomicBoolean();
    362             magnificationService.runOnServiceSync(() ->
    363                     result.set(magnificationController.reset(false)));
    364             magnificationService.runOnServiceSync(() -> magnificationService.disableSelf());
    365             assertTrue("Failed to reset", result.get());
    366         }
    367 
    368         assertEquals(4, mMotionEvents.size());
    369         // Because the MotionEvents have been captures by the view, the coordinates will
    370         // be in the View's coordinate system.
    371         magRegionCenterPoint.offset(-mViewLocation[0], -mViewLocation[1]);
    372         magRegionOffsetPoint.offset(-mViewLocation[0], -mViewLocation[1]);
    373 
    374         // The first click should be at the magnification center, as that point is invariant
    375         // for zoom only
    376         assertThat(mMotionEvents.get(0),
    377                 both(IS_ACTION_DOWN).and(isAtPoint(magRegionCenterPoint, POINT_TOL)));
    378         assertThat(mMotionEvents.get(1),
    379                 both(IS_ACTION_UP).and(isAtPoint(magRegionCenterPoint, POINT_TOL)));
    380 
    381         // The second point should be at the offset point
    382         assertThat(mMotionEvents.get(2),
    383                 both(IS_ACTION_DOWN).and(isAtPoint(magRegionOffsetPoint, POINT_TOL)));
    384         assertThat(mMotionEvents.get(3),
    385                 both(IS_ACTION_UP).and(isAtPoint(magRegionOffsetPoint, POINT_TOL)));
    386     }
    387 
    388     public void testContinuedGestures_motionEventsContinue() throws Exception {
    389         if (!mHasTouchScreen) {
    390             return;
    391         }
    392 
    393         PointF start = new PointF(10, 20);
    394         PointF mid1 = new PointF(20, 20);
    395         PointF mid2 = new PointF(20, 25);
    396         PointF end = new PointF(20, 30);
    397         int gestureTime = 500;
    398 
    399         StrokeDescription s1 = new StrokeDescription(
    400                 lineWithinView(start, mid1), 0, gestureTime, true);
    401         StrokeDescription s2 = s1.continueStroke(
    402                 lineWithinView(mid1, mid2), 0, gestureTime, true);
    403         StrokeDescription s3 = s2.continueStroke(
    404                 lineWithinView(mid2, end), 0, gestureTime, false);
    405 
    406         GestureDescription gesture1 = new GestureDescription.Builder().addStroke(s1).build();
    407         GestureDescription gesture2 = new GestureDescription.Builder().addStroke(s2).build();
    408         GestureDescription gesture3 = new GestureDescription.Builder().addStroke(s3).build();
    409         dispatch(gesture1, gestureTime + GESTURE_COMPLETION_TIMEOUT);
    410         dispatch(gesture2, gestureTime + GESTURE_COMPLETION_TIMEOUT);
    411         dispatch(gesture3, gestureTime + GESTURE_COMPLETION_TIMEOUT);
    412         waitForMotionEvents(IS_ACTION_UP, 1);
    413 
    414         assertThat(mMotionEvents.get(0), allOf(IS_ACTION_DOWN, isAtPoint(start)));
    415         assertThat(mMotionEvents.subList(1, mMotionEvents.size() - 1), everyItem(IS_ACTION_MOVE));
    416         assertThat(mMotionEvents, hasItem(isAtPoint(mid1)));
    417         assertThat(mMotionEvents, hasItem(isAtPoint(mid2)));
    418         assertThat(mMotionEvents.get(mMotionEvents.size() - 1),
    419                 allOf(IS_ACTION_UP, isAtPoint(end)));
    420     }
    421 
    422     public void testContinuedGesture_withLineDisconnect_isCancelled() throws Exception {
    423         if (!mHasTouchScreen) {
    424             return;
    425         }
    426 
    427         PointF startPoint = new PointF(10, 20);
    428         PointF midPoint = new PointF(20, 20);
    429         PointF endPoint = new PointF(20, 30);
    430         int gestureTime = 500;
    431 
    432         StrokeDescription stroke1 =
    433                 new StrokeDescription(lineWithinView(startPoint, midPoint), 0, gestureTime, true);
    434         dispatch(new GestureDescription.Builder().addStroke(stroke1).build(),
    435                 gestureTime + GESTURE_COMPLETION_TIMEOUT);
    436         waitForMotionEvents(both(IS_ACTION_MOVE).and(isAtPoint(midPoint)), 1);
    437 
    438         StrokeDescription stroke2 =
    439                 stroke1.continueStroke(lineWithinView(endPoint, midPoint), 0, gestureTime, false);
    440         mMotionEvents.clear();
    441         awaitCancellation(
    442                 dispatchGesture(mService,
    443                         new GestureDescription.Builder().addStroke(stroke2).build()),
    444                 gestureTime + GESTURE_COMPLETION_TIMEOUT, MILLISECONDS);
    445 
    446         waitForMotionEvents(IS_ACTION_CANCEL, 1);
    447         assertEquals(1, mMotionEvents.size());
    448     }
    449 
    450     public void testContinuedGesture_nextGestureDoesntContinue_isCancelled() throws Exception {
    451         if (!mHasTouchScreen) {
    452             return;
    453         }
    454 
    455         PointF startPoint = new PointF(10, 20);
    456         PointF midPoint = new PointF(20, 20);
    457         PointF endPoint = new PointF(20, 30);
    458         int gestureTime = 500;
    459 
    460         StrokeDescription stroke1 =
    461                 new StrokeDescription(lineWithinView(startPoint, midPoint), 0, gestureTime, true);
    462         dispatch(new GestureDescription.Builder().addStroke(stroke1).build(),
    463                 gestureTime + GESTURE_COMPLETION_TIMEOUT);
    464 
    465         StrokeDescription stroke2 =
    466                 new StrokeDescription(lineWithinView(midPoint, endPoint), 0, gestureTime, false);
    467         dispatch(new GestureDescription.Builder().addStroke(stroke2).build(),
    468                 gestureTime + GESTURE_COMPLETION_TIMEOUT);
    469 
    470         waitForMotionEvents(IS_ACTION_UP, 1);
    471 
    472         List<MotionEvent> cancelEvent = getEventsMatching(IS_ACTION_CANCEL);
    473         assertEquals(1, cancelEvent.size());
    474         // Confirm that a down follows the cancel
    475         assertThat(mMotionEvents.get(mMotionEvents.indexOf(cancelEvent.get(0)) + 1),
    476                 both(IS_ACTION_DOWN).and(isAtPoint(midPoint)));
    477         // Confirm that the last point is an up
    478         assertThat(mMotionEvents.get(mMotionEvents.size() - 1),
    479                 both(IS_ACTION_UP).and(isAtPoint(endPoint)));
    480     }
    481 
    482     public void testContinuingGesture_withNothingToContinue_isCancelled() {
    483         if (!mHasTouchScreen) {
    484             return;
    485         }
    486 
    487         PointF startPoint = new PointF(10, 20);
    488         PointF midPoint = new PointF(20, 20);
    489         PointF endPoint = new PointF(20, 30);
    490         int gestureTime = 500;
    491 
    492         StrokeDescription stroke1 =
    493                 new StrokeDescription(lineWithinView(startPoint, midPoint), 0, gestureTime, true);
    494 
    495         StrokeDescription stroke2 =
    496                 stroke1.continueStroke(lineWithinView(midPoint, endPoint), 0, gestureTime, false);
    497         awaitCancellation(
    498                 dispatchGesture(mService,
    499                         new GestureDescription.Builder().addStroke(stroke2).build()),
    500                 gestureTime + GESTURE_COMPLETION_TIMEOUT, MILLISECONDS);
    501     }
    502 
    503     public static class GestureDispatchActivity extends AccessibilityTestActivity {
    504         public GestureDispatchActivity() {
    505             super();
    506         }
    507 
    508         @Override
    509         public void onCreate(Bundle savedInstanceState) {
    510             super.onCreate(savedInstanceState);
    511             setContentView(R.layout.full_screen_frame_layout);
    512         }
    513     }
    514 
    515     private void waitForMotionEvents(Matcher<MotionEvent> matcher, int numEventsExpected)
    516             throws InterruptedException {
    517         synchronized (mMotionEvents) {
    518             long endMillis = SystemClock.uptimeMillis() + MOTION_EVENT_TIMEOUT;
    519             boolean gotEvents = getEventsMatching(matcher).size() >= numEventsExpected;
    520             while (!gotEvents && (SystemClock.uptimeMillis() < endMillis)) {
    521                 mMotionEvents.wait(endMillis - SystemClock.uptimeMillis());
    522                 gotEvents = getEventsMatching(matcher).size() >= numEventsExpected;
    523             }
    524             assertTrue("Did not receive required events. Got:\n" + mMotionEvents + "\n filtered:\n"
    525                     + getEventsMatching(matcher), gotEvents);
    526         }
    527     }
    528 
    529     private List<MotionEvent> getEventsMatching(Matcher<MotionEvent> matcher) {
    530         List<MotionEvent> events = new ArrayList<>();
    531         synchronized (mMotionEvents) {
    532             for (MotionEvent event : mMotionEvents) {
    533                 if (matcher.matches(event)) {
    534                     events.add(event);
    535                 }
    536             }
    537         }
    538         return events;
    539     }
    540 
    541     private float distance(MotionEvent.PointerCoords point1, MotionEvent.PointerCoords point2) {
    542         return (float) Math.hypot((double) (point1.x - point2.x), (double) (point1.y - point2.y));
    543     }
    544 
    545     private class MyTouchListener implements View.OnTouchListener {
    546         @Override
    547         public boolean onTouch(View view, MotionEvent motionEvent) {
    548             synchronized (mMotionEvents) {
    549                 if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
    550                     mGotUpEvent = true;
    551                 }
    552                 mMotionEvents.add(MotionEvent.obtain(motionEvent));
    553                 mMotionEvents.notifyAll();
    554                 return true;
    555             }
    556         }
    557     }
    558 
    559     private GestureDescription clickWithinView(PointF clickPoint) {
    560         return new GestureDescription.Builder()
    561                 .addStroke(click(withinView(clickPoint)))
    562                 .build();
    563     }
    564 
    565     private GestureDescription longClickWithinView(PointF clickPoint) {
    566         return new GestureDescription.Builder()
    567                 .addStroke(longClick(withinView(clickPoint)))
    568                 .build();
    569     }
    570 
    571     private PointF withinView(PointF clickPoint) {
    572         return add(clickPoint, mViewLocation[0], mViewLocation[1]);
    573     }
    574 
    575     private GestureDescription swipeWithinView(PointF start, PointF end, long duration) {
    576         return new GestureDescription.Builder()
    577                 .addStroke(new StrokeDescription(lineWithinView(start, end), 0, duration))
    578                 .build();
    579     }
    580 
    581     private Path lineWithinView(PointF startPoint, PointF endPoint) {
    582         return path(withinView(startPoint), withinView(endPoint));
    583     }
    584 
    585     private GestureDescription pinchWithinView(PointF centerPoint, int startSpacing,
    586             int endSpacing, float orientation, long duration) {
    587         if ((startSpacing < 0) || (endSpacing < 0)) {
    588             throw new IllegalArgumentException("Pinch spacing cannot be negative");
    589         }
    590         PointF offsetCenter = withinView(centerPoint);
    591         float[] startPoint1 = new float[2];
    592         float[] endPoint1 = new float[2];
    593         float[] startPoint2 = new float[2];
    594         float[] endPoint2 = new float[2];
    595 
    596         /* Build points for a horizontal gesture centered at the origin */
    597         startPoint1[0] = startSpacing / 2;
    598         startPoint1[1] = 0;
    599         endPoint1[0] = endSpacing / 2;
    600         endPoint1[1] = 0;
    601         startPoint2[0] = -startSpacing / 2;
    602         startPoint2[1] = 0;
    603         endPoint2[0] = -endSpacing / 2;
    604         endPoint2[1] = 0;
    605 
    606         /* Rotate and translate the points */
    607         Matrix matrix = new Matrix();
    608         matrix.setRotate(orientation);
    609         matrix.postTranslate(offsetCenter.x, offsetCenter.y);
    610         matrix.mapPoints(startPoint1);
    611         matrix.mapPoints(endPoint1);
    612         matrix.mapPoints(startPoint2);
    613         matrix.mapPoints(endPoint2);
    614 
    615         Path path1 = new Path();
    616         path1.moveTo(startPoint1[0], startPoint1[1]);
    617         path1.lineTo(endPoint1[0], endPoint1[1]);
    618         Path path2 = new Path();
    619         path2.moveTo(startPoint2[0], startPoint2[1]);
    620         path2.lineTo(endPoint2[0], endPoint2[1]);
    621 
    622         return new GestureDescription.Builder()
    623                 .addStroke(new StrokeDescription(path1, 0, duration))
    624                 .addStroke(new StrokeDescription(path2, 0, duration))
    625                 .build();
    626     }
    627 
    628     private static class MotionEventActionMatcher extends TypeSafeMatcher<MotionEvent> {
    629         int mAction;
    630 
    631         MotionEventActionMatcher(int action) {
    632             super();
    633             mAction = action;
    634         }
    635 
    636         @Override
    637         protected boolean matchesSafely(MotionEvent motionEvent) {
    638             return motionEvent.getActionMasked() == mAction;
    639         }
    640 
    641         @Override
    642         public void describeTo(Description description) {
    643             description.appendText("Matching to action " + MotionEvent.actionToString(mAction));
    644         }
    645     }
    646 
    647 
    648     Matcher<MotionEvent> isAtPoint(final PointF point) {
    649         return isAtPoint(point, 0.01f);
    650     }
    651 
    652     Matcher<MotionEvent> isAtPoint(final PointF point, final float tol) {
    653         return new TypeSafeMatcher<MotionEvent>() {
    654             @Override
    655             protected boolean matchesSafely(MotionEvent event) {
    656                 return Math.hypot(event.getX() - point.x, event.getY() - point.y) < tol;
    657             }
    658 
    659             @Override
    660             public void describeTo(Description description) {
    661                 description.appendText("Matching to point " + point);
    662             }
    663         };
    664     }
    665 }
    666