xref: /aosp_15_r20/cts/tests/inputmethod/util/src/android/view/inputmethod/cts/util/TestUtils.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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