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