blob: 831726b8a53a0a5981c1198f7b9cf34d48003bd1 [file] [log] [blame]
/*
* Copyright (C) 2018 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 com.android.launcher3.tapl;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
import static android.content.pm.PackageManager.DONT_KILL_APP;
import static android.content.pm.PackageManager.MATCH_ALL;
import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.Configurator;
import androidx.test.uiautomator.Direction;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
import com.android.launcher3.testing.TestProtocol;
import com.android.systemui.shared.system.QuickStepContract;
import org.junit.Assert;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
/**
* The main tapl object. The only object that can be explicitly constructed by the using code. It
* produces all other objects.
*/
public final class LauncherInstrumentation {
private static final String TAG = "Tapl";
private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20;
private static final int GESTURE_STEP_MS = 16;
// Types for launcher containers that the user is interacting with. "Background" is a
// pseudo-container corresponding to inactive launcher covered by another app.
enum ContainerType {
WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, BASE_OVERVIEW
}
public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
// Base class for launcher containers.
static abstract class VisibleContainer {
protected final LauncherInstrumentation mLauncher;
protected VisibleContainer(LauncherInstrumentation launcher) {
mLauncher = launcher;
launcher.setActiveContainer(this);
}
protected abstract ContainerType getContainerType();
/**
* Asserts that the launcher is in the mode matching 'this' object.
*
* @return UI object for the container.
*/
final UiObject2 verifyActiveContainer() {
mLauncher.assertTrue("Attempt to use a stale container",
this == sActiveContainer.get());
return mLauncher.verifyContainerType(getContainerType());
}
}
interface Closable extends AutoCloseable {
void close();
}
private static final String WORKSPACE_RES_ID = "workspace";
private static final String APPS_RES_ID = "apps_view";
private static final String OVERVIEW_RES_ID = "overview_panel";
private static final String WIDGETS_RES_ID = "widgets_list_view";
public static final int WAIT_TIME_MS = 60000;
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
private final UiDevice mDevice;
private final Instrumentation mInstrumentation;
private int mExpectedRotation = Surface.ROTATION_0;
private final Uri mTestProviderUri;
private final Deque<String> mDiagnosticContext = new LinkedList<>();
private Supplier<String> mSystemHealthSupplier;
/**
* Constructs the root of TAPL hierarchy. You get all other objects from it.
*/
public LauncherInstrumentation(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
mDevice = UiDevice.getInstance(instrumentation);
// Launcher should run in test harness so that custom accessibility protocol between
// Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call
// into Launcher.
assertTrue("Device must run in a test harness",
TestHelpers.isInLauncherProcess() || ActivityManager.isRunningInTestHarness());
final String testPackage = getContext().getPackageName();
final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
// Launcher package. As during inproc tests the tested launcher may not be selected as the
// current launcher, choosing target package for inproc. For out-of-proc, use the installed
// launcher package.
final String authorityPackage = testPackage.equals(targetPackage) ?
getLauncherPackageName() :
targetPackage;
String testProviderAuthority = authorityPackage + ".TestInfo";
mTestProviderUri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(testProviderAuthority)
.build();
try {
mDevice.executeShellCommand("pm grant " + testPackage +
" android.permission.WRITE_SECURE_SETTINGS");
} catch (IOException e) {
fail(e.toString());
}
PackageManager pm = getContext().getPackageManager();
ProviderInfo pi = pm.resolveContentProvider(
testProviderAuthority, MATCH_ALL | MATCH_DISABLED_COMPONENTS);
ComponentName cn = new ComponentName(pi.packageName, pi.name);
if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) {
if (TestHelpers.isInLauncherProcess()) {
getContext().getPackageManager().setComponentEnabledSetting(
cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
} else {
try {
mDevice.executeShellCommand("pm enable " + cn.flattenToString());
} catch (IOException e) {
fail(e.toString());
}
}
}
}
Context getContext() {
return mInstrumentation.getContext();
}
Bundle getTestInfo(String request) {
return getContext().getContentResolver().call(mTestProviderUri, request, null, null);
}
void setActiveContainer(VisibleContainer container) {
sActiveContainer = new WeakReference<>(container);
}
public NavigationModel getNavigationModel() {
final Context baseContext = mInstrumentation.getTargetContext();
try {
// Workaround, use constructed context because both the instrumentation context and the
// app context are not constructed with resources that take overlays into account
final Context ctx = baseContext.createPackageContext(getLauncherPackageName(), 0);
for (int i = 0; i < 100; ++i) {
final int currentInteractionMode = getCurrentInteractionMode(ctx);
final NavigationModel model = getNavigationModel(currentInteractionMode);
log("Interaction mode = " + currentInteractionMode + " (" + model + ")");
if (model != null) return model;
Thread.sleep(100);
}
fail("Can't detect navigation mode");
} catch (Exception e) {
fail(e.toString());
}
return NavigationModel.THREE_BUTTON;
}
public static NavigationModel getNavigationModel(int currentInteractionMode) {
if (QuickStepContract.isGesturalMode(currentInteractionMode)) {
return NavigationModel.ZERO_BUTTON;
} else if (QuickStepContract.isSwipeUpMode(currentInteractionMode)) {
return NavigationModel.TWO_BUTTON;
} else if (QuickStepContract.isLegacyMode(currentInteractionMode)) {
return NavigationModel.THREE_BUTTON;
}
return null;
}
public static boolean isAvd() {
return Build.MODEL.contains("Cuttlefish");
}
static void log(String message) {
Log.d(TAG, message);
}
Closable addContextLayer(String piece) {
mDiagnosticContext.addLast(piece);
log("Added context: " + getContextDescription());
return () -> {
log("Removing context: " + getContextDescription());
mDiagnosticContext.removeLast();
};
}
private void dumpViewHierarchy() {
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
mDevice.dumpWindowHierarchy(stream);
stream.flush();
stream.close();
for (String line : stream.toString().split("\\r?\\n")) {
Log.e(TAG, line.trim());
}
} catch (IOException e) {
Log.e(TAG, "error dumping XML to logcat", e);
}
}
private String getAnomalyMessage() {
UiObject2 object = mDevice.findObject(By.res("android", "alertTitle"));
if (object != null) {
return "System alert popup is visible: " + object.getText();
}
object = mDevice.findObject(By.res("android", "message"));
if (object != null) {
return "Message popup by " + object.getApplicationPackage() + " is visible: "
+ object.getText();
}
if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
return null;
}
private String getVisibleStateMessage() {
if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
return "Background";
}
public void setSystemHealthSupplier(Supplier<String> supplier) {
this.mSystemHealthSupplier = supplier;
}
private String getSystemHealthMessage() {
final String testPackage = getContext().getPackageName();
try {
mDevice.executeShellCommand("pm grant " + testPackage +
" android.permission.READ_LOGS");
mDevice.executeShellCommand("pm grant " + testPackage +
" android.permission.PACKAGE_USAGE_STATS");
} catch (IOException e) {
e.printStackTrace();
}
return mSystemHealthSupplier != null
? mSystemHealthSupplier.get()
: TestHelpers.getSystemHealthMessage(getContext());
}
private void fail(String message) {
message = "http://go/tapl : " + getContextDescription() + message;
final String anomaly = getAnomalyMessage();
if (anomaly != null) {
message = anomaly + ", which causes:\n" + message;
} else {
message = message + " (visible state: " + getVisibleStateMessage() + ")";
}
final String systemHealth = getSystemHealthMessage();
if (systemHealth != null) {
message = message + ", which might be a consequence of system health problems:\n<<<\n"
+ systemHealth + "\n>>>";
}
log("Hierarchy dump for: " + message);
dumpViewHierarchy();
Assert.fail(message);
}
private String getContextDescription() {
return mDiagnosticContext.isEmpty() ? "" : String.join(", ", mDiagnosticContext) + "; ";
}
void assertTrue(String message, boolean condition) {
if (!condition) {
fail(message);
}
}
void assertNotNull(String message, Object object) {
assertTrue(message, object != null);
}
private void failEquals(String message, Object actual) {
fail(message + ". " + "Actual: " + actual);
}
private void assertEquals(String message, int expected, int actual) {
if (expected != actual) {
fail(message + " expected: " + expected + " but was: " + actual);
}
}
private void assertEquals(String message, String expected, String actual) {
if (!TextUtils.equals(expected, actual)) {
fail(message + " expected: '" + expected + "' but was: '" + actual + "'");
}
}
void assertEquals(String message, long expected, long actual) {
if (expected != actual) {
fail(message + " expected: " + expected + " but was: " + actual);
}
}
void assertNotEquals(String message, int unexpected, int actual) {
if (unexpected == actual) {
failEquals(message, actual);
}
}
public void setExpectedRotation(int expectedRotation) {
mExpectedRotation = expectedRotation;
}
public String getNavigationModeMismatchError() {
final NavigationModel navigationModel = getNavigationModel();
final boolean hasRecentsButton = hasSystemUiObject("recent_apps");
final boolean hasHomeButton = hasSystemUiObject("home");
if ((navigationModel == NavigationModel.THREE_BUTTON) != hasRecentsButton) {
return "Presence of recents button doesn't match the interaction mode, mode="
+ navigationModel.name() + ", hasRecents=" + hasRecentsButton;
}
if ((navigationModel != NavigationModel.ZERO_BUTTON) != hasHomeButton) {
return "Presence of home button doesn't match the interaction mode, mode="
+ navigationModel.name() + ", hasHome=" + hasHomeButton;
}
return null;
}
private UiObject2 verifyContainerType(ContainerType containerType) {
waitForTouchInteractionService();
assertEquals("Unexpected display rotation",
mExpectedRotation, mDevice.getDisplayRotation());
// b/136278866
for (int i = 0; i != 100; ++i) {
if (getNavigationModeMismatchError() == null) break;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
final String error = getNavigationModeMismatchError();
assertTrue(error, error == null);
log("verifyContainerType: " + containerType);
try (Closable c = addContextLayer(
"but the current state is not " + containerType.name())) {
switch (containerType) {
case WORKSPACE: {
if (mDevice.isNaturalOrientation()) {
waitForLauncherObject(APPS_RES_ID);
} else {
waitUntilGone(APPS_RES_ID);
}
waitUntilGone(OVERVIEW_RES_ID);
waitUntilGone(WIDGETS_RES_ID);
return waitForLauncherObject(WORKSPACE_RES_ID);
}
case WIDGETS: {
waitUntilGone(WORKSPACE_RES_ID);
waitUntilGone(APPS_RES_ID);
waitUntilGone(OVERVIEW_RES_ID);
return waitForLauncherObject(WIDGETS_RES_ID);
}
case ALL_APPS: {
waitUntilGone(WORKSPACE_RES_ID);
waitUntilGone(OVERVIEW_RES_ID);
waitUntilGone(WIDGETS_RES_ID);
return waitForLauncherObject(APPS_RES_ID);
}
case OVERVIEW: {
if (mDevice.isNaturalOrientation()) {
waitForLauncherObject(APPS_RES_ID);
} else {
waitUntilGone(APPS_RES_ID);
}
waitUntilGone(WORKSPACE_RES_ID);
waitUntilGone(WIDGETS_RES_ID);
return waitForLauncherObject(OVERVIEW_RES_ID);
}
case BASE_OVERVIEW: {
return waitForFallbackLauncherObject(OVERVIEW_RES_ID);
}
case BACKGROUND: {
waitUntilGone(WORKSPACE_RES_ID);
waitUntilGone(APPS_RES_ID);
waitUntilGone(OVERVIEW_RES_ID);
waitUntilGone(WIDGETS_RES_ID);
return null;
}
default:
fail("Invalid state: " + containerType);
return null;
}
}
}
private void waitForTouchInteractionService() {
for (int i = 0; i < 100; ++i) {
if (getTestInfo(
TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
return;
}
SystemClock.sleep(100);
}
fail("TouchInteractionService didn't connect");
}
Parcelable executeAndWaitForEvent(Runnable command,
UiAutomation.AccessibilityEventFilter eventFilter, String message) {
try {
final AccessibilityEvent event =
mInstrumentation.getUiAutomation().executeAndWaitForEvent(
command, eventFilter, WAIT_TIME_MS);
assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
return event.getParcelableData();
} catch (TimeoutException e) {
fail(message);
return null;
}
}
Bundle getAnswerFromLauncher(UiObject2 view, String requestTag) {
// Send a fake set-text request to Launcher to initiate a response with requested data.
final String responseTag = requestTag + TestProtocol.RESPONSE_MESSAGE_POSTFIX;
return (Bundle) executeAndWaitForEvent(
() -> view.setText(requestTag),
event -> responseTag.equals(event.getClassName()),
"Launcher didn't respond to request: " + requestTag);
}
/**
* Presses nav bar home button.
*
* @return the Workspace object.
*/
public Workspace pressHome() {
// Click home, then wait for any accessibility event, then wait until accessibility events
// stop.
// We need waiting for any accessibility event generated after pressing Home because
// otherwise waitForIdle may return immediately in case when there was a big enough pause in
// accessibility events prior to pressing Home.
final String action;
if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
final Point displaySize = getRealDisplaySize();
if (hasLauncherObject("deep_shortcuts_container")) {
linearGesture(
displaySize.x / 2, displaySize.y - 1,
displaySize.x / 2, 0,
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME);
assertTrue("Context menu is still visible afterswiping up to home",
!hasLauncherObject("deep_shortcuts_container"));
}
if (hasLauncherObject(WORKSPACE_RES_ID)) {
log(action = "already at home");
} else {
log(action = "swiping up to home");
final int finalState = mDevice.hasObject(By.pkg(getLauncherPackageName()))
? NORMAL_STATE_ORDINAL : BACKGROUND_APP_STATE_ORDINAL;
swipeToState(
displaySize.x / 2, displaySize.y - 1,
displaySize.x / 2, 0,
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, finalState);
}
} else {
log(action = "clicking home button");
executeAndWaitForEvent(
() -> {
log("LauncherInstrumentation.pressHome before clicking");
waitForSystemUiObject("home").click();
},
event -> true,
"Pressing Home didn't produce any events");
mDevice.waitForIdle();
}
try (LauncherInstrumentation.Closable c = addContextLayer(
"performed action to switch to Home - " + action)) {
return getWorkspace();
}
}
/**
* Gets the Workspace object if the current state is "active home", i.e. workspace. Fails if the
* launcher is not in that state.
*
* @return Workspace object.
*/
@NonNull
public Workspace getWorkspace() {
try (LauncherInstrumentation.Closable c = addContextLayer("want to get workspace object")) {
return new Workspace(this);
}
}
/**
* Gets the Workspace object if the current state is "background home", i.e. some other app is
* active. Fails if the launcher is not in that state.
*
* @return Background object.
*/
@NonNull
public Background getBackground() {
return new Background(this);
}
/**
* Gets the Widgets object if the current state is showing all widgets. Fails if the launcher is
* not in that state.
*
* @return Widgets object.
*/
@NonNull
public Widgets getAllWidgets() {
try (LauncherInstrumentation.Closable c = addContextLayer("want to get widgets")) {
return new Widgets(this);
}
}
@NonNull
public AddToHomeScreenPrompt getAddToHomeScreenPrompt() {
try (LauncherInstrumentation.Closable c = addContextLayer("want to get widget cell")) {
return new AddToHomeScreenPrompt(this);
}
}
/**
* Gets the Overview object if the current state is showing the overview panel. Fails if the
* launcher is not in that state.
*
* @return Overview object.
*/
@NonNull
public Overview getOverview() {
try (LauncherInstrumentation.Closable c = addContextLayer("want to get overview")) {
return new Overview(this);
}
}
/**
* Gets the All Apps object if the current state is showing the all apps panel opened by swiping
* from workspace. Fails if the launcher is not in that state. Please don't call this method if
* App Apps was opened by swiping up from Overview, as it won't fail and will return an
* incorrect object.
*
* @return All Aps object.
*/
@NonNull
public AllApps getAllApps() {
try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) {
return new AllApps(this);
}
}
/**
* Gets the All Apps object if the current state is showing the all apps panel opened by swiping
* from overview. Fails if the launcher is not in that state. Please don't call this method if
* App Apps was opened by swiping up from home, as it won't fail and will return an
* incorrect object.
*
* @return All Aps object.
*/
@NonNull
public AllAppsFromOverview getAllAppsFromOverview() {
try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) {
return new AllAppsFromOverview(this);
}
}
void waitUntilGone(String resId) {
assertTrue("Unexpected launcher object visible: " + resId,
mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
WAIT_TIME_MS));
}
private boolean hasSystemUiObject(String resId) {
return mDevice.hasObject(By.res(SYSTEMUI_PACKAGE, resId));
}
@NonNull
UiObject2 waitForSystemUiObject(String resId) {
final UiObject2 object = mDevice.wait(
Until.findObject(By.res(SYSTEMUI_PACKAGE, resId)), WAIT_TIME_MS);
assertNotNull("Can't find a systemui object with id: " + resId, object);
return object;
}
@NonNull
UiObject2 getObjectInContainer(UiObject2 container, BySelector selector) {
final UiObject2 object = container.findObject(selector);
assertNotNull("Can't find an object with selector: " + selector, object);
return object;
}
@NonNull
List<UiObject2> getObjectsInContainer(UiObject2 container, String resName) {
return container.findObjects(getLauncherObjectSelector(resName));
}
@NonNull
UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
final UiObject2 object = container.wait(
Until.findObject(getLauncherObjectSelector(resName)),
WAIT_TIME_MS);
assertNotNull("Can't find a launcher object id: " + resName + " in container: " +
container.getResourceName(), object);
return object;
}
@NonNull
UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
final UiObject2 object = container.wait(
Until.findObject(selector),
WAIT_TIME_MS);
assertNotNull("Can't find a launcher object id: " + selector + " in container: " +
container.getResourceName(), object);
return object;
}
@Nullable
private boolean hasLauncherObject(String resId) {
return mDevice.hasObject(getLauncherObjectSelector(resId));
}
@NonNull
UiObject2 waitForLauncherObject(String resName) {
return waitForObjectBySelector(getLauncherObjectSelector(resName));
}
@NonNull
UiObject2 waitForLauncherObject(BySelector selector) {
return waitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()));
}
@NonNull
UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
return tryWaitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()), timeout);
}
@NonNull
UiObject2 waitForFallbackLauncherObject(String resName) {
return waitForObjectBySelector(getFallbackLauncherObjectSelector(resName));
}
private UiObject2 waitForObjectBySelector(BySelector selector) {
final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
assertNotNull("Can't find a launcher object; selector: " + selector, object);
return object;
}
private UiObject2 tryWaitForObjectBySelector(BySelector selector, long timeout) {
return mDevice.wait(Until.findObject(selector), timeout);
}
BySelector getLauncherObjectSelector(String resName) {
return By.res(getLauncherPackageName(), resName);
}
BySelector getFallbackLauncherObjectSelector(String resName) {
return By.res(getOverviewPackageName(), resName);
}
String getLauncherPackageName() {
return mDevice.getLauncherPackageName();
}
@NonNull
public UiDevice getDevice() {
return mDevice;
}
void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState) {
final Bundle parcel = (Bundle) executeAndWaitForEvent(
() -> linearGesture(startX, startY, endX, endY, steps),
event -> TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName()),
"Swipe failed to receive an event for the swipe end: " + startX + ", " + startY
+ ", " + endX + ", " + endY);
assertEquals("Swipe switched launcher to a wrong state;",
TestProtocol.stateOrdinalToString(expectedState),
TestProtocol.stateOrdinalToString(parcel.getInt(TestProtocol.STATE_FIELD)));
}
void scroll(UiObject2 container, Direction direction, float percent, Rect margins, int steps) {
final Rect rect = container.getVisibleBounds();
if (margins != null) {
rect.left += margins.left;
rect.top += margins.top;
rect.right -= margins.right;
rect.bottom -= margins.bottom;
}
final int startX;
final int startY;
final int endX;
final int endY;
switch (direction) {
case UP: {
startX = endX = rect.centerX();
final int vertCenter = rect.centerY();
final float halfGestureHeight = rect.height() * percent / 2.0f;
startY = (int) (vertCenter - halfGestureHeight);
endY = (int) (vertCenter + halfGestureHeight);
}
break;
case DOWN: {
startX = endX = rect.centerX();
final int vertCenter = rect.centerY();
final float halfGestureHeight = rect.height() * percent / 2.0f;
startY = (int) (vertCenter + halfGestureHeight);
endY = (int) (vertCenter - halfGestureHeight);
}
break;
case LEFT: {
startY = endY = rect.centerY();
final int horizCenter = rect.centerX();
final float halfGestureWidth = rect.width() * percent / 2.0f;
startX = (int) (horizCenter - halfGestureWidth);
endX = (int) (horizCenter + halfGestureWidth);
}
break;
case RIGHT: {
startY = endY = rect.centerY();
final int horizCenter = rect.centerX();
final float halfGestureWidth = rect.width() * percent / 2.0f;
startX = (int) (horizCenter + halfGestureWidth);
endX = (int) (horizCenter - halfGestureWidth);
}
break;
default:
fail("Unsupported direction");
return;
}
executeAndWaitForEvent(
() -> linearGesture(startX, startY, endX, endY, steps),
event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
"Didn't receive a scroll end message: " + startX + ", " + startY
+ ", " + endX + ", " + endY);
}
// Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
// fixed interval each time.
void linearGesture(int startX, int startY, int endX, int endY, int steps) {
final long downTime = SystemClock.uptimeMillis();
final Point start = new Point(startX, startY);
final Point end = new Point(endX, endY);
sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
final long endTime = movePointer(downTime, downTime, steps * GESTURE_STEP_MS, start, end);
sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end);
}
void waitForIdle() {
mDevice.waitForIdle();
}
float getDisplayDensity() {
return mInstrumentation.getTargetContext().getResources().getDisplayMetrics().density;
}
int getTouchSlop() {
return ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
public Resources getResources() {
return getContext().getResources();
}
private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
float x, float y) {
MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
properties.id = 0;
properties.toolType = Configurator.getInstance().getToolType();
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
coords.pressure = 1;
coords.size = 1;
coords.x = x;
coords.y = y;
return MotionEvent.obtain(downTime, eventTime, action, 1,
new MotionEvent.PointerProperties[]{properties},
new MotionEvent.PointerCoords[]{coords},
0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
}
void sendPointer(long downTime, long currentTime, int action, Point point) {
final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
mInstrumentation.getUiAutomation().injectInputEvent(event, true);
event.recycle();
}
long movePointer(long downTime, long startTime, long duration, Point from, Point to) {
final Point point = new Point();
long steps = duration / GESTURE_STEP_MS;
long currentTime = startTime;
for (long i = 0; i < steps; ++i) {
sleep(GESTURE_STEP_MS);
currentTime += GESTURE_STEP_MS;
final float progress = (currentTime - startTime) / (float) duration;
point.x = from.x + (int) (progress * (to.x - from.x));
point.y = from.y + (int) (progress * (to.y - from.y));
sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point);
}
return currentTime;
}
public static int getCurrentInteractionMode(Context context) {
return getSystemIntegerRes(context, "config_navBarInteractionMode");
}
private static int getSystemIntegerRes(Context context, String resName) {
Resources res = context.getResources();
int resId = res.getIdentifier(resName, "integer", "android");
if (resId != 0) {
return res.getInteger(resId);
} else {
Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
return -1;
}
}
private static int getSystemDimensionResId(Context context, String resName) {
Resources res = context.getResources();
int resId = res.getIdentifier(resName, "dimen", "android");
if (resId != 0) {
return resId;
} else {
Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
return -1;
}
}
static void sleep(int duration) {
SystemClock.sleep(duration);
}
int getEdgeSensitivityWidth() {
try {
final Context context = mInstrumentation.getTargetContext().createPackageContext(
getLauncherPackageName(), 0);
return context.getResources().getDimensionPixelSize(
getSystemDimensionResId(context, "config_backGestureInset")) + 1;
} catch (PackageManager.NameNotFoundException e) {
fail("Can't get edge sensitivity: " + e);
return 0;
}
}
Point getRealDisplaySize() {
final Point size = new Point();
getContext().getSystemService(WindowManager.class).getDefaultDisplay().getRealSize(size);
return size;
}
}