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 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
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
202            public String toString() {
203                return "mouse triple click and drag to select";
204            }
205
206            @Override
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
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 "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
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
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
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
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
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
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
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
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
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
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
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")
443    public Matcher<View> getConstraints() {
444        return allOf(isCompletelyDisplayed(), isAssignableFrom(mViewClass));
445    }
446
447    @Override
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
487    public String getDescription() {
488        return mDragger.toString();
489    }
490}
491