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