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