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