1 /* 2 * Copyright (C) 2017 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.view.inputmethod.cts.util; 18 19 import static android.view.Display.DEFAULT_DISPLAY; 20 import static android.view.MotionEvent.ACTION_DOWN; 21 import static android.view.MotionEvent.ACTION_UP; 22 import static android.view.WindowInsets.Type.displayCutout; 23 import static android.view.WindowInsets.Type.systemBars; 24 import static android.view.WindowInsets.Type.systemGestures; 25 26 import static com.android.compatibility.common.util.SystemUtil.runShellCommand; 27 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; 28 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 29 30 import static org.junit.Assert.assertFalse; 31 import static org.junit.Assert.fail; 32 33 import android.app.Activity; 34 import android.app.ActivityTaskManager; 35 import android.app.Instrumentation; 36 import android.app.KeyguardManager; 37 import android.content.Context; 38 import android.graphics.Rect; 39 import android.hardware.display.DisplayManager; 40 import android.os.PowerManager; 41 import android.os.SystemClock; 42 import android.server.wm.CtsWindowInfoUtils; 43 import android.view.Display; 44 import android.view.InputDevice; 45 import android.view.InputEvent; 46 import android.view.KeyEvent; 47 import android.view.MotionEvent; 48 import android.view.View; 49 import android.view.ViewConfiguration; 50 import android.view.WindowMetrics; 51 import android.view.inputmethod.InputMethodManager; 52 53 import androidx.annotation.NonNull; 54 import androidx.annotation.Nullable; 55 import androidx.test.platform.app.InstrumentationRegistry; 56 57 import com.android.compatibility.common.util.CommonTestUtils; 58 import com.android.compatibility.common.util.SystemUtil; 59 import com.android.cts.input.UinputStylus; 60 import com.android.cts.input.UinputTouchDevice; 61 import com.android.cts.input.UinputTouchScreen; 62 63 import java.time.Duration; 64 import java.util.ArrayList; 65 import java.util.List; 66 import java.util.concurrent.ExecutionException; 67 import java.util.concurrent.FutureTask; 68 import java.util.concurrent.TimeUnit; 69 import java.util.concurrent.TimeoutException; 70 import java.util.concurrent.atomic.AtomicBoolean; 71 import java.util.concurrent.atomic.AtomicReference; 72 import java.util.function.BooleanSupplier; 73 import java.util.function.Supplier; 74 75 public final class TestUtils { 76 private static final long TIME_SLICE = 100; // msec 77 78 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30); 79 80 /** 81 * Executes a call on the application's main thread, blocking until it is complete. 82 * 83 * <p>A simple wrapper for {@link Instrumentation#runOnMainSync(Runnable)}.</p> 84 * 85 * @param task task to be called on the UI thread 86 */ runOnMainSync(@onNull Runnable task)87 public static void runOnMainSync(@NonNull Runnable task) { 88 InstrumentationRegistry.getInstrumentation().runOnMainSync(task); 89 } 90 91 /** 92 * Executes a call on the application's main thread, blocking until it is complete. When a 93 * Throwable is thrown in the runnable, the exception is propagated back to the 94 * caller's thread. If it is an unchecked throwable, it will be rethrown as is. If it is a 95 * checked exception, it will be rethrown as a {@link RuntimeException}. 96 * 97 * <p>A simple wrapper for {@link Instrumentation#runOnMainSync(Runnable)}.</p> 98 * 99 * @param task task to be called on the UI thread 100 */ runOnMainSyncWithRethrowing(@onNull Runnable task)101 public static void runOnMainSyncWithRethrowing(@NonNull Runnable task) { 102 FutureTask<Void> wrapped = new FutureTask<>(task, null); 103 InstrumentationRegistry.getInstrumentation().runOnMainSync(wrapped); 104 try { 105 wrapped.get(); 106 } catch (InterruptedException e) { 107 throw new RuntimeException(e); 108 } catch (ExecutionException e) { 109 Throwable cause = e.getCause(); 110 if (cause instanceof RuntimeException) { 111 throw (RuntimeException) cause; 112 } else if (cause instanceof Error) { 113 throw (Error) cause; 114 } 115 throw new RuntimeException(cause); 116 } 117 } 118 119 /** 120 * Retrieves a value that needs to be obtained on the main thread. 121 * 122 * <p>A simple utility method that helps to return an object from the UI thread.</p> 123 * 124 * @param supplier callback to be called on the UI thread to return a value 125 * @param <T> Type of the value to be returned 126 * @return Value returned from {@code supplier} 127 */ getOnMainSync(@onNull Supplier<T> supplier)128 public static <T> T getOnMainSync(@NonNull Supplier<T> supplier) { 129 final AtomicReference<T> result = new AtomicReference<>(); 130 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 131 instrumentation.runOnMainSync(() -> result.set(supplier.get())); 132 return result.get(); 133 } 134 135 /** 136 * Does polling loop on the UI thread to wait until the given condition is met. 137 * 138 * @param condition Condition to be satisfied. This is guaranteed to run on the UI thread. 139 * @param timeout timeout in millisecond 140 * @param message message to display when timeout occurs. 141 * @throws TimeoutException when the no event is matched to the given condition within 142 * {@code timeout} 143 */ waitOnMainUntil( @onNull BooleanSupplier condition, long timeout, String message)144 public static void waitOnMainUntil( 145 @NonNull BooleanSupplier condition, long timeout, String message) 146 throws TimeoutException { 147 final AtomicBoolean result = new AtomicBoolean(); 148 149 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 150 while (!result.get()) { 151 if (timeout < 0) { 152 throw new TimeoutException(message); 153 } 154 instrumentation.runOnMainSync(() -> { 155 if (condition.getAsBoolean()) { 156 result.set(true); 157 } 158 }); 159 try { 160 Thread.sleep(TIME_SLICE); 161 } catch (InterruptedException e) { 162 throw new IllegalStateException(e); 163 } 164 timeout -= TIME_SLICE; 165 } 166 } 167 168 /** 169 * Does polling loop on the UI thread to wait until the given condition is met. 170 * 171 * @param condition Condition to be satisfied. This is guaranteed to run on the UI thread. 172 * @param timeout timeout in millisecond 173 * @throws TimeoutException when the no event is matched to the given condition within 174 * {@code timeout} 175 */ waitOnMainUntil(@onNull BooleanSupplier condition, long timeout)176 public static void waitOnMainUntil(@NonNull BooleanSupplier condition, long timeout) 177 throws TimeoutException { 178 waitOnMainUntil(condition, timeout, ""); 179 } 180 isInputMethodPickerShown(@onNull InputMethodManager imm)181 public static boolean isInputMethodPickerShown(@NonNull InputMethodManager imm) { 182 return SystemUtil.runWithShellPermissionIdentity(imm::isInputMethodPickerShown); 183 } 184 185 /** Returns {@code true} if the default display supports split screen multi-window. */ supportsSplitScreenMultiWindow()186 public static boolean supportsSplitScreenMultiWindow() { 187 final Context context = InstrumentationRegistry.getInstrumentation().getContext(); 188 final DisplayManager dm = context.getSystemService(DisplayManager.class); 189 final Display defaultDisplay = dm.getDisplay(DEFAULT_DISPLAY); 190 return ActivityTaskManager.supportsSplitScreenMultiWindow( 191 context.createDisplayContext(defaultDisplay)); 192 } 193 194 /** 195 * Call a command to turn screen On. 196 * 197 * This method will wait until the power state is interactive with {@link 198 * PowerManager#isInteractive()}. 199 */ turnScreenOn()200 public static void turnScreenOn() throws Exception { 201 final Context context = InstrumentationRegistry.getInstrumentation().getContext(); 202 final PowerManager pm = context.getSystemService(PowerManager.class); 203 runShellCommand("input keyevent KEYCODE_WAKEUP"); 204 CommonTestUtils.waitUntil("Device does not wake up after " + TIMEOUT + " seconds", TIMEOUT, 205 () -> pm != null && pm.isInteractive()); 206 } 207 208 /** 209 * Call a command to turn screen off. 210 * 211 * This method will wait until the power state is *NOT* interactive with 212 * {@link PowerManager#isInteractive()}. 213 * Note that {@link PowerManager#isInteractive()} may not return {@code true} when the device 214 * enables Aod mode, recommend to add (@link DisableScreenDozeRule} in the test to disable Aod 215 * for making power state reliable. 216 */ turnScreenOff()217 public static void turnScreenOff() throws Exception { 218 final Context context = InstrumentationRegistry.getInstrumentation().getContext(); 219 final PowerManager pm = context.getSystemService(PowerManager.class); 220 runShellCommand("input keyevent KEYCODE_SLEEP"); 221 CommonTestUtils.waitUntil("Device does not sleep after " + TIMEOUT + " seconds", TIMEOUT, 222 () -> pm != null && !pm.isInteractive()); 223 } 224 225 /** 226 * Simulates a {@link KeyEvent#KEYCODE_MENU} event to unlock screen. 227 * 228 * This method will retry until {@link KeyguardManager#isKeyguardLocked()} return {@code false} 229 * in given timeout. 230 * 231 * Note that {@link KeyguardManager} is not accessible in instant mode due to security concern, 232 * so this method always throw exception with instant app. 233 */ unlockScreen()234 public static void unlockScreen() throws Exception { 235 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 236 final Context context = instrumentation.getContext(); 237 final KeyguardManager kgm = context.getSystemService(KeyguardManager.class); 238 239 assertFalse("This method is currently not supported in instant apps.", 240 context.getPackageManager().isInstantApp()); 241 CommonTestUtils.waitUntil("Device does not unlock after " + TIMEOUT + " seconds", TIMEOUT, 242 () -> { 243 SystemUtil.runWithShellPermissionIdentity( 244 () -> instrumentation.sendKeyDownUpSync((KeyEvent.KEYCODE_MENU))); 245 return kgm != null && !kgm.isKeyguardLocked(); 246 }); 247 } 248 249 /** 250 * Simulates a {@link KeyEvent} event to app. 251 * @param keyCode for the {@link KeyEvent} to inject. 252 * @param instrumentation test {@link Instrumentation} used for injection. 253 */ injectKeyEvent(int keyCode, Instrumentation instrumentation)254 public static void injectKeyEvent(int keyCode, Instrumentation instrumentation) 255 throws Exception { 256 final long timestamp = SystemClock.uptimeMillis(); 257 instrumentation.getUiAutomation().injectInputEvent( 258 new KeyEvent(timestamp, timestamp, KeyEvent.ACTION_DOWN, keyCode, 0) 259 , true /* sync */); 260 instrumentation.getUiAutomation().injectInputEvent( 261 new KeyEvent(timestamp, timestamp, KeyEvent.ACTION_UP, keyCode, 0) 262 , true /* sync */); 263 } 264 265 /** 266 * Waits until the given activity is ready for input, this is only needed when directly 267 * injecting input on screen via 268 * {@link android.hardware.input.InputManager#injectInputEvent(InputEvent, int)}. 269 */ waitUntilActivityReadyForInputInjection(Activity activity, String tag, String windowDumpErrMsg)270 public static void waitUntilActivityReadyForInputInjection(Activity activity, 271 String tag, String windowDumpErrMsg) throws InterruptedException { 272 // If we requested an orientation change, just waiting for the window to be visible is not 273 // sufficient. We should first wait for the transitions to stop, and the for app's UI thread 274 // to process them before making sure the window is visible. 275 CtsWindowInfoUtils.waitForStableWindowGeometry(Duration.ofSeconds(5)); 276 if (activity.getWindow() != null 277 && !CtsWindowInfoUtils.waitForWindowOnTop(activity.getWindow())) { 278 CtsWindowInfoUtils.dumpWindowsOnScreen(tag, windowDumpErrMsg); 279 fail("Activity window did not become visible: " + activity); 280 } 281 } 282 283 /** 284 * Call a command to force stop the given application package. 285 * 286 * @param pkg The name of the package to be stopped. 287 */ forceStopPackage(@onNull String pkg)288 public static void forceStopPackage(@NonNull String pkg) { 289 runWithShellPermissionIdentity(() -> { 290 runShellCommandOrThrow("am force-stop " + pkg); 291 }); 292 } 293 294 /** 295 * Call a command to force stop the given application package. 296 * 297 * @param pkg The name of the package to be stopped. 298 * @param userId The target user ID. 299 */ forceStopPackage(@onNull String pkg, int userId)300 public static void forceStopPackage(@NonNull String pkg, int userId) { 301 runWithShellPermissionIdentity(() -> { 302 runShellCommandOrThrow("am force-stop " + pkg + " --user " + userId); 303 }); 304 } 305 306 /** 307 * Inject Stylus move on the Display inside view coordinates so that initiation can happen. 308 * @param view view on which stylus events should be overlapped. 309 */ injectStylusEvents(@onNull View view)310 public static void injectStylusEvents(@NonNull View view) { 311 int offsetX = view.getWidth() / 2; 312 int offsetY = view.getHeight() / 2; 313 injectStylusEvents(view, offsetX, offsetY); 314 } 315 316 /** 317 * Inject a stylus ACTION_DOWN event to the screen using given view's coordinates. 318 * @param view view whose coordinates are used to compute the event location. 319 * @param x the x coordinates of the stylus event in the view's location coordinates. 320 * @param y the y coordinates of the stylus event in the view's location coordinates. 321 * @return the injected MotionEvent. 322 */ injectStylusDownEvent(@onNull View view, int x, int y)323 public static MotionEvent injectStylusDownEvent(@NonNull View view, int x, int y) { 324 return injectStylusEvent(view, ACTION_DOWN, x, y); 325 } 326 327 /** 328 * Inject a stylus ACTION_DOWN event in a multi-touch environment to the screen using given 329 * view's coordinates. 330 * 331 * @param device {@link UinputTouchDevice} stylus device. 332 * @param view view whose coordinates are used to compute the event location. 333 * @param x the x coordinates of the stylus event in the view's location coordinates. 334 * @param y the y coordinates of the stylus event in the view's location coordinates. 335 * @return The object associated with the down pointer, which will later be used to 336 * trigger move or lift. 337 */ injectStylusDownEvent( @onNull UinputTouchDevice device, @NonNull View view, int x, int y)338 public static UinputTouchDevice.Pointer injectStylusDownEvent( 339 @NonNull UinputTouchDevice device, @NonNull View view, int x, int y) { 340 return injectStylusDownEventInternal(device, view, x, y); 341 } 342 343 /** 344 * Inject a stylus ACTION_DOWN event in a multi-touch environment to the screen using given 345 * view's coordinates. 346 * 347 * @param device {@link UinputTouchDevice} stylus device. 348 * @param x the x coordinates of the stylus event in display's coordinates. 349 * @param y the y coordinates of the stylus event in display's coordinates. 350 * @return The object associated with the down pointer, which will later be used to 351 * trigger move or lift. 352 */ injectStylusDownEvent( @onNull UinputTouchDevice device, int x, int y)353 public static UinputTouchDevice.Pointer injectStylusDownEvent( 354 @NonNull UinputTouchDevice device, int x, int y) { 355 return injectStylusDownEventInternal(device, null /* view */, x, y); 356 } 357 injectStylusDownEventInternal( @onNull UinputTouchDevice device, @Nullable View view, int x, int y)358 private static UinputTouchDevice.Pointer injectStylusDownEventInternal( 359 @NonNull UinputTouchDevice device, @Nullable View view, int x, int y) { 360 int[] xy = new int[2]; 361 if (view != null) { 362 view.getLocationOnScreen(xy); 363 x += xy[0]; 364 y += xy[1]; 365 } 366 367 return device.touchDown(x, y, 255 /* pressure */); 368 } 369 370 /** 371 * Inject a stylus ACTION_UP event in a multi-touch environment to the screen. 372 * 373 * @param pointer {@link #injectStylusDownEvent(UinputTouchDevice, View, int, int)} 374 * returned Pointer. 375 */ injectStylusUpEvent(@onNull UinputTouchDevice.Pointer pointer)376 public static void injectStylusUpEvent(@NonNull UinputTouchDevice.Pointer pointer) { 377 pointer.lift(); 378 } 379 380 /** 381 * Inject a stylus ACTION_UP event to the screen using given view's coordinates. 382 * @param view view whose coordinates are used to compute the event location. 383 * @param x the x coordinates of the stylus event in the view's location coordinates. 384 * @param y the y coordinates of the stylus event in the view's location coordinates. 385 * @return the injected MotionEvent. 386 */ injectStylusUpEvent(@onNull View view, int x, int y)387 public static MotionEvent injectStylusUpEvent(@NonNull View view, int x, int y) { 388 return injectStylusEvent(view, ACTION_UP, x, y); 389 } 390 injectStylusHoverEvents(@onNull View view, int x, int y)391 public static void injectStylusHoverEvents(@NonNull View view, int x, int y) { 392 injectStylusEvent(view, MotionEvent.ACTION_HOVER_ENTER, x, y); 393 injectStylusEvent(view, MotionEvent.ACTION_HOVER_MOVE, x, y); 394 injectStylusEvent(view, MotionEvent.ACTION_HOVER_EXIT, x, y); 395 } 396 injectStylusEvent(@onNull View view, int action, int x, int y)397 private static MotionEvent injectStylusEvent(@NonNull View view, int action, int x, int y) { 398 int[] xy = new int[2]; 399 view.getLocationOnScreen(xy); 400 x += xy[0]; 401 y += xy[1]; 402 403 // Inject stylus action 404 long eventTime = SystemClock.uptimeMillis(); 405 final MotionEvent event = 406 getMotionEvent(eventTime, eventTime, action, x, y, 407 MotionEvent.TOOL_TYPE_STYLUS); 408 injectMotionEvent(event, true /* sync */); 409 return event; 410 } 411 412 /** 413 * Inject a finger tap event in a multi-touch environment to the screen using given 414 * view's center coordinates. 415 * @param device {@link UinputTouchDevice} touch device. 416 * @param view view whose coordinates are used to compute the event location. 417 */ injectFingerClickOnViewCenter(UinputTouchDevice device, @NonNull View view)418 public static void injectFingerClickOnViewCenter(UinputTouchDevice device, @NonNull View view) { 419 final int[] xy = new int[2]; 420 view.getLocationOnScreen(xy); 421 422 // Inject finger touch event. 423 final int x = xy[0] + view.getWidth() / 2; 424 final int y = xy[1] + view.getHeight() / 2; 425 426 device.touchDown(x, y).lift(); 427 } 428 429 /** 430 * Inject Stylus ACTION_MOVE events to the screen using the given view's coordinates. 431 * 432 * @param view view whose coordinates are used to compute the event location. 433 * @param startX the start x coordinates of the stylus event in the view's local coordinates. 434 * @param startY the start y coordinates of the stylus event in the view's local coordinates. 435 * @param endX the end x coordinates of the stylus event in the view's local coordinates. 436 * @param endY the end y coordinates of the stylus event in the view's local coordinates. 437 * @param number the number of the motion events injected to the view. 438 * @return the injected MotionEvents. 439 */ injectStylusMoveEvents(@onNull View view, int startX, int startY, int endX, int endY, int number)440 public static List<MotionEvent> injectStylusMoveEvents(@NonNull View view, int startX, 441 int startY, int endX, int endY, int number) { 442 int[] xy = new int[2]; 443 view.getLocationOnScreen(xy); 444 445 final float incrementX = ((float) (endX - startX)) / (number - 1); 446 final float incrementY = ((float) (endY - startY)) / (number - 1); 447 448 final List<MotionEvent> injectedEvents = new ArrayList<>(number); 449 // Inject stylus ACTION_MOVE 450 for (int i = 0; i < number; i++) { 451 long time = SystemClock.uptimeMillis(); 452 float x = startX + incrementX * i + xy[0]; 453 float y = startY + incrementY * i + xy[1]; 454 final MotionEvent moveEvent = 455 getMotionEvent(time, time, MotionEvent.ACTION_MOVE, x, y, 456 MotionEvent.TOOL_TYPE_STYLUS); 457 injectMotionEvent(moveEvent, true /* sync */); 458 injectedEvents.add(moveEvent); 459 } 460 return injectedEvents; 461 } 462 463 /** 464 * Inject Stylus ACTION_MOVE events in a multi-device environment tp the screen using the given 465 * view's coordinates. 466 * 467 * @param pointer {@link #injectStylusDownEvent(UinputTouchDevice, View, int, int)} returned 468 * Pointer. 469 * @param view view whose coordinates are used to compute the event location. 470 * @param startX the start x coordinates of the stylus event in the view's local coordinates. 471 * @param startY the start y coordinates of the stylus event in the view's local coordinates. 472 * @param endX the end x coordinates of the stylus event in the view's local coordinates. 473 * @param endY the end y coordinates of the stylus event in the view's local coordinates. 474 * @param number the number of the motion events injected to the view. 475 */ injectStylusMoveEvents( @onNull UinputTouchDevice.Pointer pointer, @NonNull View view, int startX, int startY, int endX, int endY, int number)476 public static void injectStylusMoveEvents( 477 @NonNull UinputTouchDevice.Pointer pointer, @NonNull View view, int startX, int startY, 478 int endX, int endY, int number) { 479 injectStylusMoveEventsInternal( 480 pointer, view, startX, startY, endX, endY, number); 481 } 482 483 /** 484 * Inject Stylus ACTION_MOVE events in a multi-device environment tp the screen using the given 485 * view's coordinates. 486 * 487 * @param pointer {@link #injectStylusDownEvent(UinputTouchDevice, View, int, int)} returned 488 * Pointer. 489 * @param startX the start x coordinates of the stylus event in the display's coordinates. 490 * @param startY the start y coordinates of the stylus event in the display's coordinates. 491 * @param endX the end x coordinates of the stylus event in the display's coordinates. 492 * @param endY the end y coordinates of the stylus event in the display's coordinates. 493 * @param number the number of the motion events injected to the view. 494 */ injectStylusMoveEvents( @onNull UinputTouchDevice.Pointer pointer, int startX, int startY, int endX, int endY, int number)495 public static void injectStylusMoveEvents( 496 @NonNull UinputTouchDevice.Pointer pointer, int startX, int startY, int endX, int endY, 497 int number) { 498 injectStylusMoveEventsInternal( 499 pointer, null /* view */, startX, startY, endX, endY, number); 500 } 501 injectStylusMoveEventsInternal( @onNull UinputTouchDevice.Pointer pointer, @Nullable View view, int startX, int startY, int endX, int endY, int number)502 private static void injectStylusMoveEventsInternal( 503 @NonNull UinputTouchDevice.Pointer pointer, @Nullable View view, int startX, int startY, 504 int endX, int endY, int number) { 505 int[] xy = new int[2]; 506 if (view != null) { 507 view.getLocationOnScreen(xy); 508 } 509 510 final float incrementX = ((float) (endX - startX)) / (number - 1); 511 final float incrementY = ((float) (endY - startY)) / (number - 1); 512 513 // Send stylus ACTION_MOVE. 514 for (int i = 0; i < number; i++) { 515 int x = (int) (startX + incrementX * i + xy[0]); 516 int y = (int) (startY + incrementY * i + xy[1]); 517 pointer.moveTo(x, y); 518 } 519 } 520 521 /** 522 * Inject stylus move on the display at the given position defined in the given view's 523 * coordinates. 524 * 525 * @param view view whose coordinates are used to compute the event location. 526 * @param x the initial x coordinates of the injected stylus events in the view's 527 * local coordinates. 528 * @param y the initial y coordinates of the injected stylus events in the view's 529 * local coordinates. 530 */ injectStylusEvents(@onNull View view, int x, int y)531 public static void injectStylusEvents(@NonNull View view, int x, int y) { 532 injectStylusDownEvent(view, x, y); 533 // Larger than the touchSlop. 534 int endX = x + getTouchSlop(view.getContext()) * 5; 535 injectStylusMoveEvents(view, x, y, endX, y, 10); 536 injectStylusUpEvent(view, endX, y); 537 538 } 539 getMotionEvent(long downTime, long eventTime, int action, float x, float y, int toolType)540 private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, 541 float x, float y, int toolType) { 542 return getMotionEvent(downTime, eventTime, action, (int) x, (int) y, 0, toolType); 543 } 544 getMotionEvent(long downTime, long eventTime, int action, int x, int y, int displayId, int toolType)545 private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, 546 int x, int y, int displayId, int toolType) { 547 // Stylus related properties. 548 MotionEvent.PointerProperties[] properties = 549 new MotionEvent.PointerProperties[] { new MotionEvent.PointerProperties() }; 550 properties[0].toolType = toolType; 551 properties[0].id = 1; 552 MotionEvent.PointerCoords[] coords = 553 new MotionEvent.PointerCoords[] { new MotionEvent.PointerCoords() }; 554 coords[0].x = x; 555 coords[0].y = y; 556 coords[0].pressure = 1; 557 558 final MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, 559 1 /* pointerCount */, properties, coords, 0 /* metaState */, 560 0 /* buttonState */, 1 /* xPrecision */, 1 /* yPrecision */, 0 /* deviceId */, 561 0 /* edgeFlags */, InputDevice.SOURCE_STYLUS, 0 /* flags */); 562 event.setDisplayId(displayId); 563 return event; 564 } 565 injectMotionEvent(MotionEvent event, boolean sync)566 private static void injectMotionEvent(MotionEvent event, boolean sync) { 567 InstrumentationRegistry.getInstrumentation().getUiAutomation().injectInputEvent( 568 event, sync, false /* waitAnimations */); 569 } 570 injectAll(List<MotionEvent> events)571 public static void injectAll(List<MotionEvent> events) { 572 for (MotionEvent event : events) { 573 injectMotionEvent(event, true /* sync */); 574 } 575 InstrumentationRegistry.getInstrumentation().getUiAutomation().syncInputTransactions(false); 576 } getTouchSlop(Context context)577 private static int getTouchSlop(Context context) { 578 return ViewConfiguration.get(context).getScaledTouchSlop(); 579 } 580 581 /** 582 * Since MotionEvents are batched together based on overall system timings (i.e. vsync), we 583 * can't rely on them always showing up batched in the same way. In order to make sure our 584 * test results are consistent, we instead split up the batches so they end up in a 585 * consistent and reproducible stream. 586 * 587 * Note, however, that this ignores the problem of resampling, as we still don't know how to 588 * distinguish resampled events from real events. Only the latter will be consistent and 589 * reproducible. 590 * 591 * @param event The (potentially) batched MotionEvent 592 * @return List of MotionEvents, with each event guaranteed to have zero history size, and 593 * should otherwise be equivalent to the original batch MotionEvent. 594 */ splitBatchedMotionEvent(MotionEvent event)595 public static List<MotionEvent> splitBatchedMotionEvent(MotionEvent event) { 596 final List<MotionEvent> events = new ArrayList<>(); 597 final int historySize = event.getHistorySize(); 598 final int pointerCount = event.getPointerCount(); 599 final MotionEvent.PointerProperties[] properties = 600 new MotionEvent.PointerProperties[pointerCount]; 601 final MotionEvent.PointerCoords[] currentCoords = 602 new MotionEvent.PointerCoords[pointerCount]; 603 for (int p = 0; p < pointerCount; p++) { 604 properties[p] = new MotionEvent.PointerProperties(); 605 event.getPointerProperties(p, properties[p]); 606 currentCoords[p] = new MotionEvent.PointerCoords(); 607 event.getPointerCoords(p, currentCoords[p]); 608 } 609 for (int h = 0; h < historySize; h++) { 610 final long eventTime = event.getHistoricalEventTime(h); 611 MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[pointerCount]; 612 613 for (int p = 0; p < pointerCount; p++) { 614 coords[p] = new MotionEvent.PointerCoords(); 615 event.getHistoricalPointerCoords(p, h, coords[p]); 616 } 617 final MotionEvent singleEvent = 618 MotionEvent.obtain(event.getDownTime(), eventTime, event.getAction(), 619 pointerCount, properties, coords, 620 event.getMetaState(), event.getButtonState(), 621 event.getXPrecision(), event.getYPrecision(), 622 event.getDeviceId(), event.getEdgeFlags(), 623 event.getSource(), event.getFlags()); 624 singleEvent.setActionButton(event.getActionButton()); 625 events.add(singleEvent); 626 } 627 628 final MotionEvent singleEvent = 629 MotionEvent.obtain(event.getDownTime(), event.getEventTime(), event.getAction(), 630 pointerCount, properties, currentCoords, 631 event.getMetaState(), event.getButtonState(), 632 event.getXPrecision(), event.getYPrecision(), 633 event.getDeviceId(), event.getEdgeFlags(), 634 event.getSource(), event.getFlags()); 635 singleEvent.setActionButton(event.getActionButton()); 636 events.add(singleEvent); 637 return events; 638 } 639 640 /** 641 * Inject Motion Events for swipe up on navbar with stylus. 642 * @param activity current test activity. 643 * @param toolType of input {@link MotionEvent#getToolType(int)}. 644 */ injectNavBarToHomeGestureEvents( @onNull Activity activity, int toolType)645 public static void injectNavBarToHomeGestureEvents( 646 @NonNull Activity activity, int toolType) { 647 WindowMetrics metrics = activity.getWindowManager().getCurrentWindowMetrics(); 648 649 var bounds = new Rect(metrics.getBounds()); 650 bounds.inset(metrics.getWindowInsets().getInsetsIgnoringVisibility( 651 displayCutout() | systemBars() | systemGestures())); 652 int startY = bounds.bottom; 653 int startX = bounds.centerX(); 654 int endY = bounds.bottom - bounds.height() / 3; // move a third of the screen up 655 int endX = startX; 656 int steps = 10; 657 658 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 659 try (UinputTouchDevice device = 660 toolType == MotionEvent.TOOL_TYPE_STYLUS 661 ? new UinputStylus(instrumentation, activity.getDisplay()) 662 : new UinputTouchScreen(instrumentation, activity.getDisplay())) { 663 UinputTouchDevice.Pointer pointer; 664 pointer = TestUtils.injectStylusDownEvent(device, startX, startY); 665 TestUtils.injectStylusMoveEvents(pointer, startX, startY, endX, endY, steps); 666 TestUtils.injectStylusUpEvent(pointer); 667 } 668 } 669 } 670