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