blob: 631e8f10c62a1e7a207c3e08a6f87b403aaaa745 [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.Folder.FOLDER_CONTENT_RES_ID;
import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
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.ContentProviderClient;
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.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Parcelable;
import android.os.RemoteException;
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.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.Configurator;
import androidx.test.uiautomator.Direction;
import androidx.test.uiautomator.StaleObjectException;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
import com.android.launcher3.ResourceUtils;
import com.android.launcher3.testing.TestProtocol;
import com.android.systemui.shared.system.ContextUtils;
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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 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;
private static final long FORCE_PAUSE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(2);
static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
private static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
static final Pattern EVENT_START = Pattern.compile("start:");
static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
static final Pattern EVENT_KEY_BACK_DOWN = getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK");
static final Pattern EVENT_KEY_BACK_UP = getKeyEventPattern("ACTION_UP", "KEYCODE_BACK");
private final String mLauncherPackage;
private Boolean mIsLauncher3;
private long mTestStartTime = -1;
// Types for launcher containers that the user is interacting with. "Background" is a
// pseudo-container corresponding to inactive launcher covered by another app.
public enum ContainerType {
WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, FALLBACK_OVERVIEW
}
public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
// Where the gesture happens: outside of Launcher, inside or from inside to outside and
// whether the gesture recognition triggers pilfer.
public enum GestureScope {
OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE,
INSIDE_TO_OUTSIDE_WITHOUT_PILFER,
}
// 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());
}
}
public 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 = "primary_widgets_list_view";
private static final String CONTEXT_MENU_RES_ID = "popup_container";
public static final int WAIT_TIME_MS = 60000;
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
private static final String ANDROID_PACKAGE = "android";
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 Function<Long, String> mSystemHealthSupplier;
private Consumer<ContainerType> mOnSettledStateAction;
private LogEventChecker mEventChecker;
private boolean mCheckEventsForSuccessfulGestures = false;
private Runnable mOnLauncherCrashed;
private static Pattern getTouchEventPattern(String prefix, String action) {
// The pattern includes checks that we don't get a multi-touch events or other surprises.
return Pattern.compile(
prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
+ ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount=1");
}
private static Pattern getTouchEventPattern(String action) {
return getTouchEventPattern("Touch event", action);
}
private static Pattern getTouchEventPatternTIS(String action) {
return getTouchEventPattern("TouchInteractionService.onInputEvent", action);
}
private static Pattern getKeyEventPattern(String action, String keyCode) {
return Pattern.compile("Key event: KeyEvent.*action=" + action + ".*keyCode=" + keyCode);
}
/**
* Constructs the root of TAPL hierarchy. You get all other objects from it.
*/
public LauncherInstrumentation() {
this(InstrumentationRegistry.getInstrumentation());
}
/**
* Constructs the root of TAPL hierarchy. You get all other objects from it.
* Deprecated: use the constructor without parameters instead.
*/
@Deprecated
public LauncherInstrumentation(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
mDevice = UiDevice.getInstance(instrumentation);
try {
mDevice.executeShellCommand("am wait-for-broadcast-idle");
} catch (IOException e) {
log("Failed to wait for broadcast idle");
}
// 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.
mLauncherPackage = testPackage.equals(targetPackage)
? getLauncherPackageName()
: targetPackage;
String testProviderAuthority = mLauncherPackage + ".TestInfo";
mTestProviderUri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(testProviderAuthority)
.build();
mInstrumentation.getUiAutomation().grantRuntimePermission(
testPackage, "android.permission.WRITE_SECURE_SETTINGS");
PackageManager pm = getContext().getPackageManager();
ProviderInfo pi = pm.resolveContentProvider(
testProviderAuthority, MATCH_ALL | MATCH_DISABLED_COMPONENTS);
assertNotNull("Cannot find content provider for " + testProviderAuthority, pi);
ComponentName cn = new ComponentName(pi.packageName, pi.name);
if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) {
if (TestHelpers.isInLauncherProcess()) {
pm.setComponentEnabledSetting(cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
// b/195031154
SystemClock.sleep(5000);
} else {
try {
final int userId = ContextUtils.getUserId(getContext());
final String launcherPidCommand = "pidof " + pi.packageName;
final String initialPid = mDevice.executeShellCommand(launcherPidCommand)
.replaceAll("\\s", "");
mDevice.executeShellCommand(
"pm enable --user " + userId + " " + cn.flattenToString());
// Wait for Launcher restart after enabling test provider.
for (int i = 0; i < 100; ++i) {
final String currentPid = mDevice.executeShellCommand(launcherPidCommand)
.replaceAll("\\s", "");
if (!currentPid.isEmpty() && !currentPid.equals(initialPid)) break;
if (i == 99) fail("Launcher didn't restart after enabling test provider");
SystemClock.sleep(100);
}
} catch (IOException e) {
fail(e.toString());
}
}
}
}
public void enableCheckEventsForSuccessfulGestures() {
mCheckEventsForSuccessfulGestures = true;
}
public void setOnLauncherCrashed(Runnable onLauncherCrashed) {
mOnLauncherCrashed = onLauncherCrashed;
}
Context getContext() {
return mInstrumentation.getContext();
}
Bundle getTestInfo(String request) {
return getTestInfo(request, /*arg=*/ null);
}
Bundle getTestInfo(String request, String arg) {
try (ContentProviderClient client = getContext().getContentResolver()
.acquireContentProviderClient(mTestProviderUri)) {
return client.call(request, arg, null);
} catch (DeadObjectException e) {
fail("Launcher crashed");
return null;
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
Insets getTargetInsets() {
return getTestInfo(TestProtocol.REQUEST_TARGET_INSETS)
.getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
Insets getWindowInsets() {
return getTestInfo(TestProtocol.REQUEST_WINDOW_INSETS)
.getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
public boolean isTablet() {
return getTestInfo(TestProtocol.REQUEST_IS_TABLET)
.getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
public boolean isTwoPanels() {
return getTestInfo(TestProtocol.REQUEST_IS_TWO_PANELS)
.getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
int getFocusedTaskHeightForTablet() {
return getTestInfo(TestProtocol.REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET).getInt(
TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
Rect getGridTaskRectForTablet() {
return ((Rect) getTestInfo(TestProtocol.REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET)
.getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD));
}
float getExactScreenCenterX() {
return getRealDisplaySize().x / 2f;
}
private void setForcePauseTimeout(long timeout) {
getTestInfo(TestProtocol.REQUEST_SET_FORCE_PAUSE_TIMEOUT, Long.toString(timeout));
}
public void setEnableRotation(boolean on) {
getTestInfo(TestProtocol.REQUEST_ENABLE_ROTATION, Boolean.toString(on));
}
public boolean hadNontestEvents() {
return getTestInfo(TestProtocol.REQUEST_GET_HAD_NONTEST_EVENTS)
.getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
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;
}
static void log(String message) {
Log.d(TAG, message);
}
Closable addContextLayer(String piece) {
mDiagnosticContext.addLast(piece);
log("Entering context: " + piece);
return () -> {
log("Leaving context: " + piece);
mDiagnosticContext.removeLast();
};
}
public 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 getSystemAnomalyMessage(
boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) {
try {
{
final StringBuilder sb = new StringBuilder();
UiObject2 object = mDevice.findObject(By.res("android", "alertTitle"));
if (object != null) {
sb.append("TITLE: ").append(object.getText());
}
object = mDevice.findObject(By.res("android", "message"));
if (object != null) {
sb.append(" PACKAGE: ").append(object.getApplicationPackage())
.append(" MESSAGE: ").append(object.getText());
}
if (sb.length() != 0) {
return "System alert popup is visible: " + sb;
}
}
if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
if (!ignoreOnlySystemUiViews) {
final String visibleApps = mDevice.findObjects(getAnyObjectSelector())
.stream()
.map(LauncherInstrumentation::getApplicationPackageSafe)
.distinct()
.filter(pkg -> pkg != null)
.collect(Collectors.joining(","));
if (SYSTEMUI_PACKAGE.equals(visibleApps)) return "Only System UI views are visible";
}
if (!ignoreNavmodeChangeStates) {
if (!mDevice.wait(Until.hasObject(getAnyObjectSelector()), WAIT_TIME_MS)) {
return "Screen is empty";
}
}
final String navigationModeError = getNavigationModeMismatchError(true);
if (navigationModeError != null) return navigationModeError;
} catch (Throwable e) {
Log.w(TAG, "getSystemAnomalyMessage failed", e);
}
return null;
}
private void checkForAnomaly() {
checkForAnomaly(false, false);
}
public void checkForAnomaly(
boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) {
final String systemAnomalyMessage =
getSystemAnomalyMessage(ignoreNavmodeChangeStates, ignoreOnlySystemUiViews);
if (systemAnomalyMessage != null) {
Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
"http://go/tapl : Tests are broken by a non-Launcher system error: "
+ systemAnomalyMessage, false)));
}
}
private String getVisiblePackages() {
final String apps = mDevice.findObjects(getAnyObjectSelector())
.stream()
.map(LauncherInstrumentation::getApplicationPackageSafe)
.distinct()
.filter(pkg -> pkg != null && !SYSTEMUI_PACKAGE.equals(pkg))
.collect(Collectors.joining(", "));
return !apps.isEmpty()
? "active app: " + apps
: "the test doesn't see views from any app, including Launcher";
}
private static String getApplicationPackageSafe(UiObject2 object) {
try {
return object.getApplicationPackage();
} catch (StaleObjectException e) {
// We are looking at all object in the system; external ones can suddenly go away.
return null;
}
}
private String getVisibleStateMessage() {
if (hasLauncherObject(CONTEXT_MENU_RES_ID)) return "Context Menu";
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 (" + getVisiblePackages() + ")";
}
public void setSystemHealthSupplier(Function<Long, String> supplier) {
this.mSystemHealthSupplier = supplier;
}
public void setOnSettledStateAction(Consumer<ContainerType> onSettledStateAction) {
mOnSettledStateAction = onSettledStateAction;
}
public void onTestStart() {
mTestStartTime = System.currentTimeMillis();
}
public void onTestFinish() {
mTestStartTime = -1;
}
private String formatSystemHealthMessage(String message) {
final String testPackage = getContext().getPackageName();
mInstrumentation.getUiAutomation().grantRuntimePermission(
testPackage, "android.permission.READ_LOGS");
mInstrumentation.getUiAutomation().grantRuntimePermission(
testPackage, "android.permission.PACKAGE_USAGE_STATS");
if (mTestStartTime > 0) {
final String systemHealth = mSystemHealthSupplier != null
? mSystemHealthSupplier.apply(mTestStartTime)
: TestHelpers.getSystemHealthMessage(getContext(), mTestStartTime);
if (systemHealth != null) {
message += ";\nPerhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
+ systemHealth + "\n>>>>>>>>>>>>>>>>>>";
}
}
Log.d(TAG, "About to throw the error: " + message, new Exception());
return message;
}
private String formatErrorWithEvents(String message, boolean checkEvents) {
if (mEventChecker != null) {
final LogEventChecker eventChecker = mEventChecker;
mEventChecker = null;
if (checkEvents) {
final String eventMismatch = eventChecker.verify(0, false);
if (eventMismatch != null) {
message = message + ";\n" + eventMismatch;
}
} else {
eventChecker.finishNoWait();
}
}
dumpDiagnostics(message);
log("Hierarchy dump for: " + message);
dumpViewHierarchy();
return message;
}
private void dumpDiagnostics(String message) {
log("Diagnostics for failure: " + message);
log("Input:");
logShellCommand("dumpsys input");
log("TIS:");
logShellCommand("dumpsys activity service TouchInteractionService");
}
private void logShellCommand(String command) {
try {
for (String line : mDevice.executeShellCommand(command).split("\\n")) {
SystemClock.sleep(10);
log(line);
}
} catch (IOException e) {
log("Failed to execute " + command);
}
}
void fail(String message) {
checkForAnomaly();
Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
"http://go/tapl test failure: " + message + ";\nContext: " + getContextDescription()
+ "; now visible state is " + getVisibleStateMessage(), true)));
}
private String getContextDescription() {
return mDiagnosticContext.isEmpty()
? "(no context)" : 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);
}
}
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(boolean waitForCorrectState) {
final int waitTime = waitForCorrectState ? WAIT_TIME_MS : 0;
final NavigationModel navigationModel = getNavigationModel();
String resPackage = getNavigationButtonResPackage();
if (navigationModel == NavigationModel.THREE_BUTTON) {
if (!mDevice.wait(Until.hasObject(By.res(resPackage, "recent_apps")), waitTime)) {
return "Recents button not present in 3-button mode";
}
} else {
if (!mDevice.wait(Until.gone(By.res(resPackage, "recent_apps")), waitTime)) {
return "Recents button is present in non-3-button mode";
}
}
if (navigationModel == NavigationModel.ZERO_BUTTON) {
if (!mDevice.wait(Until.gone(By.res(resPackage, "home")), waitTime)) {
return "Home button is present in gestural mode";
}
} else {
if (!mDevice.wait(Until.hasObject(By.res(resPackage, "home")), waitTime)) {
return "Home button not present in non-gestural mode";
}
}
return null;
}
private String getNavigationButtonResPackage() {
return isTablet() ? getLauncherPackageName() : SYSTEMUI_PACKAGE;
}
private UiObject2 verifyContainerType(ContainerType containerType) {
waitForLauncherInitialized();
assertEquals("Unexpected display rotation",
mExpectedRotation, mDevice.getDisplayRotation());
final String error = getNavigationModeMismatchError(true);
assertTrue(error, error == null);
log("verifyContainerType: " + containerType);
final UiObject2 container = verifyVisibleObjects(containerType);
if (mOnSettledStateAction != null) mOnSettledStateAction.accept(containerType);
return container;
}
private UiObject2 verifyVisibleObjects(ContainerType containerType) {
try (Closable c = addContextLayer(
"but the current state is not " + containerType.name())) {
switch (containerType) {
case WORKSPACE: {
waitUntilLauncherObjectGone(APPS_RES_ID);
waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
return waitForLauncherObject(WORKSPACE_RES_ID);
}
case WIDGETS: {
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(APPS_RES_ID);
waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
return waitForLauncherObject(WIDGETS_RES_ID);
}
case ALL_APPS: {
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
return waitForLauncherObject(APPS_RES_ID);
}
case OVERVIEW: {
waitUntilLauncherObjectGone(APPS_RES_ID);
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
return waitForLauncherObject(OVERVIEW_RES_ID);
}
case FALLBACK_OVERVIEW: {
return waitForFallbackLauncherObject(OVERVIEW_RES_ID);
}
case BACKGROUND: {
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(APPS_RES_ID);
waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
return null;
}
default:
fail("Invalid state: " + containerType);
return null;
}
}
}
public void waitForLauncherInitialized() {
for (int i = 0; i < 100; ++i) {
if (getTestInfo(
TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
return;
}
SystemClock.sleep(100);
}
checkForAnomaly();
fail("Launcher didn't initialize");
}
Parcelable executeAndWaitForLauncherEvent(Runnable command,
UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message,
String actionName) {
return executeAndWaitForEvent(
command,
e -> mLauncherPackage.equals(e.getPackageName()) && eventFilter.accept(e),
message, actionName);
}
Parcelable executeAndWaitForEvent(Runnable command,
UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message,
String actionName) {
try (LauncherInstrumentation.Closable c = addContextLayer(actionName)) {
try {
final AccessibilityEvent event =
mInstrumentation.getUiAutomation().executeAndWaitForEvent(
command, eventFilter, WAIT_TIME_MS);
assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
final Parcelable parcelableData = event.getParcelableData();
event.recycle();
return parcelableData;
} catch (TimeoutException e) {
fail(message.get());
return null;
}
}
}
/**
* Get the resource ID of visible floating view.
*/
private Optional<String> getFloatingResId() {
if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
return Optional.of(CONTEXT_MENU_RES_ID);
}
if (hasLauncherObject(FOLDER_CONTENT_RES_ID)) {
return Optional.of(FOLDER_CONTENT_RES_ID);
}
return Optional.empty();
}
/**
* Using swiping up gesture to dismiss closable floating views, such as Menu or Folder Content.
*/
private void swipeUpToCloseFloatingView(boolean gestureStartFromLauncher) {
final Point displaySize = getRealDisplaySize();
final Optional<String> floatingRes = getFloatingResId();
if (!floatingRes.isPresent()) {
return;
}
GestureScope gestureScope = gestureStartFromLauncher
? (isTablet() ? GestureScope.INSIDE : GestureScope.INSIDE_TO_OUTSIDE)
: GestureScope.OUTSIDE_WITH_PILFER;
linearGesture(
displaySize.x / 2, displaySize.y - 1,
displaySize.x / 2, 0,
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
false, gestureScope);
try (LauncherInstrumentation.Closable c1 = addContextLayer(
String.format("Swiped up from floating view %s to home", floatingRes.get()))) {
waitUntilLauncherObjectGone(floatingRes.get());
waitForLauncherObject(getAnyObjectSelector());
}
}
/**
* Presses nav bar home button.
*
* @return the Workspace object.
*/
public Workspace pressHome() {
try (LauncherInstrumentation.Closable e = eventsCheck();
LauncherInstrumentation.Closable c = addContextLayer("want to switch to home")) {
waitForLauncherInitialized();
// 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) {
checkForAnomaly(false, true);
setForcePauseTimeout(FORCE_PAUSE_TIMEOUT_MS);
final Point displaySize = getRealDisplaySize();
boolean gestureStartFromLauncher = isTablet()
? !isLauncher3() || hasLauncherObject(WORKSPACE_RES_ID)
: isLauncherVisible();
// CLose floating views before going back to home.
swipeUpToCloseFloatingView(gestureStartFromLauncher);
if (hasLauncherObject(WORKSPACE_RES_ID)) {
log(action = "already at home");
} else {
log("Hierarchy before swiping up to home:");
dumpViewHierarchy();
action = "swiping up to home";
swipeToState(
displaySize.x / 2, displaySize.y - 1,
displaySize.x / 2, 0,
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
gestureStartFromLauncher ? GestureScope.INSIDE_TO_OUTSIDE
: GestureScope.OUTSIDE_WITH_PILFER);
}
} else {
log("Hierarchy before clicking home:");
dumpViewHierarchy();
action = "clicking home button";
if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) {
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
}
if (isTablet()) {
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_UP);
}
runToState(
waitForNavigationUiObject("home")::click,
NORMAL_STATE_ORDINAL,
!hasLauncherObject(WORKSPACE_RES_ID)
&& (hasLauncherObject(APPS_RES_ID)
|| hasLauncherObject(OVERVIEW_RES_ID)),
action);
}
try (LauncherInstrumentation.Closable c1 = addContextLayer(
"performed action to switch to Home - " + action)) {
return getWorkspace();
}
}
}
/**
* Press navbar back button or swipe back if in gesture navigation mode.
*/
public void pressBack() {
try (Closable e = eventsCheck(); Closable c = addContextLayer("want to press back")) {
waitForLauncherInitialized();
final boolean launcherVisible =
isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
final Point displaySize = getRealDisplaySize();
final GestureScope gestureScope =
launcherVisible ? GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER
: GestureScope.OUTSIDE_WITHOUT_PILFER;
linearGesture(0, displaySize.y / 2, displaySize.x / 2, displaySize.y / 2,
10, false, gestureScope);
} else {
waitForNavigationUiObject("back").click();
if (isTablet()) {
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_UP);
} else if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) {
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
}
}
if (launcherVisible) {
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_DOWN);
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_UP);
}
}
}
private static BySelector getAnyObjectSelector() {
return By.textStartsWith("");
}
boolean isLauncherVisible() {
mDevice.waitForIdle();
return hasLauncherObject(getAnyObjectSelector());
}
boolean isLauncherContainerVisible() {
final String[] containerResources = {WORKSPACE_RES_ID, OVERVIEW_RES_ID, APPS_RES_ID};
return Arrays.stream(containerResources).anyMatch(r -> hasLauncherObject(r));
}
/**
* 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 waitUntilLauncherObjectGone(String resId) {
waitUntilGoneBySelector(getLauncherObjectSelector(resId));
}
void waitUntilOverviewObjectGone(String resId) {
waitUntilGoneBySelector(getOverviewObjectSelector(resId));
}
void waitUntilLauncherObjectGone(BySelector selector) {
waitUntilGoneBySelector(makeLauncherSelector(selector));
}
private void waitUntilGoneBySelector(BySelector launcherSelector) {
assertTrue("Unexpected launcher object visible: " + launcherSelector,
mDevice.wait(Until.gone(launcherSelector),
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 waitForNavigationUiObject(String resId) {
String resPackage = getNavigationButtonResPackage();
final UiObject2 object = mDevice.wait(
Until.findObject(By.res(resPackage, resId)), WAIT_TIME_MS);
assertNotNull("Can't find a navigation UI object with id: " + resId, object);
return object;
}
@Nullable
UiObject2 findObjectInContainer(UiObject2 container, BySelector selector) {
try {
return container.findObject(selector);
} catch (StaleObjectException e) {
fail("The container disappeared from screen");
return null;
}
}
@NonNull
List<UiObject2> getObjectsInContainer(UiObject2 container, String resName) {
try {
return container.findObjects(getLauncherObjectSelector(resName));
} catch (StaleObjectException e) {
fail("The container disappeared from screen");
return null;
}
}
@NonNull
UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
try {
final UiObject2 object = container.wait(
Until.findObject(getLauncherObjectSelector(resName)),
WAIT_TIME_MS);
assertNotNull("Can't find a view in Launcher, id: " + resName + " in container: "
+ container.getResourceName(), object);
return object;
} catch (StaleObjectException e) {
fail("The container disappeared from screen");
return null;
}
}
void waitForObjectEnabled(UiObject2 object, String waitReason) {
try {
assertTrue("Timed out waiting for object to be enabled for " + waitReason + " "
+ object.getResourceName(),
object.wait(Until.enabled(true), WAIT_TIME_MS));
} catch (StaleObjectException e) {
fail("The object disappeared from screen");
}
}
@NonNull
UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
try {
final UiObject2 object = container.wait(
Until.findObject(selector),
WAIT_TIME_MS);
assertNotNull("Can't find a view in Launcher, id: " + selector + " in container: "
+ container.getResourceName(), object);
return object;
} catch (StaleObjectException e) {
fail("The container disappeared from screen");
return null;
}
}
List<UiObject2> getChildren(UiObject2 container) {
try {
return container.getChildren();
} catch (StaleObjectException e) {
fail("The container disappeared from screen");
return null;
}
}
private boolean hasLauncherObject(String resId) {
return mDevice.hasObject(getLauncherObjectSelector(resId));
}
boolean hasLauncherObject(BySelector selector) {
return mDevice.hasObject(makeLauncherSelector(selector));
}
private BySelector makeLauncherSelector(BySelector selector) {
return By.copy(selector).pkg(getLauncherPackageName());
}
@NonNull
UiObject2 waitForOverviewObject(String resName) {
return waitForObjectBySelector(getOverviewObjectSelector(resName));
}
@NonNull
UiObject2 waitForLauncherObject(String resName) {
return waitForObjectBySelector(getLauncherObjectSelector(resName));
}
@NonNull
UiObject2 waitForLauncherObject(BySelector selector) {
return waitForObjectBySelector(makeLauncherSelector(selector));
}
@NonNull
UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
return tryWaitForObjectBySelector(makeLauncherSelector(selector), timeout);
}
@NonNull
UiObject2 waitForFallbackLauncherObject(String resName) {
return waitForObjectBySelector(getOverviewObjectSelector(resName));
}
@NonNull
UiObject2 waitForAndroidObject(String resId) {
final UiObject2 object = TestHelpers.wait(
Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
assertNotNull("Can't find a android object with id: " + resId, object);
return object;
}
private UiObject2 waitForObjectBySelector(BySelector selector) {
final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
assertNotNull("Can't find a view in Launcher, 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 getOverviewObjectSelector(String resName) {
return By.res(getOverviewPackageName(), resName);
}
String getLauncherPackageName() {
return mDevice.getLauncherPackageName();
}
boolean isFallbackOverview() {
return !getOverviewPackageName().equals(getLauncherPackageName());
}
@NonNull
public UiDevice getDevice() {
return mDevice;
}
private static String eventListToString(List<Integer> actualEvents) {
if (actualEvents.isEmpty()) return "no events";
return "["
+ actualEvents.stream()
.map(state -> TestProtocol.stateOrdinalToString(state))
.collect(Collectors.joining(", "))
+ "]";
}
void runToState(Runnable command, int expectedState, boolean requireEvent, String actionName) {
if (requireEvent) {
runToState(command, expectedState, actionName);
} else {
command.run();
}
}
void runToState(Runnable command, int expectedState, String actionName) {
final List<Integer> actualEvents = new ArrayList<>();
executeAndWaitForLauncherEvent(
command,
event -> isSwitchToStateEvent(event, expectedState, actualEvents),
() -> "Failed to receive an event for the state change: expected ["
+ TestProtocol.stateOrdinalToString(expectedState)
+ "], actual: " + eventListToString(actualEvents),
actionName);
}
private boolean isSwitchToStateEvent(
AccessibilityEvent event, int expectedState, List<Integer> actualEvents) {
if (!TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName())) return false;
final Bundle parcel = (Bundle) event.getParcelableData();
final int actualState = parcel.getInt(TestProtocol.STATE_FIELD);
actualEvents.add(actualState);
return actualState == expectedState;
}
void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState,
GestureScope gestureScope) {
runToState(
() -> linearGesture(startX, startY, endX, endY, steps, false, gestureScope),
expectedState,
"swiping");
}
int getBottomGestureSize() {
return Math.max(getWindowInsets().bottom, ResourceUtils.getNavbarSize(
ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources())) + 1;
}
int getBottomGestureMarginInContainer(UiObject2 container) {
final int bottomGestureStartOnScreen = getBottomGestureStartOnScreen();
return getVisibleBounds(container).bottom - bottomGestureStartOnScreen;
}
int getRightGestureMarginInContainer(UiObject2 container) {
final int rightGestureStartOnScreen = getRightGestureStartOnScreen();
return getVisibleBounds(container).right - rightGestureStartOnScreen;
}
int getBottomGestureStartOnScreen() {
return getRealDisplaySize().y - getBottomGestureSize();
}
int getRightGestureStartOnScreen() {
return getRealDisplaySize().x - getWindowInsets().right;
}
void clickLauncherObject(UiObject2 object) {
waitForObjectEnabled(object, "clickLauncherObject");
expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);
if (!isLauncher3() && getNavigationModel() != NavigationModel.THREE_BUTTON) {
expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
}
object.click();
}
void scrollToLastVisibleRow(
UiObject2 container,
Collection<UiObject2> items,
int topPaddingInContainer) {
final UiObject2 lowestItem = Collections.max(items, (i1, i2) ->
Integer.compare(getVisibleBounds(i1).top, getVisibleBounds(i2).top));
final int itemRowCurrentTopOnScreen = getVisibleBounds(lowestItem).top;
final Rect containerRect = getVisibleBounds(container);
final int itemRowNewTopOnScreen = containerRect.top + topPaddingInContainer;
final int distance = itemRowCurrentTopOnScreen - itemRowNewTopOnScreen + getTouchSlop();
scrollDownByDistance(container, distance);
}
void scrollDownByDistance(UiObject2 container, int distance) {
final Rect containerRect = getVisibleBounds(container);
final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
scroll(
container,
Direction.DOWN,
new Rect(
0,
containerRect.height() - distance - bottomGestureMarginInContainer,
0,
bottomGestureMarginInContainer),
10,
true);
}
void scrollLeftByDistance(UiObject2 container, int distance) {
final Rect containerRect = getVisibleBounds(container);
final int rightGestureMarginInContainer = getRightGestureMarginInContainer(container);
scroll(
container,
Direction.LEFT,
new Rect(
0,
containerRect.width() - distance - rightGestureMarginInContainer,
0,
rightGestureMarginInContainer),
10,
true);
}
void scroll(
UiObject2 container, Direction direction, Rect margins, int steps, boolean slowDown) {
final Rect rect = getVisibleBounds(container);
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();
startY = rect.top;
endY = rect.bottom - 1;
}
break;
case DOWN: {
startX = endX = rect.centerX();
startY = rect.bottom - 1;
endY = rect.top;
}
break;
case LEFT: {
startY = endY = rect.centerY();
startX = rect.left;
endX = rect.right - 1;
}
break;
case RIGHT: {
startY = endY = rect.centerY();
startX = rect.right - 1;
endX = rect.left;
}
break;
default:
fail("Unsupported direction");
return;
}
executeAndWaitForLauncherEvent(
() -> linearGesture(
startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
() -> "Didn't receive a scroll end message: " + startX + ", " + startY
+ ", " + endX + ", " + endY,
"scrolling");
}
// Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
// fixed interval each time.
public void linearGesture(int startX, int startY, int endX, int endY, int steps,
boolean slowDown,
GestureScope gestureScope) {
log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
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, gestureScope);
final long endTime = movePointer(
start, end, steps, false, downTime, slowDown, gestureScope);
sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
}
long movePointer(Point start, Point end, int steps, boolean isDecelerating,
long downTime, boolean slowDown, GestureScope gestureScope) {
long endTime = movePointer(downTime, downTime, steps * GESTURE_STEP_MS,
isDecelerating, start, end, gestureScope);
if (slowDown) {
endTime = movePointer(downTime, endTime + GESTURE_STEP_MS, 5 * GESTURE_STEP_MS, end,
end, gestureScope);
}
return endTime;
}
void waitForIdle() {
mDevice.waitForIdle();
}
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);
}
public void sendPointer(long downTime, long currentTime, int action, Point point,
GestureScope gestureScope) {
final boolean notLauncher3 = !isLauncher3();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
&& gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
}
if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
}
break;
case MotionEvent.ACTION_UP:
if (notLauncher3 && gestureScope != GestureScope.INSIDE
&& gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER
&& (gestureScope == GestureScope.OUTSIDE_WITH_PILFER
|| gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) {
expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
}
if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
&& gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
expectEvent(TestProtocol.SEQUENCE_MAIN,
gestureScope == GestureScope.INSIDE
|| gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
}
if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
}
break;
}
final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
assertTrue("injectInputEvent failed",
mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
event.recycle();
}
public long movePointer(long downTime, long startTime, long duration, Point from, Point to,
GestureScope gestureScope) {
return movePointer(
downTime, startTime, duration, false, from, to, gestureScope);
}
public long movePointer(long downTime, long startTime, long duration, boolean isDecelerating,
Point from, Point to, GestureScope gestureScope) {
log("movePointer: " + from + " to " + to);
final Point point = new Point();
long steps = duration / GESTURE_STEP_MS;
long currentTime = startTime;
if (isDecelerating) {
// formula: V = V0 - D*T, assuming V = 0 when T = duration
// vx0: initial speed at the x-dimension, set as twice the avg speed
// dx: the constant deceleration at the x-dimension
double vx0 = 2 * (to.x - from.x) / duration;
double dx = vx0 / duration;
// vy0: initial speed at the y-dimension, set as twice the avg speed
// dy: the constant deceleration at the y-dimension
double vy0 = 2 * (to.y - from.y) / duration;
double dy = vy0 / duration;
for (long i = 0; i < steps; ++i) {
sleep(GESTURE_STEP_MS);
currentTime += GESTURE_STEP_MS;
// formula: P = P0 + V0*T - (D*T^2/2)
final double t = (i + 1) * GESTURE_STEP_MS;
point.x = from.x + (int) (vx0 * t - 0.5 * dx * t * t);
point.y = from.y + (int) (vy0 * t - 0.5 * dy * t * t);
sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point, gestureScope);
}
} else {
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, gestureScope);
}
}
return currentTime;
}
public static int getCurrentInteractionMode(Context context) {
return getSystemIntegerRes(context, "config_navBarInteractionMode");
}
@NonNull
UiObject2 clickAndGet(
@NonNull final UiObject2 target, @NonNull String resName, Pattern longClickEvent) {
final Point targetCenter = target.getVisibleCenter();
final long downTime = SystemClock.uptimeMillis();
sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter, GestureScope.INSIDE);
expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
final UiObject2 result = waitForLauncherObject(resName);
sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
GestureScope.INSIDE);
return result;
}
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;
}
public void enableDebugTracing() {
getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
}
private void disableSensorRotation() {
getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
}
public void disableDebugTracing() {
getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
}
public void forceGc() {
// GC the system & sysui first before gc'ing launcher
logShellCommand("cmd statusbar run-gc");
getTestInfo(TestProtocol.REQUEST_FORCE_GC);
}
public Integer getPid() {
final Bundle testInfo = getTestInfo(TestProtocol.REQUEST_PID);
return testInfo != null ? testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD) : null;
}
public void produceViewLeak() {
getTestInfo(TestProtocol.REQUEST_VIEW_LEAK);
}
public ArrayList<ComponentName> getRecentTasks() {
ArrayList<ComponentName> tasks = new ArrayList<>();
ArrayList<String> components = getTestInfo(TestProtocol.REQUEST_RECENT_TASKS_LIST)
.getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
for (String s : components) {
tasks.add(ComponentName.unflattenFromString(s));
}
return tasks;
}
public void clearLauncherData() {
getTestInfo(TestProtocol.REQUEST_CLEAR_DATA);
}
private String[] getActivities() {
return getTestInfo(TestProtocol.REQUEST_GET_ACTIVITIES)
.getStringArray(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
public String getRootedActivitiesList() {
return String.join(", ", getActivities());
}
public boolean noLeakedActivities() {
final String[] activities = getActivities();
for (String activity : activities) {
if (activity.contains("(destroyed)")) {
return false;
}
}
return activities.length <= 2;
}
public int getActivitiesCreated() {
return getTestInfo(TestProtocol.REQUEST_GET_ACTIVITIES_CREATED_COUNT)
.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
public Closable eventsCheck() {
Assert.assertTrue("Nested event checking", mEventChecker == null);
disableSensorRotation();
final Integer initialPid = getPid();
final LogEventChecker eventChecker = new LogEventChecker(this);
if (eventChecker.start()) mEventChecker = eventChecker;
return () -> {
if (initialPid != null && initialPid.intValue() != getPid()) {
if (mOnLauncherCrashed != null) mOnLauncherCrashed.run();
checkForAnomaly();
Assert.fail(
formatSystemHealthMessage(
formatErrorWithEvents("Launcher crashed", false)));
}
if (mEventChecker != null) {
mEventChecker = null;
if (mCheckEventsForSuccessfulGestures) {
final String message = eventChecker.verify(WAIT_TIME_MS, true);
if (message != null) {
dumpDiagnostics(message);
checkForAnomaly();
Assert.fail(formatSystemHealthMessage(
"http://go/tapl : successful gesture produced " + message));
}
} else {
eventChecker.finishNoWait();
}
}
};
}
boolean isLauncher3() {
if (mIsLauncher3 == null) {
mIsLauncher3 = "com.android.launcher3".equals(getLauncherPackageName());
}
return mIsLauncher3;
}
void expectEvent(String sequence, Pattern expected) {
if (mEventChecker != null) {
mEventChecker.expectPattern(sequence, expected);
} else {
Log.d(TAG, "Expecting: " + sequence + " / " + expected);
}
}
Rect getVisibleBounds(UiObject2 object) {
try {
return object.getVisibleBounds();
} catch (StaleObjectException e) {
fail("Object disappeared from screen");
return null;
} catch (Throwable t) {
fail(t.toString());
return null;
}
}
float getWindowCornerRadius() {
// TODO(b/197326121): Check if the touch is overlapping with the corners by offsetting
final float tmpBuffer = 100f;
final Resources resources = getResources();
if (!supportsRoundedCornersOnWindows(resources)) {
Log.d(TAG, "No rounded corners");
return tmpBuffer;
}
// Radius that should be used in case top or bottom aren't defined.
float defaultRadius = ResourceUtils.getDimenByName("rounded_corner_radius", resources, 0);
float topRadius = ResourceUtils.getDimenByName("rounded_corner_radius_top", resources, 0);
if (topRadius == 0f) {
topRadius = defaultRadius;
}
float bottomRadius = ResourceUtils.getDimenByName(
"rounded_corner_radius_bottom", resources, 0);
if (bottomRadius == 0f) {
bottomRadius = defaultRadius;
}
// Always use the smallest radius to make sure the rounded corners will
// completely cover the display.
Log.d(TAG, "Rounded corners top: " + topRadius + " bottom: " + bottomRadius);
return Math.max(topRadius, bottomRadius) + tmpBuffer;
}
private static boolean supportsRoundedCornersOnWindows(Resources resources) {
return ResourceUtils.getBoolByName(
"config_supportsRoundedCornersOnWindows", resources, false);
}
}