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