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 android.accessibilityservice.AccessibilityService;
     18 import android.accessibilityservice.GestureDescription;
     19 import android.content.pm.PackageManager;
     20 import android.content.res.Resources;
     21 import android.graphics.Matrix;
     22 import android.graphics.Path;
     23 import android.graphics.Rect;
     24 import android.os.Bundle;
     25 import android.os.SystemClock;
     26 import android.test.ActivityInstrumentationTestCase2;
     27 import android.util.DisplayMetrics;
     28 import android.view.MotionEvent;
     29 import android.view.View;
     30 import android.view.ViewConfiguration;
     31 import android.widget.TextView;
     32 
     33 import java.util.ArrayList;
     34 import java.util.List;
     35 import java.util.concurrent.atomic.AtomicBoolean;
     36 
     37 /**
     38  * Verify that gestures dispatched from an accessibility service show up in the current UI
     39  */
     40 public class AccessibilityGestureDispatchTest extends
     41         ActivityInstrumentationTestCase2<AccessibilityGestureDispatchTest.GestureDispatchActivity> {
     42     private static final int GESTURE_COMPLETION_TIMEOUT = 5000; // millis
     43     private static final int MOTION_EVENT_TIMEOUT = 1000; // millis
     44 
     45     final List<MotionEvent> mMotionEvents = new ArrayList<>();
     46     StubGestureAccessibilityService mService;
     47     MyTouchListener mMyTouchListener = new MyTouchListener();
     48     MyGestureCallback mCallback;
     49     TextView mFullScreenTextView;
     50     Rect mViewBounds = new Rect();
     51     boolean mGotUpEvent;
     52     // Without a touch screen, there's no point in testing this feature
     53     boolean mHasTouchScreen;
     54     boolean mHasMultiTouch;
     55 
     56     public AccessibilityGestureDispatchTest() {
     57         super(GestureDispatchActivity.class);
     58     }
     59 
     60     @Override
     61     public void setUp() throws Exception {
     62         super.setUp();
     63 
     64         PackageManager pm = getInstrumentation().getContext().getPackageManager();
     65         mHasTouchScreen = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
     66                 || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH);
     67         if (!mHasTouchScreen) {
     68             return;
     69         }
     70 
     71         mHasMultiTouch = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
     72                 || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT);
     73 
     74         mFullScreenTextView =
     75                 (TextView) getActivity().findViewById(R.id.full_screen_text_view);
     76         getInstrumentation().runOnMainSync(() -> {
     77             mFullScreenTextView.getGlobalVisibleRect(mViewBounds);
     78             mFullScreenTextView.setOnTouchListener(mMyTouchListener);
     79         });
     80 
     81         mService = StubGestureAccessibilityService.enableSelf(this);
     82 
     83         mMotionEvents.clear();
     84         mCallback = new MyGestureCallback();
     85         mGotUpEvent = false;
     86     }
     87 
     88     @Override
     89     public void tearDown() throws Exception {
     90         if (!mHasTouchScreen) {
     91             return;
     92         }
     93 
     94         mService.runOnServiceSync(() -> mService.disableSelf());
     95         super.tearDown();
     96     }
     97 
     98     public void testClickAt_producesDownThenUp() throws InterruptedException {
     99         if (!mHasTouchScreen) {
    100             return;
    101         }
    102 
    103         final int clickXInsideView = 10;
    104         final int clickYInsideView = 20;
    105         int clickX = clickXInsideView + mViewBounds.left;
    106         int clickY = clickYInsideView + mViewBounds.top;
    107         GestureDescription click = createClick(clickX, clickY);
    108         mService.runOnServiceSync(() -> mService.doDispatchGesture(click, mCallback, null));
    109         mCallback.assertGestureCompletes(GESTURE_COMPLETION_TIMEOUT);
    110         waitForMotionEvents(2);
    111 
    112         assertEquals(2, mMotionEvents.size());
    113         MotionEvent clickDown = mMotionEvents.get(0);
    114         MotionEvent clickUp = mMotionEvents.get(1);
    115 
    116         assertEquals(MotionEvent.ACTION_DOWN, clickDown.getActionMasked());
    117         assertEquals(0, clickDown.getActionIndex());
    118         assertEquals(0, clickDown.getDeviceId());
    119         assertEquals(0, clickDown.getEdgeFlags());
    120         assertEquals(1F, clickDown.getXPrecision());
    121         assertEquals(1F, clickDown.getYPrecision());
    122         assertEquals(1, clickDown.getPointerCount());
    123         assertEquals(1F, clickDown.getPressure());
    124         assertEquals((float) clickXInsideView, clickDown.getX());
    125         assertEquals((float) clickYInsideView, clickDown.getY());
    126         assertEquals(clickDown.getDownTime(), clickDown.getEventTime());
    127 
    128         assertEquals(MotionEvent.ACTION_UP, clickUp.getActionMasked());
    129         assertEquals(clickDown.getDownTime(), clickUp.getDownTime());
    130         assertEquals(ViewConfiguration.getTapTimeout(),
    131                 clickUp.getEventTime() - clickUp.getDownTime());
    132         assertTrue(clickDown.getEventTime() + ViewConfiguration.getLongPressTimeout()
    133                 > clickUp.getEventTime());
    134         assertEquals((float) clickXInsideView, clickUp.getX());
    135         assertEquals((float) clickYInsideView, clickUp.getY());
    136     }
    137 
    138     public void testLongClickAt_producesEventsWithLongClickTiming() throws InterruptedException {
    139         if (!mHasTouchScreen) {
    140             return;
    141         }
    142 
    143         final int clickXInsideView = 10;
    144         final int clickYInsideView = 20;
    145         int clickX = clickXInsideView + mViewBounds.left;
    146         int clickY = clickYInsideView + mViewBounds.top;
    147         GestureDescription longClick = createLongClick(clickX, clickY);
    148         mService.runOnServiceSync(() -> mService.doDispatchGesture(longClick, mCallback, null));
    149         mCallback.assertGestureCompletes(
    150                 ViewConfiguration.getLongPressTimeout() + GESTURE_COMPLETION_TIMEOUT);
    151 
    152         waitForMotionEvents(2);
    153         MotionEvent clickDown = mMotionEvents.get(0);
    154         MotionEvent clickUp = mMotionEvents.get(1);
    155 
    156         assertEquals(MotionEvent.ACTION_DOWN, clickDown.getActionMasked());
    157 
    158         assertEquals((float) clickXInsideView, clickDown.getX());
    159         assertEquals((float) clickYInsideView, clickDown.getY());
    160 
    161         assertEquals(MotionEvent.ACTION_UP, clickUp.getActionMasked());
    162         assertTrue(clickDown.getEventTime() + ViewConfiguration.getLongPressTimeout()
    163                 <= clickUp.getEventTime());
    164         assertEquals(clickDown.getDownTime(), clickUp.getDownTime());
    165         assertEquals((float) clickXInsideView, clickUp.getX());
    166         assertEquals((float) clickYInsideView, clickUp.getY());
    167     }
    168 
    169     public void testSwipe_shouldContainPointsInALine() throws InterruptedException {
    170         if (!mHasTouchScreen) {
    171             return;
    172         }
    173 
    174         int startXInsideView = 10;
    175         int startYInsideView = 20;
    176         int endXInsideView = 20;
    177         int endYInsideView = 40;
    178         int startX = startXInsideView + mViewBounds.left;
    179         int startY = startYInsideView + mViewBounds.top;
    180         int endX = endXInsideView + mViewBounds.left;
    181         int endY = endYInsideView + mViewBounds.top;
    182         int gestureTime = 500;
    183         float swipeTolerance = 2.0f;
    184 
    185         GestureDescription swipe = createSwipe(startX, startY, endX, endY, gestureTime);
    186         mService.runOnServiceSync(() -> mService.doDispatchGesture(swipe, mCallback, null));
    187         mCallback.assertGestureCompletes(gestureTime + GESTURE_COMPLETION_TIMEOUT);
    188         waitForUpEvent();
    189         int numEvents = mMotionEvents.size();
    190 
    191         MotionEvent downEvent = mMotionEvents.get(0);
    192         assertEquals(MotionEvent.ACTION_DOWN, downEvent.getActionMasked());
    193         assertEquals(startXInsideView, (int) downEvent.getX());
    194         assertEquals(startYInsideView, (int) downEvent.getY());
    195 
    196         MotionEvent upEvent = mMotionEvents.get(numEvents - 1);
    197         assertEquals(MotionEvent.ACTION_UP, upEvent.getActionMasked());
    198         assertEquals(endXInsideView, (int) upEvent.getX());
    199         assertEquals(endYInsideView, (int) upEvent.getY());
    200         assertEquals(gestureTime, upEvent.getEventTime() - downEvent.getEventTime());
    201 
    202         long lastEventTime = downEvent.getEventTime();
    203         for (int i = 1; i < numEvents - 1; i++) {
    204             MotionEvent moveEvent = mMotionEvents.get(i);
    205             assertEquals(MotionEvent.ACTION_MOVE, moveEvent.getActionMasked());
    206             assertTrue(moveEvent.getEventTime() >= lastEventTime);
    207             float fractionOfSwipe =
    208                     ((float) (moveEvent.getEventTime() - downEvent.getEventTime())) / gestureTime;
    209             float fractionX = ((float) (endXInsideView - startXInsideView)) * fractionOfSwipe;
    210             float fractionY = ((float) (endYInsideView - startYInsideView)) * fractionOfSwipe;
    211             assertEquals(startXInsideView + fractionX, moveEvent.getX(), swipeTolerance);
    212             assertEquals(startYInsideView + fractionY, moveEvent.getY(), swipeTolerance);
    213             lastEventTime = moveEvent.getEventTime();
    214         }
    215     }
    216 
    217     public void testSlowSwipe_shouldNotContainMovesForTinyMovement() throws InterruptedException {
    218         if (!mHasTouchScreen) {
    219             return;
    220         }
    221 
    222         int startXInsideView = 10;
    223         int startYInsideView = 20;
    224         int endXInsideView = 11;
    225         int endYInsideView = 22;
    226         int startX = startXInsideView + mViewBounds.left;
    227         int startY = startYInsideView + mViewBounds.top;
    228         int endX = endXInsideView + mViewBounds.left;
    229         int endY = endYInsideView + mViewBounds.top;
    230         int gestureTime = 1000;
    231 
    232         GestureDescription swipe = createSwipe(startX, startY, endX, endY, gestureTime);
    233         mService.runOnServiceSync(() -> mService.doDispatchGesture(swipe, mCallback, null));
    234         mCallback.assertGestureCompletes(gestureTime + GESTURE_COMPLETION_TIMEOUT);
    235         waitForUpEvent();
    236 
    237         assertEquals(5, mMotionEvents.size());
    238 
    239         assertEquals(MotionEvent.ACTION_DOWN, mMotionEvents.get(0).getActionMasked());
    240         assertEquals(MotionEvent.ACTION_MOVE, mMotionEvents.get(1).getActionMasked());
    241         assertEquals(MotionEvent.ACTION_MOVE, mMotionEvents.get(2).getActionMasked());
    242         assertEquals(MotionEvent.ACTION_MOVE, mMotionEvents.get(3).getActionMasked());
    243         assertEquals(MotionEvent.ACTION_UP, mMotionEvents.get(4).getActionMasked());
    244 
    245         assertEquals(startXInsideView, (int) mMotionEvents.get(0).getX());
    246         assertEquals(startXInsideView, (int) mMotionEvents.get(1).getX());
    247         assertEquals(startXInsideView + 1, (int) mMotionEvents.get(2).getX());
    248         assertEquals(startXInsideView + 1, (int) mMotionEvents.get(3).getX());
    249         assertEquals(startXInsideView + 1, (int) mMotionEvents.get(4).getX());
    250 
    251         assertEquals(startYInsideView, (int) mMotionEvents.get(0).getY());
    252         assertEquals(startYInsideView + 1, (int) mMotionEvents.get(1).getY());
    253         assertEquals(startYInsideView + 1, (int) mMotionEvents.get(2).getY());
    254         assertEquals(startYInsideView + 2, (int) mMotionEvents.get(3).getY());
    255         assertEquals(startYInsideView + 2, (int) mMotionEvents.get(4).getY());
    256     }
    257 
    258     public void testAngledPinch_looksReasonable() throws InterruptedException {
    259         if (!(mHasTouchScreen && mHasMultiTouch)) {
    260             return;
    261         }
    262 
    263         int centerXInsideView = 50;
    264         int centerYInsideView = 60;
    265         int centerX = centerXInsideView + mViewBounds.left;
    266         int centerY = centerYInsideView + mViewBounds.top;
    267         int startSpacing = 100;
    268         int endSpacing = 50;
    269         int gestureTime = 500;
    270         float pinchTolerance = 2.0f;
    271 
    272         GestureDescription pinch = createPinch(centerX, centerY, startSpacing,
    273                 endSpacing, 45.0F, gestureTime);
    274         mService.runOnServiceSync(() -> mService.doDispatchGesture(pinch, mCallback, null));
    275         mCallback.assertGestureCompletes(gestureTime + GESTURE_COMPLETION_TIMEOUT);
    276         waitForUpEvent();
    277         int numEvents = mMotionEvents.size();
    278 
    279         // First two events are the initial down and the pointer down
    280         assertEquals(MotionEvent.ACTION_DOWN, mMotionEvents.get(0).getActionMasked());
    281         assertEquals(MotionEvent.ACTION_POINTER_DOWN, mMotionEvents.get(1).getActionMasked());
    282 
    283         // The second event must have two pointers at the initial spacing along a 45 degree angle
    284         MotionEvent firstEventWithTwoPointers = mMotionEvents.get(1);
    285         assertEquals(2, firstEventWithTwoPointers.getPointerCount());
    286         MotionEvent.PointerCoords coords0 = new MotionEvent.PointerCoords();
    287         MotionEvent.PointerCoords coords1 = new MotionEvent.PointerCoords();
    288         firstEventWithTwoPointers.getPointerCoords(0, coords0);
    289         firstEventWithTwoPointers.getPointerCoords(1, coords1);
    290         // Verify center point
    291         assertEquals((float) centerXInsideView, (coords0.x + coords1.x) / 2, pinchTolerance);
    292         assertEquals((float) centerYInsideView, (coords0.y + coords1.y) / 2, pinchTolerance);
    293         // Verify angle
    294         assertEquals(coords0.x - centerXInsideView, coords0.y - centerYInsideView, pinchTolerance);
    295         assertEquals(coords1.x - centerXInsideView, coords1.y - centerYInsideView, pinchTolerance);
    296         // Verify spacing
    297         assertEquals(startSpacing, distance(coords0, coords1), pinchTolerance);
    298 
    299         // The last two events are the pointer up and the final up
    300         assertEquals(MotionEvent.ACTION_UP, mMotionEvents.get(numEvents - 1).getActionMasked());
    301 
    302         MotionEvent lastEventWithTwoPointers = mMotionEvents.get(numEvents - 2);
    303         assertEquals(MotionEvent.ACTION_POINTER_UP, lastEventWithTwoPointers.getActionMasked());
    304         lastEventWithTwoPointers.getPointerCoords(0, coords0);
    305         lastEventWithTwoPointers.getPointerCoords(1, coords1);
    306         // Verify center point
    307         assertEquals((float) centerXInsideView, (coords0.x + coords1.x) / 2, pinchTolerance);
    308         assertEquals((float) centerYInsideView, (coords0.y + coords1.y) / 2, pinchTolerance);
    309         // Verify angle
    310         assertEquals(coords0.x - centerXInsideView, coords0.y - centerYInsideView, pinchTolerance);
    311         assertEquals(coords1.x - centerXInsideView, coords1.y - centerYInsideView, pinchTolerance);
    312         // Verify spacing
    313         assertEquals(endSpacing, distance(coords0, coords1), pinchTolerance);
    314 
    315         float lastSpacing = startSpacing;
    316         for (int i = 2; i < numEvents - 2; i++) {
    317             MotionEvent eventInMiddle = mMotionEvents.get(i);
    318             assertEquals(MotionEvent.ACTION_MOVE, eventInMiddle.getActionMasked());
    319             eventInMiddle.getPointerCoords(0, coords0);
    320             eventInMiddle.getPointerCoords(1, coords1);
    321             // Verify center point
    322             assertEquals((float) centerXInsideView, (coords0.x + coords1.x) / 2, pinchTolerance);
    323             assertEquals((float) centerYInsideView, (coords0.y + coords1.y) / 2, pinchTolerance);
    324             // Verify angle
    325             assertEquals(coords0.x - centerXInsideView, coords0.y - centerYInsideView,
    326                     pinchTolerance);
    327             assertEquals(coords1.x - centerXInsideView, coords1.y - centerYInsideView,
    328                     pinchTolerance);
    329             float spacing = distance(coords0, coords1);
    330             assertTrue(spacing <= lastSpacing + pinchTolerance);
    331             assertTrue(spacing >= endSpacing - pinchTolerance);
    332             lastSpacing = spacing;
    333         }
    334     }
    335 
    336     // This test assumes device's screen contains its center (W/2, H/2) with some surroundings
    337     // and should work for rectangular, round and round with chin screens.
    338     public void testClickWhenMagnified_matchesActualTouch() throws InterruptedException {
    339         if (!mHasTouchScreen) {
    340             return;
    341         }
    342 
    343         final int clickShiftFromCenterX = 10;
    344         final int clickShiftFromCenterY = 20;
    345         final Resources res = getInstrumentation().getTargetContext().getResources();
    346         final DisplayMetrics metrics = res.getDisplayMetrics();
    347         final int centerX = metrics.widthPixels / 2;
    348         final int centerY = metrics.heightPixels / 2;
    349         final float TOUCH_TOLERANCE = 2.0f;
    350 
    351         StubMagnificationAccessibilityService magnificationService =
    352                 StubMagnificationAccessibilityService.enableSelf(this);
    353         android.accessibilityservice.AccessibilityService.MagnificationController
    354                 magnificationController = magnificationService.getMagnificationController();
    355         try {
    356             // Magnify screen by 2x with a magnification center in the center of the screen
    357             final AtomicBoolean setScale = new AtomicBoolean();
    358             final float magnificationFactor = 2.0f;
    359             magnificationService.runOnServiceSync(() -> {
    360                         setScale.set(magnificationController.setScale(magnificationFactor, false));
    361                         magnificationController.setCenter(centerX, centerY, false);
    362                     });
    363             assertTrue("Failed to set scale", setScale.get());
    364 
    365             final int clickMagnifiedX = (int) (centerX + magnificationFactor * clickShiftFromCenterX);
    366             final int clickMagnifiedY = (int) (centerY + magnificationFactor * clickShiftFromCenterY);
    367             GestureDescription click = createClick(clickMagnifiedX, clickMagnifiedY);
    368             mService.runOnServiceSync(() -> mService.doDispatchGesture(click, mCallback, null));
    369             mCallback.assertGestureCompletes(GESTURE_COMPLETION_TIMEOUT);
    370             waitForMotionEvents(3);
    371         } finally {
    372             // Reset magnification
    373             final AtomicBoolean result = new AtomicBoolean();
    374             magnificationService.runOnServiceSync(() ->
    375                     result.set(magnificationController.reset(false)));
    376             magnificationService.runOnServiceSync(() -> magnificationService.disableSelf());
    377             assertTrue("Failed to reset", result.get());
    378         }
    379 
    380         assertEquals(2, mMotionEvents.size());
    381         MotionEvent clickDown = mMotionEvents.get(0);
    382         MotionEvent clickUp = mMotionEvents.get(1);
    383 
    384         final int centerXInsideView = centerX - mViewBounds.left;
    385         final int centerYInsideView = centerY - mViewBounds.top;
    386         final int expectedClickXInsideView = centerXInsideView + clickShiftFromCenterX;
    387         final int expectedClickYInsideView = centerYInsideView + clickShiftFromCenterY;
    388         assertEquals(MotionEvent.ACTION_DOWN, clickDown.getActionMasked());
    389         assertEquals((float) expectedClickXInsideView, clickDown.getX(), TOUCH_TOLERANCE);
    390         assertEquals((float) expectedClickYInsideView, clickDown.getY(), TOUCH_TOLERANCE);
    391         assertEquals(clickDown.getDownTime(), clickDown.getEventTime());
    392 
    393         assertEquals(MotionEvent.ACTION_UP, clickUp.getActionMasked());
    394         assertEquals((float) expectedClickXInsideView, clickUp.getX(), TOUCH_TOLERANCE);
    395         assertEquals((float) expectedClickYInsideView, clickUp.getY(), TOUCH_TOLERANCE);
    396     }
    397 
    398 
    399     public static class GestureDispatchActivity extends AccessibilityTestActivity {
    400         public GestureDispatchActivity() {
    401             super();
    402         }
    403 
    404         @Override
    405         public void onCreate(Bundle savedInstanceState) {
    406             super.onCreate(savedInstanceState);
    407             setContentView(R.layout.full_screen_frame_layout);
    408         }
    409     }
    410 
    411     public static class MyGestureCallback extends AccessibilityService.GestureResultCallback {
    412         private boolean mCompleted;
    413         private boolean mCancelled;
    414 
    415         @Override
    416         public synchronized void onCompleted(GestureDescription gestureDescription) {
    417             mCompleted = true;
    418             notifyAll();
    419         }
    420 
    421         @Override
    422         public synchronized void onCancelled(GestureDescription gestureDescription) {
    423             mCancelled = true;
    424             notifyAll();
    425         }
    426 
    427         public synchronized void assertGestureCompletes(long timeout) {
    428             if (mCompleted) {
    429                 return;
    430             }
    431             try {
    432                 wait(timeout);
    433             } catch (InterruptedException e) {
    434                 throw new RuntimeException(e);
    435             }
    436             assertTrue("Gesture did not complete.", mCompleted);
    437         }
    438     }
    439 
    440     private void waitForMotionEvents(int numEventsExpected) throws InterruptedException {
    441         synchronized (mMotionEvents) {
    442             long endMillis = SystemClock.uptimeMillis() + MOTION_EVENT_TIMEOUT;
    443             while ((mMotionEvents.size() < numEventsExpected)
    444                     && (SystemClock.uptimeMillis() < endMillis)) {
    445                 mMotionEvents.wait(endMillis - SystemClock.uptimeMillis());
    446             }
    447         }
    448     }
    449 
    450     private void waitForUpEvent() throws InterruptedException {
    451         synchronized (mMotionEvents) {
    452             long endMillis = SystemClock.uptimeMillis() + MOTION_EVENT_TIMEOUT;
    453             while (!mGotUpEvent && (SystemClock.uptimeMillis() < endMillis)) {
    454                 mMotionEvents.wait(endMillis - SystemClock.uptimeMillis());
    455             }
    456         }
    457     }
    458 
    459     private float distance(MotionEvent.PointerCoords point1, MotionEvent.PointerCoords point2) {
    460         return (float) Math.hypot((double) (point1.x - point2.x), (double) (point1.y - point2.y));
    461     }
    462 
    463     private class MyTouchListener implements View.OnTouchListener {
    464         @Override
    465         public boolean onTouch(View view, MotionEvent motionEvent) {
    466             synchronized (mMotionEvents) {
    467                 if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
    468                     mGotUpEvent = true;
    469                 }
    470                 mMotionEvents.add(MotionEvent.obtain(motionEvent));
    471                 mMotionEvents.notifyAll();
    472                 return true;
    473             }
    474         }
    475     }
    476 
    477     private GestureDescription createClick(int x, int y) {
    478         Path clickPath = new Path();
    479         clickPath.moveTo(x, y);
    480         GestureDescription.StrokeDescription clickStroke =
    481                 new GestureDescription.StrokeDescription(clickPath, 0, ViewConfiguration.getTapTimeout());
    482         GestureDescription.Builder clickBuilder = new GestureDescription.Builder();
    483         clickBuilder.addStroke(clickStroke);
    484         return clickBuilder.build();
    485     }
    486 
    487     private GestureDescription createLongClick(int x, int y) {
    488         Path clickPath = new Path();
    489         clickPath.moveTo(x, y);
    490         int longPressTime = ViewConfiguration.getLongPressTimeout();
    491 
    492         GestureDescription.StrokeDescription longClickStroke =
    493                 new GestureDescription.StrokeDescription(clickPath, 0, longPressTime + (longPressTime / 2));
    494         GestureDescription.Builder longClickBuilder = new GestureDescription.Builder();
    495         longClickBuilder.addStroke(longClickStroke);
    496         return longClickBuilder.build();
    497     }
    498 
    499     private GestureDescription createSwipe(
    500             int startX, int startY, int endX, int endY, long duration) {
    501         Path swipePath = new Path();
    502         swipePath.moveTo(startX, startY);
    503         swipePath.lineTo(endX, endY);
    504 
    505         GestureDescription.StrokeDescription swipeStroke = new GestureDescription.StrokeDescription(swipePath, 0, duration);
    506         GestureDescription.Builder swipeBuilder = new GestureDescription.Builder();
    507         swipeBuilder.addStroke(swipeStroke);
    508         return swipeBuilder.build();
    509     }
    510 
    511     private GestureDescription createPinch(int centerX, int centerY, int startSpacing,
    512             int endSpacing, float orientation, long duration) {
    513         if ((startSpacing < 0) || (endSpacing < 0)) {
    514             throw new IllegalArgumentException("Pinch spacing cannot be negative");
    515         }
    516         float[] startPoint1 = new float[2];
    517         float[] endPoint1 = new float[2];
    518         float[] startPoint2 = new float[2];
    519         float[] endPoint2 = new float[2];
    520 
    521         /* Build points for a horizontal gesture centered at the origin */
    522         startPoint1[0] = startSpacing / 2;
    523         startPoint1[1] = 0;
    524         endPoint1[0] = endSpacing / 2;
    525         endPoint1[1] = 0;
    526         startPoint2[0] = -startSpacing / 2;
    527         startPoint2[1] = 0;
    528         endPoint2[0] = -endSpacing / 2;
    529         endPoint2[1] = 0;
    530 
    531         /* Rotate and translate the points */
    532         Matrix matrix = new Matrix();
    533         matrix.setRotate(orientation);
    534         matrix.postTranslate(centerX, centerY);
    535         matrix.mapPoints(startPoint1);
    536         matrix.mapPoints(endPoint1);
    537         matrix.mapPoints(startPoint2);
    538         matrix.mapPoints(endPoint2);
    539 
    540         Path path1 = new Path();
    541         path1.moveTo(startPoint1[0], startPoint1[1]);
    542         path1.lineTo(endPoint1[0], endPoint1[1]);
    543         Path path2 = new Path();
    544         path2.moveTo(startPoint2[0], startPoint2[1]);
    545         path2.lineTo(endPoint2[0], endPoint2[1]);
    546 
    547         GestureDescription.StrokeDescription path1Stroke = new GestureDescription.StrokeDescription(path1, 0, duration);
    548         GestureDescription.StrokeDescription path2Stroke = new GestureDescription.StrokeDescription(path2, 0, duration);
    549         GestureDescription.Builder swipeBuilder = new GestureDescription.Builder();
    550         swipeBuilder.addStroke(path1Stroke);
    551         swipeBuilder.addStroke(path2Stroke);
    552         return swipeBuilder.build();
    553     }
    554 }
    555