1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package android.widget.espresso; 18 19 import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom; 20 import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; 21 22 import static com.android.internal.util.Preconditions.checkNotNull; 23 24 import static org.hamcrest.Matchers.allOf; 25 26 import android.annotation.Nullable; 27 import android.os.SystemClock; 28 import android.util.Log; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewConfiguration; 32 33 import androidx.test.espresso.PerformException; 34 import androidx.test.espresso.UiController; 35 import androidx.test.espresso.ViewAction; 36 import androidx.test.espresso.action.CoordinatesProvider; 37 import androidx.test.espresso.action.MotionEvents; 38 import androidx.test.espresso.action.PrecisionDescriber; 39 import androidx.test.espresso.action.Swiper; 40 import androidx.test.espresso.util.HumanReadables; 41 42 import org.hamcrest.Matcher; 43 44 /** 45 * Drags on a View using touch events.<br> 46 * <br> 47 * View constraints: 48 * <ul> 49 * <li>must be displayed on screen 50 * <ul> 51 */ 52 public final class DragAction implements ViewAction { 53 public interface Dragger extends Swiper { wrapUiController(UiController uiController)54 UiController wrapUiController(UiController uiController); 55 } 56 57 /** 58 * Executes different drag types to given positions. 59 */ 60 public enum Drag implements Dragger { 61 62 /** 63 * Starts a drag with a mouse down. 64 */ 65 MOUSE_DOWN { 66 private DownMotionPerformer downMotion = new DownMotionPerformer() { 67 @Override 68 public MotionEvent perform( 69 UiController uiController, float[] coordinates, float[] precision) { 70 MotionEvent downEvent = MotionEvents.sendDown( 71 uiController, coordinates, precision) 72 .down; 73 return downEvent; 74 } 75 }; 76 77 @Override sendSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision)78 public Status sendSwipe( 79 UiController uiController, 80 float[] startCoordinates, float[] endCoordinates, float[] precision) { 81 return sendLinearDrag( 82 uiController, downMotion, startCoordinates, endCoordinates, precision); 83 } 84 85 @Override toString()86 public String toString() { 87 return "mouse down and drag"; 88 } 89 90 @Override wrapUiController(UiController uiController)91 public UiController wrapUiController(UiController uiController) { 92 return new MouseUiController(uiController); 93 } 94 }, 95 96 /** 97 * Starts a drag with a mouse double click. 98 */ 99 MOUSE_DOUBLE_CLICK { 100 private DownMotionPerformer downMotion = new DownMotionPerformer() { 101 @Override 102 @Nullable 103 public MotionEvent perform( 104 UiController uiController, float[] coordinates, float[] precision) { 105 return performDoubleTap(uiController, coordinates, precision); 106 } 107 }; 108 109 @Override sendSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision)110 public Status sendSwipe( 111 UiController uiController, 112 float[] startCoordinates, float[] endCoordinates, float[] precision) { 113 return sendLinearDrag( 114 uiController, downMotion, startCoordinates, endCoordinates, precision); 115 } 116 117 @Override toString()118 public String toString() { 119 return "mouse double click and drag to select"; 120 } 121 122 @Override wrapUiController(UiController uiController)123 public UiController wrapUiController(UiController uiController) { 124 return new MouseUiController(uiController); 125 } 126 }, 127 128 /** 129 * Starts a drag with a mouse long click. 130 */ 131 MOUSE_LONG_CLICK { 132 private DownMotionPerformer downMotion = new DownMotionPerformer() { 133 @Override 134 public MotionEvent perform( 135 UiController uiController, float[] coordinates, float[] precision) { 136 MotionEvent downEvent = MotionEvents.sendDown( 137 uiController, coordinates, precision) 138 .down; 139 return performLongPress(uiController, coordinates, precision); 140 } 141 }; 142 143 @Override sendSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision)144 public Status sendSwipe( 145 UiController uiController, 146 float[] startCoordinates, float[] endCoordinates, float[] precision) { 147 return sendLinearDrag( 148 uiController, downMotion, startCoordinates, endCoordinates, precision); 149 } 150 151 @Override toString()152 public String toString() { 153 return "mouse long click and drag to select"; 154 } 155 156 @Override wrapUiController(UiController uiController)157 public UiController wrapUiController(UiController uiController) { 158 return new MouseUiController(uiController); 159 } 160 }, 161 162 /** 163 * Starts a drag with a mouse triple click. 164 */ 165 MOUSE_TRIPLE_CLICK { 166 private DownMotionPerformer downMotion = new DownMotionPerformer() { 167 @Override 168 @Nullable 169 public MotionEvent perform( 170 UiController uiController, float[] coordinates, float[] precision) { 171 MotionEvent downEvent = MotionEvents.sendDown( 172 uiController, coordinates, precision) 173 .down; 174 for (int i = 0; i < 2; ++i) { 175 try { 176 if (!MotionEvents.sendUp(uiController, downEvent)) { 177 String logMessage = "Injection of up event as part of the triple " 178 + "click failed. Sending cancel event."; 179 Log.d(TAG, logMessage); 180 MotionEvents.sendCancel(uiController, downEvent); 181 return null; 182 } 183 184 long doubleTapMinimumTimeout = ViewConfiguration.getDoubleTapMinTime(); 185 uiController.loopMainThreadForAtLeast(doubleTapMinimumTimeout); 186 } finally { 187 downEvent.recycle(); 188 } 189 downEvent = MotionEvents.sendDown( 190 uiController, coordinates, precision).down; 191 } 192 return downEvent; 193 } 194 }; 195 196 @Override sendSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision)197 public Status sendSwipe( 198 UiController uiController, 199 float[] startCoordinates, float[] endCoordinates, float[] precision) { 200 return sendLinearDrag( 201 uiController, downMotion, startCoordinates, endCoordinates, precision); 202 } 203 204 @Override toString()205 public String toString() { 206 return "mouse triple click and drag to select"; 207 } 208 209 @Override wrapUiController(UiController uiController)210 public UiController wrapUiController(UiController uiController) { 211 return new MouseUiController(uiController); 212 } 213 }, 214 215 /** 216 * Starts a drag with a tap. 217 */ 218 TAP { 219 private DownMotionPerformer downMotion = new DownMotionPerformer() { 220 @Override 221 public MotionEvent perform( 222 UiController uiController, float[] coordinates, float[] precision) { 223 MotionEvent downEvent = MotionEvents.sendDown( 224 uiController, coordinates, precision) 225 .down; 226 return downEvent; 227 } 228 }; 229 230 @Override sendSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision)231 public Status sendSwipe( 232 UiController uiController, 233 float[] startCoordinates, float[] endCoordinates, float[] precision) { 234 return sendLinearDrag( 235 uiController, downMotion, startCoordinates, endCoordinates, precision); 236 } 237 238 @Override toString()239 public String toString() { 240 return "tap and drag"; 241 } 242 }, 243 244 /** 245 * Starts a drag with a long-press. 246 */ 247 LONG_PRESS { 248 private DownMotionPerformer downMotion = new DownMotionPerformer() { 249 @Override 250 public MotionEvent perform( 251 UiController uiController, float[] coordinates, float[] precision) { 252 return performLongPress(uiController, coordinates, precision); 253 } 254 }; 255 256 @Override sendSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision)257 public Status sendSwipe( 258 UiController uiController, 259 float[] startCoordinates, float[] endCoordinates, float[] precision) { 260 return sendLinearDrag( 261 uiController, downMotion, startCoordinates, endCoordinates, precision); 262 } 263 264 @Override toString()265 public String toString() { 266 return "long press and drag"; 267 } 268 }, 269 270 /** 271 * Starts a drag with a double-tap. 272 */ 273 DOUBLE_TAP { 274 private DownMotionPerformer downMotion = new DownMotionPerformer() { 275 @Override 276 @Nullable 277 public MotionEvent perform( 278 UiController uiController, float[] coordinates, float[] precision) { 279 return performDoubleTap(uiController, coordinates, precision); 280 } 281 }; 282 283 @Override sendSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision)284 public Status sendSwipe( 285 UiController uiController, 286 float[] startCoordinates, float[] endCoordinates, float[] precision) { 287 return sendLinearDrag( 288 uiController, downMotion, startCoordinates, endCoordinates, precision); 289 } 290 291 @Override toString()292 public String toString() { 293 return "double-tap and drag"; 294 } 295 }; 296 297 private static final String TAG = Drag.class.getSimpleName(); 298 299 /** The number of move events to send for each drag. */ 300 private static final int DRAG_STEP_COUNT = 10; 301 302 /** Length of time a drag should last for, in milliseconds. */ 303 private static final int DRAG_DURATION = 1500; 304 305 /** Duration between the last move event and the up event, in milliseconds. */ 306 private static final int WAIT_BEFORE_SENDING_UP = 400; 307 sendLinearDrag( UiController uiController, DownMotionPerformer downMotion, float[] startCoordinates, float[] endCoordinates, float[] precision)308 private static Status sendLinearDrag( 309 UiController uiController, DownMotionPerformer downMotion, 310 float[] startCoordinates, float[] endCoordinates, float[] precision) { 311 float[][] steps = interpolate(startCoordinates, endCoordinates); 312 final int delayBetweenMovements = DRAG_DURATION / steps.length; 313 314 MotionEvent downEvent = downMotion.perform(uiController, startCoordinates, precision); 315 if (downEvent == null) { 316 return Status.FAILURE; 317 } 318 319 try { 320 for (int i = 0; i < steps.length; i++) { 321 if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) { 322 String logMessage = "Injection of move event as part of the drag failed. " + 323 "Sending cancel event."; 324 Log.e(TAG, logMessage); 325 MotionEvents.sendCancel(uiController, downEvent); 326 return Status.FAILURE; 327 } 328 329 long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i; 330 long timeUntilDesired = desiredTime - SystemClock.uptimeMillis(); 331 if (timeUntilDesired > 10) { 332 // If the wait time until the next event isn't long enough, skip the wait 333 // and execute the next event. 334 uiController.loopMainThreadForAtLeast(timeUntilDesired); 335 } 336 } 337 338 // Wait before sending up because some drag handling logic may discard move events 339 // that has been sent immediately before the up event. e.g. HandleView. 340 uiController.loopMainThreadForAtLeast(WAIT_BEFORE_SENDING_UP); 341 342 if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) { 343 String logMessage = "Injection of up event as part of the drag failed. " + 344 "Sending cancel event."; 345 Log.e(TAG, logMessage); 346 MotionEvents.sendCancel(uiController, downEvent); 347 return Status.FAILURE; 348 } 349 } finally { 350 downEvent.recycle(); 351 } 352 return Status.SUCCESS; 353 } 354 interpolate(float[] start, float[] end)355 private static float[][] interpolate(float[] start, float[] end) { 356 float[][] res = new float[DRAG_STEP_COUNT][2]; 357 358 for (int i = 0; i < DRAG_STEP_COUNT; i++) { 359 res[i][0] = start[0] + (end[0] - start[0]) * i / (DRAG_STEP_COUNT - 1f); 360 res[i][1] = start[1] + (end[1] - start[1]) * i / (DRAG_STEP_COUNT - 1f); 361 } 362 363 return res; 364 } 365 performLongPress( UiController uiController, float[] coordinates, float[] precision)366 private static MotionEvent performLongPress( 367 UiController uiController, float[] coordinates, float[] precision) { 368 MotionEvent downEvent = MotionEvents.sendDown( 369 uiController, coordinates, precision) 370 .down; 371 // Duration before a press turns into a long press. 372 // Factor 1.5 is needed, otherwise a long press is not safely detected. 373 // See android.test.TouchUtils longClickView 374 long longPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); 375 uiController.loopMainThreadForAtLeast(longPressTimeout); 376 return downEvent; 377 } 378 379 @Nullable performDoubleTap( UiController uiController, float[] coordinates, float[] precision)380 private static MotionEvent performDoubleTap( 381 UiController uiController, float[] coordinates, float[] precision) { 382 MotionEvent downEvent = MotionEvents.sendDown( 383 uiController, coordinates, precision) 384 .down; 385 try { 386 if (!MotionEvents.sendUp(uiController, downEvent)) { 387 String logMessage = "Injection of up event as part of the double tap " + 388 "failed. Sending cancel event."; 389 Log.d(TAG, logMessage); 390 MotionEvents.sendCancel(uiController, downEvent); 391 return null; 392 } 393 394 long doubleTapMinimumTimeout = ViewConfiguration.getDoubleTapMinTime(); 395 uiController.loopMainThreadForAtLeast(doubleTapMinimumTimeout); 396 397 return MotionEvents.sendDown(uiController, coordinates, precision).down; 398 } finally { 399 downEvent.recycle(); 400 } 401 } 402 403 @Override wrapUiController(UiController uiController)404 public UiController wrapUiController(UiController uiController) { 405 return uiController; 406 } 407 } 408 409 /** 410 * Interface to implement different "down motion" types. 411 */ 412 private interface DownMotionPerformer { 413 /** 414 * Performs and returns a down motion. 415 * 416 * @param uiController a UiController to use to send MotionEvents to the screen. 417 * @param coordinates a float[] with x and y values of center of the tap. 418 * @param precision a float[] with x and y values of precision of the tap. 419 * @return the down motion event or null if the down motion event failed. 420 */ 421 @Nullable perform(UiController uiController, float[] coordinates, float[] precision)422 MotionEvent perform(UiController uiController, float[] coordinates, float[] precision); 423 } 424 425 private final Dragger mDragger; 426 private final CoordinatesProvider mStartCoordinatesProvider; 427 private final CoordinatesProvider mEndCoordinatesProvider; 428 private final PrecisionDescriber mPrecisionDescriber; 429 private final Class<? extends View> mViewClass; 430 DragAction( Dragger dragger, CoordinatesProvider startCoordinatesProvider, CoordinatesProvider endCoordinatesProvider, PrecisionDescriber precisionDescriber, Class<? extends View> viewClass)431 public DragAction( 432 Dragger dragger, 433 CoordinatesProvider startCoordinatesProvider, 434 CoordinatesProvider endCoordinatesProvider, 435 PrecisionDescriber precisionDescriber, 436 Class<? extends View> viewClass) { 437 mDragger = checkNotNull(dragger); 438 mStartCoordinatesProvider = checkNotNull(startCoordinatesProvider); 439 mEndCoordinatesProvider = checkNotNull(endCoordinatesProvider); 440 mPrecisionDescriber = checkNotNull(precisionDescriber); 441 mViewClass = viewClass; 442 } 443 444 @Override 445 @SuppressWarnings("unchecked") getConstraints()446 public Matcher<View> getConstraints() { 447 return allOf(isCompletelyDisplayed(), isAssignableFrom(mViewClass)); 448 } 449 450 @Override perform(UiController uiController, View view)451 public void perform(UiController uiController, View view) { 452 checkNotNull(uiController); 453 checkNotNull(view); 454 455 uiController = mDragger.wrapUiController(uiController); 456 457 float[] startCoordinates = mStartCoordinatesProvider.calculateCoordinates(view); 458 float[] endCoordinates = mEndCoordinatesProvider.calculateCoordinates(view); 459 float[] precision = mPrecisionDescriber.describePrecision(); 460 461 Swiper.Status status; 462 463 try { 464 status = mDragger.sendSwipe( 465 uiController, startCoordinates, endCoordinates, precision); 466 } catch (RuntimeException re) { 467 throw new PerformException.Builder() 468 .withActionDescription(this.getDescription()) 469 .withViewDescription(HumanReadables.describe(view)) 470 .withCause(re) 471 .build(); 472 } 473 474 int duration = ViewConfiguration.getPressedStateDuration(); 475 // ensures that all work enqueued to process the swipe has been run. 476 if (duration > 0) { 477 uiController.loopMainThreadForAtLeast(duration); 478 } 479 480 if (status == Swiper.Status.FAILURE) { 481 throw new PerformException.Builder() 482 .withActionDescription(getDescription()) 483 .withViewDescription(HumanReadables.describe(view)) 484 .withCause(new RuntimeException(getDescription() + " failed")) 485 .build(); 486 } 487 } 488 489 @Override getDescription()490 public String getDescription() { 491 return mDragger.toString(); 492 } 493 } 494