1 /*
2  * Copyright (C) 2016 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 com.android.compatibility.common.util;
18 
19 import android.app.Instrumentation;
20 import android.app.UiAutomation;
21 import android.graphics.Point;
22 import android.os.SystemClock;
23 import android.util.SparseArray;
24 import android.view.InputDevice;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.ViewConfiguration;
28 import android.view.ViewGroup;
29 import android.view.ViewTreeObserver;
30 
31 import androidx.annotation.Nullable;
32 import androidx.test.rule.ActivityTestRule;
33 
34 /**
35  * Test utilities for touch emulation.
36  */
37 public final class CtsTouchUtils {
38     /**
39      * Interface definition for a callback to be invoked when an event has been injected.
40      */
41     public interface EventInjectionListener {
42         /**
43          * Callback method to be invoked when a {MotionEvent#ACTION_DOWN} has been injected.
44          * @param xOnScreen X coordinate of the injected event.
45          * @param yOnScreen Y coordinate of the injected event.
46          */
onDownInjected(int xOnScreen, int yOnScreen)47         public void onDownInjected(int xOnScreen, int yOnScreen);
48 
49         /**
50          * Callback method to be invoked when a {MotionEvent#ACTION_MOVE} has been injected.
51          * @param xOnScreen X coordinates of the injected event.
52          * @param yOnScreen Y coordinates of the injected event.
53          */
onMoveInjected(int[] xOnScreen, int[] yOnScreen)54         public void onMoveInjected(int[] xOnScreen, int[] yOnScreen);
55 
56         /**
57          * Callback method to be invoked when a {MotionEvent#ACTION_UP} has been injected.
58          * @param xOnScreen X coordinate of the injected event.
59          * @param yOnScreen Y coordinate of the injected event.
60          */
onUpInjected(int xOnScreen, int yOnScreen)61         public void onUpInjected(int xOnScreen, int yOnScreen);
62     }
63 
CtsTouchUtils()64     private CtsTouchUtils() {}
65 
66     /**
67      * Emulates a tap in the center of the passed {@link View}.
68      *
69      * @param instrumentation the instrumentation used to run the test
70      * @param view the view to "tap"
71      */
emulateTapOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view)72     public static void emulateTapOnViewCenter(Instrumentation instrumentation,
73             ActivityTestRule<?> activityTestRule, View view) {
74         emulateTapOnViewCenter(instrumentation, activityTestRule, view, true);
75     }
76 
77     /**
78      * Emulates a tap in the center of the passed {@link View}.
79      *
80      * @param instrumentation the instrumentation used to run the test
81      * @param view the view to "tap"
82      * @param waitForAnimations wait for animations to complete before sending an event
83      */
emulateTapOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, boolean waitForAnimations)84     public static void emulateTapOnViewCenter(Instrumentation instrumentation,
85             ActivityTestRule<?> activityTestRule, View view, boolean waitForAnimations) {
86         emulateTapOnView(instrumentation, activityTestRule, view, view.getWidth() / 2,
87                 view.getHeight() / 2, waitForAnimations);
88     }
89 
90     /**
91      * Emulates a tap on a point relative to the top-left corner of the passed {@link View}. Offset
92      * parameters are used to compute the final screen coordinates of the tap point.
93      *
94      * @param instrumentation the instrumentation used to run the test
95      * @param anchorView the anchor view to determine the tap location on the screen
96      * @param offsetX extra X offset for the tap
97      * @param offsetY extra Y offset for the tap
98      */
emulateTapOnView(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View anchorView, int offsetX, int offsetY)99     public static void emulateTapOnView(Instrumentation instrumentation,
100             ActivityTestRule<?> activityTestRule, View anchorView,
101             int offsetX, int offsetY) {
102         emulateTapOnView(instrumentation, activityTestRule, anchorView, offsetX, offsetY, true);
103     }
104 
105     /**
106      * Emulates a tap on a point relative to the top-left corner of the passed {@link View}. Offset
107      * parameters are used to compute the final screen coordinates of the tap point.
108      *
109      * @param instrumentation the instrumentation used to run the test
110      * @param anchorView the anchor view to determine the tap location on the screen
111      * @param offsetX extra X offset for the tap
112      * @param offsetY extra Y offset for the tap
113      * @param waitForAnimations wait for animations to complete before sending an event
114      */
emulateTapOnView(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View anchorView, int offsetX, int offsetY, boolean waitForAnimations)115     public static void emulateTapOnView(Instrumentation instrumentation,
116             ActivityTestRule<?> activityTestRule, View anchorView,
117             int offsetX, int offsetY, boolean waitForAnimations) {
118         final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop();
119         // Get anchor coordinates on the screen
120         final int[] viewOnScreenXY = new int[2];
121         anchorView.getLocationOnScreen(viewOnScreenXY);
122         int xOnScreen = viewOnScreenXY[0] + offsetX;
123         int yOnScreen = viewOnScreenXY[1] + offsetY;
124         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
125         final long downTime = SystemClock.uptimeMillis();
126 
127         injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, waitForAnimations, null);
128         injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen, waitForAnimations);
129         injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen,
130                 waitForAnimations, null);
131 
132         // Wait for the system to process all events in the queue
133         if (activityTestRule != null) {
134             WidgetTestUtils.runOnMainAndDrawSync(activityTestRule,
135                     activityTestRule.getActivity().getWindow().getDecorView(), null);
136         } else {
137             instrumentation.waitForIdleSync();
138         }
139     }
140 
141     /**
142      * Emulates a double tap in the center of the passed {@link View}.
143      *
144      * @param instrumentation the instrumentation used to run the test
145      * @param view the view to "double tap"
146      */
emulateDoubleTapOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view)147     public static void emulateDoubleTapOnViewCenter(Instrumentation instrumentation,
148             ActivityTestRule<?> activityTestRule, View view) {
149         emulateDoubleTapOnView(instrumentation, activityTestRule, view, view.getWidth() / 2,
150                 view.getHeight() / 2);
151     }
152 
153     /**
154      * Emulates a double tap on a point relative to the top-left corner of the passed {@link View}.
155      * Offset parameters are used to compute the final screen coordinates of the tap points.
156      *
157      * @param instrumentation the instrumentation used to run the test
158      * @param anchorView the anchor view to determine the tap location on the screen
159      * @param offsetX extra X offset for the taps
160      * @param offsetY extra Y offset for the taps
161      */
emulateDoubleTapOnView(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View anchorView, int offsetX, int offsetY)162     public static void emulateDoubleTapOnView(Instrumentation instrumentation,
163             ActivityTestRule<?> activityTestRule, View anchorView,
164             int offsetX, int offsetY) {
165         final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop();
166         // Get anchor coordinates on the screen
167         final int[] viewOnScreenXY = new int[2];
168         anchorView.getLocationOnScreen(viewOnScreenXY);
169         int xOnScreen = viewOnScreenXY[0] + offsetX;
170         int yOnScreen = viewOnScreenXY[1] + offsetY;
171         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
172         final long downTime = SystemClock.uptimeMillis();
173 
174         injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
175         injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen, true);
176         injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
177         injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
178         injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen, true);
179         injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
180 
181         // Wait for the system to process all events in the queue
182         if (activityTestRule != null) {
183             WidgetTestUtils.runOnMainAndDrawSync(activityTestRule,
184                     activityTestRule.getActivity().getWindow().getDecorView(), null);
185         } else {
186             instrumentation.waitForIdleSync();
187         }
188     }
189 
190     /**
191      * Emulates a linear drag gesture between 2 points across the screen.
192      *
193      * @param instrumentation the instrumentation used to run the test
194      * @param dragStartX Start X of the emulated drag gesture
195      * @param dragStartY Start Y of the emulated drag gesture
196      * @param dragAmountX X amount of the emulated drag gesture
197      * @param dragAmountY Y amount of the emulated drag gesture
198      */
emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY)199     public static void emulateDragGesture(Instrumentation instrumentation,
200             ActivityTestRule<?> activityTestRule,
201             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY) {
202         emulateDragGesture(instrumentation, activityTestRule,
203                 dragStartX, dragStartY, dragAmountX, dragAmountY,
204                 2000, 20, null);
205     }
206 
emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, int dragDurationMs, int moveEventCount)207     private static void emulateDragGesture(Instrumentation instrumentation,
208             ActivityTestRule<?> activityTestRule,
209             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY,
210             int dragDurationMs, int moveEventCount) {
211         emulateDragGesture(instrumentation, activityTestRule,
212                 dragStartX, dragStartY, dragAmountX, dragAmountY,
213                 dragDurationMs, moveEventCount, null);
214     }
215 
216     /**
217      * Emulates a linear drag gesture between 2 points across the screen.
218      *
219      * @param instrumentation the instrumentation used to run the test
220      * @param dragStartX Start X of the emulated drag gesture
221      * @param dragStartY Start Y of the emulated drag gesture
222      * @param dragAmountX X amount of the emulated drag gesture
223      * @param dragAmountY Y amount of the emulated drag gesture
224      * @param dragDurationMs The time in milliseconds over which the drag occurs
225      * @param moveEventCount The number of events that produce the movement
226      * @param eventInjectionListener Called after each down, move, and up events.
227      */
emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, int dragDurationMs, int moveEventCount, @Nullable EventInjectionListener eventInjectionListener)228     public static void emulateDragGesture(Instrumentation instrumentation,
229             ActivityTestRule<?> activityTestRule,
230             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY,
231             int dragDurationMs, int moveEventCount,
232             @Nullable EventInjectionListener eventInjectionListener) {
233         emulateDragGesture(instrumentation, activityTestRule, dragStartX, dragStartY, dragAmountX,
234                 dragAmountY, dragDurationMs, moveEventCount, true, eventInjectionListener);
235     }
236 
237     /**
238      * Emulates a linear drag gesture between 2 points across the screen.
239      *
240      * @param instrumentation the instrumentation used to run the test
241      * @param dragStartX Start X of the emulated drag gesture
242      * @param dragStartY Start Y of the emulated drag gesture
243      * @param dragAmountX X amount of the emulated drag gesture
244      * @param dragAmountY Y amount of the emulated drag gesture
245      * @param dragDurationMs The time in milliseconds over which the drag occurs
246      * @param moveEventCount The number of events that produce the movement
247      * @param waitForAnimations wait for animations to complete before sending an event
248      * @param eventInjectionListener Called after each down, move, and up events.
249      */
emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, int dragDurationMs, int moveEventCount, boolean waitForAnimations, @Nullable EventInjectionListener eventInjectionListener)250     public static void emulateDragGesture(Instrumentation instrumentation,
251             ActivityTestRule<?> activityTestRule,
252             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY,
253             int dragDurationMs, int moveEventCount,
254             boolean waitForAnimations, @Nullable EventInjectionListener eventInjectionListener) {
255         // We are using the UiAutomation object to inject events so that drag works
256         // across view / window boundaries (such as for the emulated drag and drop
257         // sequences)
258         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
259         final long downTime = SystemClock.uptimeMillis();
260 
261         injectDownEvent(uiAutomation, downTime, dragStartX, dragStartY, waitForAnimations,
262                 eventInjectionListener);
263 
264         // Inject a sequence of MOVE events that emulate the "move" part of the gesture
265         injectMoveEventsForDrag(uiAutomation, downTime, true, dragStartX, dragStartY,
266                 dragStartX + dragAmountX, dragStartY + dragAmountY, moveEventCount, dragDurationMs,
267                 waitForAnimations, eventInjectionListener);
268 
269         injectUpEvent(uiAutomation, downTime, true, dragStartX + dragAmountX,
270                 dragStartY + dragAmountY, waitForAnimations, eventInjectionListener);
271 
272         // Wait for the system to process all events in the queue
273         if (activityTestRule != null) {
274             WidgetTestUtils.runOnMainAndDrawSync(activityTestRule,
275                     activityTestRule.getActivity().getWindow().getDecorView(), null);
276         } else {
277             instrumentation.waitForIdleSync();
278         }
279     }
280 
281     /**
282      * Emulates a series of linear drag gestures across the screen between multiple points without
283      * lifting the finger. Note that this function does not support curve movements between the
284      * points.
285      *
286      * @param instrumentation the instrumentation used to run the test
287      * @param coordinates the ordered list of points for the drag gesture
288      */
emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, SparseArray<Point> coordinates)289     public static void emulateDragGesture(Instrumentation instrumentation,
290             ActivityTestRule<?> activityTestRule, SparseArray<Point> coordinates) {
291         emulateDragGesture(instrumentation, activityTestRule, coordinates, 2000, 20);
292     }
293 
emulateDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, SparseArray<Point> coordinates, int dragDurationMs, int moveEventCount)294     private static void emulateDragGesture(Instrumentation instrumentation,
295             ActivityTestRule<?> activityTestRule,
296             SparseArray<Point> coordinates, int dragDurationMs, int moveEventCount) {
297         final int coordinatesSize = coordinates.size();
298         if (coordinatesSize < 2) {
299             throw new IllegalArgumentException("Need at least 2 points for emulating drag");
300         }
301         // We are using the UiAutomation object to inject events so that drag works
302         // across view / window boundaries (such as for the emulated drag and drop
303         // sequences)
304         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
305         final long downTime = SystemClock.uptimeMillis();
306 
307         injectDownEvent(uiAutomation, downTime, coordinates.get(0).x, coordinates.get(0).y, null);
308 
309         // Move to each coordinate.
310         for (int i = 0; i < coordinatesSize - 1; i++) {
311             // Inject a sequence of MOVE events that emulate the "move" part of the gesture.
312             injectMoveEventsForDrag(uiAutomation,
313                     downTime,
314                     true,
315                     coordinates.get(i).x,
316                     coordinates.get(i).y,
317                     coordinates.get(i + 1).x,
318                     coordinates.get(i + 1).y,
319                     moveEventCount,
320                     dragDurationMs,
321                     true,
322                     null);
323         }
324 
325         injectUpEvent(uiAutomation,
326                 downTime,
327                 true,
328                 coordinates.get(coordinatesSize - 1).x,
329                 coordinates.get(coordinatesSize - 1).y,
330                 null);
331 
332         // Wait for the system to process all events in the queue
333         if (activityTestRule != null) {
334             WidgetTestUtils.runOnMainAndDrawSync(activityTestRule,
335                     activityTestRule.getActivity().getWindow().getDecorView(), null);
336         } else {
337             instrumentation.waitForIdleSync();
338         }
339     }
340 
341     /**
342      * Injects an {@link MotionEvent#ACTION_DOWN} event at the given coordinates.
343      *
344      * @param downTime The time of the event, usually from {@link SystemClock#uptimeMillis()}
345      * @param xOnScreen The x screen coordinate to press on
346      * @param yOnScreen The y screen coordinate to press on
347      * @param eventInjectionListener The listener to call back immediately after the down was
348      *                               sent.
349      * @return <code>downTime</code>
350      */
injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen, int yOnScreen, @Nullable EventInjectionListener eventInjectionListener)351     public static long injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen,
352             int yOnScreen, @Nullable EventInjectionListener eventInjectionListener) {
353         return injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, true,
354                 eventInjectionListener);
355     }
356 
357     /**
358      * Injects an {@link MotionEvent#ACTION_DOWN} event at the given coordinates.
359      *
360      * @param downTime The time of the event, usually from {@link SystemClock#uptimeMillis()}
361      * @param xOnScreen The x screen coordinate to press on
362      * @param yOnScreen The y screen coordinate to press on
363      * @param waitForAnimations wait for animations to complete before sending an event
364      * @param eventInjectionListener The listener to call back immediately after the down was
365      *                               sent.
366      * @return <code>downTime</code>
367      */
injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen, int yOnScreen, boolean waitForAnimations, @Nullable EventInjectionListener eventInjectionListener)368     public static long injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen,
369             int yOnScreen, boolean waitForAnimations,
370             @Nullable EventInjectionListener eventInjectionListener) {
371         MotionEvent eventDown = MotionEvent.obtain(
372                 downTime, downTime, MotionEvent.ACTION_DOWN, xOnScreen, yOnScreen, 1);
373         eventDown.setSource(InputDevice.SOURCE_TOUCHSCREEN);
374         uiAutomation.injectInputEvent(eventDown, true, waitForAnimations);
375         if (eventInjectionListener != null) {
376             eventInjectionListener.onDownInjected(xOnScreen, yOnScreen);
377         }
378         eventDown.recycle();
379         return downTime;
380     }
381 
injectMoveEventForTap(UiAutomation uiAutomation, long downTime, int touchSlop, int xOnScreen, int yOnScreen, boolean waitForAnimations)382     private static void injectMoveEventForTap(UiAutomation uiAutomation, long downTime,
383             int touchSlop, int xOnScreen, int yOnScreen, boolean waitForAnimations) {
384         MotionEvent eventMove = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_MOVE,
385                 xOnScreen + (touchSlop / 2.0f), yOnScreen + (touchSlop / 2.0f), 1);
386         eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN);
387         uiAutomation.injectInputEvent(eventMove, true);
388         eventMove.recycle();
389     }
390 
injectMoveEventsForDrag(UiAutomation uiAutomation, long downTime, boolean useCurrentEventTime, int dragStartX, int dragStartY, int dragEndX, int dragEndY, int moveEventCount, int dragDurationMs, boolean waitForAnimations, EventInjectionListener eventInjectionListener)391     private static void injectMoveEventsForDrag(UiAutomation uiAutomation, long downTime,
392             boolean useCurrentEventTime, int dragStartX, int dragStartY, int dragEndX, int dragEndY,
393             int moveEventCount, int dragDurationMs, boolean waitForAnimations,
394             EventInjectionListener eventInjectionListener) {
395         final int dragAmountX = dragEndX - dragStartX;
396         final int dragAmountY = dragEndY - dragStartY;
397         final int sleepTime = dragDurationMs / moveEventCount;
398 
399         // sleep for a bit to emulate the overall drag gesture.
400         long prevEventTime = downTime;
401         SystemClock.sleep(sleepTime);
402         for (int i = 0; i < moveEventCount; i++) {
403             // Note that the first MOVE event is generated "away" from the coordinates
404             // of the start / DOWN event, and the last MOVE event is generated
405             // at the same coordinates as the subsequent UP event.
406             final int moveX = dragStartX + dragAmountX * (i  + 1) / moveEventCount;
407             final int moveY = dragStartY + dragAmountY * (i  + 1) / moveEventCount;
408             long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime;
409 
410             // If necessary, generate history for our next MOVE event. The history is generated
411             // to be spaced at 10 millisecond intervals, interpolating the coordinates from the
412             // last generated MOVE event to our current one.
413             int historyEventCount = (int) ((eventTime - prevEventTime) / 10);
414             int[] xCoordsForListener = (eventInjectionListener == null) ? null :
415                     new int[Math.max(1, historyEventCount)];
416             int[] yCoordsForListener = (eventInjectionListener == null) ? null :
417                     new int[Math.max(1, historyEventCount)];
418             MotionEvent eventMove = null;
419             if (historyEventCount == 0) {
420                 eventMove = MotionEvent.obtain(
421                         downTime, eventTime, MotionEvent.ACTION_MOVE, moveX, moveY, 1);
422                 if (eventInjectionListener != null) {
423                     xCoordsForListener[0] = moveX;
424                     yCoordsForListener[0] = moveY;
425                 }
426             } else {
427                 final int prevMoveX = dragStartX + dragAmountX * i / moveEventCount;
428                 final int prevMoveY = dragStartY + dragAmountY * i / moveEventCount;
429                 final int deltaMoveX = moveX - prevMoveX;
430                 final int deltaMoveY = moveY - prevMoveY;
431                 final long deltaTime = (eventTime - prevEventTime);
432                 for (int historyIndex = 0; historyIndex < historyEventCount; historyIndex++) {
433                     int stepMoveX = prevMoveX + deltaMoveX * (historyIndex + 1) / historyEventCount;
434                     int stepMoveY = prevMoveY + deltaMoveY * (historyIndex + 1) / historyEventCount;
435                     long stepEventTime = useCurrentEventTime
436                             ? prevEventTime + deltaTime * (historyIndex + 1) / historyEventCount
437                             : downTime;
438                     if (historyIndex == 0) {
439                         // Generate the first event in our sequence
440                         eventMove = MotionEvent.obtain(downTime, stepEventTime,
441                                 MotionEvent.ACTION_MOVE, stepMoveX, stepMoveY, 1);
442                     } else {
443                         // and then add to it
444                         eventMove.addBatch(stepEventTime, stepMoveX, stepMoveY, 1.0f, 1.0f, 1);
445                     }
446                     if (eventInjectionListener != null) {
447                         xCoordsForListener[historyIndex] = stepMoveX;
448                         yCoordsForListener[historyIndex] = stepMoveY;
449                     }
450                 }
451             }
452 
453             eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN);
454             uiAutomation.injectInputEvent(eventMove, true, waitForAnimations);
455             if (eventInjectionListener != null) {
456                 eventInjectionListener.onMoveInjected(xCoordsForListener, yCoordsForListener);
457             }
458             eventMove.recycle();
459             prevEventTime = eventTime;
460 
461             // sleep for a bit to emulate the overall drag gesture.
462             SystemClock.sleep(sleepTime);
463         }
464     }
465 
466     /**
467      * Injects an {@link MotionEvent#ACTION_UP} event at the given coordinates.
468      *
469      * @param downTime The time of the event, usually from {@link SystemClock#uptimeMillis()}
470      * @param useCurrentEventTime <code>true</code> if it should use the current time for the
471      *                            up event or <code>false</code> to use <code>downTime</code>.
472      * @param xOnScreen The x screen coordinate to press on
473      * @param yOnScreen The y screen coordinate to press on
474      * @param eventInjectionListener The listener to call back immediately after the up was
475      *                               sent.
476      */
injectUpEvent(UiAutomation uiAutomation, long downTime, boolean useCurrentEventTime, int xOnScreen, int yOnScreen, EventInjectionListener eventInjectionListener)477     public static void injectUpEvent(UiAutomation uiAutomation, long downTime,
478             boolean useCurrentEventTime, int xOnScreen, int yOnScreen,
479             EventInjectionListener eventInjectionListener) {
480         injectUpEvent(uiAutomation, downTime, useCurrentEventTime, xOnScreen, yOnScreen, true,
481                 eventInjectionListener);
482     }
483 
484     /**
485      * Injects an {@link MotionEvent#ACTION_UP} event at the given coordinates.
486      *
487      * @param downTime The time of the event, usually from {@link SystemClock#uptimeMillis()}
488      * @param useCurrentEventTime <code>true</code> if it should use the current time for the
489      *                            up event or <code>false</code> to use <code>downTime</code>.
490      * @param xOnScreen The x screen coordinate to press on
491      * @param yOnScreen The y screen coordinate to press on
492      * @param waitForAnimations wait for animations to complete before sending an event
493      * @param eventInjectionListener The listener to call back immediately after the up was
494      *                               sent.
495      */
injectUpEvent(UiAutomation uiAutomation, long downTime, boolean useCurrentEventTime, int xOnScreen, int yOnScreen, boolean waitForAnimations, EventInjectionListener eventInjectionListener)496     public static void injectUpEvent(UiAutomation uiAutomation, long downTime,
497             boolean useCurrentEventTime, int xOnScreen, int yOnScreen,
498             boolean waitForAnimations, EventInjectionListener eventInjectionListener) {
499         long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime;
500         MotionEvent eventUp = MotionEvent.obtain(
501                 downTime, eventTime, MotionEvent.ACTION_UP, xOnScreen, yOnScreen, 1);
502         eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
503         uiAutomation.injectInputEvent(eventUp, true, waitForAnimations);
504         if (eventInjectionListener != null) {
505             eventInjectionListener.onUpInjected(xOnScreen, yOnScreen);
506         }
507         eventUp.recycle();
508     }
509 
510     /**
511      * Emulates a fling gesture across the horizontal center of the passed view.
512      *
513      * @param instrumentation the instrumentation used to run the test
514      * @param view the view to fling
515      * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will
516      *      be a downwards gesture
517      * @return The vertical amount of emulated fling in pixels
518      */
emulateFlingGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture)519     public static int emulateFlingGesture(Instrumentation instrumentation,
520             ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture) {
521         return emulateFlingGesture(instrumentation, activityTestRule,
522                 view, isDownwardsFlingGesture, null);
523     }
524 
525     /**
526      * Emulates a fling gesture across the horizontal center of the passed view.
527      *
528      * @param instrumentation the instrumentation used to run the test
529      * @param view the view to fling
530      * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will
531      *      be a downwards gesture
532      * @param eventInjectionListener optional listener to notify about the injected events
533      * @return The vertical amount of emulated fling in pixels
534      */
emulateFlingGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture, EventInjectionListener eventInjectionListener)535     public static int emulateFlingGesture(Instrumentation instrumentation,
536             ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture,
537             EventInjectionListener eventInjectionListener) {
538         return emulateFlingGesture(instrumentation, activityTestRule, view, isDownwardsFlingGesture,
539                 true, eventInjectionListener);
540     }
541 
542     /**
543      * Emulates a fling gesture across the horizontal center of the passed view.
544      *
545      * @param instrumentation the instrumentation used to run the test
546      * @param view the view to fling
547      * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will
548      *      be a downwards gesture
549      * @param waitForAnimations wait for animations to complete before sending an event
550      * @param eventInjectionListener optional listener to notify about the injected events
551      * @return The vertical amount of emulated fling in pixels
552      */
emulateFlingGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture, boolean waitForAnimations, EventInjectionListener eventInjectionListener)553     public static int emulateFlingGesture(Instrumentation instrumentation,
554             ActivityTestRule<?> activityTestRule, View view, boolean isDownwardsFlingGesture,
555             boolean waitForAnimations, EventInjectionListener eventInjectionListener) {
556         final ViewConfiguration configuration = ViewConfiguration.get(view.getContext());
557         final int flingVelocity = (configuration.getScaledMinimumFlingVelocity() +
558                 configuration.getScaledMaximumFlingVelocity()) / 2;
559         // Get view coordinates on the screen
560         final int[] viewOnScreenXY = new int[2];
561         view.getLocationOnScreen(viewOnScreenXY);
562 
563         // Our fling gesture will be from 25% height of the view to 75% height of the view
564         // for downwards fling gesture, and the other way around for upwards fling gesture
565         final int viewHeight = view.getHeight();
566         final int x = viewOnScreenXY[0] + view.getWidth() / 2;
567         final int startY = isDownwardsFlingGesture ? viewOnScreenXY[1] + viewHeight / 4
568                 : viewOnScreenXY[1] + 3 * viewHeight / 4;
569         final int amountY = isDownwardsFlingGesture ? viewHeight / 2 : -viewHeight / 2;
570 
571         // Compute fling gesture duration based on the distance (50% height of the view) and
572         // fling velocity
573         final int durationMs = (1000 * viewHeight) / (2 * flingVelocity);
574 
575         // And do the same event injection sequence as our generic drag gesture
576         emulateDragGesture(instrumentation, activityTestRule,
577                 x, startY, 0, amountY, durationMs, durationMs / 16,
578             waitForAnimations, eventInjectionListener);
579 
580         return amountY;
581     }
582 
583     private static class ViewStateSnapshot {
584         final View mFirst;
585         final View mLast;
586         final int mFirstTop;
587         final int mLastBottom;
588         final int mChildCount;
ViewStateSnapshot(ViewGroup viewGroup)589         private ViewStateSnapshot(ViewGroup viewGroup) {
590             mChildCount = viewGroup.getChildCount();
591             if (mChildCount == 0) {
592                 mFirst = mLast = null;
593                 mFirstTop = mLastBottom = Integer.MIN_VALUE;
594             } else {
595                 mFirst = viewGroup.getChildAt(0);
596                 mLast = viewGroup.getChildAt(mChildCount - 1);
597                 mFirstTop = mFirst.getTop();
598                 mLastBottom = mLast.getBottom();
599             }
600         }
601 
602         @Override
equals(Object o)603         public boolean equals(Object o) {
604             if (this == o) {
605                 return true;
606             }
607             if (o == null || getClass() != o.getClass()) {
608                 return false;
609             }
610 
611             final ViewStateSnapshot that = (ViewStateSnapshot) o;
612             return mFirstTop == that.mFirstTop &&
613                     mLastBottom == that.mLastBottom &&
614                     mFirst == that.mFirst &&
615                     mLast == that.mLast &&
616                     mChildCount == that.mChildCount;
617         }
618 
619         @Override
hashCode()620         public int hashCode() {
621             int result = mFirst != null ? mFirst.hashCode() : 0;
622             result = 31 * result + (mLast != null ? mLast.hashCode() : 0);
623             result = 31 * result + mFirstTop;
624             result = 31 * result + mLastBottom;
625             result = 31 * result + mChildCount;
626             return result;
627         }
628     }
629 
630     /**
631      * Emulates a scroll to the bottom of the specified {@link ViewGroup}.
632      *
633      * @param instrumentation the instrumentation used to run the test
634      * @param viewGroup View group
635      */
emulateScrollToBottom(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, ViewGroup viewGroup)636     public static void emulateScrollToBottom(Instrumentation instrumentation,
637             ActivityTestRule<?> activityTestRule, ViewGroup viewGroup) throws Throwable {
638         final int[] viewGroupOnScreenXY = new int[2];
639         viewGroup.getLocationOnScreen(viewGroupOnScreenXY);
640 
641         final int emulatedX = viewGroupOnScreenXY[0] + viewGroup.getWidth() / 2;
642         final int emulatedStartY = viewGroupOnScreenXY[1] + 3 * viewGroup.getHeight() / 4;
643         final int swipeAmount = viewGroup.getHeight() / 2;
644 
645         ViewStateSnapshot prev;
646         ViewStateSnapshot next = new ViewStateSnapshot(viewGroup);
647         do {
648             prev = next;
649             emulateDragGesture(instrumentation, activityTestRule,
650                     emulatedX, emulatedStartY, 0, -swipeAmount, 300, 10);
651             next = new ViewStateSnapshot(viewGroup);
652         } while (!prev.equals(next));
653 
654         // wait until the overscroll animation completes
655         final boolean[] redrawn = new boolean[1];
656         final boolean[] animationFinished = new boolean[1];
657         final ViewTreeObserver.OnDrawListener onDrawListener = () -> {
658             redrawn[0] = true;
659         };
660 
661         activityTestRule.runOnUiThread(() -> {
662             viewGroup.getViewTreeObserver().addOnDrawListener(onDrawListener);
663         });
664         while (!animationFinished[0]) {
665             activityTestRule.runOnUiThread(() -> {
666                 if (!redrawn[0]) {
667                     animationFinished[0] = true;
668                 }
669                 redrawn[0] = false;
670             });
671         }
672         activityTestRule.runOnUiThread(() -> {
673             viewGroup.getViewTreeObserver().removeOnDrawListener(onDrawListener);
674         });
675     }
676 
677     /**
678      * Emulates a long press in the center of the passed {@link View}.
679      *
680      * @param instrumentation the instrumentation used to run the test
681      * @param view the view to "long press"
682      */
emulateLongPressOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view)683     public static void emulateLongPressOnViewCenter(Instrumentation instrumentation,
684             ActivityTestRule<?> activityTestRule, View view) {
685         emulateLongPressOnViewCenter(instrumentation, activityTestRule, view, 0);
686     }
687 
688     /**
689      * Emulates a long press in the center of the passed {@link View}.
690      *
691      * @param instrumentation the instrumentation used to run the test
692      * @param view the view to "long press"
693      * @param extraWaitMs the duration of emulated "long press" in milliseconds starting
694      *      after system-level long press timeout.
695      */
emulateLongPressOnViewCenter(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, long extraWaitMs)696     public static void emulateLongPressOnViewCenter(Instrumentation instrumentation,
697             ActivityTestRule<?> activityTestRule, View view, long extraWaitMs) {
698         final int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
699         // Use instrumentation to emulate a tap on the spinner to bring down its popup
700         final int[] viewOnScreenXY = new int[2];
701         view.getLocationOnScreen(viewOnScreenXY);
702         int xOnScreen = viewOnScreenXY[0] + view.getWidth() / 2;
703         int yOnScreen = viewOnScreenXY[1] + view.getHeight() / 2;
704 
705         emulateLongPressOnScreen(instrumentation, activityTestRule,
706                 xOnScreen, yOnScreen, touchSlop, extraWaitMs, true);
707     }
708 
709     /**
710      * Emulates a long press confirmed on a point relative to the top-left corner of the passed
711      * {@link View}. Offset parameters are used to compute the final screen coordinates of the
712      * press point.
713      *
714      * @param instrumentation the instrumentation used to run the test
715      * @param view the view to "long press"
716      * @param offsetX extra X offset for the tap
717      * @param offsetY extra Y offset for the tap
718      */
emulateLongPressOnView(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, View view, int offsetX, int offsetY)719     public static void emulateLongPressOnView(Instrumentation instrumentation,
720             ActivityTestRule<?> activityTestRule, View view, int offsetX, int offsetY) {
721         final int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
722         final int[] viewOnScreenXY = new int[2];
723         view.getLocationOnScreen(viewOnScreenXY);
724         int xOnScreen = viewOnScreenXY[0] + offsetX;
725         int yOnScreen = viewOnScreenXY[1] + offsetY;
726 
727         emulateLongPressOnScreen(instrumentation, activityTestRule,
728                 xOnScreen, yOnScreen, touchSlop, 0, true);
729     }
730 
731     /**
732      * Emulates a long press then a linear drag gesture between 2 points across the screen.
733      * This is used for drag selection.
734      *
735      * @param instrumentation the instrumentation used to run the test
736      * @param dragStartX Start X of the emulated drag gesture
737      * @param dragStartY Start Y of the emulated drag gesture
738      * @param dragAmountX X amount of the emulated drag gesture
739      * @param dragAmountY Y amount of the emulated drag gesture
740      */
emulateLongPressAndDragGesture(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY)741     public static void emulateLongPressAndDragGesture(Instrumentation instrumentation,
742             ActivityTestRule<?> activityTestRule,
743             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY) {
744         emulateLongPressOnScreen(instrumentation, activityTestRule, dragStartX, dragStartY,
745                 0 /* touchSlop */, 0 /* extraWaitMs */, false /* upGesture */);
746         emulateDragGesture(instrumentation, activityTestRule, dragStartX, dragStartY, dragAmountX,
747                 dragAmountY);
748     }
749 
750     /**
751      * Emulates a long press on the screen.
752      *
753      * @param instrumentation the instrumentation used to run the test
754      * @param xOnScreen X position on screen for the "long press"
755      * @param yOnScreen Y position on screen for the "long press"
756      * @param extraWaitMs extra duration of emulated long press in milliseconds added
757      *        after the system-level "long press" timeout.
758      * @param upGesture whether to include an up event.
759      */
emulateLongPressOnScreen(Instrumentation instrumentation, ActivityTestRule<?> activityTestRule, int xOnScreen, int yOnScreen, int touchSlop, long extraWaitMs, boolean upGesture)760     private static void emulateLongPressOnScreen(Instrumentation instrumentation,
761             ActivityTestRule<?> activityTestRule,
762             int xOnScreen, int yOnScreen, int touchSlop, long extraWaitMs, boolean upGesture) {
763         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
764         final long downTime = SystemClock.uptimeMillis();
765 
766         injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
767         injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen, true);
768         SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f) + extraWaitMs);
769         if (upGesture) {
770             injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
771         }
772 
773         // Wait for the system to process all events in the queue
774         if (activityTestRule != null) {
775             WidgetTestUtils.runOnMainAndDrawSync(activityTestRule,
776                     activityTestRule.getActivity().getWindow().getDecorView(), null);
777         } else {
778             instrumentation.waitForIdleSync();
779         }
780     }
781 }
782