xref: /aosp_15_r20/platform_testing/libraries/flicker/utils/src/android/tools/helpers/GestureHelper.java (revision dd0948b35e70be4c0246aabd6c72554a5eb8b22a)
1 /*
2  * Copyright (C) 2022 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 android.tools.helpers;
18 
19 import android.annotation.NonNull;
20 import android.app.Instrumentation;
21 import android.app.UiAutomation;
22 import android.os.SystemClock;
23 import android.view.InputDevice;
24 import android.view.InputEvent;
25 import android.view.MotionEvent;
26 import android.view.MotionEvent.PointerCoords;
27 import android.view.MotionEvent.PointerProperties;
28 
29 import androidx.annotation.Nullable;
30 
31 /** Injects gestures given an {@link Instrumentation} object. */
32 public class GestureHelper {
33     // Inserted after each motion event injection.
34     private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
35 
36     private final UiAutomation mUiAutomation;
37 
38     /** Primary pointer should be cached here for separate release */
39     @Nullable private PointerProperties mPrimaryPtrProp;
40 
41     @Nullable private PointerCoords mPrimaryPtrCoord;
42     private long mPrimaryPtrDownTime;
43 
44     /** A pair of floating point values. */
45     public static class Tuple {
46         public float x;
47         public float y;
48 
Tuple(float x, float y)49         public Tuple(float x, float y) {
50             this.x = x;
51             this.y = y;
52         }
53     }
54 
GestureHelper(Instrumentation instrumentation)55     public GestureHelper(Instrumentation instrumentation) {
56         mUiAutomation = instrumentation.getUiAutomation();
57     }
58 
59     /**
60      * Injects a series of {@link MotionEvent}s to simulate tapping.
61      *
62      * @param point coordinates of pointer to tap
63      * @param times the number of times to tap
64      */
tap(@onNull Tuple point, int times)65     public boolean tap(@NonNull Tuple point, int times) throws InterruptedException {
66         PointerProperties ptrProp = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER);
67         PointerCoords ptrCoord = getPointerCoord(point.x, point.y, 1, 1);
68 
69         for (int i = 0; i <= times; i++) {
70             // If already tapped, inject delay in between movements
71             if (times > 0) {
72                 SystemClock.sleep(50L);
73             }
74             if (!primaryPointerDown(ptrProp, ptrCoord, SystemClock.uptimeMillis())) {
75                 return false;
76             }
77             // Delay before releasing tap
78             SystemClock.sleep(100L);
79             if (!primaryPointerUp(ptrProp, ptrCoord, SystemClock.uptimeMillis())) {
80                 return false;
81             }
82         }
83         return true;
84     }
85 
86     /**
87      * Injects a series of {@link MotionEvent}s to simulate a drag gesture without pointer release.
88      *
89      * <p>Simulates a drag gesture without releasing the primary pointer. The primary pointer info
90      * will be cached for potential release later on by {@code releasePrimaryPointer()}
91      *
92      * @param startPoint initial coordinates of the primary pointer
93      * @param endPoint final coordinates of the primary pointer
94      * @param steps number of steps to take to animate dragging
95      * @return true if gesture is injected successfully
96      */
dragWithoutRelease( @onNull Tuple startPoint, @NonNull Tuple endPoint, int steps)97     public boolean dragWithoutRelease(
98             @NonNull Tuple startPoint, @NonNull Tuple endPoint, int steps) {
99         PointerProperties ptrProp = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER);
100         PointerCoords ptrCoord = getPointerCoord(startPoint.x, startPoint.y, 1, 1);
101 
102         PointerProperties[] ptrProps = new PointerProperties[] {ptrProp};
103         PointerCoords[] ptrCoords = new PointerCoords[] {ptrCoord};
104 
105         long downTime = SystemClock.uptimeMillis();
106 
107         if (!primaryPointerDown(ptrProp, ptrCoord, downTime)) {
108             return false;
109         }
110 
111         // cache the primary pointer info for later potential release
112         mPrimaryPtrProp = ptrProp;
113         mPrimaryPtrCoord = ptrCoord;
114         mPrimaryPtrDownTime = downTime;
115 
116         return movePointers(ptrProps, ptrCoords, new Tuple[] {endPoint}, downTime, steps);
117     }
118 
119     /**
120      * Release primary pointer if previous gesture has cached the primary pointer info.
121      *
122      * @return true if the release was injected successfully
123      */
releasePrimaryPointer()124     public boolean releasePrimaryPointer() {
125         if (mPrimaryPtrProp != null && mPrimaryPtrCoord != null) {
126             return primaryPointerUp(mPrimaryPtrProp, mPrimaryPtrCoord, mPrimaryPtrDownTime);
127         }
128 
129         return false;
130     }
131 
132     /**
133      * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture.
134      *
135      * @param startPoint1 initial coordinates of the first pointer
136      * @param startPoint2 initial coordinates of the second pointer
137      * @param endPoint1 final coordinates of the first pointer
138      * @param endPoint2 final coordinates of the second pointer
139      * @param steps number of steps to take to animate pinching
140      * @return true if gesture is injected successfully
141      */
pinch( @onNull Tuple startPoint1, @NonNull Tuple startPoint2, @NonNull Tuple endPoint1, @NonNull Tuple endPoint2, int steps)142     public boolean pinch(
143             @NonNull Tuple startPoint1,
144             @NonNull Tuple startPoint2,
145             @NonNull Tuple endPoint1,
146             @NonNull Tuple endPoint2,
147             int steps) {
148         PointerProperties ptrProp1 = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER);
149         PointerProperties ptrProp2 = getPointerProp(1, MotionEvent.TOOL_TYPE_FINGER);
150 
151         PointerCoords ptrCoord1 = getPointerCoord(startPoint1.x, startPoint1.y, 1, 1);
152         PointerCoords ptrCoord2 = getPointerCoord(startPoint2.x, startPoint2.y, 1, 1);
153 
154         PointerProperties[] ptrProps = new PointerProperties[] {ptrProp1, ptrProp2};
155 
156         PointerCoords[] ptrCoords = new PointerCoords[] {ptrCoord1, ptrCoord2};
157 
158         long downTime = SystemClock.uptimeMillis();
159 
160         if (!primaryPointerDown(ptrProp1, ptrCoord1, downTime)) {
161             return false;
162         }
163 
164         if (!nonPrimaryPointerDown(ptrProps, ptrCoords, downTime, 1)) {
165             return false;
166         }
167 
168         if (!movePointers(
169                 ptrProps, ptrCoords, new Tuple[] {endPoint1, endPoint2}, downTime, steps)) {
170             return false;
171         }
172 
173         if (!nonPrimaryPointerUp(ptrProps, ptrCoords, downTime, 1)) {
174             return false;
175         }
176 
177         return primaryPointerUp(ptrProp1, ptrCoord1, downTime);
178     }
179 
primaryPointerDown( @onNull PointerProperties prop, @NonNull PointerCoords coord, long downTime)180     private boolean primaryPointerDown(
181             @NonNull PointerProperties prop, @NonNull PointerCoords coord, long downTime) {
182         MotionEvent event =
183                 getMotionEvent(
184                         downTime,
185                         downTime,
186                         MotionEvent.ACTION_DOWN,
187                         1,
188                         new PointerProperties[] {prop},
189                         new PointerCoords[] {coord});
190 
191         return injectEventSync(event);
192     }
193 
nonPrimaryPointerDown( @onNull PointerProperties[] props, @NonNull PointerCoords[] coords, long downTime, int index)194     private boolean nonPrimaryPointerDown(
195             @NonNull PointerProperties[] props,
196             @NonNull PointerCoords[] coords,
197             long downTime,
198             int index) {
199         // at least 2 pointers are needed
200         if (props.length != coords.length || coords.length < 2) {
201             return false;
202         }
203 
204         long eventTime = SystemClock.uptimeMillis();
205 
206         MotionEvent event =
207                 getMotionEvent(
208                         downTime,
209                         eventTime,
210                         MotionEvent.ACTION_POINTER_DOWN
211                                 + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT),
212                         coords.length,
213                         props,
214                         coords);
215 
216         return injectEventSync(event);
217     }
218 
movePointers( @onNull PointerProperties[] props, @NonNull PointerCoords[] coords, @NonNull Tuple[] endPoints, long downTime, int steps)219     private boolean movePointers(
220             @NonNull PointerProperties[] props,
221             @NonNull PointerCoords[] coords,
222             @NonNull Tuple[] endPoints,
223             long downTime,
224             int steps) {
225         // the number of endpoints should be the same as the number of pointers
226         if (props.length != coords.length || coords.length != endPoints.length) {
227             return false;
228         }
229 
230         // prevent division by 0 and negative number of steps
231         if (steps < 1) {
232             steps = 1;
233         }
234 
235         // save the starting points before updating any pointers
236         Tuple[] startPoints = new Tuple[coords.length];
237 
238         for (int i = 0; i < coords.length; i++) {
239             startPoints[i] = new Tuple(coords[i].x, coords[i].y);
240         }
241 
242         MotionEvent event;
243         long eventTime;
244 
245         for (int i = 0; i < steps; i++) {
246             // inject a delay between movements
247             SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
248 
249             // update the coordinates
250             for (int j = 0; j < coords.length; j++) {
251                 coords[j].x += (endPoints[j].x - startPoints[j].x) / steps;
252                 coords[j].y += (endPoints[j].y - startPoints[j].y) / steps;
253             }
254 
255             eventTime = SystemClock.uptimeMillis();
256 
257             event =
258                     getMotionEvent(
259                             downTime,
260                             eventTime,
261                             MotionEvent.ACTION_MOVE,
262                             coords.length,
263                             props,
264                             coords);
265 
266             boolean didInject = injectEventSync(event);
267 
268             if (!didInject) {
269                 return false;
270             }
271         }
272 
273         return true;
274     }
275 
primaryPointerUp( @onNull PointerProperties prop, @NonNull PointerCoords coord, long downTime)276     private boolean primaryPointerUp(
277             @NonNull PointerProperties prop, @NonNull PointerCoords coord, long downTime) {
278         long eventTime = SystemClock.uptimeMillis();
279 
280         MotionEvent event =
281                 getMotionEvent(
282                         downTime,
283                         eventTime,
284                         MotionEvent.ACTION_UP,
285                         1,
286                         new PointerProperties[] {prop},
287                         new PointerCoords[] {coord});
288 
289         return injectEventSync(event);
290     }
291 
nonPrimaryPointerUp( @onNull PointerProperties[] props, @NonNull PointerCoords[] coords, long downTime, int index)292     private boolean nonPrimaryPointerUp(
293             @NonNull PointerProperties[] props,
294             @NonNull PointerCoords[] coords,
295             long downTime,
296             int index) {
297         // at least 2 pointers are needed
298         if (props.length != coords.length || coords.length < 2) {
299             return false;
300         }
301 
302         long eventTime = SystemClock.uptimeMillis();
303 
304         MotionEvent event =
305                 getMotionEvent(
306                         downTime,
307                         eventTime,
308                         MotionEvent.ACTION_POINTER_UP
309                                 + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT),
310                         coords.length,
311                         props,
312                         coords);
313 
314         return injectEventSync(event);
315     }
316 
getPointerCoord(float x, float y, float pressure, float size)317     private PointerCoords getPointerCoord(float x, float y, float pressure, float size) {
318         PointerCoords ptrCoord = new PointerCoords();
319         ptrCoord.x = x;
320         ptrCoord.y = y;
321         ptrCoord.pressure = pressure;
322         ptrCoord.size = size;
323         return ptrCoord;
324     }
325 
getPointerProp(int id, int toolType)326     private PointerProperties getPointerProp(int id, int toolType) {
327         PointerProperties ptrProp = new PointerProperties();
328         ptrProp.id = id;
329         ptrProp.toolType = toolType;
330         return ptrProp;
331     }
332 
getMotionEvent( long downTime, long eventTime, int action, int pointerCount, PointerProperties[] ptrProps, PointerCoords[] ptrCoords)333     private static MotionEvent getMotionEvent(
334             long downTime,
335             long eventTime,
336             int action,
337             int pointerCount,
338             PointerProperties[] ptrProps,
339             PointerCoords[] ptrCoords) {
340         return MotionEvent.obtain(
341                 downTime,
342                 eventTime,
343                 action,
344                 pointerCount,
345                 ptrProps,
346                 ptrCoords,
347                 0,
348                 0,
349                 1.0f,
350                 1.0f,
351                 0,
352                 0,
353                 InputDevice.SOURCE_TOUCHSCREEN,
354                 0);
355     }
356 
injectEventSync(InputEvent event)357     private boolean injectEventSync(InputEvent event) {
358         return mUiAutomation.injectInputEvent(event, true);
359     }
360 }
361