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