• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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