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