1 /* 2 * Copyright (C) 2016 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 com.android.compatibility.common.util; 18 19 import android.app.Instrumentation; 20 import android.content.Context; 21 import android.graphics.Point; 22 import android.os.SystemClock; 23 import android.util.Log; 24 import android.util.SparseArray; 25 import android.view.InputDevice; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewConfiguration; 29 import android.view.ViewGroup; 30 import android.view.ViewTreeObserver; 31 32 import androidx.annotation.Nullable; 33 import androidx.test.rule.ActivityTestRule; 34 import androidx.test.uiautomator.UiDevice; 35 36 import java.util.Objects; 37 38 /** 39 * Test utilities for touch emulation. 40 */ 41 public final class CtsTouchUtils { 42 43 private static final String TAG = CtsTouchUtils.class.getSimpleName(); 44 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 45 46 private final UserHelper mUserHelper; 47 CtsTouchUtils(Context context)48 public CtsTouchUtils(Context context) { 49 this(new UserHelper(context)); 50 } 51 CtsTouchUtils(UserHelper userHelper)52 public CtsTouchUtils(UserHelper userHelper) { 53 mUserHelper = Objects.requireNonNull(userHelper); 54 if (DEBUG) { 55 Log.d(TAG, "Creating CtsTouchUtils() for " + userHelper); 56 } 57 } 58 59 /** 60 * Interface definition for a callback to be invoked when an event has been injected. 61 */ 62 public interface EventInjectionListener { 63 /** 64 * Callback method to be invoked when a {MotionEvent#ACTION_DOWN} has been injected. 65 * @param xOnScreen X coordinate of the injected event. 66 * @param yOnScreen Y coordinate of the injected event. 67 */ onDownInjected(int xOnScreen, int yOnScreen)68 public void onDownInjected(int xOnScreen, int yOnScreen); 69 70 /** 71 * Callback method to be invoked when a {MotionEvent#ACTION_MOVE} has been injected. 72 * @param xOnScreen X coordinates of the injected event. 73 * @param yOnScreen Y coordinates of the injected event. 74 */ onMoveInjected(int[] xOnScreen, int[] yOnScreen)75 public void onMoveInjected(int[] xOnScreen, int[] yOnScreen); 76 77 /** 78 * Callback method to be invoked when a {MotionEvent#ACTION_UP} has been injected. 79 * @param xOnScreen X coordinate of the injected event. 80 * @param yOnScreen Y coordinate of the injected event. 81 */ onUpInjected(int xOnScreen, int yOnScreen)82 public void onUpInjected(int xOnScreen, int yOnScreen); 83 } 84 85 /** 86 * Emulates a tap in the center of the passed {@link View}. 87 * 88 * @param instrumentation the instrumentation used to run the test 89 * @param view the view to "tap" 90 */ emulateTapOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view)91 public void emulateTapOnViewCenter(Instrumentation instrumentation, 92 ActivityTestRule<?> activityTestRule, View view) { 93 emulateTapOnView(instrumentation, activityTestRule, view, view.getWidth() / 2, 94 view.getHeight() / 2); 95 } 96 97 /** 98 * Emulates a tap on a point relative to the top-left corner of the passed {@link View}. Offset 99 * parameters are used to compute the final screen coordinates of the tap point. 100 * 101 * @param instrumentation the instrumentation used to run the test 102 * @param anchorView the anchor view to determine the tap location on the screen 103 * @param offsetX extra X offset for the tap 104 * @param offsetY extra Y offset for the tap 105 */ emulateTapOnView(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View anchorView, int offsetX, int offsetY)106 public void emulateTapOnView(Instrumentation instrumentation, 107 ActivityTestRule<?> activityTestRule, View anchorView, 108 int offsetX, int offsetY) { 109 final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop(); 110 // Get anchor coordinates on the screen 111 final int[] viewOnScreenXY = new int[2]; 112 anchorView.getLocationOnScreen(viewOnScreenXY); 113 int xOnScreen = viewOnScreenXY[0] + offsetX; 114 int yOnScreen = viewOnScreenXY[1] + offsetY; 115 final long downTime = SystemClock.uptimeMillis(); 116 117 injectDownEvent(instrumentation, downTime, xOnScreen, yOnScreen, 118 /* eventInjectionListener= */ null); 119 injectMoveEventForTap(instrumentation, downTime, touchSlop, xOnScreen, yOnScreen); 120 injectUpEvent(instrumentation, downTime, /* useCurrentEventTime= */ false, 121 xOnScreen, yOnScreen, /* eventInjectionListener= */ null); 122 123 // Wait for the system to process all events in the queue 124 if (activityTestRule != null) { 125 WidgetTestUtils.runOnMainAndDrawSync(activityTestRule, 126 activityTestRule.getActivity().getWindow().getDecorView(), null); 127 } else { 128 instrumentation.waitForIdleSync(); 129 } 130 } 131 132 /** 133 * Emulates a double tap in the center of the passed {@link View}. 134 * 135 * @param instrumentation the instrumentation used to run the test 136 * @param view the view to "double tap" 137 */ emulateDoubleTapOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view)138 public void emulateDoubleTapOnViewCenter(Instrumentation instrumentation, 139 ActivityTestRule<?> activityTestRule, View view) { 140 emulateDoubleTapOnView(instrumentation, activityTestRule, view, view.getWidth() / 2, 141 view.getHeight() / 2); 142 } 143 144 /** 145 * Emulates a double tap on a point relative to the top-left corner of the passed {@link View}. 146 * Offset parameters are used to compute the final screen coordinates of the tap points. 147 * 148 * @param instrumentation the instrumentation used to run the test 149 * @param anchorView the anchor view to determine the tap location on the screen 150 * @param offsetX extra X offset for the taps 151 * @param offsetY extra Y offset for the taps 152 */ emulateDoubleTapOnView(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View anchorView, int offsetX, int offsetY)153 public void emulateDoubleTapOnView(Instrumentation instrumentation, 154 ActivityTestRule<?> activityTestRule, View anchorView, 155 int offsetX, int offsetY) { 156 final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop(); 157 // Get anchor coordinates on the screen 158 final int[] viewOnScreenXY = new int[2]; 159 anchorView.getLocationOnScreen(viewOnScreenXY); 160 int xOnScreen = viewOnScreenXY[0] + offsetX; 161 int yOnScreen = viewOnScreenXY[1] + offsetY; 162 final long downTime = SystemClock.uptimeMillis(); 163 164 injectDownEvent(instrumentation, downTime, xOnScreen, yOnScreen, null); 165 injectMoveEventForTap(instrumentation, downTime, touchSlop, xOnScreen, yOnScreen); 166 injectUpEvent(instrumentation, downTime, false, xOnScreen, yOnScreen, null); 167 injectDownEvent(instrumentation, downTime, xOnScreen, yOnScreen, null); 168 injectMoveEventForTap(instrumentation, downTime, touchSlop, xOnScreen, yOnScreen); 169 injectUpEvent(instrumentation, downTime, false, xOnScreen, yOnScreen, null); 170 171 // Wait for the system to process all events in the queue 172 if (activityTestRule != null) { 173 WidgetTestUtils.runOnMainAndDrawSync(activityTestRule, 174 activityTestRule.getActivity().getWindow().getDecorView(), null); 175 } else { 176 instrumentation.waitForIdleSync(); 177 } 178 } 179 180 /** 181 * Emulates a linear drag gesture between 2 points across the screen. 182 * 183 * @param instrumentation the instrumentation used to run the test 184 * @param dragStartX Start X of the emulated drag gesture 185 * @param dragStartY Start Y of the emulated drag gesture 186 * @param dragAmountX X amount of the emulated drag gesture 187 * @param dragAmountY Y amount of the emulated drag gesture 188 */ emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY)189 public void emulateDragGesture(Instrumentation instrumentation, 190 ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, 191 int dragAmountX, int dragAmountY) { 192 emulateDragGesture(instrumentation, activityTestRule, 193 dragStartX, dragStartY, dragAmountX, dragAmountY, /* dragDurationMs= */ 2000, 194 /* moveEventCount= */ 20, /* eventInjectionListener= */ null); 195 } 196 emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, int dragDurationMs, int moveEventCount)197 private void emulateDragGesture(Instrumentation instrumentation, 198 ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, 199 int dragAmountY, int dragDurationMs, int moveEventCount) { 200 emulateDragGesture(instrumentation, activityTestRule, dragStartX, dragStartY, dragAmountX, 201 dragAmountY, dragDurationMs, moveEventCount, /* eventInjectionListener= */ null); 202 } 203 204 /** 205 * Emulates a linear drag gesture between 2 points across the screen. 206 * 207 * @param instrumentation the instrumentation used to run the test 208 * @param dragStartX Start X of the emulated drag gesture 209 * @param dragStartY Start Y of the emulated drag gesture 210 * @param dragAmountX X amount of the emulated drag gesture 211 * @param dragAmountY Y amount of the emulated drag gesture 212 * @param dragDurationMs The time in milliseconds over which the drag occurs 213 * @param moveEventCount The number of events that produce the movement 214 * @param eventInjectionListener Called after each down, move, and up events. 215 */ emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, int dragDurationMs, int moveEventCount, @Nullable EventInjectionListener eventInjectionListener)216 public void emulateDragGesture(Instrumentation instrumentation, 217 ActivityTestRule<?> activityTestRule, 218 int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, 219 int dragDurationMs, int moveEventCount, 220 @Nullable EventInjectionListener eventInjectionListener) { 221 emulateDragGesture(instrumentation, activityTestRule, dragStartX, dragStartY, dragAmountX, 222 dragAmountY, dragDurationMs, moveEventCount, /* waitForAnimations= */ true, 223 eventInjectionListener); 224 } 225 226 /** 227 * Emulates a linear drag gesture between 2 points across the screen. 228 * 229 * @param instrumentation the instrumentation used to run the test 230 * @param dragStartX Start X of the emulated drag gesture 231 * @param dragStartY Start Y of the emulated drag gesture 232 * @param dragAmountX X amount of the emulated drag gesture 233 * @param dragAmountY Y amount of the emulated drag gesture 234 * @param dragDurationMs The time in milliseconds over which the drag occurs 235 * @param moveEventCount The number of events that produce the movement 236 * @param waitForAnimations wait for animations to complete before sending an event 237 * @param eventInjectionListener Called after each down, move, and up events. 238 */ emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, int dragDurationMs, int moveEventCount, boolean waitForAnimations, @Nullable EventInjectionListener eventInjectionListener)239 public void emulateDragGesture(Instrumentation instrumentation, 240 ActivityTestRule<?> activityTestRule, 241 int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, 242 int dragDurationMs, int moveEventCount, 243 boolean waitForAnimations, @Nullable EventInjectionListener eventInjectionListener) { 244 final long downTime = SystemClock.uptimeMillis(); 245 246 injectDownEvent(instrumentation, downTime, dragStartX, dragStartY, 247 eventInjectionListener); 248 249 // Inject a sequence of MOVE events that emulate the "move" part of the gesture 250 injectMoveEventsForDrag(instrumentation, downTime, dragStartX, dragStartY, 251 dragStartX + dragAmountX, dragStartY + dragAmountY, moveEventCount, 252 dragDurationMs, waitForAnimations, eventInjectionListener); 253 254 injectUpEvent(instrumentation, downTime, true, dragStartX + dragAmountX, 255 dragStartY + dragAmountY, eventInjectionListener); 256 257 // Wait for the system to process all events in the queue 258 if (activityTestRule != null) { 259 WidgetTestUtils.runOnMainAndDrawSync(activityTestRule, 260 activityTestRule.getActivity().getWindow().getDecorView(), null); 261 } else { 262 instrumentation.waitForIdleSync(); 263 } 264 } 265 266 /** 267 * Emulates a series of linear drag gestures across the screen between multiple points without 268 * lifting the finger. Note that this function does not support curve movements between the 269 * points. 270 * 271 * @param instrumentation the instrumentation used to run the test 272 * @param coordinates the ordered list of points for the drag gesture 273 */ emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, SparseArray<Point> coordinates)274 public void emulateDragGesture(Instrumentation instrumentation, 275 ActivityTestRule<?> activityTestRule, SparseArray<Point> coordinates) { 276 final int moveEventCount = 20; 277 int dragDurationMs = 2000; 278 279 final int touchSlop = ViewConfiguration.get( 280 activityTestRule.getActivity()).getScaledTouchSlop(); 281 final long longPressTimeoutMs = ViewConfiguration.getLongPressTimeout(); 282 final int maxDragDurationMs = getMaxDragDuration(touchSlop, longPressTimeoutMs, coordinates, 283 moveEventCount); 284 if (maxDragDurationMs < dragDurationMs) { 285 Log.d(TAG, "emulateDragGesture: Lowering standard drag duration from " + dragDurationMs 286 + " ms to " + maxDragDurationMs + " ms to avoid triggering a long press "); 287 dragDurationMs = maxDragDurationMs; 288 } 289 290 emulateDragGesture(instrumentation, activityTestRule, coordinates, dragDurationMs, 291 moveEventCount); 292 } 293 294 /** 295 * Get the maximal drag duration that assures not triggering a long press during a drag gesture 296 * considering long press timeout and touch slop. 297 * 298 * The calculation is based on the distance between the first and the second point of provided 299 * coordinates. 300 */ getMaxDragDuration(int touchSlop, long longPressTimeoutMs, SparseArray<Point> coordinates, int moveEventCount)301 private int getMaxDragDuration(int touchSlop, long longPressTimeoutMs, 302 SparseArray<Point> coordinates, int moveEventCount) { 303 final int coordinatesSize = coordinates.size(); 304 if (coordinatesSize < 2) { 305 throw new IllegalArgumentException("Need at least 2 points for emulating drag"); 306 } 307 308 final int deltaX = coordinates.get(0).x - coordinates.get(1).x; 309 final int deltaY = coordinates.get(0).y - coordinates.get(1).y; 310 final double dragDistance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); 311 final double moveEventDistance = (double) dragDistance / moveEventCount; 312 313 // Number of move events needed to drag outside of the touch slop. 314 // The initial sleep before the drag gesture begins is considered by adding one extra event. 315 final double neededMoveEventsToExceedTouchSlop = touchSlop / moveEventDistance + 1; 316 317 // Get maximal drag duration that assures a drag speed that does not trigger a long press. 318 // Multiply with 0.9 to be on the safe side. 319 int maxDragDuration = (int) (longPressTimeoutMs * 0.9 * moveEventCount 320 / neededMoveEventsToExceedTouchSlop); 321 return maxDragDuration; 322 } 323 emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, SparseArray<Point> coordinates, int dragDurationMs, int moveEventCount)324 private void emulateDragGesture(Instrumentation instrumentation, 325 ActivityTestRule<?> activityTestRule, 326 SparseArray<Point> coordinates, int dragDurationMs, int moveEventCount) { 327 final int coordinatesSize = coordinates.size(); 328 if (coordinatesSize < 2) { 329 throw new IllegalArgumentException("Need at least 2 points for emulating drag"); 330 } 331 332 final long downTime = SystemClock.uptimeMillis(); 333 334 injectDownEvent(instrumentation, downTime, coordinates.get(0).x, coordinates.get(0).y, 335 /* eventInjectionListener= */ null); 336 337 // Move to each coordinate. 338 for (int i = 0; i < coordinatesSize - 1; i++) { 339 // Inject a sequence of MOVE events that emulate the "move" part of the gesture. 340 injectMoveEventsForDrag(instrumentation, 341 downTime, 342 coordinates.get(i).x, 343 coordinates.get(i).y, 344 coordinates.get(i + 1).x, 345 coordinates.get(i + 1).y, 346 moveEventCount, 347 dragDurationMs, 348 true, 349 null); 350 } 351 352 injectUpEvent(instrumentation, 353 downTime, 354 true, 355 coordinates.get(coordinatesSize - 1).x, 356 coordinates.get(coordinatesSize - 1).y, 357 null); 358 359 // Wait for the system to process all events in the queue 360 if (activityTestRule != null) { 361 WidgetTestUtils.runOnMainAndDrawSync(activityTestRule, 362 activityTestRule.getActivity().getWindow().getDecorView(), null); 363 } else { 364 instrumentation.waitForIdleSync(); 365 } 366 } 367 368 /** 369 * Injects an {@link MotionEvent#ACTION_DOWN} event at the given coordinates. 370 * 371 * @param uiAutomation the uiAutomation used to run the test 372 * @param downTime The time of the event, usually from {@link SystemClock#uptimeMillis()} 373 * @param xOnScreen The x screen coordinate to press on 374 * @param yOnScreen The y screen coordinate to press on 375 * @param eventInjectionListener The listener to call back immediately after the down was 376 * sent. 377 * 378 * @return <code>downTime</code> 379 */ injectDownEvent(Instrumentation instrumentation, long downTime, int xOnScreen, int yOnScreen, @Nullable EventInjectionListener eventInjectionListener)380 public long injectDownEvent(Instrumentation instrumentation, long downTime, int xOnScreen, 381 int yOnScreen, @Nullable EventInjectionListener eventInjectionListener) { 382 MotionEvent eventDown = MotionEvent.obtain( 383 downTime, downTime, MotionEvent.ACTION_DOWN, xOnScreen, yOnScreen, 1); 384 injectDisplayIdIfNeeded(eventDown); 385 eventDown.setSource(InputDevice.SOURCE_TOUCHSCREEN); 386 injectPointerEvent(instrumentation, eventDown); 387 388 if (eventInjectionListener != null) { 389 eventInjectionListener.onDownInjected(xOnScreen, yOnScreen); 390 } 391 eventDown.recycle(); 392 return downTime; 393 } 394 injectMoveEventForTap(Instrumentation instrumentation, long downTime, int touchSlop, int xOnScreen, int yOnScreen)395 private void injectMoveEventForTap(Instrumentation instrumentation, long downTime, 396 int touchSlop, int xOnScreen, int yOnScreen) { 397 MotionEvent eventMove = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_MOVE, 398 xOnScreen + (touchSlop / 2.0f), yOnScreen + (touchSlop / 2.0f), 1); 399 injectDisplayIdIfNeeded(eventMove); 400 eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN); 401 402 injectPointerEvent(instrumentation, eventMove); 403 eventMove.recycle(); 404 } 405 injectMoveEventsForDrag(Instrumentation instrumentation, long downTime, int dragStartX, int dragStartY, int dragEndX, int dragEndY, int moveEventCount, int dragDurationMs, boolean waitForAnimations, EventInjectionListener eventInjectionListener)406 private void injectMoveEventsForDrag(Instrumentation instrumentation, long downTime, 407 int dragStartX, int dragStartY, int dragEndX, int dragEndY, int moveEventCount, 408 int dragDurationMs, boolean waitForAnimations, 409 EventInjectionListener eventInjectionListener) { 410 411 final int dragAmountX = dragEndX - dragStartX; 412 final int dragAmountY = dragEndY - dragStartY; 413 final int sleepTime = dragDurationMs / moveEventCount; 414 415 long prevEventTime = downTime; 416 417 for (int i = 0; i < moveEventCount; i++) { 418 // Note that the first MOVE event is generated "away" from the coordinates 419 // of the start / DOWN event, and the last MOVE event is generated 420 // at the same coordinates as the subsequent UP event. 421 final int moveX = dragStartX + dragAmountX * (i + 1) / moveEventCount; 422 final int moveY = dragStartY + dragAmountY * (i + 1) / moveEventCount; 423 // Sleep for a bit to emulate the overall drag gesture. Take into account the amount 424 // of time we already spent injecting the event (since the injection could be 425 // synchronous). 426 final long remainingSleep = sleepTime - (SystemClock.uptimeMillis() - prevEventTime); 427 SystemClock.sleep(Math.max(0, remainingSleep)); 428 final long eventTime = SystemClock.uptimeMillis(); 429 430 // If necessary, generate history for our next MOVE event. The history is generated 431 // to be spaced at 10 millisecond intervals, interpolating the coordinates from the 432 // last generated MOVE event to our current one. 433 int historyEventCount = (int) ((eventTime - prevEventTime) / 10); 434 int[] xCoordsForListener = (eventInjectionListener == null) ? null : 435 new int[Math.max(1, historyEventCount)]; 436 int[] yCoordsForListener = (eventInjectionListener == null) ? null : 437 new int[Math.max(1, historyEventCount)]; 438 MotionEvent eventMove = null; 439 if (historyEventCount == 0) { 440 eventMove = MotionEvent.obtain( 441 downTime, eventTime, MotionEvent.ACTION_MOVE, moveX, moveY, 1); 442 injectDisplayIdIfNeeded(eventMove); 443 if (eventInjectionListener != null) { 444 xCoordsForListener[0] = moveX; 445 yCoordsForListener[0] = moveY; 446 } 447 } else { 448 final int prevMoveX = dragStartX + dragAmountX * i / moveEventCount; 449 final int prevMoveY = dragStartY + dragAmountY * i / moveEventCount; 450 final int deltaMoveX = moveX - prevMoveX; 451 final int deltaMoveY = moveY - prevMoveY; 452 final long deltaTime = (eventTime - prevEventTime); 453 for (int historyIndex = 0; historyIndex < historyEventCount; historyIndex++) { 454 int stepMoveX = prevMoveX + deltaMoveX * (historyIndex + 1) / historyEventCount; 455 int stepMoveY = prevMoveY + deltaMoveY * (historyIndex + 1) / historyEventCount; 456 long stepEventTime = 457 prevEventTime + deltaTime * (historyIndex + 1) / historyEventCount; 458 if (historyIndex == 0) { 459 // Generate the first event in our sequence 460 eventMove = MotionEvent.obtain(downTime, stepEventTime, 461 MotionEvent.ACTION_MOVE, stepMoveX, stepMoveY, 1); 462 injectDisplayIdIfNeeded(eventMove); 463 } else { 464 // and then add to it 465 eventMove.addBatch(stepEventTime, stepMoveX, stepMoveY, 1.0f, 1.0f, 1); 466 } 467 if (eventInjectionListener != null) { 468 xCoordsForListener[historyIndex] = stepMoveX; 469 yCoordsForListener[historyIndex] = stepMoveY; 470 } 471 } 472 } 473 474 eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN); 475 injectPointerEvent(instrumentation, eventMove); 476 477 if (eventInjectionListener != null) { 478 eventInjectionListener.onMoveInjected(xCoordsForListener, yCoordsForListener); 479 } 480 eventMove.recycle(); 481 prevEventTime = eventTime; 482 } 483 } 484 485 /** 486 * Injects an {@link MotionEvent#ACTION_UP} event at the given coordinates. 487 * 488 * @param instrumentation the Instrumentation used to run the test 489 * @param downTime The time of the event, usually from {@link SystemClock#uptimeMillis()} 490 * @param useCurrentEventTime <code>true</code> if it should use the current time for the 491 * up event or <code>false</code> to use <code>downTime</code>. 492 * @param xOnScreen The x screen coordinate to press on 493 * @param yOnScreen The y screen coordinate to press on 494 * @param eventInjectionListener The listener to call back immediately after the up was 495 * sent. 496 */ injectUpEvent(Instrumentation instrumentation, long downTime, boolean useCurrentEventTime, int xOnScreen, int yOnScreen, EventInjectionListener eventInjectionListener)497 public void injectUpEvent(Instrumentation instrumentation, long downTime, 498 boolean useCurrentEventTime, int xOnScreen, int yOnScreen, 499 EventInjectionListener eventInjectionListener) { 500 long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime; 501 MotionEvent eventUp = MotionEvent.obtain( 502 downTime, eventTime, MotionEvent.ACTION_UP, xOnScreen, yOnScreen, 1); 503 injectDisplayIdIfNeeded(eventUp); 504 eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN); 505 injectPointerEvent(instrumentation, eventUp); 506 507 if (eventInjectionListener != null) { 508 eventInjectionListener.onUpInjected(xOnScreen, yOnScreen); 509 } 510 eventUp.recycle(); 511 } 512 injectPointerEvent(Instrumentation instrumentation, MotionEvent event)513 private void injectPointerEvent(Instrumentation instrumentation, MotionEvent event) { 514 instrumentation.sendPointerSync(event); 515 } 516 517 /** 518 * Emulates a fling gesture across the horizontal center of the passed view. 519 * 520 * @param instrumentation the instrumentation used to run the test 521 * @param view the view to fling 522 * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will 523 * be a downwards gesture 524 * @return The vertical amount of emulated fling in pixels 525 */ emulateFlingGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture)526 public int emulateFlingGesture(Instrumentation instrumentation, 527 ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture) { 528 return emulateFlingGesture(instrumentation, activityTestRule, 529 view, isDownwardsFlingGesture, null); 530 } 531 532 /** 533 * Emulates a fling gesture across the horizontal center of the passed view. 534 * 535 * @param instrumentation the instrumentation used to run the test 536 * @param view the view to fling 537 * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will 538 * be a downwards gesture 539 * @param eventInjectionListener optional listener to notify about the injected events 540 * @return The vertical amount of emulated fling in pixels 541 */ emulateFlingGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture, EventInjectionListener eventInjectionListener)542 public int emulateFlingGesture(Instrumentation instrumentation, 543 ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture, 544 EventInjectionListener eventInjectionListener) { 545 return emulateFlingGesture(instrumentation, activityTestRule, view, isDownwardsFlingGesture, 546 true, eventInjectionListener); 547 } 548 549 /** 550 * Emulates a fling gesture across the horizontal center of the passed view. 551 * 552 * @param instrumentation the instrumentation used to run the test 553 * @param view the view to fling 554 * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will 555 * be a downwards gesture 556 * @param waitForAnimations wait for animations to complete before sending an event 557 * @param eventInjectionListener optional listener to notify about the injected events 558 * @return The vertical amount of emulated fling in pixels 559 */ emulateFlingGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture, boolean waitForAnimations, EventInjectionListener eventInjectionListener)560 public int emulateFlingGesture(Instrumentation instrumentation, 561 ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture, 562 boolean waitForAnimations, EventInjectionListener eventInjectionListener) { 563 final ViewConfiguration configuration = ViewConfiguration.get(view.getContext()); 564 final int flingVelocity = (configuration.getScaledMinimumFlingVelocity() + 565 configuration.getScaledMaximumFlingVelocity()) / 2; 566 // Get view coordinates on the screen 567 final int[] viewOnScreenXY = new int[2]; 568 view.getLocationOnScreen(viewOnScreenXY); 569 570 // Our fling gesture will be from 25% height of the view to 75% height of the view 571 // for downwards fling gesture, and the other way around for upwards fling gesture 572 final int viewHeight = view.getHeight(); 573 final int x = viewOnScreenXY[0] + view.getWidth() / 2; 574 final int startY = isDownwardsFlingGesture ? viewOnScreenXY[1] + viewHeight / 4 575 : viewOnScreenXY[1] + 3 * viewHeight / 4; 576 final int amountY = isDownwardsFlingGesture ? viewHeight / 2 : -viewHeight / 2; 577 578 // Compute fling gesture duration based on the distance (50% height of the view) and 579 // fling velocity 580 final int durationMs = (1000 * viewHeight) / (2 * flingVelocity); 581 582 // And do the same event injection sequence as our generic drag gesture 583 emulateDragGesture(instrumentation, activityTestRule, 584 x, startY, 0, amountY, durationMs, durationMs / 16, 585 waitForAnimations, eventInjectionListener); 586 587 return amountY; 588 } 589 590 private static final class ViewStateSnapshot { 591 final View mFirst; 592 final View mLast; 593 final int mFirstTop; 594 final int mLastBottom; 595 final int mChildCount; ViewStateSnapshot(ViewGroup viewGroup)596 private ViewStateSnapshot(ViewGroup viewGroup) { 597 mChildCount = viewGroup.getChildCount(); 598 if (mChildCount == 0) { 599 mFirst = mLast = null; 600 mFirstTop = mLastBottom = Integer.MIN_VALUE; 601 } else { 602 mFirst = viewGroup.getChildAt(0); 603 mLast = viewGroup.getChildAt(mChildCount - 1); 604 mFirstTop = mFirst.getTop(); 605 mLastBottom = mLast.getBottom(); 606 } 607 } 608 609 @Override equals(Object o)610 public boolean equals(Object o) { 611 if (this == o) { 612 return true; 613 } 614 if (o == null || getClass() != o.getClass()) { 615 return false; 616 } 617 618 final ViewStateSnapshot that = (ViewStateSnapshot) o; 619 return mFirstTop == that.mFirstTop && 620 mLastBottom == that.mLastBottom && 621 mFirst == that.mFirst && 622 mLast == that.mLast && 623 mChildCount == that.mChildCount; 624 } 625 626 @Override hashCode()627 public int hashCode() { 628 int result = mFirst != null ? mFirst.hashCode() : 0; 629 result = 31 * result + (mLast != null ? mLast.hashCode() : 0); 630 result = 31 * result + mFirstTop; 631 result = 31 * result + mLastBottom; 632 result = 31 * result + mChildCount; 633 return result; 634 } 635 } 636 637 /** 638 * Emulates a scroll to the bottom of the specified {@link ViewGroup}. 639 * 640 * @param instrumentation the instrumentation used to run the test 641 * @param viewGroup View group 642 */ emulateScrollToBottom(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, ViewGroup viewGroup)643 public void emulateScrollToBottom(Instrumentation instrumentation, 644 ActivityTestRule<?> activityTestRule, ViewGroup viewGroup) throws Throwable { 645 final int[] viewGroupOnScreenXY = new int[2]; 646 viewGroup.getLocationOnScreen(viewGroupOnScreenXY); 647 648 final int emulatedX = viewGroupOnScreenXY[0] + viewGroup.getWidth() / 2; 649 final int emulatedStartY = viewGroupOnScreenXY[1] + 3 * viewGroup.getHeight() / 4; 650 final int swipeAmount = viewGroup.getHeight() / 2; 651 652 ViewStateSnapshot prev; 653 ViewStateSnapshot next = new ViewStateSnapshot(viewGroup); 654 do { 655 prev = next; 656 emulateDragGesture(instrumentation, activityTestRule, 657 emulatedX, emulatedStartY, 0, -swipeAmount, 300, 10); 658 next = new ViewStateSnapshot(viewGroup); 659 // Wait for the UI Thread to become idle. 660 final UiDevice device = UiDevice.getInstance(instrumentation); 661 device.waitForIdle(); 662 } while (!prev.equals(next)); 663 664 // wait until the overscroll animation completes 665 final boolean[] redrawn = new boolean[1]; 666 final boolean[] animationFinished = new boolean[1]; 667 final ViewTreeObserver.OnDrawListener onDrawListener = () -> { 668 redrawn[0] = true; 669 }; 670 671 activityTestRule.runOnUiThread(() -> { 672 viewGroup.getViewTreeObserver().addOnDrawListener(onDrawListener); 673 }); 674 while (!animationFinished[0]) { 675 activityTestRule.runOnUiThread(() -> { 676 if (!redrawn[0]) { 677 animationFinished[0] = true; 678 } 679 redrawn[0] = false; 680 }); 681 } 682 activityTestRule.runOnUiThread(() -> { 683 viewGroup.getViewTreeObserver().removeOnDrawListener(onDrawListener); 684 }); 685 } 686 687 /** 688 * Emulates a long press in the center of the passed {@link View}. 689 * 690 * @param instrumentation the instrumentation used to run the test 691 * @param view the view to "long press" 692 */ emulateLongPressOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view)693 public void emulateLongPressOnViewCenter(Instrumentation instrumentation, 694 ActivityTestRule<?> activityTestRule, View view) { 695 emulateLongPressOnViewCenter(instrumentation, activityTestRule, view, 0); 696 } 697 698 /** 699 * Emulates a long press in the center of the passed {@link View}. 700 * 701 * @param instrumentation the instrumentation used to run the test 702 * @param view the view to "long press" 703 * @param extraWaitMs the duration of emulated "long press" in milliseconds starting 704 * after system-level long press timeout. 705 */ emulateLongPressOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, long extraWaitMs)706 public void emulateLongPressOnViewCenter(Instrumentation instrumentation, 707 ActivityTestRule<?> activityTestRule, View view, long extraWaitMs) { 708 final int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop(); 709 // Use instrumentation to emulate a tap on the spinner to bring down its popup 710 final int[] viewOnScreenXY = new int[2]; 711 view.getLocationOnScreen(viewOnScreenXY); 712 int xOnScreen = viewOnScreenXY[0] + view.getWidth() / 2; 713 int yOnScreen = viewOnScreenXY[1] + view.getHeight() / 2; 714 715 emulateLongPressOnScreen(instrumentation, activityTestRule, 716 xOnScreen, yOnScreen, touchSlop, extraWaitMs, true); 717 } 718 719 /** 720 * Emulates a long press confirmed on a point relative to the top-left corner of the passed 721 * {@link View}. Offset parameters are used to compute the final screen coordinates of the 722 * press point. 723 * 724 * @param instrumentation the instrumentation used to run the test 725 * @param view the view to "long press" 726 * @param offsetX extra X offset for the tap 727 * @param offsetY extra Y offset for the tap 728 */ emulateLongPressOnView(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, int offsetX, int offsetY)729 public void emulateLongPressOnView(Instrumentation instrumentation, 730 ActivityTestRule<?> activityTestRule, View view, int offsetX, int offsetY) { 731 final int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop(); 732 final int[] viewOnScreenXY = new int[2]; 733 view.getLocationOnScreen(viewOnScreenXY); 734 int xOnScreen = viewOnScreenXY[0] + offsetX; 735 int yOnScreen = viewOnScreenXY[1] + offsetY; 736 737 emulateLongPressOnScreen(instrumentation, activityTestRule, 738 xOnScreen, yOnScreen, touchSlop, 0, true); 739 } 740 741 /** 742 * Emulates a long press then a linear drag gesture between 2 points across the screen. 743 * This is used for drag selection. 744 * 745 * @param instrumentation the instrumentation used to run the test 746 * @param dragStartX Start X of the emulated drag gesture 747 * @param dragStartY Start Y of the emulated drag gesture 748 * @param dragAmountX X amount of the emulated drag gesture 749 * @param dragAmountY Y amount of the emulated drag gesture 750 */ emulateLongPressAndDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY)751 public void emulateLongPressAndDragGesture(Instrumentation instrumentation, 752 ActivityTestRule<?> activityTestRule, 753 int dragStartX, int dragStartY, int dragAmountX, int dragAmountY) { 754 emulateLongPressOnScreen(instrumentation, activityTestRule, dragStartX, dragStartY, 755 0 /* touchSlop */, 0 /* extraWaitMs */, false /* upGesture */); 756 emulateDragGesture(instrumentation, activityTestRule, dragStartX, dragStartY, dragAmountX, 757 dragAmountY); 758 } 759 760 /** 761 * Emulates a long press on the screen. 762 * 763 * @param instrumentation the instrumentation used to run the test 764 * @param xOnScreen X position on screen for the "long press" 765 * @param yOnScreen Y position on screen for the "long press" 766 * @param extraWaitMs extra duration of emulated long press in milliseconds added 767 * after the system-level "long press" timeout. 768 * @param upGesture whether to include an up event. 769 */ emulateLongPressOnScreen(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int xOnScreen, int yOnScreen, int touchSlop, long extraWaitMs, boolean upGesture)770 private void emulateLongPressOnScreen(Instrumentation instrumentation, 771 ActivityTestRule<?> activityTestRule, 772 int xOnScreen, int yOnScreen, int touchSlop, long extraWaitMs, boolean upGesture) { 773 final long downTime = SystemClock.uptimeMillis(); 774 775 injectDownEvent(instrumentation, downTime, xOnScreen, yOnScreen, null); 776 injectMoveEventForTap(instrumentation, downTime, touchSlop, xOnScreen, yOnScreen); 777 SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f) + extraWaitMs); 778 if (upGesture) { 779 injectUpEvent(instrumentation, downTime, false, xOnScreen, yOnScreen, null); 780 } 781 782 // Wait for the system to process all events in the queue 783 if (activityTestRule != null) { 784 WidgetTestUtils.runOnMainAndDrawSync(activityTestRule, 785 activityTestRule.getActivity().getWindow().getDecorView(), null); 786 } else { 787 instrumentation.waitForIdleSync(); 788 } 789 } 790 injectDisplayIdIfNeeded(MotionEvent event)791 private void injectDisplayIdIfNeeded(MotionEvent event) { 792 mUserHelper.injectDisplayIdIfNeeded(event); 793 } 794 } 795