1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.accessibilityservice.cts.utils; 18 19 import static org.junit.Assert.assertTrue; 20 21 import android.accessibility.cts.common.InstrumentedAccessibilityService; 22 import android.accessibilityservice.AccessibilityService.GestureResultCallback; 23 import android.accessibilityservice.GestureDescription; 24 import android.accessibilityservice.GestureDescription.StrokeDescription; 25 import android.graphics.Path; 26 import android.graphics.PointF; 27 import android.view.MotionEvent; 28 import android.view.ViewConfiguration; 29 30 import org.hamcrest.Description; 31 import org.hamcrest.Matcher; 32 import org.hamcrest.TypeSafeMatcher; 33 34 import java.util.concurrent.CompletableFuture; 35 36 public class GestureUtils { 37 38 public static final long STROKE_TIME_GAP_MS = 40; 39 40 public static final Matcher<MotionEvent> IS_ACTION_DOWN = 41 new MotionEventActionMatcher(MotionEvent.ACTION_DOWN); 42 public static final Matcher<MotionEvent> IS_ACTION_POINTER_DOWN = 43 new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_DOWN); 44 public static final Matcher<MotionEvent> IS_ACTION_UP = 45 new MotionEventActionMatcher(MotionEvent.ACTION_UP); 46 public static final Matcher<MotionEvent> IS_ACTION_POINTER_UP = 47 new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_UP); 48 public static final Matcher<MotionEvent> IS_ACTION_CANCEL = 49 new MotionEventActionMatcher(MotionEvent.ACTION_CANCEL); 50 public static final Matcher<MotionEvent> IS_ACTION_MOVE = 51 new MotionEventActionMatcher(MotionEvent.ACTION_MOVE); 52 GestureUtils()53 private GestureUtils() {} 54 dispatchGesture( InstrumentedAccessibilityService service, GestureDescription gesture)55 public static CompletableFuture<Void> dispatchGesture( 56 InstrumentedAccessibilityService service, 57 GestureDescription gesture) { 58 CompletableFuture<Void> result = new CompletableFuture<>(); 59 GestureResultCallback callback = new GestureResultCallback() { 60 @Override 61 public void onCompleted(GestureDescription gestureDescription) { 62 result.complete(null); 63 } 64 65 @Override 66 public void onCancelled(GestureDescription gestureDescription) { 67 result.cancel(false); 68 } 69 }; 70 service.runOnServiceSync(() -> { 71 if (!service.dispatchGesture(gesture, callback, null)) { 72 result.completeExceptionally(new IllegalStateException()); 73 } 74 }); 75 return result; 76 } 77 pointerDown(PointF point)78 public static StrokeDescription pointerDown(PointF point) { 79 return new StrokeDescription(path(point), 0, ViewConfiguration.getTapTimeout(), true); 80 } 81 pointerUp(StrokeDescription lastStroke)82 public static StrokeDescription pointerUp(StrokeDescription lastStroke) { 83 return lastStroke.continueStroke(path(lastPointOf(lastStroke)), 84 endTimeOf(lastStroke), ViewConfiguration.getTapTimeout(), false); 85 } 86 lastPointOf(StrokeDescription stroke)87 public static PointF lastPointOf(StrokeDescription stroke) { 88 float[] p = stroke.getPath().approximate(0.3f); 89 return new PointF(p[p.length - 2], p[p.length - 1]); 90 } 91 click(PointF point)92 public static StrokeDescription click(PointF point) { 93 return new StrokeDescription(path(point), 0, ViewConfiguration.getTapTimeout()); 94 } 95 longClick(PointF point)96 public static StrokeDescription longClick(PointF point) { 97 return new StrokeDescription(path(point), 0, 98 ViewConfiguration.getLongPressTimeout() * 3); 99 } 100 swipe(PointF from, PointF to)101 public static StrokeDescription swipe(PointF from, PointF to) { 102 return swipe(from, to, ViewConfiguration.getTapTimeout()); 103 } 104 swipe(PointF from, PointF to, long duration)105 public static StrokeDescription swipe(PointF from, PointF to, long duration) { 106 return new StrokeDescription(path(from, to), 0, duration); 107 } 108 drag(StrokeDescription from, PointF to)109 public static StrokeDescription drag(StrokeDescription from, PointF to) { 110 return from.continueStroke( 111 path(lastPointOf(from), to), 112 endTimeOf(from), ViewConfiguration.getTapTimeout(), true); 113 } 114 path(PointF first, PointF... rest)115 public static Path path(PointF first, PointF... rest) { 116 Path path = new Path(); 117 path.moveTo(first.x, first.y); 118 for (PointF point : rest) { 119 path.lineTo(point.x, point.y); 120 } 121 return path; 122 } 123 startingAt(long timeMs, StrokeDescription prototype)124 public static StrokeDescription startingAt(long timeMs, StrokeDescription prototype) { 125 return new StrokeDescription( 126 prototype.getPath(), timeMs, prototype.getDuration(), prototype.willContinue()); 127 } 128 endTimeOf(StrokeDescription stroke)129 public static long endTimeOf(StrokeDescription stroke) { 130 return stroke.getStartTime() + stroke.getDuration(); 131 } 132 distance(PointF a, PointF b)133 public static float distance(PointF a, PointF b) { 134 if (a == null) throw new NullPointerException(); 135 if (b == null) throw new NullPointerException(); 136 return (float) Math.hypot(a.x - b.x, a.y - b.y); 137 } 138 add(PointF a, float x, float y)139 public static PointF add(PointF a, float x, float y) { 140 return new PointF(a.x + x, a.y + y); 141 } 142 add(PointF a, PointF b)143 public static PointF add(PointF a, PointF b) { 144 return add(a, b.x, b.y); 145 } 146 diff(PointF a, PointF b)147 public static PointF diff(PointF a, PointF b) { 148 return add(a, -b.x, -b.y); 149 } 150 negate(PointF p)151 public static PointF negate(PointF p) { 152 return times(-1, p); 153 } 154 times(float mult, PointF p)155 public static PointF times(float mult, PointF p) { 156 return new PointF(p.x * mult, p.y * mult); 157 } 158 length(PointF p)159 public static float length(PointF p) { 160 return (float) Math.hypot(p.x, p.y); 161 } 162 ceil(PointF p)163 public static PointF ceil(PointF p) { 164 return new PointF((float) Math.ceil(p.x), (float) Math.ceil(p.y)); 165 } 166 doubleTap(PointF point)167 public static GestureDescription doubleTap(PointF point) { 168 return multiTap(point, 2); 169 } 170 tripleTap(PointF point)171 public static GestureDescription tripleTap(PointF point) { 172 return multiTap(point, 3); 173 } 174 multiTap(PointF point, int taps)175 public static GestureDescription multiTap(PointF point, int taps) { 176 return multiTap(point, taps, 0); 177 } 178 multiTap(PointF point, int taps, int slop)179 public static GestureDescription multiTap(PointF point, int taps, int slop) { 180 GestureDescription.Builder builder = new GestureDescription.Builder(); 181 long time = 0; 182 if (taps > 0) { 183 // Place first tap on the point itself. 184 // Subsequent taps will be offset somewhere within slop radius. 185 // If slop is 0 subsequent taps will also be on the point itself. 186 StrokeDescription stroke = click(point); 187 builder.addStroke(stroke); 188 time += stroke.getDuration() + STROKE_TIME_GAP_MS; 189 for (int i = 1; i < taps; i++) { 190 stroke = click(getPointWithinSlop(point, slop)); 191 builder.addStroke(startingAt(time, stroke)); 192 time += stroke.getDuration() + STROKE_TIME_GAP_MS; 193 } 194 } 195 return builder.build(); 196 } 197 doubleTapAndHold(PointF point)198 public static GestureDescription doubleTapAndHold(PointF point) { 199 GestureDescription.Builder builder = new GestureDescription.Builder(); 200 StrokeDescription tap1 = click(point); 201 StrokeDescription tap2 = 202 startingAt(endTimeOf(tap1) + STROKE_TIME_GAP_MS, longClick(point)); 203 builder.addStroke(tap1); 204 builder.addStroke(tap2); 205 return builder.build(); 206 } 207 getGestureBuilder( int displayId, StrokeDescription... strokes)208 public static GestureDescription.Builder getGestureBuilder( 209 int displayId, StrokeDescription... strokes) { 210 GestureDescription.Builder builder = new GestureDescription.Builder(); 211 builder.setDisplayId(displayId); 212 for (StrokeDescription s : strokes) builder.addStroke(s); 213 return builder; 214 } 215 216 private static class MotionEventActionMatcher extends TypeSafeMatcher<MotionEvent> { 217 int mAction; 218 MotionEventActionMatcher(int action)219 MotionEventActionMatcher(int action) { 220 super(); 221 mAction = action; 222 } 223 224 @Override matchesSafely(MotionEvent motionEvent)225 protected boolean matchesSafely(MotionEvent motionEvent) { 226 return motionEvent.getActionMasked() == mAction; 227 } 228 229 @Override describeTo(Description description)230 public void describeTo(Description description) { 231 description.appendText("Matching to action " + MotionEvent.actionToString(mAction)); 232 } 233 234 @Override describeMismatchSafely(MotionEvent event, Description description)235 public void describeMismatchSafely(MotionEvent event, Description description) { 236 description.appendText( 237 "received " + MotionEvent.actionToString(event.getActionMasked())); 238 } 239 } 240 isAtPoint(final PointF point)241 public static Matcher<MotionEvent> isAtPoint(final PointF point) { 242 return isAtPoint(point, 0.01f); 243 } 244 isAtPoint(final PointF point, final float tol)245 public static Matcher<MotionEvent> isAtPoint(final PointF point, final float tol) { 246 return new TypeSafeMatcher<MotionEvent>() { 247 @Override 248 protected boolean matchesSafely(MotionEvent event) { 249 return Math.hypot(event.getX() - point.x, event.getY() - point.y) <= tol; 250 } 251 252 @Override 253 public void describeTo(Description description) { 254 description.appendText("Matching to point " + point); 255 } 256 257 @Override 258 public void describeMismatchSafely(MotionEvent event, Description description) { 259 description.appendText( 260 "received (" + event.getX() + ", " + event.getY() + ")"); 261 } 262 }; 263 } 264 265 public static Matcher<MotionEvent> isRawAtPoint(final PointF point) { 266 return isRawAtPoint(point, 0.01f); 267 } 268 269 public static Matcher<MotionEvent> isRawAtPoint(final PointF point, final float tol) { 270 return new TypeSafeMatcher<MotionEvent>() { 271 @Override 272 protected boolean matchesSafely(MotionEvent event) { 273 return Math.hypot(event.getRawX() - point.x, event.getRawY() - point.y) <= tol; 274 } 275 276 @Override 277 public void describeTo(Description description) { 278 description.appendText("Matching to point " + point); 279 } 280 281 @Override 282 public void describeMismatchSafely(MotionEvent event, Description description) { 283 description.appendText( 284 "received (" + event.getRawX() + ", " + event.getRawY() + ")"); 285 } 286 }; 287 } 288 289 public static PointF getPointWithinSlop(PointF point, int slop) { 290 return add(point, slop / 2, 0); 291 } 292 293 /** 294 * Simulates a user placing one finger on the screen for a specified amount of time and then multi-tapping with a second finger. 295 * @param explorePoint Where to place the first finger. 296 * @param tapPoint Where to tap with the second finger. 297 * @param taps The number of second-finger taps. 298 * @param waitTime How long to hold the first finger before tapping with the second finger. 299 */ 300 public static GestureDescription secondFingerMultiTap( 301 PointF explorePoint, PointF tapPoint, int taps, int waitTime) { 302 GestureDescription.Builder builder = new GestureDescription.Builder(); 303 long time = waitTime; 304 for (int i = 0; i < taps; i++) { 305 StrokeDescription stroke = click(tapPoint); 306 builder.addStroke(startingAt(time, stroke)); 307 time += stroke.getDuration(); 308 time += ViewConfiguration.getDoubleTapTimeout() / 3; 309 } 310 builder.addStroke(swipe(explorePoint, explorePoint, time)); 311 return builder.build(); 312 } 313 314 /** 315 * Simulates a user placing multiple fingers on the specified screen 316 * and then multi-tapping with these fingers. 317 * 318 * The location of fingers based on <code>basePoint<code/> are shifted by <code>delta<code/>. 319 * Like (baseX, baseY), (baseX + deltaX, baseY + deltaY), and so on. 320 * 321 * @param basePoint Where to place the first finger. 322 * @param delta Offset to basePoint where to place the 2nd or 3rd finger. 323 * @param fingerCount The number of fingers. 324 * @param tapCount The number of taps to fingers. 325 * @param slop Slop range the finger tapped. 326 * @param displayId Which display to dispatch the gesture. 327 */ 328 public static GestureDescription multiFingerMultiTap(PointF basePoint, PointF delta, 329 int fingerCount, int tapCount, int slop, int displayId) { 330 assertTrue(fingerCount >= 2); 331 assertTrue(tapCount > 0); 332 final int strokeCount = fingerCount * tapCount; 333 final PointF[] pointers = new PointF[fingerCount]; 334 final StrokeDescription[] strokes = new StrokeDescription[strokeCount]; 335 336 // The first tap 337 for (int i = 0; i < fingerCount; i++) { 338 pointers[i] = add(basePoint, times(i, delta)); 339 strokes[i] = click(pointers[i]); 340 } 341 // The rest of taps 342 for (int tapIndex = 1; tapIndex < tapCount; tapIndex++) { 343 for (int i = 0; i < fingerCount; i++) { 344 final StrokeDescription lastStroke = strokes[(tapIndex - 1) * fingerCount + i]; 345 final long nextStartTime = endTimeOf(lastStroke) + STROKE_TIME_GAP_MS; 346 final int nextIndex = tapIndex * fingerCount + i; 347 pointers[i] = getPointWithinSlop(pointers[i], slop); 348 strokes[nextIndex] = startingAt(nextStartTime, click(pointers[i])); 349 } 350 } 351 return getGestureBuilder(displayId, strokes).build(); 352 } 353 354 /** 355 * Simulates a user placing multiple fingers on the specified screen 356 * and then multi-tapping and holding with these fingers. 357 * 358 * The location of fingers based on <code>basePoint<code/> are shifted by <code>delta<code/>. 359 * Like (baseX, baseY), (baseX + deltaX, baseY + deltaY), and so on. 360 * 361 * @param basePoint Where to place the first finger. 362 * @param delta Offset to basePoint where to place the 2nd or 3rd finger. 363 * @param fingerCount The number of fingers. 364 * @param tapCount The number of taps to fingers. 365 * @param slop Slop range the finger tapped. 366 * @param displayId Which display to dispatch the gesture. 367 */ 368 public static GestureDescription multiFingerMultiTapAndHold( 369 PointF basePoint, 370 PointF delta, 371 int fingerCount, 372 int tapCount, 373 int slop, 374 int displayId) { 375 assertTrue(fingerCount >= 2); 376 assertTrue(tapCount > 0); 377 final int strokeCount = fingerCount * tapCount; 378 final PointF[] pointers = new PointF[fingerCount]; 379 final StrokeDescription[] strokes = new StrokeDescription[strokeCount]; 380 381 // The first tap 382 for (int i = 0; i < fingerCount; i++) { 383 pointers[i] = add(basePoint, times(i, delta)); 384 if(tapCount == 1) { 385 strokes[i] = longClick(pointers[i]); 386 } else { 387 strokes[i] = click(pointers[i]); 388 } 389 } 390 // The rest of taps 391 for (int tapIndex = 1; tapIndex < tapCount; tapIndex++) { 392 for (int i = 0; i < fingerCount; i++) { 393 final StrokeDescription lastStroke = strokes[(tapIndex - 1) * fingerCount + i]; 394 final long nextStartTime = endTimeOf(lastStroke) + STROKE_TIME_GAP_MS; 395 final int nextIndex = tapIndex * fingerCount + i; 396 pointers[i] = getPointWithinSlop(pointers[i], slop); 397 if (tapIndex + 1 == tapCount) { 398 // Last tap so do long click. 399 strokes[nextIndex] = startingAt(nextStartTime, longClick(pointers[i])); 400 } else { 401 strokes[nextIndex] = startingAt(nextStartTime, click(pointers[i])); 402 } 403 } 404 } 405 return getGestureBuilder(displayId, strokes).build(); 406 } 407 } 408