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