1 /*
2  * Copyright 2019 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 androidx.testutils;
18 
19 import static android.os.SystemClock.uptimeMillis;
20 
21 import android.annotation.SuppressLint;
22 import android.app.Instrumentation;
23 import android.os.SystemClock;
24 import android.view.MotionEvent;
25 import android.view.View;
26 
27 import androidx.annotation.NonNull;
28 import androidx.test.espresso.action.CoordinatesProvider;
29 
30 /**
31  * Injects motion events for custom gestures using Instrumentation. Use this to "draw" the gestures,
32  * in a similar way as a Path is defined. Create an instance of this class, start a drag, move it
33  * around and then finish it. For example, this injects a "Swipe right then left" gesture from
34  * TalkBack:
35  *
36  * <pre>
37  *     View view = findViewById(R.id.view_to_swipe_on);
38  *
39  *     SwipeInjector swiper = new SwipeInjector(InstrumentationRegistry.getInstrumentation());
40  *     swiper.startDrag(GeneralLocation.LEFT, view);      // Start at the left side of the view
41  *     swiper.dragTo(GeneralLocation.RIGHT, view, 200);   // Swipe to the right in 200 ms
42  *     swiper.dragTo(GeneralLocation.LEFT, view, 200);    // Swipe to the left in 200 ms
43  *     swiper.finishDrag();                               // Finish the gesture
44  * </pre>
45  *
46  * @see androidx.test.espresso.action.GeneralLocation GeneralLocation
47  * @see TranslatedCoordinatesProvider
48  */
49 public class SwipeInjector {
50 
51     private static final int X = 0;
52     private static final int Y = 1;
53 
54     private Instrumentation mInstrumentation;
55 
56     private long mStartTime = 0;
57     private long mCurrentTime = 0;
58     private float[] mFloats = new float[2];
59 
60     /**
61      * Creates an injector.
62      */
SwipeInjector(@onNull Instrumentation instrumentation)63     public SwipeInjector(@NonNull Instrumentation instrumentation) {
64         mInstrumentation = instrumentation;
65     }
66 
67     /**
68      * Start a drag on the coordinate provided for the given view. For example,
69      * {@code startDrag(GeneralLocation.CENTER, mView)}.
70      *
71      * @param provider provider of the coordinate of the pointer down event, like
72      *        {@link androidx.test.espresso.action.GeneralLocation#CENTER GeneralLocation.CENTER}
73      * @param view the view passed to the
74      *        {@link CoordinatesProvider#calculateCoordinates(View) coordinates provider}
75      */
76     @SuppressLint("LambdaLast")
startDrag(@onNull CoordinatesProvider provider, @NonNull View view)77     public void startDrag(@NonNull CoordinatesProvider provider, @NonNull View view) {
78         float[] floats = provider.calculateCoordinates(view);
79         startDrag(floats[X], floats[Y]);
80     }
81 
82     /**
83      * Start a drag on the given coordinate. For example, {@code startDrag(100, 200)}. Note that the
84      * coordinates are absolute coordinates for the screen.
85      *
86      * @param x the x coordinate of the pointer down event
87      * @param y the y coordinate of the pointer down event
88      */
startDrag(float x, float y)89     public void startDrag(float x, float y) {
90         mStartTime = uptimeMillis();
91         mFloats[X] = x;
92         mFloats[Y] = y;
93         injectMotionEvent(obtainDownEvent(mStartTime, mFloats));
94     }
95 
96     /**
97      * Extend the drag with a single motion event to the coordinates provided for the given view.
98      * For example, {@code dragTo(GeneralLocation.LEFT, mView)}. Call one of the {@code startDrag}
99      * methods before calling any of the {@code dragTo} methods.
100      *
101      * @param provider provider of the coordinate of the pointer move event, like
102      *        {@link androidx.test.espresso.action.GeneralLocation#CENTER GeneralLocation.CENTER}
103      * @param view the view passed to the
104      *        {@link CoordinatesProvider#calculateCoordinates(View) coordinates provider}
105      */
106     @SuppressLint("LambdaLast")
dragTo(@onNull CoordinatesProvider provider, @NonNull View view)107     public void dragTo(@NonNull CoordinatesProvider provider, @NonNull View view) {
108         float[] floats = provider.calculateCoordinates(view);
109         dragTo(floats[X], floats[Y]);
110     }
111 
112     /**
113      * Extend the drag with a single motion event to the given coordinate. For example,
114      * {@code dragTo(0, 10)}. Call one of the {@code startDrag} methods before calling any of the
115      * {@code dragTo} methods. Note that the coordinates are absolute coordinates for the screen.
116      *
117      * @param x the x coordinate of the pointer move event
118      * @param y the y coordinate of the pointer move event
119      */
dragTo(float x, float y)120     public void dragTo(float x, float y) {
121         mFloats[X] = x;
122         mFloats[Y] = y;
123         injectMotionEvent(obtainMoveEvent(mStartTime, uptimeMillis(), mFloats));
124     }
125 
126     /**
127      * Extend the drag with a range of motion events to the coordinate provided for the given view.
128      * An event will be injected every 10ms, interpolating linearly to the destination. For example,
129      * {@code dragTo(GeneralLocation.LEFT, mView, 300)}. Call one of the {@code startDrag} methods
130      * before calling any of the {@code dragTo} methods.
131      *
132      * @param provider provider of the final coordinate of this part of the drag, like
133      *        {@link androidx.test.espresso.action.GeneralLocation#CENTER GeneralLocation.CENTER}
134      * @param view the view passed to the
135      *        {@link CoordinatesProvider#calculateCoordinates(View) coordinates provider}
136      * @param duration the time in milliseconds this part of the drag should take. Actual time will
137      *        be different if not a multiple of 10.
138      */
139     @SuppressLint("LambdaLast")
dragTo(@onNull CoordinatesProvider provider, @NonNull View view, long duration)140     public void dragTo(@NonNull CoordinatesProvider provider, @NonNull View view, long duration) {
141         float[] floats = provider.calculateCoordinates(view);
142         dragTo(floats[X], floats[Y], duration);
143     }
144 
145     /**
146      * Extend the drag with a range of motion events to the given coordinate. An event will be
147      * injected every 10ms, interpolating linearly to the destination. For example,
148      * {@code dragTo(10, 0, 350)}. Call one of the {@code startDrag} methods before calling any of
149      * the {@code dragTo} methods. Note that the coordinates are absolute coordinates for the
150      * screen.
151      *
152      * @param x the final x coordinate of this part of the drag
153      * @param y the final y coordinate of this part of the drag
154      * @param duration the time in milliseconds this part of the drag should take. Actual time will
155      *        be different if not a multiple of 10.
156      */
dragTo(float x, float y, long duration)157     public void dragTo(float x, float y, long duration) {
158         float x0 = mFloats[X];
159         float y0 = mFloats[Y];
160         float dx = x - x0;
161         float dy = y - y0;
162         int steps = Math.max(1, Math.round(duration / 10f));
163         for (int i = 1; i <= steps; i++) {
164             float progress = (float) i / steps;
165             mFloats[X] = x0 + dx * progress;
166             mFloats[Y] = y0 + dy * progress;
167             injectMotionEvent(obtainMoveEvent(mStartTime, mCurrentTime + 10L, mFloats));
168         }
169     }
170 
171     /**
172      * Extend the drag with a single motion event covering the given delta. For example,
173      * {@code dragBy(10, -20)}. Call one of the {@code startDrag} methods before calling any of
174      * the {@code dragBy} methods.
175      *
176      * @param dx the distance in x direction
177      * @param dy the distance in y direction
178      */
dragBy(float dx, float dy)179     public void dragBy(float dx, float dy) {
180         dragTo(mFloats[X] + dx, mFloats[Y] + dy);
181     }
182 
183     /**
184      * Extend the drag with a range of motion events covering the given delta. An event will be
185      * injected every 10ms, interpolating linearly to the destination. For example,
186      * {@code dragBy(-50, 0, 100)}. Call one of the {@code startDrag} methods before calling any of
187      * the {@code dragBy} methods.
188      *
189      * @param dx the distance in x direction
190      * @param dy the distance in y direction
191      * @param duration the time in milliseconds this part of the drag should take. Actual time will
192      *        be different if not a multiple of 10.
193      */
dragBy(float dx, float dy, long duration)194     public void dragBy(float dx, float dy, long duration) {
195         dragTo(mFloats[X] + dx, mFloats[Y] + dy, duration);
196     }
197 
198     /**
199      * Finish the drag with a pointer up event. A new drag can be started after this with one of the
200      * {@code startDrag} methods.
201      */
finishDrag()202     public void finishDrag() {
203         injectMotionEvent(obtainUpEvent(mStartTime, uptimeMillis(), mFloats));
204     }
205 
obtainDownEvent(long time, float[] coord)206     private static MotionEvent obtainDownEvent(long time, float[] coord) {
207         return MotionEvent.obtain(time, time,
208                 MotionEvent.ACTION_DOWN, coord[X], coord[Y], 0);
209     }
210 
obtainMoveEvent(long startTime, long time, float[] coord)211     private static MotionEvent obtainMoveEvent(long startTime, long time, float[] coord) {
212         return MotionEvent.obtain(startTime, time,
213                 MotionEvent.ACTION_MOVE, coord[X], coord[Y], 0);
214     }
215 
obtainUpEvent(long startTime, long time, float[] coord)216     private static MotionEvent obtainUpEvent(long startTime, long time, float[] coord) {
217         return MotionEvent.obtain(startTime, time,
218                 MotionEvent.ACTION_UP, coord[X], coord[Y], 0);
219     }
220 
injectMotionEvent(MotionEvent event)221     private void injectMotionEvent(MotionEvent event) {
222         try {
223             long eventTime = event.getEventTime();
224             long now = uptimeMillis();
225             if (eventTime - now > 0) {
226                 SystemClock.sleep(eventTime - now);
227             }
228             mInstrumentation.sendPointerSync(event);
229             mCurrentTime = eventTime;
230         } finally {
231             event.recycle();
232         }
233     }
234 }
235