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