1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compatibility.common.util;
18 
19 import android.app.Instrumentation;
20 import android.app.UiAutomation;
21 import android.graphics.Point;
22 import android.os.SystemClock;
23 import android.util.SparseArray;
24 import android.view.InputDevice;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.ViewConfiguration;
28 import android.view.ViewGroup;
29 
30 /**
31  * Test utilities for touch emulation.
32  */
33 public final class CtsTouchUtils {
34     /**
35      * Interface definition for a callback to be invoked when an event has been injected.
36      */
37     public interface EventInjectionListener {
38         /**
39          * Callback method to be invoked when a {MotionEvent#ACTION_DOWN} has been injected.
40          * @param xOnScreen X coordinate of the injected event.
41          * @param yOnScreen Y coordinate of the injected event.
42          */
onDownInjected(int xOnScreen, int yOnScreen)43         public void onDownInjected(int xOnScreen, int yOnScreen);
44 
45         /**
46          * Callback method to be invoked when a {MotionEvent#ACTION_MOVE} has been injected.
47          * @param xOnScreen X coordinates of the injected event.
48          * @param yOnScreen Y coordinates of the injected event.
49          */
onMoveInjected(int[] xOnScreen, int[] yOnScreen)50         public void onMoveInjected(int[] xOnScreen, int[] yOnScreen);
51 
52         /**
53          * Callback method to be invoked when a {MotionEvent#ACTION_UP} has been injected.
54          * @param xOnScreen X coordinate of the injected event.
55          * @param yOnScreen Y coordinate of the injected event.
56          */
onUpInjected(int xOnScreen, int yOnScreen)57         public void onUpInjected(int xOnScreen, int yOnScreen);
58     }
59 
CtsTouchUtils()60     private CtsTouchUtils() {}
61 
62     /**
63      * Emulates a tap in the center of the passed {@link View}.
64      *
65      * @param instrumentation the instrumentation used to run the test
66      * @param view the view to "tap"
67      */
emulateTapOnViewCenter(Instrumentation instrumentation, View view)68     public static void emulateTapOnViewCenter(Instrumentation instrumentation, View view) {
69         emulateTapOnView(instrumentation, view, view.getWidth() / 2, view.getHeight() / 2);
70     }
71 
72     /**
73      * Emulates a tap on a point relative to the top-left corner of the passed {@link View}. Offset
74      * parameters are used to compute the final screen coordinates of the tap point.
75      *
76      * @param instrumentation the instrumentation used to run the test
77      * @param anchorView the anchor view to determine the tap location on the screen
78      * @param offsetX extra X offset for the tap
79      * @param offsetY extra Y offset for the tap
80      */
emulateTapOnView(Instrumentation instrumentation, View anchorView, int offsetX, int offsetY)81     public static void emulateTapOnView(Instrumentation instrumentation, View anchorView,
82             int offsetX, int offsetY) {
83         final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop();
84         // Get anchor coordinates on the screen
85         final int[] viewOnScreenXY = new int[2];
86         anchorView.getLocationOnScreen(viewOnScreenXY);
87         int xOnScreen = viewOnScreenXY[0] + offsetX;
88         int yOnScreen = viewOnScreenXY[1] + offsetY;
89         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
90         final long downTime = SystemClock.uptimeMillis();
91 
92         injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
93         injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen);
94         injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
95 
96         // Wait for the system to process all events in the queue
97         instrumentation.waitForIdleSync();
98     }
99 
100     /**
101      * Emulates a double tap in the center of the passed {@link View}.
102      *
103      * @param instrumentation the instrumentation used to run the test
104      * @param view the view to "double tap"
105      */
emulateDoubleTapOnViewCenter(Instrumentation instrumentation, View view)106     public static void emulateDoubleTapOnViewCenter(Instrumentation instrumentation, View view) {
107         emulateDoubleTapOnView(instrumentation, view, view.getWidth() / 2, view.getHeight() / 2);
108     }
109 
110     /**
111      * Emulates a double tap on a point relative to the top-left corner of the passed {@link View}.
112      * Offset parameters are used to compute the final screen coordinates of the tap points.
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 taps
117      * @param offsetY extra Y offset for the taps
118      */
emulateDoubleTapOnView(Instrumentation instrumentation, View anchorView, int offsetX, int offsetY)119     public static void emulateDoubleTapOnView(Instrumentation instrumentation, View anchorView,
120             int offsetX, int offsetY) {
121         final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop();
122         // Get anchor coordinates on the screen
123         final int[] viewOnScreenXY = new int[2];
124         anchorView.getLocationOnScreen(viewOnScreenXY);
125         int xOnScreen = viewOnScreenXY[0] + offsetX;
126         int yOnScreen = viewOnScreenXY[1] + offsetY;
127         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
128         final long downTime = SystemClock.uptimeMillis();
129 
130         injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
131         injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen);
132         injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
133         injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
134         injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen);
135         injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
136 
137         // Wait for the system to process all events in the queue
138         instrumentation.waitForIdleSync();
139     }
140 
141     /**
142      * Emulates a linear drag gesture between 2 points across the screen.
143      *
144      * @param instrumentation the instrumentation used to run the test
145      * @param dragStartX Start X of the emulated drag gesture
146      * @param dragStartY Start Y of the emulated drag gesture
147      * @param dragAmountX X amount of the emulated drag gesture
148      * @param dragAmountY Y amount of the emulated drag gesture
149      */
emulateDragGesture(Instrumentation instrumentation, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY)150     public static void emulateDragGesture(Instrumentation instrumentation,
151             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY) {
152         emulateDragGesture(instrumentation, dragStartX, dragStartY, dragAmountX, dragAmountY,
153                 2000, 20, null);
154     }
155 
emulateDragGesture(Instrumentation instrumentation, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, int dragDurationMs, int moveEventCount)156     private static void emulateDragGesture(Instrumentation instrumentation,
157             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY,
158             int dragDurationMs, int moveEventCount) {
159         emulateDragGesture(instrumentation, dragStartX, dragStartY, dragAmountX, dragAmountY,
160                 dragDurationMs, moveEventCount, null);
161     }
162 
emulateDragGesture(Instrumentation instrumentation, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY, int dragDurationMs, int moveEventCount, EventInjectionListener eventInjectionListener)163     private static void emulateDragGesture(Instrumentation instrumentation,
164             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY,
165             int dragDurationMs, int moveEventCount,
166             EventInjectionListener eventInjectionListener) {
167         // We are using the UiAutomation object to inject events so that drag works
168         // across view / window boundaries (such as for the emulated drag and drop
169         // sequences)
170         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
171         final long downTime = SystemClock.uptimeMillis();
172 
173         injectDownEvent(uiAutomation, downTime, dragStartX, dragStartY, eventInjectionListener);
174 
175         // Inject a sequence of MOVE events that emulate the "move" part of the gesture
176         injectMoveEventsForDrag(uiAutomation, downTime, true, dragStartX, dragStartY,
177                 dragStartX + dragAmountX, dragStartY + dragAmountY, moveEventCount, dragDurationMs,
178             eventInjectionListener);
179 
180         injectUpEvent(uiAutomation, downTime, true, dragStartX + dragAmountX,
181                 dragStartY + dragAmountY, eventInjectionListener);
182 
183         // Wait for the system to process all events in the queue
184         instrumentation.waitForIdleSync();
185     }
186 
187     /**
188      * Emulates a series of linear drag gestures across the screen between multiple points without
189      * lifting the finger. Note that this function does not support curve movements between the
190      * points.
191      *
192      * @param instrumentation the instrumentation used to run the test
193      * @param coordinates the ordered list of points for the drag gesture
194      */
emulateDragGesture(Instrumentation instrumentation, SparseArray<Point> coordinates)195     public static void emulateDragGesture(Instrumentation instrumentation,
196             SparseArray<Point> coordinates) {
197         emulateDragGesture(instrumentation, coordinates, 2000, 20);
198     }
199 
emulateDragGesture(Instrumentation instrumentation, SparseArray<Point> coordinates, int dragDurationMs, int moveEventCount)200     private static void emulateDragGesture(Instrumentation instrumentation,
201             SparseArray<Point> coordinates, int dragDurationMs, int moveEventCount) {
202         final int coordinatesSize = coordinates.size();
203         if (coordinatesSize < 2) {
204             throw new IllegalArgumentException("Need at least 2 points for emulating drag");
205         }
206         // We are using the UiAutomation object to inject events so that drag works
207         // across view / window boundaries (such as for the emulated drag and drop
208         // sequences)
209         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
210         final long downTime = SystemClock.uptimeMillis();
211 
212         injectDownEvent(uiAutomation, downTime, coordinates.get(0).x, coordinates.get(0).y, null);
213 
214         // Move to each coordinate.
215         for (int i = 0; i < coordinatesSize - 1; i++) {
216             // Inject a sequence of MOVE events that emulate the "move" part of the gesture.
217             injectMoveEventsForDrag(uiAutomation,
218                     downTime,
219                     true,
220                     coordinates.get(i).x,
221                     coordinates.get(i).y,
222                     coordinates.get(i + 1).x,
223                     coordinates.get(i + 1).y,
224                     moveEventCount,
225                     dragDurationMs,
226                     null);
227         }
228 
229         injectUpEvent(uiAutomation,
230                 downTime,
231                 true,
232                 coordinates.get(coordinatesSize - 1).x,
233                 coordinates.get(coordinatesSize - 1).y,
234                 null);
235 
236         // Wait for the system to process all events in the queue
237         instrumentation.waitForIdleSync();
238     }
239 
injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen, int yOnScreen, EventInjectionListener eventInjectionListener)240     private static long injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen,
241             int yOnScreen, EventInjectionListener eventInjectionListener) {
242         MotionEvent eventDown = MotionEvent.obtain(
243                 downTime, downTime, MotionEvent.ACTION_DOWN, xOnScreen, yOnScreen, 1);
244         eventDown.setSource(InputDevice.SOURCE_TOUCHSCREEN);
245         uiAutomation.injectInputEvent(eventDown, true);
246         if (eventInjectionListener != null) {
247             eventInjectionListener.onDownInjected(xOnScreen, yOnScreen);
248         }
249         eventDown.recycle();
250         return downTime;
251     }
252 
injectMoveEventForTap(UiAutomation uiAutomation, long downTime, int touchSlop, int xOnScreen, int yOnScreen)253     private static void injectMoveEventForTap(UiAutomation uiAutomation, long downTime,
254             int touchSlop, int xOnScreen, int yOnScreen) {
255         MotionEvent eventMove = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_MOVE,
256                 xOnScreen + (touchSlop / 2.0f), yOnScreen + (touchSlop / 2.0f), 1);
257         eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN);
258         uiAutomation.injectInputEvent(eventMove, true);
259         eventMove.recycle();
260     }
261 
injectMoveEventsForDrag(UiAutomation uiAutomation, long downTime, boolean useCurrentEventTime, int dragStartX, int dragStartY, int dragEndX, int dragEndY, int moveEventCount, int dragDurationMs, EventInjectionListener eventInjectionListener)262     private static void injectMoveEventsForDrag(UiAutomation uiAutomation, long downTime,
263             boolean useCurrentEventTime, int dragStartX, int dragStartY, int dragEndX, int dragEndY,
264             int moveEventCount, int dragDurationMs, EventInjectionListener eventInjectionListener) {
265         final int dragAmountX = dragEndX - dragStartX;
266         final int dragAmountY = dragEndY - dragStartY;
267         final int sleepTime = dragDurationMs / moveEventCount;
268 
269         // sleep for a bit to emulate the overall drag gesture.
270         long prevEventTime = downTime;
271         SystemClock.sleep(sleepTime);
272         for (int i = 0; i < moveEventCount; i++) {
273             // Note that the first MOVE event is generated "away" from the coordinates
274             // of the start / DOWN event, and the last MOVE event is generated
275             // at the same coordinates as the subsequent UP event.
276             final int moveX = dragStartX + dragAmountX * (i  + 1) / moveEventCount;
277             final int moveY = dragStartY + dragAmountY * (i  + 1) / moveEventCount;
278             long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime;
279 
280             // If necessary, generate history for our next MOVE event. The history is generated
281             // to be spaced at 10 millisecond intervals, interpolating the coordinates from the
282             // last generated MOVE event to our current one.
283             int historyEventCount = (int) ((eventTime - prevEventTime) / 10);
284             int[] xCoordsForListener = (eventInjectionListener == null) ? null :
285                     new int[Math.max(1, historyEventCount)];
286             int[] yCoordsForListener = (eventInjectionListener == null) ? null :
287                     new int[Math.max(1, historyEventCount)];
288             MotionEvent eventMove = null;
289             if (historyEventCount == 0) {
290                 eventMove = MotionEvent.obtain(
291                         downTime, eventTime, MotionEvent.ACTION_MOVE, moveX, moveY, 1);
292                 if (eventInjectionListener != null) {
293                     xCoordsForListener[0] = moveX;
294                     yCoordsForListener[0] = moveY;
295                 }
296             } else {
297                 final int prevMoveX = dragStartX + dragAmountX * i / moveEventCount;
298                 final int prevMoveY = dragStartY + dragAmountY * i / moveEventCount;
299                 final int deltaMoveX = moveX - prevMoveX;
300                 final int deltaMoveY = moveY - prevMoveY;
301                 final long deltaTime = (eventTime - prevEventTime);
302                 for (int historyIndex = 0; historyIndex < historyEventCount; historyIndex++) {
303                     int stepMoveX = prevMoveX + deltaMoveX * (historyIndex + 1) / historyEventCount;
304                     int stepMoveY = prevMoveY + deltaMoveY * (historyIndex + 1) / historyEventCount;
305                     long stepEventTime = useCurrentEventTime
306                             ? prevEventTime + deltaTime * (historyIndex + 1) / historyEventCount
307                             : downTime;
308                     if (historyIndex == 0) {
309                         // Generate the first event in our sequence
310                         eventMove = MotionEvent.obtain(downTime, stepEventTime,
311                                 MotionEvent.ACTION_MOVE, stepMoveX, stepMoveY, 1);
312                     } else {
313                         // and then add to it
314                         eventMove.addBatch(stepEventTime, stepMoveX, stepMoveY, 1.0f, 1.0f, 1);
315                     }
316                     if (eventInjectionListener != null) {
317                         xCoordsForListener[historyIndex] = stepMoveX;
318                         yCoordsForListener[historyIndex] = stepMoveY;
319                     }
320                 }
321             }
322 
323             eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN);
324             uiAutomation.injectInputEvent(eventMove, true);
325             if (eventInjectionListener != null) {
326                 eventInjectionListener.onMoveInjected(xCoordsForListener, yCoordsForListener);
327             }
328             eventMove.recycle();
329             prevEventTime = eventTime;
330 
331             // sleep for a bit to emulate the overall drag gesture.
332             SystemClock.sleep(sleepTime);
333         }
334     }
335 
injectUpEvent(UiAutomation uiAutomation, long downTime, boolean useCurrentEventTime, int xOnScreen, int yOnScreen, EventInjectionListener eventInjectionListener)336     private static void injectUpEvent(UiAutomation uiAutomation, long downTime,
337             boolean useCurrentEventTime, int xOnScreen, int yOnScreen,
338             EventInjectionListener eventInjectionListener) {
339         long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime;
340         MotionEvent eventUp = MotionEvent.obtain(
341                 downTime, eventTime, MotionEvent.ACTION_UP, xOnScreen, yOnScreen, 1);
342         eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
343         uiAutomation.injectInputEvent(eventUp, true);
344         if (eventInjectionListener != null) {
345             eventInjectionListener.onUpInjected(xOnScreen, yOnScreen);
346         }
347         eventUp.recycle();
348     }
349 
350     /**
351      * Emulates a fling gesture across the horizontal center of the passed view.
352      *
353      * @param instrumentation the instrumentation used to run the test
354      * @param view the view to fling
355      * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will
356      *      be a downwards gesture
357      * @return The vertical amount of emulated fling in pixels
358      */
emulateFlingGesture(Instrumentation instrumentation, View view, boolean isDownwardsFlingGesture)359     public static int emulateFlingGesture(Instrumentation instrumentation,
360             View view, boolean isDownwardsFlingGesture) {
361         return emulateFlingGesture(instrumentation, view, isDownwardsFlingGesture, null);
362     }
363 
364     /**
365      * Emulates a fling gesture across the horizontal center of the passed view.
366      *
367      * @param instrumentation the instrumentation used to run the test
368      * @param view the view to fling
369      * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will
370      *      be a downwards gesture
371      * @param eventInjectionListener optional listener to notify about the injected events
372      * @return The vertical amount of emulated fling in pixels
373      */
emulateFlingGesture(Instrumentation instrumentation, View view, boolean isDownwardsFlingGesture, EventInjectionListener eventInjectionListener)374     public static int emulateFlingGesture(Instrumentation instrumentation,
375             View view, boolean isDownwardsFlingGesture,
376             EventInjectionListener eventInjectionListener) {
377         final ViewConfiguration configuration = ViewConfiguration.get(view.getContext());
378         final int flingVelocity = (configuration.getScaledMinimumFlingVelocity() +
379                 configuration.getScaledMaximumFlingVelocity()) / 2;
380         // Get view coordinates on the screen
381         final int[] viewOnScreenXY = new int[2];
382         view.getLocationOnScreen(viewOnScreenXY);
383 
384         // Our fling gesture will be from 25% height of the view to 75% height of the view
385         // for downwards fling gesture, and the other way around for upwards fling gesture
386         final int viewHeight = view.getHeight();
387         final int x = viewOnScreenXY[0] + view.getWidth() / 2;
388         final int startY = isDownwardsFlingGesture ? viewOnScreenXY[1] + viewHeight / 4
389                 : viewOnScreenXY[1] + 3 * viewHeight / 4;
390         final int amountY = isDownwardsFlingGesture ? viewHeight / 2 : -viewHeight / 2;
391 
392         // Compute fling gesture duration based on the distance (50% height of the view) and
393         // fling velocity
394         final int durationMs = (1000 * viewHeight) / (2 * flingVelocity);
395 
396         // And do the same event injection sequence as our generic drag gesture
397         emulateDragGesture(instrumentation, x, startY, 0, amountY, durationMs, durationMs / 16,
398             eventInjectionListener);
399 
400         return amountY;
401     }
402 
403     private static class ViewStateSnapshot {
404         final View mFirst;
405         final View mLast;
406         final int mFirstTop;
407         final int mLastBottom;
408         final int mChildCount;
ViewStateSnapshot(ViewGroup viewGroup)409         private ViewStateSnapshot(ViewGroup viewGroup) {
410             mChildCount = viewGroup.getChildCount();
411             if (mChildCount == 0) {
412                 mFirst = mLast = null;
413                 mFirstTop = mLastBottom = Integer.MIN_VALUE;
414             } else {
415                 mFirst = viewGroup.getChildAt(0);
416                 mLast = viewGroup.getChildAt(mChildCount - 1);
417                 mFirstTop = mFirst.getTop();
418                 mLastBottom = mLast.getBottom();
419             }
420         }
421 
422         @Override
equals(Object o)423         public boolean equals(Object o) {
424             if (this == o) {
425                 return true;
426             }
427             if (o == null || getClass() != o.getClass()) {
428                 return false;
429             }
430 
431             final ViewStateSnapshot that = (ViewStateSnapshot) o;
432             return mFirstTop == that.mFirstTop &&
433                     mLastBottom == that.mLastBottom &&
434                     mFirst == that.mFirst &&
435                     mLast == that.mLast &&
436                     mChildCount == that.mChildCount;
437         }
438 
439         @Override
hashCode()440         public int hashCode() {
441             int result = mFirst != null ? mFirst.hashCode() : 0;
442             result = 31 * result + (mLast != null ? mLast.hashCode() : 0);
443             result = 31 * result + mFirstTop;
444             result = 31 * result + mLastBottom;
445             result = 31 * result + mChildCount;
446             return result;
447         }
448     }
449 
450     /**
451      * Emulates a scroll to the bottom of the specified {@link ViewGroup}.
452      *
453      * @param instrumentation the instrumentation used to run the test
454      * @param viewGroup View group
455      */
emulateScrollToBottom(Instrumentation instrumentation, ViewGroup viewGroup)456     public static void emulateScrollToBottom(Instrumentation instrumentation, ViewGroup viewGroup) {
457         final int[] viewGroupOnScreenXY = new int[2];
458         viewGroup.getLocationOnScreen(viewGroupOnScreenXY);
459 
460         final int emulatedX = viewGroupOnScreenXY[0] + viewGroup.getWidth() / 2;
461         final int emulatedStartY = viewGroupOnScreenXY[1] + 3 * viewGroup.getHeight() / 4;
462         final int swipeAmount = viewGroup.getHeight() / 2;
463 
464         ViewStateSnapshot prev;
465         ViewStateSnapshot next = new ViewStateSnapshot(viewGroup);
466         do {
467             prev = next;
468             emulateDragGesture(instrumentation, emulatedX, emulatedStartY, 0, -swipeAmount,
469                     300, 10);
470             next = new ViewStateSnapshot(viewGroup);
471         } while (!prev.equals(next));
472     }
473 
474     /**
475      * Emulates a long press in the center of the passed {@link View}.
476      *
477      * @param instrumentation the instrumentation used to run the test
478      * @param view the view to "long press"
479      */
emulateLongPressOnViewCenter(Instrumentation instrumentation, View view)480     public static void emulateLongPressOnViewCenter(Instrumentation instrumentation, View view) {
481         emulateLongPressOnViewCenter(instrumentation, view, 0);
482     }
483 
484     /**
485      * Emulates a long press in the center of the passed {@link View}.
486      *
487      * @param instrumentation the instrumentation used to run the test
488      * @param view the view to "long press"
489      * @param extraWaitMs the duration of emulated "long press" in milliseconds starting
490      *      after system-level long press timeout.
491      */
emulateLongPressOnViewCenter(Instrumentation instrumentation, View view, long extraWaitMs)492     public static void emulateLongPressOnViewCenter(Instrumentation instrumentation, View view,
493             long extraWaitMs) {
494         final int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
495         // Use instrumentation to emulate a tap on the spinner to bring down its popup
496         final int[] viewOnScreenXY = new int[2];
497         view.getLocationOnScreen(viewOnScreenXY);
498         int xOnScreen = viewOnScreenXY[0] + view.getWidth() / 2;
499         int yOnScreen = viewOnScreenXY[1] + view.getHeight() / 2;
500 
501         emulateLongPressOnScreen(
502                 instrumentation, xOnScreen, yOnScreen, touchSlop, extraWaitMs, true);
503     }
504 
505     /**
506      * Emulates a long press confirmed on a point relative to the top-left corner of the passed
507      * {@link View}. Offset parameters are used to compute the final screen coordinates of the
508      * press point.
509      *
510      * @param instrumentation the instrumentation used to run the test
511      * @param view the view to "long press"
512      * @param offsetX extra X offset for the tap
513      * @param offsetY extra Y offset for the tap
514      */
emulateLongPressOnView(Instrumentation instrumentation, View view, int offsetX, int offsetY)515     public static void emulateLongPressOnView(Instrumentation instrumentation, View view,
516             int offsetX, int offsetY) {
517         final int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
518         final int[] viewOnScreenXY = new int[2];
519         view.getLocationOnScreen(viewOnScreenXY);
520         int xOnScreen = viewOnScreenXY[0] + offsetX;
521         int yOnScreen = viewOnScreenXY[1] + offsetY;
522 
523         emulateLongPressOnScreen(instrumentation, xOnScreen, yOnScreen, touchSlop, 0, true);
524     }
525 
526     /**
527      * Emulates a long press then a linear drag gesture between 2 points across the screen.
528      * This is used for drag selection.
529      *
530      * @param instrumentation the instrumentation used to run the test
531      * @param dragStartX Start X of the emulated drag gesture
532      * @param dragStartY Start Y of the emulated drag gesture
533      * @param dragAmountX X amount of the emulated drag gesture
534      * @param dragAmountY Y amount of the emulated drag gesture
535      */
emulateLongPressAndDragGesture(Instrumentation instrumentation, int dragStartX, int dragStartY, int dragAmountX, int dragAmountY)536     public static void emulateLongPressAndDragGesture(Instrumentation instrumentation,
537             int dragStartX, int dragStartY, int dragAmountX, int dragAmountY) {
538         emulateLongPressOnScreen(instrumentation, dragStartX, dragStartY,
539                 0 /* touchSlop */, 0 /* extraWaitMs */, false /* upGesture */);
540         emulateDragGesture(instrumentation, dragStartX, dragStartY, dragAmountX, dragAmountY);
541     }
542 
543     /**
544      * Emulates a long press on the screen.
545      *
546      * @param instrumentation the instrumentation used to run the test
547      * @param xOnScreen X position on screen for the "long press"
548      * @param yOnScreen Y position on screen for the "long press"
549      * @param extraWaitMs extra duration of emulated long press in milliseconds added
550      *        after the system-level "long press" timeout.
551      * @param upGesture whether to include an up event.
552      */
emulateLongPressOnScreen(Instrumentation instrumentation, int xOnScreen, int yOnScreen, int touchSlop, long extraWaitMs, boolean upGesture)553     private static void emulateLongPressOnScreen(Instrumentation instrumentation,
554             int xOnScreen, int yOnScreen, int touchSlop, long extraWaitMs, boolean upGesture) {
555         final UiAutomation uiAutomation = instrumentation.getUiAutomation();
556         final long downTime = SystemClock.uptimeMillis();
557 
558         injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
559         injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen);
560         SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f) + extraWaitMs);
561         if (upGesture) {
562             injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
563         }
564 
565         // Wait for the system to process all events in the queue
566         instrumentation.waitForIdleSync();
567     }
568 }
569