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 public void testClickWhenMagnified_matchesActualTouch() throws InterruptedException { 337 if (!mHasTouchScreen) { 338 return; 339 } 340 341 final int clickXInsideView = 10; 342 final int clickYInsideView = 20; 343 int clickX = clickXInsideView + mViewBounds.left; 344 int clickY = clickYInsideView + mViewBounds.top; 345 final float TOUCH_TOLERANCE = 2.0f; 346 347 StubMagnificationAccessibilityService magnificationService = 348 StubMagnificationAccessibilityService.enableSelf(this); 349 android.accessibilityservice.AccessibilityService.MagnificationController 350 magnificationController = magnificationService.getMagnificationController(); 351 final Resources res = getInstrumentation().getTargetContext().getResources(); 352 final DisplayMetrics metrics = res.getDisplayMetrics(); 353 try { 354 // Magnify screen by 2x from upper left corner 355 final AtomicBoolean setScale = new AtomicBoolean(); 356 final float magnificationFactor = 2.0f; 357 // Center to have (0,0) in the upper-left corner 358 final float centerX = metrics.widthPixels / (2.0f * magnificationFactor) - 1.0f; 359 final float centerY = metrics.heightPixels / (2.0f * magnificationFactor) - 1.0f; 360 magnificationService.runOnServiceSync(() -> { 361 setScale.set(magnificationController.setScale(magnificationFactor, false)); 362 // Make sure the upper right corner is on the screen 363 magnificationController.setCenter(centerX, centerY, false); 364 }); 365 assertTrue("Failed to set scale", setScale.get()); 366 367 GestureDescription click = createClick((int) (clickX * magnificationFactor), 368 (int) (clickY * magnificationFactor)); 369 mService.runOnServiceSync(() -> mService.doDispatchGesture(click, mCallback, null)); 370 mCallback.assertGestureCompletes(GESTURE_COMPLETION_TIMEOUT); 371 waitForMotionEvents(3); 372 } finally { 373 // Reset magnification 374 final AtomicBoolean result = new AtomicBoolean(); 375 magnificationService.runOnServiceSync(() -> 376 result.set(magnificationController.reset(false))); 377 magnificationService.runOnServiceSync(() -> magnificationService.disableSelf()); 378 assertTrue("Failed to reset", result.get()); 379 } 380 381 assertEquals(2, mMotionEvents.size()); 382 MotionEvent clickDown = mMotionEvents.get(0); 383 MotionEvent clickUp = mMotionEvents.get(1); 384 385 assertEquals(MotionEvent.ACTION_DOWN, clickDown.getActionMasked()); 386 assertEquals((float) clickXInsideView, clickDown.getX(), TOUCH_TOLERANCE); 387 assertEquals((float) clickYInsideView, clickDown.getY(), TOUCH_TOLERANCE); 388 assertEquals(clickDown.getDownTime(), clickDown.getEventTime()); 389 390 assertEquals(MotionEvent.ACTION_UP, clickUp.getActionMasked()); 391 assertEquals((float) clickXInsideView, clickUp.getX(), TOUCH_TOLERANCE); 392 assertEquals((float) clickYInsideView, clickUp.getY(), TOUCH_TOLERANCE); 393 } 394 395 396 public static class GestureDispatchActivity extends AccessibilityTestActivity { 397 public GestureDispatchActivity() { 398 super(); 399 } 400 401 @Override 402 public void onCreate(Bundle savedInstanceState) { 403 super.onCreate(savedInstanceState); 404 setContentView(R.layout.full_screen_frame_layout); 405 } 406 } 407 408 public static class MyGestureCallback extends AccessibilityService.GestureResultCallback { 409 private boolean mCompleted; 410 private boolean mCancelled; 411 412 @Override 413 public synchronized void onCompleted(GestureDescription gestureDescription) { 414 mCompleted = true; 415 notifyAll(); 416 } 417 418 @Override 419 public synchronized void onCancelled(GestureDescription gestureDescription) { 420 mCancelled = true; 421 notifyAll(); 422 } 423 424 public synchronized void assertGestureCompletes(long timeout) { 425 if (mCompleted) { 426 return; 427 } 428 try { 429 wait(timeout); 430 } catch (InterruptedException e) { 431 throw new RuntimeException(e); 432 } 433 assertTrue("Gesture did not complete.", mCompleted); 434 } 435 } 436 437 private void waitForMotionEvents(int numEventsExpected) throws InterruptedException { 438 synchronized (mMotionEvents) { 439 long endMillis = SystemClock.uptimeMillis() + MOTION_EVENT_TIMEOUT; 440 while ((mMotionEvents.size() < numEventsExpected) 441 && (SystemClock.uptimeMillis() < endMillis)) { 442 mMotionEvents.wait(endMillis - SystemClock.uptimeMillis()); 443 } 444 } 445 } 446 447 private void waitForUpEvent() throws InterruptedException { 448 synchronized (mMotionEvents) { 449 long endMillis = SystemClock.uptimeMillis() + MOTION_EVENT_TIMEOUT; 450 while (!mGotUpEvent && (SystemClock.uptimeMillis() < endMillis)) { 451 mMotionEvents.wait(endMillis - SystemClock.uptimeMillis()); 452 } 453 } 454 } 455 456 private float distance(MotionEvent.PointerCoords point1, MotionEvent.PointerCoords point2) { 457 return (float) Math.hypot((double) (point1.x - point2.x), (double) (point1.y - point2.y)); 458 } 459 460 private class MyTouchListener implements View.OnTouchListener { 461 @Override 462 public boolean onTouch(View view, MotionEvent motionEvent) { 463 synchronized (mMotionEvents) { 464 if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) { 465 mGotUpEvent = true; 466 } 467 mMotionEvents.add(MotionEvent.obtain(motionEvent)); 468 mMotionEvents.notifyAll(); 469 return true; 470 } 471 } 472 } 473 474 private GestureDescription createClick(int x, int y) { 475 Path clickPath = new Path(); 476 clickPath.moveTo(x, y); 477 GestureDescription.StrokeDescription clickStroke = 478 new GestureDescription.StrokeDescription(clickPath, 0, ViewConfiguration.getTapTimeout()); 479 GestureDescription.Builder clickBuilder = new GestureDescription.Builder(); 480 clickBuilder.addStroke(clickStroke); 481 return clickBuilder.build(); 482 } 483 484 private GestureDescription createLongClick(int x, int y) { 485 Path clickPath = new Path(); 486 clickPath.moveTo(x, y); 487 int longPressTime = ViewConfiguration.getLongPressTimeout(); 488 489 GestureDescription.StrokeDescription longClickStroke = 490 new GestureDescription.StrokeDescription(clickPath, 0, longPressTime + (longPressTime / 2)); 491 GestureDescription.Builder longClickBuilder = new GestureDescription.Builder(); 492 longClickBuilder.addStroke(longClickStroke); 493 return longClickBuilder.build(); 494 } 495 496 private GestureDescription createSwipe( 497 int startX, int startY, int endX, int endY, long duration) { 498 Path swipePath = new Path(); 499 swipePath.moveTo(startX, startY); 500 swipePath.lineTo(endX, endY); 501 502 GestureDescription.StrokeDescription swipeStroke = new GestureDescription.StrokeDescription(swipePath, 0, duration); 503 GestureDescription.Builder swipeBuilder = new GestureDescription.Builder(); 504 swipeBuilder.addStroke(swipeStroke); 505 return swipeBuilder.build(); 506 } 507 508 private GestureDescription createPinch(int centerX, int centerY, int startSpacing, 509 int endSpacing, float orientation, long duration) { 510 if ((startSpacing < 0) || (endSpacing < 0)) { 511 throw new IllegalArgumentException("Pinch spacing cannot be negative"); 512 } 513 float[] startPoint1 = new float[2]; 514 float[] endPoint1 = new float[2]; 515 float[] startPoint2 = new float[2]; 516 float[] endPoint2 = new float[2]; 517 518 /* Build points for a horizontal gesture centered at the origin */ 519 startPoint1[0] = startSpacing / 2; 520 startPoint1[1] = 0; 521 endPoint1[0] = endSpacing / 2; 522 endPoint1[1] = 0; 523 startPoint2[0] = -startSpacing / 2; 524 startPoint2[1] = 0; 525 endPoint2[0] = -endSpacing / 2; 526 endPoint2[1] = 0; 527 528 /* Rotate and translate the points */ 529 Matrix matrix = new Matrix(); 530 matrix.setRotate(orientation); 531 matrix.postTranslate(centerX, centerY); 532 matrix.mapPoints(startPoint1); 533 matrix.mapPoints(endPoint1); 534 matrix.mapPoints(startPoint2); 535 matrix.mapPoints(endPoint2); 536 537 Path path1 = new Path(); 538 path1.moveTo(startPoint1[0], startPoint1[1]); 539 path1.lineTo(endPoint1[0], endPoint1[1]); 540 Path path2 = new Path(); 541 path2.moveTo(startPoint2[0], startPoint2[1]); 542 path2.lineTo(endPoint2[0], endPoint2[1]); 543 544 GestureDescription.StrokeDescription path1Stroke = new GestureDescription.StrokeDescription(path1, 0, duration); 545 GestureDescription.StrokeDescription path2Stroke = new GestureDescription.StrokeDescription(path2, 0, duration); 546 GestureDescription.Builder swipeBuilder = new GestureDescription.Builder(); 547 swipeBuilder.addStroke(path1Stroke); 548 swipeBuilder.addStroke(path2Stroke); 549 return swipeBuilder.build(); 550 } 551 } 552