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 * Returns given display's rotation. 267 */ getRotation(int displayId)268 public static String getRotation(int displayId) { 269 return SystemUtil.runShellCommandOrThrow("wm user-rotation -d " + displayId); 270 } 271 272 /** 273 * Set a locked rotation 274 * @param displayId display to set rotation on. 275 * @param rotation the fixed rotation to apply. 276 */ setLockedRotation(int displayId, String rotation)277 public static void setLockedRotation(int displayId, String rotation) { 278 SystemUtil.runShellCommandOrThrow( 279 "wm user-rotation -d " + displayId + " lock " + rotation); 280 } 281 282 /** 283 * Set display rotation in degrees. 284 * @param displayId display to set rotation on. 285 * @param rotation the fixed rotation to apply. 286 */ setRotation(int displayId, String rotation)287 public static void setRotation(int displayId, String rotation) { 288 SystemUtil.runShellCommandOrThrow("wm user-rotation -d " + displayId + " " + rotation); 289 } 290 291 /** 292 * Waits until the given activity is ready for input, this is only needed when directly 293 * injecting input on screen via 294 * {@link android.hardware.input.InputManager#injectInputEvent(InputEvent, int)}. 295 */ waitUntilActivityReadyForInputInjection(Activity activity, String tag, String windowDumpErrMsg)296 public static void waitUntilActivityReadyForInputInjection(Activity activity, 297 String tag, String windowDumpErrMsg) throws InterruptedException { 298 // If we requested an orientation change, just waiting for the window to be visible is not 299 // sufficient. We should first wait for the transitions to stop, and the for app's UI thread 300 // to process them before making sure the window is visible. 301 CtsWindowInfoUtils.waitForStableWindowGeometry(Duration.ofSeconds(5)); 302 if (activity.getWindow() != null 303 && !CtsWindowInfoUtils.waitForWindowOnTop(activity.getWindow())) { 304 CtsWindowInfoUtils.dumpWindowsOnScreen(tag, windowDumpErrMsg); 305 fail("Activity window did not become visible: " + activity); 306 } 307 } 308 309 /** 310 * Call a command to force stop the given application package. 311 * 312 * @param pkg The name of the package to be stopped. 313 */ forceStopPackage(@onNull String pkg)314 public static void forceStopPackage(@NonNull String pkg) { 315 runWithShellPermissionIdentity(() -> { 316 runShellCommandOrThrow("am force-stop " + pkg); 317 }); 318 } 319 320 /** 321 * Call a command to force stop the given application package. 322 * 323 * @param pkg The name of the package to be stopped. 324 * @param userId The target user ID. 325 */ forceStopPackage(@onNull String pkg, int userId)326 public static void forceStopPackage(@NonNull String pkg, int userId) { 327 runWithShellPermissionIdentity(() -> { 328 runShellCommandOrThrow("am force-stop " + pkg + " --user " + userId); 329 }); 330 } 331 332 /** 333 * Inject Stylus move on the Display inside view coordinates so that initiation can happen. 334 * @param view view on which stylus events should be overlapped. 335 */ injectStylusEvents(@onNull View view)336 public static void injectStylusEvents(@NonNull View view) { 337 int offsetX = view.getWidth() / 2; 338 int offsetY = view.getHeight() / 2; 339 injectStylusEvents(view, offsetX, offsetY); 340 } 341 342 /** 343 * Inject a stylus ACTION_DOWN event to the screen using given view's coordinates. 344 * @param view view whose coordinates are used to compute the event location. 345 * @param x the x coordinates of the stylus event in the view's location coordinates. 346 * @param y the y coordinates of the stylus event in the view's location coordinates. 347 * @return the injected MotionEvent. 348 */ injectStylusDownEvent(@onNull View view, int x, int y)349 public static MotionEvent injectStylusDownEvent(@NonNull View view, int x, int y) { 350 return injectStylusEvent(view, ACTION_DOWN, x, y); 351 } 352 353 /** 354 * Inject a stylus ACTION_DOWN event in a multi-touch environment to the screen using given 355 * view's coordinates. 356 * 357 * @param device {@link UinputTouchDevice} stylus device. 358 * @param view view whose coordinates are used to compute the event location. 359 * @param x the x coordinates of the stylus event in the view's location coordinates. 360 * @param y the y coordinates of the stylus event in the view's location coordinates. 361 * @return The object associated with the down pointer, which will later be used to 362 * trigger move or lift. 363 */ injectStylusDownEvent( @onNull UinputTouchDevice device, @NonNull View view, int x, int y)364 public static UinputTouchDevice.Pointer injectStylusDownEvent( 365 @NonNull UinputTouchDevice device, @NonNull View view, int x, int y) { 366 return injectStylusDownEventInternal(device, view, x, y); 367 } 368 369 /** 370 * Inject a stylus ACTION_DOWN event in a multi-touch environment to the screen using given 371 * view's coordinates. 372 * 373 * @param device {@link UinputTouchDevice} stylus device. 374 * @param x the x coordinates of the stylus event in display's coordinates. 375 * @param y the y coordinates of the stylus event in display's coordinates. 376 * @return The object associated with the down pointer, which will later be used to 377 * trigger move or lift. 378 */ injectStylusDownEvent( @onNull UinputTouchDevice device, int x, int y)379 public static UinputTouchDevice.Pointer injectStylusDownEvent( 380 @NonNull UinputTouchDevice device, int x, int y) { 381 return injectStylusDownEventInternal(device, null /* view */, x, y); 382 } 383 injectStylusDownEventInternal( @onNull UinputTouchDevice device, @Nullable View view, int x, int y)384 private static UinputTouchDevice.Pointer injectStylusDownEventInternal( 385 @NonNull UinputTouchDevice device, @Nullable View view, int x, int y) { 386 int[] xy = new int[2]; 387 if (view != null) { 388 view.getLocationOnScreen(xy); 389 x += xy[0]; 390 y += xy[1]; 391 } 392 393 return device.touchDown(x, y, 255 /* pressure */); 394 } 395 396 /** 397 * Inject a stylus ACTION_UP event in a multi-touch environment to the screen. 398 * 399 * @param pointer {@link #injectStylusDownEvent(UinputTouchDevice, View, int, int)} 400 * returned Pointer. 401 */ injectStylusUpEvent(@onNull UinputTouchDevice.Pointer pointer)402 public static void injectStylusUpEvent(@NonNull UinputTouchDevice.Pointer pointer) { 403 pointer.lift(); 404 } 405 406 /** 407 * Inject a stylus ACTION_UP event to the screen using given view's coordinates. 408 * @param view view whose coordinates are used to compute the event location. 409 * @param x the x coordinates of the stylus event in the view's location coordinates. 410 * @param y the y coordinates of the stylus event in the view's location coordinates. 411 * @return the injected MotionEvent. 412 */ injectStylusUpEvent(@onNull View view, int x, int y)413 public static MotionEvent injectStylusUpEvent(@NonNull View view, int x, int y) { 414 return injectStylusEvent(view, ACTION_UP, x, y); 415 } 416 injectStylusHoverEvents(@onNull View view, int x, int y)417 public static void injectStylusHoverEvents(@NonNull View view, int x, int y) { 418 injectStylusEvent(view, MotionEvent.ACTION_HOVER_ENTER, x, y); 419 injectStylusEvent(view, MotionEvent.ACTION_HOVER_MOVE, x, y); 420 injectStylusEvent(view, MotionEvent.ACTION_HOVER_EXIT, x, y); 421 } 422 injectStylusEvent(@onNull View view, int action, int x, int y)423 private static MotionEvent injectStylusEvent(@NonNull View view, int action, int x, int y) { 424 int[] xy = new int[2]; 425 view.getLocationOnScreen(xy); 426 x += xy[0]; 427 y += xy[1]; 428 429 // Inject stylus action 430 long eventTime = SystemClock.uptimeMillis(); 431 final MotionEvent event = 432 getMotionEvent(eventTime, eventTime, action, x, y, 433 MotionEvent.TOOL_TYPE_STYLUS); 434 injectMotionEvent(event, true /* sync */); 435 return event; 436 } 437 438 /** 439 * Inject a finger tap event in a multi-touch environment to the screen using given 440 * view's center coordinates. 441 * @param device {@link UinputTouchDevice} touch device. 442 * @param view view whose coordinates are used to compute the event location. 443 */ injectFingerClickOnViewCenter(UinputTouchDevice device, @NonNull View view)444 public static void injectFingerClickOnViewCenter(UinputTouchDevice device, @NonNull View view) { 445 final int[] xy = new int[2]; 446 view.getLocationOnScreen(xy); 447 448 // Inject finger touch event. 449 final int x = xy[0] + view.getWidth() / 2; 450 final int y = xy[1] + view.getHeight() / 2; 451 452 device.touchDown(x, y).lift(); 453 } 454 455 /** 456 * Inject Stylus ACTION_MOVE events to the screen using the given view's coordinates. 457 * 458 * @param view view whose coordinates are used to compute the event location. 459 * @param startX the start x coordinates of the stylus event in the view's local coordinates. 460 * @param startY the start y coordinates of the stylus event in the view's local coordinates. 461 * @param endX the end x coordinates of the stylus event in the view's local coordinates. 462 * @param endY the end y coordinates of the stylus event in the view's local coordinates. 463 * @param number the number of the motion events injected to the view. 464 * @return the injected MotionEvents. 465 */ injectStylusMoveEvents(@onNull View view, int startX, int startY, int endX, int endY, int number)466 public static List<MotionEvent> injectStylusMoveEvents(@NonNull View view, int startX, 467 int startY, int endX, int endY, int number) { 468 int[] xy = new int[2]; 469 view.getLocationOnScreen(xy); 470 471 final float incrementX = ((float) (endX - startX)) / (number - 1); 472 final float incrementY = ((float) (endY - startY)) / (number - 1); 473 474 final List<MotionEvent> injectedEvents = new ArrayList<>(number); 475 // Inject stylus ACTION_MOVE 476 for (int i = 0; i < number; i++) { 477 long time = SystemClock.uptimeMillis(); 478 float x = startX + incrementX * i + xy[0]; 479 float y = startY + incrementY * i + xy[1]; 480 final MotionEvent moveEvent = 481 getMotionEvent(time, time, MotionEvent.ACTION_MOVE, x, y, 482 MotionEvent.TOOL_TYPE_STYLUS); 483 injectMotionEvent(moveEvent, true /* sync */); 484 injectedEvents.add(moveEvent); 485 } 486 return injectedEvents; 487 } 488 489 /** 490 * Inject Stylus ACTION_MOVE events in a multi-device environment tp the screen using the given 491 * view's coordinates. 492 * 493 * @param pointer {@link #injectStylusDownEvent(UinputTouchDevice, View, int, int)} returned 494 * Pointer. 495 * @param view view whose coordinates are used to compute the event location. 496 * @param startX the start x coordinates of the stylus event in the view's local coordinates. 497 * @param startY the start y coordinates of the stylus event in the view's local coordinates. 498 * @param endX the end x coordinates of the stylus event in the view's local coordinates. 499 * @param endY the end y coordinates of the stylus event in the view's local coordinates. 500 * @param number the number of the motion events injected to the view. 501 */ injectStylusMoveEvents( @onNull UinputTouchDevice.Pointer pointer, @NonNull View view, int startX, int startY, int endX, int endY, int number)502 public static void injectStylusMoveEvents( 503 @NonNull UinputTouchDevice.Pointer pointer, @NonNull View view, int startX, int startY, 504 int endX, int endY, int number) { 505 injectStylusMoveEventsInternal( 506 pointer, view, startX, startY, endX, endY, number); 507 } 508 509 /** 510 * Inject Stylus ACTION_MOVE events in a multi-device environment tp the screen using the given 511 * view's coordinates. 512 * 513 * @param pointer {@link #injectStylusDownEvent(UinputTouchDevice, View, int, int)} returned 514 * Pointer. 515 * @param startX the start x coordinates of the stylus event in the display's coordinates. 516 * @param startY the start y coordinates of the stylus event in the display's coordinates. 517 * @param endX the end x coordinates of the stylus event in the display's coordinates. 518 * @param endY the end y coordinates of the stylus event in the display's coordinates. 519 * @param number the number of the motion events injected to the view. 520 */ injectStylusMoveEvents( @onNull UinputTouchDevice.Pointer pointer, int startX, int startY, int endX, int endY, int number)521 public static void injectStylusMoveEvents( 522 @NonNull UinputTouchDevice.Pointer pointer, int startX, int startY, int endX, int endY, 523 int number) { 524 injectStylusMoveEventsInternal( 525 pointer, null /* view */, startX, startY, endX, endY, number); 526 } 527 injectStylusMoveEventsInternal( @onNull UinputTouchDevice.Pointer pointer, @Nullable View view, int startX, int startY, int endX, int endY, int number)528 private static void injectStylusMoveEventsInternal( 529 @NonNull UinputTouchDevice.Pointer pointer, @Nullable View view, int startX, int startY, 530 int endX, int endY, int number) { 531 int[] xy = new int[2]; 532 if (view != null) { 533 view.getLocationOnScreen(xy); 534 } 535 536 final float incrementX = ((float) (endX - startX)) / (number - 1); 537 final float incrementY = ((float) (endY - startY)) / (number - 1); 538 539 // Send stylus ACTION_MOVE. 540 for (int i = 0; i < number; i++) { 541 int x = (int) (startX + incrementX * i + xy[0]); 542 int y = (int) (startY + incrementY * i + xy[1]); 543 pointer.moveTo(x, y); 544 } 545 } 546 547 /** 548 * Inject stylus move on the display at the given position defined in the given view's 549 * coordinates. 550 * 551 * @param view view whose coordinates are used to compute the event location. 552 * @param x the initial x coordinates of the injected stylus events in the view's 553 * local coordinates. 554 * @param y the initial y coordinates of the injected stylus events in the view's 555 * local coordinates. 556 */ injectStylusEvents(@onNull View view, int x, int y)557 public static void injectStylusEvents(@NonNull View view, int x, int y) { 558 injectStylusDownEvent(view, x, y); 559 // Larger than the touchSlop. 560 int endX = x + getTouchSlop(view.getContext()) * 5; 561 injectStylusMoveEvents(view, x, y, endX, y, 10); 562 injectStylusUpEvent(view, endX, y); 563 564 } 565 getMotionEvent(long downTime, long eventTime, int action, float x, float y, int toolType)566 private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, 567 float x, float y, int toolType) { 568 return getMotionEvent(downTime, eventTime, action, (int) x, (int) y, 0, toolType); 569 } 570 getMotionEvent(long downTime, long eventTime, int action, int x, int y, int displayId, int toolType)571 private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, 572 int x, int y, int displayId, int toolType) { 573 // Stylus related properties. 574 MotionEvent.PointerProperties[] properties = 575 new MotionEvent.PointerProperties[] { new MotionEvent.PointerProperties() }; 576 properties[0].toolType = toolType; 577 properties[0].id = 1; 578 MotionEvent.PointerCoords[] coords = 579 new MotionEvent.PointerCoords[] { new MotionEvent.PointerCoords() }; 580 coords[0].x = x; 581 coords[0].y = y; 582 coords[0].pressure = 1; 583 584 final MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, 585 1 /* pointerCount */, properties, coords, 0 /* metaState */, 586 0 /* buttonState */, 1 /* xPrecision */, 1 /* yPrecision */, 0 /* deviceId */, 587 0 /* edgeFlags */, InputDevice.SOURCE_STYLUS, 0 /* flags */); 588 event.setDisplayId(displayId); 589 return event; 590 } 591 injectMotionEvent(MotionEvent event, boolean sync)592 private static void injectMotionEvent(MotionEvent event, boolean sync) { 593 InstrumentationRegistry.getInstrumentation().getUiAutomation().injectInputEvent( 594 event, sync, false /* waitAnimations */); 595 } 596 injectAll(List<MotionEvent> events)597 public static void injectAll(List<MotionEvent> events) { 598 for (MotionEvent event : events) { 599 injectMotionEvent(event, true /* sync */); 600 } 601 InstrumentationRegistry.getInstrumentation().getUiAutomation().syncInputTransactions(false); 602 } getTouchSlop(Context context)603 private static int getTouchSlop(Context context) { 604 return ViewConfiguration.get(context).getScaledTouchSlop(); 605 } 606 607 /** 608 * Since MotionEvents are batched together based on overall system timings (i.e. vsync), we 609 * can't rely on them always showing up batched in the same way. In order to make sure our 610 * test results are consistent, we instead split up the batches so they end up in a 611 * consistent and reproducible stream. 612 * 613 * Note, however, that this ignores the problem of resampling, as we still don't know how to 614 * distinguish resampled events from real events. Only the latter will be consistent and 615 * reproducible. 616 * 617 * @param event The (potentially) batched MotionEvent 618 * @return List of MotionEvents, with each event guaranteed to have zero history size, and 619 * should otherwise be equivalent to the original batch MotionEvent. 620 */ splitBatchedMotionEvent(MotionEvent event)621 public static List<MotionEvent> splitBatchedMotionEvent(MotionEvent event) { 622 final List<MotionEvent> events = new ArrayList<>(); 623 final int historySize = event.getHistorySize(); 624 final int pointerCount = event.getPointerCount(); 625 final MotionEvent.PointerProperties[] properties = 626 new MotionEvent.PointerProperties[pointerCount]; 627 final MotionEvent.PointerCoords[] currentCoords = 628 new MotionEvent.PointerCoords[pointerCount]; 629 for (int p = 0; p < pointerCount; p++) { 630 properties[p] = new MotionEvent.PointerProperties(); 631 event.getPointerProperties(p, properties[p]); 632 currentCoords[p] = new MotionEvent.PointerCoords(); 633 event.getPointerCoords(p, currentCoords[p]); 634 } 635 for (int h = 0; h < historySize; h++) { 636 final long eventTime = event.getHistoricalEventTime(h); 637 MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[pointerCount]; 638 639 for (int p = 0; p < pointerCount; p++) { 640 coords[p] = new MotionEvent.PointerCoords(); 641 event.getHistoricalPointerCoords(p, h, coords[p]); 642 } 643 final MotionEvent singleEvent = 644 MotionEvent.obtain(event.getDownTime(), eventTime, event.getAction(), 645 pointerCount, properties, coords, 646 event.getMetaState(), event.getButtonState(), 647 event.getXPrecision(), event.getYPrecision(), 648 event.getDeviceId(), event.getEdgeFlags(), 649 event.getSource(), event.getFlags()); 650 singleEvent.setActionButton(event.getActionButton()); 651 events.add(singleEvent); 652 } 653 654 final MotionEvent singleEvent = 655 MotionEvent.obtain(event.getDownTime(), event.getEventTime(), event.getAction(), 656 pointerCount, properties, currentCoords, 657 event.getMetaState(), event.getButtonState(), 658 event.getXPrecision(), event.getYPrecision(), 659 event.getDeviceId(), event.getEdgeFlags(), 660 event.getSource(), event.getFlags()); 661 singleEvent.setActionButton(event.getActionButton()); 662 events.add(singleEvent); 663 return events; 664 } 665 666 /** 667 * Inject Motion Events for swipe up on navbar with stylus. 668 * @param activity current test activity. 669 * @param toolType of input {@link MotionEvent#getToolType(int)}. 670 */ injectNavBarToHomeGestureEvents( @onNull Activity activity, int toolType)671 public static void injectNavBarToHomeGestureEvents( 672 @NonNull Activity activity, int toolType) { 673 WindowMetrics metrics = activity.getWindowManager().getCurrentWindowMetrics(); 674 675 var bounds = new Rect(metrics.getBounds()); 676 bounds.inset(metrics.getWindowInsets().getInsetsIgnoringVisibility( 677 displayCutout() | systemBars() | systemGestures())); 678 int startY = bounds.bottom; 679 int startX = bounds.centerX(); 680 int endY = bounds.bottom - bounds.height() / 3; // move a third of the screen up 681 int endX = startX; 682 int steps = 10; 683 684 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 685 UinputTouchDevice device = (toolType == MotionEvent.TOOL_TYPE_STYLUS) 686 ? new UinputStylus(instrumentation, activity.getDisplay()) 687 : new UinputTouchScreen(instrumentation, activity.getDisplay()); 688 UinputTouchDevice.Pointer pointer; 689 pointer = TestUtils.injectStylusDownEvent(device, startX, startY); 690 TestUtils.injectStylusMoveEvents(pointer, startX, startY, endX, endY, steps); 691 TestUtils.injectStylusUpEvent(pointer); 692 } 693 } 694