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