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