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