DragAction.java revision 2ff41d4afca7216cca4a224228caec2a5efaf278
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 17package android.widget.espresso; 18 19import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; 20import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; 21import static com.android.internal.util.Preconditions.checkNotNull; 22import static org.hamcrest.Matchers.allOf; 23import android.annotation.Nullable; 24import android.os.SystemClock; 25import android.support.test.espresso.UiController; 26import android.support.test.espresso.PerformException; 27import android.support.test.espresso.ViewAction; 28import android.support.test.espresso.action.CoordinatesProvider; 29import android.support.test.espresso.action.MotionEvents; 30import android.support.test.espresso.action.PrecisionDescriber; 31import android.support.test.espresso.action.Swiper; 32import android.support.test.espresso.util.HumanReadables; 33import android.util.Log; 34import android.view.MotionEvent; 35import android.view.View; 36import android.view.ViewConfiguration; 37 38import 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 */ 49public final class DragAction implements ViewAction { 50 public interface Dragger extends Swiper { 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 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 83 public String toString() { 84 return "mouse down and drag"; 85 } 86 87 @Override 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 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 115 public String toString() { 116 return "mouse double click and drag to select"; 117 } 118 119 @Override 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 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 149 public String toString() { 150 return "mouse long click and drag to select"; 151 } 152 153 @Override 154 public UiController wrapUiController(UiController uiController) { 155 return new MouseUiController(uiController); 156 } 157 }, 158 159 /** 160 * Starts a drag with a tap. 161 */ 162 TAP { 163 private DownMotionPerformer downMotion = new DownMotionPerformer() { 164 @Override 165 public MotionEvent perform( 166 UiController uiController, float[] coordinates, float[] precision) { 167 MotionEvent downEvent = MotionEvents.sendDown( 168 uiController, coordinates, precision) 169 .down; 170 return downEvent; 171 } 172 }; 173 174 @Override 175 public Status sendSwipe( 176 UiController uiController, 177 float[] startCoordinates, float[] endCoordinates, float[] precision) { 178 return sendLinearDrag( 179 uiController, downMotion, startCoordinates, endCoordinates, precision); 180 } 181 182 @Override 183 public String toString() { 184 return "tap and drag"; 185 } 186 }, 187 188 /** 189 * Starts a drag with a long-press. 190 */ 191 LONG_PRESS { 192 private DownMotionPerformer downMotion = new DownMotionPerformer() { 193 @Override 194 public MotionEvent perform( 195 UiController uiController, float[] coordinates, float[] precision) { 196 return performLongPress(uiController, coordinates, precision); 197 } 198 }; 199 200 @Override 201 public Status sendSwipe( 202 UiController uiController, 203 float[] startCoordinates, float[] endCoordinates, float[] precision) { 204 return sendLinearDrag( 205 uiController, downMotion, startCoordinates, endCoordinates, precision); 206 } 207 208 @Override 209 public String toString() { 210 return "long press and drag"; 211 } 212 }, 213 214 /** 215 * Starts a drag with a double-tap. 216 */ 217 DOUBLE_TAP { 218 private DownMotionPerformer downMotion = new DownMotionPerformer() { 219 @Override 220 @Nullable 221 public MotionEvent perform( 222 UiController uiController, float[] coordinates, float[] precision) { 223 return performDoubleTap(uiController, coordinates, precision); 224 } 225 }; 226 227 @Override 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 236 public String toString() { 237 return "double-tap and drag"; 238 } 239 }; 240 241 private static final String TAG = Drag.class.getSimpleName(); 242 243 /** The number of move events to send for each drag. */ 244 private static final int DRAG_STEP_COUNT = 10; 245 246 /** Length of time a drag should last for, in milliseconds. */ 247 private static final int DRAG_DURATION = 1500; 248 249 /** Duration between the last move event and the up event, in milliseconds. */ 250 private static final int WAIT_BEFORE_SENDING_UP = 400; 251 252 private static Status sendLinearDrag( 253 UiController uiController, DownMotionPerformer downMotion, 254 float[] startCoordinates, float[] endCoordinates, float[] precision) { 255 float[][] steps = interpolate(startCoordinates, endCoordinates); 256 final int delayBetweenMovements = DRAG_DURATION / steps.length; 257 258 MotionEvent downEvent = downMotion.perform(uiController, startCoordinates, precision); 259 if (downEvent == null) { 260 return Status.FAILURE; 261 } 262 263 try { 264 for (int i = 0; i < steps.length; i++) { 265 if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) { 266 String logMessage = "Injection of move event as part of the drag failed. " + 267 "Sending cancel event."; 268 Log.e(TAG, logMessage); 269 MotionEvents.sendCancel(uiController, downEvent); 270 return Status.FAILURE; 271 } 272 273 long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i; 274 long timeUntilDesired = desiredTime - SystemClock.uptimeMillis(); 275 if (timeUntilDesired > 10) { 276 // If the wait time until the next event isn't long enough, skip the wait 277 // and execute the next event. 278 uiController.loopMainThreadForAtLeast(timeUntilDesired); 279 } 280 } 281 282 // Wait before sending up because some drag handling logic may discard move events 283 // that has been sent immediately before the up event. e.g. HandleView. 284 uiController.loopMainThreadForAtLeast(WAIT_BEFORE_SENDING_UP); 285 286 if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) { 287 String logMessage = "Injection of up event as part of the drag failed. " + 288 "Sending cancel event."; 289 Log.e(TAG, logMessage); 290 MotionEvents.sendCancel(uiController, downEvent); 291 return Status.FAILURE; 292 } 293 } finally { 294 downEvent.recycle(); 295 } 296 return Status.SUCCESS; 297 } 298 299 private static float[][] interpolate(float[] start, float[] end) { 300 float[][] res = new float[DRAG_STEP_COUNT][2]; 301 302 for (int i = 0; i < DRAG_STEP_COUNT; i++) { 303 res[i][0] = start[0] + (end[0] - start[0]) * i / (DRAG_STEP_COUNT - 1f); 304 res[i][1] = start[1] + (end[1] - start[1]) * i / (DRAG_STEP_COUNT - 1f); 305 } 306 307 return res; 308 } 309 310 private static MotionEvent performLongPress( 311 UiController uiController, float[] coordinates, float[] precision) { 312 MotionEvent downEvent = MotionEvents.sendDown( 313 uiController, coordinates, precision) 314 .down; 315 // Duration before a press turns into a long press. 316 // Factor 1.5 is needed, otherwise a long press is not safely detected. 317 // See android.test.TouchUtils longClickView 318 long longPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); 319 uiController.loopMainThreadForAtLeast(longPressTimeout); 320 return downEvent; 321 } 322 323 @Nullable 324 private static MotionEvent performDoubleTap( 325 UiController uiController, float[] coordinates, float[] precision) { 326 MotionEvent downEvent = MotionEvents.sendDown( 327 uiController, coordinates, precision) 328 .down; 329 try { 330 if (!MotionEvents.sendUp(uiController, downEvent)) { 331 String logMessage = "Injection of up event as part of the double tap " + 332 "failed. Sending cancel event."; 333 Log.d(TAG, logMessage); 334 MotionEvents.sendCancel(uiController, downEvent); 335 return null; 336 } 337 338 long doubleTapMinimumTimeout = ViewConfiguration.getDoubleTapMinTime(); 339 uiController.loopMainThreadForAtLeast(doubleTapMinimumTimeout); 340 341 return MotionEvents.sendDown(uiController, coordinates, precision).down; 342 } finally { 343 downEvent.recycle(); 344 } 345 } 346 347 @Override 348 public UiController wrapUiController(UiController uiController) { 349 return uiController; 350 } 351 } 352 353 /** 354 * Interface to implement different "down motion" types. 355 */ 356 private interface DownMotionPerformer { 357 /** 358 * Performs and returns a down motion. 359 * 360 * @param uiController a UiController to use to send MotionEvents to the screen. 361 * @param coordinates a float[] with x and y values of center of the tap. 362 * @param precision a float[] with x and y values of precision of the tap. 363 * @return the down motion event or null if the down motion event failed. 364 */ 365 @Nullable 366 MotionEvent perform(UiController uiController, float[] coordinates, float[] precision); 367 } 368 369 private final Dragger mDragger; 370 private final CoordinatesProvider mStartCoordinatesProvider; 371 private final CoordinatesProvider mEndCoordinatesProvider; 372 private final PrecisionDescriber mPrecisionDescriber; 373 private final Class<? extends View> mViewClass; 374 375 public DragAction( 376 Dragger dragger, 377 CoordinatesProvider startCoordinatesProvider, 378 CoordinatesProvider endCoordinatesProvider, 379 PrecisionDescriber precisionDescriber, 380 Class<? extends View> viewClass) { 381 mDragger = checkNotNull(dragger); 382 mStartCoordinatesProvider = checkNotNull(startCoordinatesProvider); 383 mEndCoordinatesProvider = checkNotNull(endCoordinatesProvider); 384 mPrecisionDescriber = checkNotNull(precisionDescriber); 385 mViewClass = viewClass; 386 } 387 388 @Override 389 @SuppressWarnings("unchecked") 390 public Matcher<View> getConstraints() { 391 return allOf(isCompletelyDisplayed(), isAssignableFrom(mViewClass)); 392 } 393 394 @Override 395 public void perform(UiController uiController, View view) { 396 checkNotNull(uiController); 397 checkNotNull(view); 398 399 uiController = mDragger.wrapUiController(uiController); 400 401 float[] startCoordinates = mStartCoordinatesProvider.calculateCoordinates(view); 402 float[] endCoordinates = mEndCoordinatesProvider.calculateCoordinates(view); 403 float[] precision = mPrecisionDescriber.describePrecision(); 404 405 Swiper.Status status; 406 407 try { 408 status = mDragger.sendSwipe( 409 uiController, startCoordinates, endCoordinates, precision); 410 } catch (RuntimeException re) { 411 throw new PerformException.Builder() 412 .withActionDescription(this.getDescription()) 413 .withViewDescription(HumanReadables.describe(view)) 414 .withCause(re) 415 .build(); 416 } 417 418 int duration = ViewConfiguration.getPressedStateDuration(); 419 // ensures that all work enqueued to process the swipe has been run. 420 if (duration > 0) { 421 uiController.loopMainThreadForAtLeast(duration); 422 } 423 424 if (status == Swiper.Status.FAILURE) { 425 throw new PerformException.Builder() 426 .withActionDescription(getDescription()) 427 .withViewDescription(HumanReadables.describe(view)) 428 .withCause(new RuntimeException(getDescription() + " failed")) 429 .build(); 430 } 431 } 432 433 @Override 434 public String getDescription() { 435 return mDragger.toString(); 436 } 437} 438