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