| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view.inputmethod.cts.util; |
| |
| import static com.android.compatibility.common.util.SystemUtil.runShellCommand; |
| import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; |
| import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; |
| |
| import static org.junit.Assert.assertFalse; |
| |
| import android.app.Instrumentation; |
| import android.app.KeyguardManager; |
| import android.content.Context; |
| import android.os.PowerManager; |
| import android.os.SystemClock; |
| import android.view.InputDevice; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| |
| import androidx.annotation.NonNull; |
| import androidx.test.platform.app.InstrumentationRegistry; |
| |
| import com.android.compatibility.common.util.CommonTestUtils; |
| import com.android.compatibility.common.util.SystemUtil; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.BooleanSupplier; |
| import java.util.function.Supplier; |
| |
| public final class TestUtils { |
| private static final long TIME_SLICE = 100; // msec |
| /** |
| * Executes a call on the application's main thread, blocking until it is complete. |
| * |
| * <p>A simple wrapper for {@link Instrumentation#runOnMainSync(Runnable)}.</p> |
| * |
| * @param task task to be called on the UI thread |
| */ |
| public static void runOnMainSync(@NonNull Runnable task) { |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(task); |
| } |
| |
| /** |
| * Retrieves a value that needs to be obtained on the main thread. |
| * |
| * <p>A simple utility method that helps to return an object from the UI thread.</p> |
| * |
| * @param supplier callback to be called on the UI thread to return a value |
| * @param <T> Type of the value to be returned |
| * @return Value returned from {@code supplier} |
| */ |
| public static <T> T getOnMainSync(@NonNull Supplier<T> supplier) { |
| final AtomicReference<T> result = new AtomicReference<>(); |
| final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); |
| instrumentation.runOnMainSync(() -> result.set(supplier.get())); |
| return result.get(); |
| } |
| |
| /** |
| * Does polling loop on the UI thread to wait until the given condition is met. |
| * |
| * @param condition Condition to be satisfied. This is guaranteed to run on the UI thread. |
| * @param timeout timeout in millisecond |
| * @param message message to display when timeout occurs. |
| * @throws TimeoutException when the no event is matched to the given condition within |
| * {@code timeout} |
| */ |
| public static void waitOnMainUntil( |
| @NonNull BooleanSupplier condition, long timeout, String message) |
| throws TimeoutException { |
| final AtomicBoolean result = new AtomicBoolean(); |
| |
| final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); |
| while (!result.get()) { |
| if (timeout < 0) { |
| throw new TimeoutException(message); |
| } |
| instrumentation.runOnMainSync(() -> { |
| if (condition.getAsBoolean()) { |
| result.set(true); |
| } |
| }); |
| try { |
| Thread.sleep(TIME_SLICE); |
| } catch (InterruptedException e) { |
| throw new IllegalStateException(e); |
| } |
| timeout -= TIME_SLICE; |
| } |
| } |
| |
| /** |
| * Does polling loop on the UI thread to wait until the given condition is met. |
| * |
| * @param condition Condition to be satisfied. This is guaranteed to run on the UI thread. |
| * @param timeout timeout in millisecond |
| * @throws TimeoutException when the no event is matched to the given condition within |
| * {@code timeout} |
| */ |
| public static void waitOnMainUntil(@NonNull BooleanSupplier condition, long timeout) |
| throws TimeoutException { |
| waitOnMainUntil(condition, timeout, ""); |
| } |
| |
| /** |
| * Call a command to turn screen On. |
| * |
| * This method will wait until the power state is interactive with {@link |
| * PowerManager#isInteractive()}. |
| */ |
| public static void turnScreenOn() throws Exception { |
| final Context context = InstrumentationRegistry.getInstrumentation().getContext(); |
| final PowerManager pm = context.getSystemService(PowerManager.class); |
| runShellCommand("input keyevent KEYCODE_WAKEUP"); |
| CommonTestUtils.waitUntil("Device does not wake up after 5 seconds", 5, |
| () -> pm != null && pm.isInteractive()); |
| } |
| |
| /** |
| * Call a command to turn screen off. |
| * |
| * This method will wait until the power state is *NOT* interactive with |
| * {@link PowerManager#isInteractive()}. |
| * Note that {@link PowerManager#isInteractive()} may not return {@code true} when the device |
| * enables Aod mode, recommend to add (@link DisableScreenDozeRule} in the test to disable Aod |
| * for making power state reliable. |
| */ |
| public static void turnScreenOff() throws Exception { |
| final Context context = InstrumentationRegistry.getInstrumentation().getContext(); |
| final PowerManager pm = context.getSystemService(PowerManager.class); |
| runShellCommand("input keyevent KEYCODE_SLEEP"); |
| CommonTestUtils.waitUntil("Device does not sleep after 5 seconds", 5, |
| () -> pm != null && !pm.isInteractive()); |
| } |
| |
| /** |
| * Simulates a {@link KeyEvent#KEYCODE_MENU} event to unlock screen. |
| * |
| * This method will retry until {@link KeyguardManager#isKeyguardLocked()} return {@code false} |
| * in given timeout. |
| * |
| * Note that {@link KeyguardManager} is not accessible in instant mode due to security concern, |
| * so this method always throw exception with instant app. |
| */ |
| public static void unlockScreen() throws Exception { |
| final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); |
| final Context context = instrumentation.getContext(); |
| final KeyguardManager kgm = context.getSystemService(KeyguardManager.class); |
| |
| assertFalse("This method is currently not supported in instant apps.", |
| context.getPackageManager().isInstantApp()); |
| CommonTestUtils.waitUntil("Device does not unlock after 3 seconds", 3, |
| () -> { |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> instrumentation.sendKeyDownUpSync((KeyEvent.KEYCODE_MENU))); |
| return kgm != null && !kgm.isKeyguardLocked(); |
| }); |
| } |
| |
| /** |
| * Call a command to force stop the given application package. |
| * |
| * @param pkg The name of the package to be stopped. |
| */ |
| public static void forceStopPackage(@NonNull String pkg) { |
| runWithShellPermissionIdentity(() -> { |
| runShellCommandOrThrow("am force-stop " + pkg); |
| }); |
| } |
| |
| |
| /** |
| * Inject Stylus move on the Display inside view coordinates so that initiation can happen. |
| * @param view view on which stylus events should be overlapped. |
| */ |
| public static void injectStylusEvents(@NonNull View view) { |
| int offsetX = view.getWidth() / 2; |
| int offsetY = view.getHeight() / 2; |
| injectStylusEvents(view, offsetX, offsetY); |
| } |
| |
| /** |
| * Inject a stylus ACTION_DOWN event to the screen using given view's coordinates. |
| * @param view view whose coordinates are used to compute the event location. |
| * @param x the x coordinates of the stylus event in the view's location coordinates. |
| * @param y the y coordinates of the stylus event in the view's location coordinates. |
| * @return the injected MotionEvent. |
| */ |
| public static MotionEvent injectStylusDownEvent(@NonNull View view, int x, int y) { |
| int[] xy = new int[2]; |
| view.getLocationOnScreen(xy); |
| x += xy[0]; |
| y += xy[1]; |
| |
| // Inject stylus ACTION_DOWN |
| long downTime = SystemClock.uptimeMillis(); |
| final MotionEvent downEvent = |
| getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, x, y); |
| injectMotionEvent(downEvent, true /* sync */); |
| return downEvent; |
| } |
| |
| /** |
| * Inject a stylus ACTION_UP event to the screen using given view's coordinates. |
| * @param view view whose coordinates are used to compute the event location. |
| * @param x the x coordinates of the stylus event in the view's location coordinates. |
| * @param y the y coordinates of the stylus event in the view's location coordinates. |
| * @return the injected MotionEvent. |
| */ |
| public static MotionEvent injectStylusUpEvent(@NonNull View view, int x, int y) { |
| int[] xy = new int[2]; |
| view.getLocationOnScreen(xy); |
| x += xy[0]; |
| y += xy[1]; |
| |
| // Inject stylus ACTION_DOWN |
| long downTime = SystemClock.uptimeMillis(); |
| final MotionEvent upEvent = getMotionEvent(downTime, downTime, MotionEvent.ACTION_UP, x, y); |
| injectMotionEvent(upEvent, true /* sync */); |
| return upEvent; |
| } |
| |
| /** |
| * Inject Stylus ACTION_MOVE events to the screen using the given view's coordinates. |
| * |
| * @param view view whose coordinates are used to compute the event location. |
| * @param startX the start x coordinates of the stylus event in the view's local coordinates. |
| * @param startY the start y coordinates of the stylus event in the view's local coordinates. |
| * @param endX the end x coordinates of the stylus event in the view's local coordinates. |
| * @param endY the end y coordinates of the stylus event in the view's local coordinates. |
| * @param number the number of the motion events injected to the view. |
| * @return the injected MotionEvents. |
| */ |
| public static List<MotionEvent> injectStylusMoveEvents(@NonNull View view, int startX, |
| int startY, int endX, int endY, int number) { |
| int[] xy = new int[2]; |
| view.getLocationOnScreen(xy); |
| |
| final float incrementX = ((float) (endX - startX)) / (number - 1); |
| final float incrementY = ((float) (endY - startY)) / (number - 1); |
| |
| final List<MotionEvent> injectedEvents = new ArrayList<>(number); |
| // Inject stylus ACTION_MOVE |
| for (int i = 0; i < number; i++) { |
| long time = SystemClock.uptimeMillis(); |
| float x = startX + incrementX * i; |
| float y = startY + incrementY * i; |
| final MotionEvent moveEvent = |
| getMotionEvent(time, time, MotionEvent.ACTION_MOVE, x, y); |
| injectMotionEvent(moveEvent, true /* sync */); |
| injectedEvents.add(moveEvent); |
| } |
| return injectedEvents; |
| } |
| |
| /** |
| * Inject stylus move on the display at the given position defined in the given view's |
| * coordinates. |
| * |
| * @param view view whose coordinates are used to compute the event location. |
| * @param x the initial x coordinates of the injected stylus events in the view's |
| * local coordinates. |
| * @param y the initial y coordinates of the injected stylus events in the view's |
| * local coordinates. |
| */ |
| public static void injectStylusEvents(@NonNull View view, int x, int y) { |
| injectStylusDownEvent(view, x, y); |
| // Larger than the touchSlop. |
| int endX = x + getTouchSlop(view.getContext()) * 5; |
| injectStylusMoveEvents(view, x, y, endX, y, 10); |
| injectStylusUpEvent(view, endX, y); |
| |
| } |
| |
| private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, |
| float x, float y) { |
| return getMotionEvent(downTime, eventTime, action, (int) x, (int) y, 0); |
| } |
| |
| private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, |
| int x, int y, int displayId) { |
| // Stylus related properties. |
| MotionEvent.PointerProperties[] properties = |
| new MotionEvent.PointerProperties[] { new MotionEvent.PointerProperties() }; |
| properties[0].toolType = MotionEvent.TOOL_TYPE_STYLUS; |
| properties[0].id = 1; |
| MotionEvent.PointerCoords[] coords = |
| new MotionEvent.PointerCoords[] { new MotionEvent.PointerCoords() }; |
| coords[0].x = x; |
| coords[0].y = y; |
| coords[0].pressure = 1; |
| |
| final MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, |
| 1 /* pointerCount */, properties, coords, 0 /* metaState */, |
| 0 /* buttonState */, 1 /* xPrecision */, 1 /* yPrecision */, 0 /* deviceId */, |
| 0 /* edgeFlags */, InputDevice.SOURCE_STYLUS, 0 /* flags */); |
| event.setDisplayId(displayId); |
| return event; |
| } |
| |
| private static void injectMotionEvent(MotionEvent event, boolean sync) { |
| InstrumentationRegistry.getInstrumentation().getUiAutomation().injectInputEvent( |
| event, sync, false /* waitAnimations */); |
| } |
| |
| public static void injectAll(List<MotionEvent> events) { |
| for (MotionEvent event : events) { |
| injectMotionEvent(event, true /* sync */); |
| } |
| InstrumentationRegistry.getInstrumentation().getUiAutomation().syncInputTransactions(false); |
| } |
| private static int getTouchSlop(Context context) { |
| return ViewConfiguration.get(context).getScaledTouchSlop(); |
| } |
| } |