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