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