89f72882ce
Test: Rerun the tapl tests Bug: 325162802 Change-Id: Iffeeb0a60b634e66bc80e4ba45db816cdc8ba564
2469 lines
97 KiB
Java
2469 lines
97 KiB
Java
/*
|
||
* 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 android.view.KeyEvent.ACTION_DOWN;
|
||
import static android.view.MotionEvent.ACTION_SCROLL;
|
||
import static android.view.MotionEvent.ACTION_UP;
|
||
import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
|
||
|
||
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.shared.TestProtocol.NORMAL_STATE_ORDINAL;
|
||
import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_GET_SPLIT_SELECTION_ACTIVE;
|
||
import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_NUM_ALL_APPS_COLUMNS;
|
||
import static com.android.launcher3.testing.shared.TestProtocol.TEST_INFO_RESPONSE_FIELD;
|
||
|
||
import android.app.ActivityManager;
|
||
import android.app.Instrumentation;
|
||
import android.app.UiAutomation;
|
||
import android.app.UiModeManager;
|
||
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.Configuration;
|
||
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.InputEvent;
|
||
import android.view.KeyCharacterMap;
|
||
import android.view.KeyEvent;
|
||
import android.view.MotionEvent;
|
||
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.testing.shared.ResourceUtils;
|
||
import com.android.launcher3.testing.shared.TestInformationRequest;
|
||
import com.android.launcher3.testing.shared.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.ArrayList;
|
||
import java.util.Arrays;
|
||
import java.util.Deque;
|
||
import java.util.LinkedList;
|
||
import java.util.List;
|
||
import java.util.Optional;
|
||
import java.util.concurrent.TimeoutException;
|
||
import java.util.function.BooleanSupplier;
|
||
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 = 15;
|
||
private static final int GESTURE_STEP_MS = 16;
|
||
|
||
static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
|
||
static final Pattern EVENT_START = Pattern.compile("start:");
|
||
|
||
private static final Pattern EVENT_KEY_BACK_DOWN =
|
||
getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK");
|
||
private static final Pattern EVENT_KEY_BACK_UP =
|
||
getKeyEventPattern("ACTION_UP", "KEYCODE_BACK");
|
||
private static final Pattern EVENT_ON_BACK_INVOKED = Pattern.compile("onBackInvoked");
|
||
|
||
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, HOME_ALL_APPS, OVERVIEW, SPLIT_SCREEN_SELECT, WIDGETS, FALLBACK_OVERVIEW,
|
||
LAUNCHED_APP, TASKBAR_ALL_APPS
|
||
}
|
||
|
||
public enum NavigationModel {ZERO_BUTTON, THREE_BUTTON}
|
||
|
||
// Defines whether the gesture recognition triggers pilfer.
|
||
public enum GestureScope {
|
||
DONT_EXPECT_PILFER,
|
||
EXPECT_PILFER,
|
||
}
|
||
|
||
public enum TrackpadGestureType {
|
||
NONE,
|
||
TWO_FINGER,
|
||
THREE_FINGER,
|
||
FOUR_FINGER
|
||
}
|
||
|
||
// Base class for launcher containers.
|
||
abstract static 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();
|
||
}
|
||
|
||
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";
|
||
private static final String OPEN_FOLDER_RES_ID = "folder_content";
|
||
static final String TASKBAR_RES_ID = "taskbar_view";
|
||
private static final String SPLIT_PLACEHOLDER_RES_ID = "split_placeholder";
|
||
static final String KEYBOARD_QUICK_SWITCH_RES_ID = "keyboard_quick_switch_view";
|
||
public static final int WAIT_TIME_MS = 30000;
|
||
static final long DEFAULT_POLL_INTERVAL = 1000;
|
||
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
|
||
private static final String ANDROID_PACKAGE = "android";
|
||
private static final String ASSISTANT_PACKAGE = "com.google.android.googlequicksearchbox";
|
||
private static final String ASSISTANT_GO_HOME_RES_ID = "home_icon";
|
||
|
||
private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
|
||
|
||
private final UiDevice mDevice;
|
||
private final Instrumentation mInstrumentation;
|
||
private Integer mExpectedRotation = null;
|
||
private boolean mExpectedRotationCheckEnabled = true;
|
||
private final Uri mTestProviderUri;
|
||
private final Deque<String> mDiagnosticContext = new LinkedList<>();
|
||
private Function<Long, String> mSystemHealthSupplier;
|
||
|
||
private boolean mIgnoreTaskbarVisibility = false;
|
||
|
||
private Consumer<ContainerType> mOnSettledStateAction;
|
||
|
||
private LogEventChecker mEventChecker;
|
||
|
||
// UI anomaly checker provided by the test.
|
||
private Runnable mTestAnomalyChecker;
|
||
|
||
private boolean mCheckEventsForSuccessfulGestures = false;
|
||
private Runnable mOnLauncherCrashed;
|
||
|
||
private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
|
||
private int mPointerCount = 0;
|
||
|
||
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(), false);
|
||
}
|
||
|
||
/**
|
||
* Constructs the root of TAPL hierarchy. You get all other objects from it.
|
||
*/
|
||
public LauncherInstrumentation(boolean isLauncherTest) {
|
||
this(InstrumentationRegistry.getInstrumentation(), isLauncherTest);
|
||
}
|
||
|
||
/**
|
||
* Constructs the root of TAPL hierarchy. You get all other objects from it.
|
||
*
|
||
* @deprecated use the constructor without Instrumentation parameter instead.
|
||
*/
|
||
@Deprecated
|
||
public LauncherInstrumentation(Instrumentation instrumentation) {
|
||
this(instrumentation, false);
|
||
}
|
||
|
||
/**
|
||
* Constructs the root of TAPL hierarchy. You get all other objects from it.
|
||
*
|
||
* @deprecated use the constructor without Instrumentation parameter instead.
|
||
*/
|
||
@Deprecated
|
||
public LauncherInstrumentation(Instrumentation instrumentation, boolean isLauncherTest) {
|
||
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. "
|
||
+ "Run `adb shell setprop ro.test_harness 1` to enable it.",
|
||
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) || isGradleInstrumentation()
|
||
? 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);
|
||
|
||
final int iterations = isLauncherTest ? 300 : 100;
|
||
|
||
if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) {
|
||
if (TestHelpers.isInLauncherProcess()) {
|
||
pm.setComponentEnabledSetting(cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
|
||
} else {
|
||
try {
|
||
final int userId = getContext().getUserId();
|
||
final String launcherPidCommand = "pidof " + pi.packageName;
|
||
final String initialPid = mDevice.executeShellCommand(launcherPidCommand);
|
||
|
||
mDevice.executeShellCommand(
|
||
"pm enable --user " + userId + " " + cn.flattenToString());
|
||
|
||
// Wait for Launcher restart after enabling test provider.
|
||
for (int i = 0; i < iterations; ++i) {
|
||
final String currentPid = mDevice.executeShellCommand(launcherPidCommand)
|
||
.replaceAll("\\s", "");
|
||
if (!currentPid.isEmpty() && !currentPid.equals(initialPid)) break;
|
||
if (i == iterations - 1) {
|
||
fail("Launcher didn't restart after enabling test provider");
|
||
}
|
||
SystemClock.sleep(100);
|
||
}
|
||
} catch (IOException e) {
|
||
fail(e.toString());
|
||
}
|
||
}
|
||
|
||
// Wait for Launcher content provider to become enabled.
|
||
for (int i = 0; i < iterations; ++i) {
|
||
final ContentProviderClient testProvider = getContext().getContentResolver()
|
||
.acquireContentProviderClient(mTestProviderUri);
|
||
if (testProvider != null) {
|
||
testProvider.close();
|
||
break;
|
||
}
|
||
if (i == iterations - 1) {
|
||
fail("Launcher content provider is still not enabled");
|
||
}
|
||
SystemClock.sleep(100);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gradle only supports out of process instrumentation. The test package is automatically
|
||
* generated by appending `.test` to the target package.
|
||
*/
|
||
private boolean isGradleInstrumentation() {
|
||
final String testPackage = getContext().getPackageName();
|
||
final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
|
||
final String testSuffix = ".test";
|
||
|
||
return testPackage.endsWith(testSuffix) && testPackage.length() > testSuffix.length()
|
||
&& testPackage.substring(0, testPackage.length() - testSuffix.length())
|
||
.equals(targetPackage);
|
||
}
|
||
|
||
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) {
|
||
return getTestInfo(request, arg, null);
|
||
}
|
||
|
||
Bundle getTestInfo(String request, String arg, Bundle extra) {
|
||
try (ContentProviderClient client = getContext().getContentResolver()
|
||
.acquireContentProviderClient(mTestProviderUri)) {
|
||
return client.call(request, arg, extra);
|
||
} catch (DeadObjectException e) {
|
||
fail("Launcher crashed");
|
||
return null;
|
||
} catch (RemoteException e) {
|
||
throw new RuntimeException(e);
|
||
}
|
||
}
|
||
|
||
Bundle getTestInfo(TestInformationRequest request) {
|
||
Bundle extra = new Bundle();
|
||
extra.putParcelable(TestProtocol.TEST_INFO_REQUEST_FIELD, request);
|
||
return getTestInfo(request.getRequestName(), null, extra);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
Insets getSystemGestureRegion() {
|
||
return getTestInfo(TestProtocol.REQUEST_SYSTEM_GESTURE_REGION)
|
||
.getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
public int getNumAllAppsColumns() {
|
||
return getTestInfo(REQUEST_NUM_ALL_APPS_COLUMNS).getInt(
|
||
TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
public boolean isTablet() {
|
||
return getTestInfo(TestProtocol.REQUEST_IS_TABLET)
|
||
.getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
public boolean isTaskbarNavbarUnificationEnabled() {
|
||
return getTestInfo(TestProtocol.REQUEST_ENABLE_TASKBAR_NAVBAR_UNIFICATION)
|
||
.getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
public boolean isTwoPanels() {
|
||
return getTestInfo(TestProtocol.REQUEST_IS_TWO_PANELS)
|
||
.getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
int getCellLayoutBoarderHeight() {
|
||
return getTestInfo(TestProtocol.REQUEST_CELL_LAYOUT_BOARDER_HEIGHT)
|
||
.getInt(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));
|
||
}
|
||
|
||
int getOverviewPageSpacing() {
|
||
return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_PAGE_SPACING)
|
||
.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
public int getOverviewCurrentPageIndex() {
|
||
return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_CURRENT_PAGE_INDEX)
|
||
.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
float getExactScreenCenterX() {
|
||
return getRealDisplaySize().x / 2f;
|
||
}
|
||
|
||
public void setEnableRotation(boolean on) {
|
||
getTestInfo(TestProtocol.REQUEST_ENABLE_ROTATION, Boolean.toString(on));
|
||
}
|
||
|
||
public void setEnableSuggestion(boolean enableSuggestion) {
|
||
getTestInfo(TestProtocol.REQUEST_ENABLE_SUGGESTION, Boolean.toString(enableSuggestion));
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* Sets the accesibility interactive timeout to be effectively indefinite (UI using this
|
||
* accesibility timeout will not automatically dismiss if true).
|
||
*/
|
||
void setIndefiniteAccessibilityInteractiveUiTimeout(boolean indefiniteTimeout) {
|
||
final String cmd = indefiniteTimeout
|
||
? "settings put secure accessibility_interactive_ui_timeout_ms 10000"
|
||
: "settings delete secure accessibility_interactive_ui_timeout_ms";
|
||
logShellCommand(cmd);
|
||
}
|
||
|
||
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.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);
|
||
}
|
||
}
|
||
|
||
public String getSystemAnomalyMessage(
|
||
boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) {
|
||
try {
|
||
{
|
||
final StringBuilder sb = new StringBuilder();
|
||
|
||
UiObject2 object =
|
||
mDevice.findObject(By.res("android", "alertTitle").pkg("android"));
|
||
if (object != null) {
|
||
sb.append("TITLE: ").append(object.getText());
|
||
}
|
||
|
||
object = mDevice.findObject(By.res("android", "message").pkg("android"));
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* Allows the test to provide a pluggable anomaly checker. It’s supposed to throw an exception
|
||
* if the check fails. The test may provide its own anomaly checker, for example, if it wants to
|
||
* check for an anomaly that’s recognized by the standard TAPL anomaly checker, but wants a
|
||
* custom error message, such as adding information whether the keyguard is seen for the first
|
||
* time during the shard execution.
|
||
*/
|
||
public void setAnomalyChecker(Runnable anomalyChecker) {
|
||
mTestAnomalyChecker = anomalyChecker;
|
||
}
|
||
|
||
/**
|
||
* Verifies that there are no visible UI anomalies. An "anomaly" is a state of UI that should
|
||
* never happen during the text execution. Anomaly is something different from just “regular”
|
||
* unexpected state of the Launcher such as when we see Workspace after swiping up to All Apps.
|
||
* Workspace is a normal state. We can contrast this with an anomaly, when, for example, we see
|
||
* a lock screen. Launcher tests can never bring the lock screen, so the very presence of the
|
||
* lock screen is an indication that something went very wrong, and perhaps is caused by reasons
|
||
* outside of the Launcher and its tests, perhaps, by a crash in System UI. Diagnosing anomalies
|
||
* helps to understand faster whether the problem is in the Launcher or its tests, or outside.
|
||
*/
|
||
public void checkForAnomaly(
|
||
boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) {
|
||
if (mTestAnomalyChecker != null) mTestAnomalyChecker.run();
|
||
|
||
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(OPEN_FOLDER_RES_ID)) return "Open Folder";
|
||
if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
|
||
if (hasSystemLauncherObject(OVERVIEW_RES_ID)) return "Overview";
|
||
if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
|
||
if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
|
||
if (hasLauncherObject(TASKBAR_RES_ID)) return "Taskbar";
|
||
if (hasLauncherObject("wallpaper_carousel")) return "Launcher Settings Popup";
|
||
if (mDevice.hasObject(By.pkg(getLauncherPackageName()).depth(0))) {
|
||
return "<Launcher in invalid state>";
|
||
}
|
||
return "LaunchedApp (" + 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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Whether to ignore verifying the task bar visibility during instrumenting.
|
||
*
|
||
* @param ignoreTaskbarVisibility {@code true} will ignore the instrumentation implicitly
|
||
* verifying the task bar visibility with
|
||
* {@link VisibleContainer#verifyActiveContainer}.
|
||
* {@code false} otherwise.
|
||
*/
|
||
public void setIgnoreTaskbarVisibility(boolean ignoreTaskbarVisibility) {
|
||
mIgnoreTaskbarVisibility = ignoreTaskbarVisibility;
|
||
}
|
||
|
||
/**
|
||
* Set the trackpad gesture type of the interaction.
|
||
*
|
||
* @param trackpadGestureType whether it's not from trackpad, two-finger, three-finger, or
|
||
* four-finger gesture.
|
||
*/
|
||
public void setTrackpadGestureType(TrackpadGestureType trackpadGestureType) {
|
||
mTrackpadGestureType = trackpadGestureType;
|
||
}
|
||
|
||
TrackpadGestureType getTrackpadGestureType() {
|
||
return mTrackpadGestureType;
|
||
}
|
||
|
||
/**
|
||
* Sets expected rotation.
|
||
* TAPL periodically checks that Launcher didn't suddenly change the rotation to unexpected one.
|
||
* Null parameter disables checks. The initial state is "no checks".
|
||
*/
|
||
public void setExpectedRotation(Integer expectedRotation) {
|
||
mExpectedRotation = expectedRotation;
|
||
}
|
||
|
||
public void setExpectedRotationCheckEnabled(boolean expectedRotationCheckEnabled) {
|
||
mExpectedRotationCheckEnabled = expectedRotationCheckEnabled;
|
||
}
|
||
|
||
public boolean getExpectedRotationCheckEnabled() {
|
||
return mExpectedRotationCheckEnabled;
|
||
}
|
||
|
||
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() || isTaskbarNavbarUnificationEnabled()
|
||
? getLauncherPackageName() : SYSTEMUI_PACKAGE;
|
||
}
|
||
|
||
UiObject2 verifyContainerType(ContainerType containerType) {
|
||
waitForLauncherInitialized();
|
||
|
||
if (mExpectedRotationCheckEnabled && mExpectedRotation != null) {
|
||
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(WIDGETS_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
|
||
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
|
||
|
||
return waitForLauncherObject(WORKSPACE_RES_ID);
|
||
}
|
||
case WIDGETS: {
|
||
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
|
||
waitUntilLauncherObjectGone(APPS_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
|
||
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
|
||
|
||
return waitForLauncherObject(WIDGETS_RES_ID);
|
||
}
|
||
case TASKBAR_ALL_APPS: {
|
||
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
|
||
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
|
||
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
|
||
|
||
return waitForLauncherObject(APPS_RES_ID);
|
||
}
|
||
case HOME_ALL_APPS: {
|
||
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
|
||
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
|
||
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
|
||
|
||
if (is3PLauncher() && isTablet() && !isTransientTaskbar()) {
|
||
waitForSystemLauncherObject(TASKBAR_RES_ID);
|
||
} else {
|
||
waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
|
||
}
|
||
|
||
boolean splitSelectionActive = getTestInfo(REQUEST_GET_SPLIT_SELECTION_ACTIVE)
|
||
.getBoolean(TEST_INFO_RESPONSE_FIELD);
|
||
if (!splitSelectionActive) {
|
||
waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
|
||
} // do nothing, we expect that view
|
||
|
||
return waitForLauncherObject(APPS_RES_ID);
|
||
}
|
||
case OVERVIEW:
|
||
case FALLBACK_OVERVIEW: {
|
||
waitUntilLauncherObjectGone(APPS_RES_ID);
|
||
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
|
||
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
|
||
if (isTablet() && !is3PLauncher()) {
|
||
waitForSystemLauncherObject(TASKBAR_RES_ID);
|
||
} else {
|
||
waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
|
||
}
|
||
waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
|
||
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
|
||
|
||
return waitForSystemLauncherObject(OVERVIEW_RES_ID);
|
||
}
|
||
case SPLIT_SCREEN_SELECT: {
|
||
waitUntilLauncherObjectGone(APPS_RES_ID);
|
||
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
|
||
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
|
||
if (isTablet()) {
|
||
waitForSystemLauncherObject(TASKBAR_RES_ID);
|
||
} else {
|
||
waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
|
||
}
|
||
|
||
waitForSystemLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
|
||
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
|
||
return waitForSystemLauncherObject(OVERVIEW_RES_ID);
|
||
}
|
||
case LAUNCHED_APP: {
|
||
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
|
||
waitUntilLauncherObjectGone(APPS_RES_ID);
|
||
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
|
||
waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
|
||
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
|
||
|
||
if (mIgnoreTaskbarVisibility) {
|
||
return null;
|
||
}
|
||
|
||
if (isTablet()) {
|
||
// Only check that Persistent Taskbar is visible, since Transient Taskbar
|
||
// may or may not be visible by design.
|
||
if (!isTransientTaskbar()) {
|
||
waitForSystemLauncherObject(TASKBAR_RES_ID);
|
||
}
|
||
} else {
|
||
waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
|
||
}
|
||
return null;
|
||
}
|
||
default:
|
||
fail("Invalid state: " + containerType);
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
public void waitForModelQueueCleared() {
|
||
getTestInfo(TestProtocol.REQUEST_MODEL_QUEUE_CLEARED);
|
||
}
|
||
|
||
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");
|
||
}
|
||
|
||
public boolean isLauncherActivityStarted() {
|
||
return getTestInfo(
|
||
TestProtocol.REQUEST_IS_LAUNCHER_LAUNCHER_ACTIVITY_STARTED).
|
||
getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
void executeAndWaitForLauncherStop(Runnable command, String actionName) {
|
||
executeAndWaitForLauncherEvent(
|
||
() -> command.run(),
|
||
event -> TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE
|
||
.equals(event.getClassName().toString()),
|
||
() -> "Launcher activity didn't stop", actionName);
|
||
}
|
||
|
||
/**
|
||
* 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() {
|
||
final Point displaySize = getRealDisplaySize();
|
||
|
||
final Optional<String> floatingRes = getFloatingResId();
|
||
|
||
if (!floatingRes.isPresent()) {
|
||
return;
|
||
}
|
||
|
||
if (isLauncher3()) {
|
||
gestureToDismissPopup(displaySize);
|
||
} else {
|
||
runToState(() -> gestureToDismissPopup(displaySize), NORMAL_STATE_ORDINAL, "swiping");
|
||
}
|
||
|
||
try (LauncherInstrumentation.Closable c1 = addContextLayer(
|
||
String.format("Swiped up from floating view %s to home", floatingRes.get()))) {
|
||
waitUntilLauncherObjectGone(floatingRes.get());
|
||
waitForLauncherObject(getAnyObjectSelector());
|
||
}
|
||
}
|
||
|
||
private void gestureToDismissPopup(Point displaySize) {
|
||
linearGesture(
|
||
displaySize.x / 2, displaySize.y - 1,
|
||
displaySize.x / 2, 0,
|
||
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
|
||
false, GestureScope.EXPECT_PILFER);
|
||
}
|
||
|
||
/**
|
||
* @return the Workspace object.
|
||
* @deprecated use goHome().
|
||
* Presses nav bar home button.
|
||
*/
|
||
@Deprecated
|
||
public Workspace pressHome() {
|
||
return goHome();
|
||
}
|
||
|
||
/**
|
||
* Goes to home from immersive fullscreen app by first swiping up to bring navbar, and then
|
||
* performing {@code goHome()} action.
|
||
* Currently only supports gesture navigation mode.
|
||
*
|
||
* @return the Workspace object.
|
||
*/
|
||
public Workspace goHomeFromImmersiveFullscreenApp() {
|
||
assertTrue("expected gesture navigation mode",
|
||
getNavigationModel() == NavigationModel.ZERO_BUTTON);
|
||
final Point displaySize = getRealDisplaySize();
|
||
linearGesture(
|
||
displaySize.x / 2, displaySize.y - 1,
|
||
displaySize.x / 2, 0,
|
||
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
|
||
false, GestureScope.EXPECT_PILFER);
|
||
return goHome();
|
||
}
|
||
|
||
/**
|
||
* Goes to home by swiping up in zero-button mode or pressing Home button.
|
||
* Calling it after another TAPL call is safe because all TAPL methods wait for the animations
|
||
* to finish.
|
||
* When calling it after a non-TAPL method, make sure that all animations have already
|
||
* completed, otherwise it may detect the current state (for example "Application" or "Home")
|
||
* incorrectly.
|
||
* The method expects either app or Launcher to be active when it's called. Other states, such
|
||
* as visible notification shade are not supported.
|
||
*
|
||
* @return the Workspace object.
|
||
*/
|
||
public Workspace goHome() {
|
||
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.
|
||
boolean isThreeFingerTrackpadGesture =
|
||
mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
|
||
final String action;
|
||
if (getNavigationModel() == NavigationModel.ZERO_BUTTON
|
||
|| isThreeFingerTrackpadGesture) {
|
||
checkForAnomaly(false, true);
|
||
|
||
final Point displaySize = getRealDisplaySize();
|
||
|
||
// CLose floating views before going back to home.
|
||
swipeUpToCloseFloatingView();
|
||
|
||
if (hasLauncherObject(WORKSPACE_RES_ID)) {
|
||
log(action = "already at home");
|
||
} else {
|
||
action = "swiping up to home";
|
||
|
||
int startY = isThreeFingerTrackpadGesture ? displaySize.y * 3 / 4
|
||
: displaySize.y - 1;
|
||
int endY = isThreeFingerTrackpadGesture ? displaySize.y / 4 : displaySize.y / 2;
|
||
swipeToState(
|
||
displaySize.x / 2, startY,
|
||
displaySize.x / 2, endY,
|
||
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
|
||
GestureScope.EXPECT_PILFER);
|
||
}
|
||
} else {
|
||
log("Hierarchy before clicking home:");
|
||
dumpViewHierarchy();
|
||
action = "clicking home button";
|
||
|
||
runToState(
|
||
getHomeButton()::click,
|
||
NORMAL_STATE_ORDINAL,
|
||
!hasLauncherObject(WORKSPACE_RES_ID)
|
||
&& (hasLauncherObject(APPS_RES_ID)
|
||
|| hasSystemLauncherObject(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")) {
|
||
pressBackImpl();
|
||
}
|
||
}
|
||
|
||
void pressBackImpl() {
|
||
waitForLauncherInitialized();
|
||
final boolean launcherVisible =
|
||
isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
|
||
boolean isThreeFingerTrackpadGesture =
|
||
mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
|
||
if (getNavigationModel() == NavigationModel.ZERO_BUTTON
|
||
|| isThreeFingerTrackpadGesture) {
|
||
final Point displaySize = getRealDisplaySize();
|
||
// TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
|
||
// issue is solved.
|
||
int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
|
||
int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
|
||
linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
|
||
10, false, GestureScope.DONT_EXPECT_PILFER);
|
||
} else {
|
||
waitForNavigationUiObject("back").click();
|
||
}
|
||
if (launcherVisible) {
|
||
if (getContext().getApplicationInfo().isOnBackInvokedCallbackEnabled()) {
|
||
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ON_BACK_INVOKED);
|
||
} else {
|
||
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 -> r.equals(OVERVIEW_RES_ID) ? hasSystemLauncherObject(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 LaunchedApp object if another app is active. Fails if the launcher is not in that
|
||
* state.
|
||
*
|
||
* @return LaunchedApp object.
|
||
*/
|
||
@NonNull
|
||
public LaunchedAppState getLaunchedAppState() {
|
||
return new LaunchedAppState(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 homescreen 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 Home All Apps object.
|
||
*/
|
||
@NonNull
|
||
public HomeAllApps getAllApps() {
|
||
try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) {
|
||
return new HomeAllApps(this);
|
||
}
|
||
}
|
||
|
||
LaunchedAppState assertAppLaunched(@NonNull String expectedPackageName) {
|
||
BySelector packageSelector = By.pkg(expectedPackageName);
|
||
assertTrue("App didn't start: (" + packageSelector + ")",
|
||
mDevice.wait(Until.hasObject(packageSelector),
|
||
LauncherInstrumentation.WAIT_TIME_MS));
|
||
return new LaunchedAppState(this);
|
||
}
|
||
|
||
void waitUntilLauncherObjectGone(String resId) {
|
||
waitUntilGoneBySelector(getLauncherObjectSelector(resId));
|
||
}
|
||
|
||
void waitUntilOverviewObjectGone(String resId) {
|
||
waitUntilGoneBySelector(getOverviewObjectSelector(resId));
|
||
}
|
||
|
||
void waitUntilSystemLauncherObjectGone(String resId) {
|
||
if (is3PLauncher()) {
|
||
waitUntilOverviewObjectGone(resId);
|
||
} else {
|
||
waitUntilLauncherObjectGone(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 waitForSystemUiObject(BySelector selector) {
|
||
final UiObject2 object = TestHelpers.wait(
|
||
Until.findObject(selector), WAIT_TIME_MS);
|
||
assertNotNull("Can't find a systemui object with selector: " + selector, object);
|
||
return object;
|
||
}
|
||
|
||
@NonNull
|
||
private UiObject2 getHomeButton() {
|
||
UiModeManager uiManager =
|
||
(UiModeManager) getContext().getSystemService(Context.UI_MODE_SERVICE);
|
||
if (uiManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
|
||
return waitForAssistantHomeButton();
|
||
} else {
|
||
return waitForNavigationUiObject("home");
|
||
}
|
||
}
|
||
|
||
/* Assistant Home button is present when system is in car mode. */
|
||
@NonNull
|
||
UiObject2 waitForAssistantHomeButton() {
|
||
final UiObject2 object = mDevice.wait(
|
||
Until.findObject(By.res(ASSISTANT_PACKAGE, ASSISTANT_GO_HOME_RES_ID)),
|
||
WAIT_TIME_MS);
|
||
assertNotNull(
|
||
"Can't find an assistant UI object with id: " + ASSISTANT_GO_HOME_RES_ID, 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, String resName) {
|
||
try {
|
||
return container.findObject(getLauncherObjectSelector(resName));
|
||
} catch (StaleObjectException e) {
|
||
fail("The container disappeared from screen");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
@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");
|
||
}
|
||
}
|
||
|
||
void waitForObjectFocused(UiObject2 object, String waitReason) {
|
||
try {
|
||
assertTrue("Timed out waiting for object to be focused for " + waitReason + " "
|
||
+ object.getResourceName(),
|
||
object.wait(Until.focused(true), WAIT_TIME_MS));
|
||
} catch (StaleObjectException e) {
|
||
fail("The object disappeared from screen");
|
||
}
|
||
}
|
||
|
||
@NonNull
|
||
UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
|
||
return waitForObjectsInContainer(container, selector).get(0);
|
||
}
|
||
|
||
@NonNull
|
||
List<UiObject2> waitForObjectsInContainer(
|
||
UiObject2 container, BySelector selector) {
|
||
try {
|
||
final List<UiObject2> objects = container.wait(
|
||
Until.findObjects(selector),
|
||
WAIT_TIME_MS);
|
||
assertNotNull("Can't find views in Launcher, id: " + selector + " in container: "
|
||
+ container.getResourceName(), objects);
|
||
assertTrue("Can't find views in Launcher, id: " + selector + " in container: "
|
||
+ container.getResourceName(), objects.size() > 0);
|
||
return objects;
|
||
} 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));
|
||
}
|
||
|
||
private boolean hasSystemLauncherObject(String resId) {
|
||
return mDevice.hasObject(is3PLauncher() ? getOverviewObjectSelector(resId)
|
||
: 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 waitForSystemLauncherObject(String resName) {
|
||
return is3PLauncher() ? waitForOverviewObject(resName)
|
||
: waitForLauncherObject(resName);
|
||
}
|
||
|
||
@NonNull
|
||
UiObject2 waitForLauncherObject(BySelector selector) {
|
||
return waitForObjectBySelector(makeLauncherSelector(selector));
|
||
}
|
||
|
||
@NonNull
|
||
UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
|
||
return tryWaitForObjectBySelector(makeLauncherSelector(selector), timeout);
|
||
}
|
||
|
||
@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;
|
||
}
|
||
|
||
@NonNull
|
||
List<UiObject2> waitForObjectsBySelector(BySelector selector) {
|
||
final List<UiObject2> objects = mDevice.wait(Until.findObjects(selector), WAIT_TIME_MS);
|
||
assertNotNull("Can't find any view in Launcher, selector: " + selector, objects);
|
||
return objects;
|
||
}
|
||
|
||
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 is3PLauncher() {
|
||
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();
|
||
}
|
||
}
|
||
|
||
/** Run an action and wait for the specified Launcher state. */
|
||
public 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 - 1;
|
||
}
|
||
|
||
/**
|
||
* Click on the ui object right away without waiting for animation.
|
||
*
|
||
* [UiObject2.click] would wait for all animations finished before clicking. Not waiting for
|
||
* animations because in some scenarios there is a playing animations when the click is
|
||
* attempted.
|
||
*/
|
||
void clickObject(UiObject2 uiObject) {
|
||
final long clickTime = SystemClock.uptimeMillis();
|
||
final Point center = uiObject.getVisibleCenter();
|
||
sendPointer(clickTime, clickTime, MotionEvent.ACTION_DOWN, center,
|
||
GestureScope.DONT_EXPECT_PILFER);
|
||
sendPointer(clickTime, clickTime, MotionEvent.ACTION_UP, center,
|
||
GestureScope.DONT_EXPECT_PILFER);
|
||
}
|
||
|
||
void clickLauncherObject(UiObject2 object) {
|
||
clickObject(object);
|
||
}
|
||
|
||
void scrollToLastVisibleRow(
|
||
UiObject2 container, Rect bottomVisibleIconBounds, int topPaddingInContainer,
|
||
int appsListBottomPadding) {
|
||
final int itemRowCurrentTopOnScreen = bottomVisibleIconBounds.top;
|
||
final Rect containerRect = getVisibleBounds(container);
|
||
final int itemRowNewTopOnScreen = containerRect.top + topPaddingInContainer;
|
||
final int distance = itemRowCurrentTopOnScreen - itemRowNewTopOnScreen + getTouchSlop();
|
||
|
||
scrollDownByDistance(container, distance, appsListBottomPadding);
|
||
}
|
||
|
||
void scrollDownByDistance(UiObject2 container, int distance) {
|
||
scrollDownByDistance(container, distance, 0);
|
||
}
|
||
|
||
void scrollDownByDistance(UiObject2 container, int distance, int bottomPadding) {
|
||
final Rect containerRect = getVisibleBounds(container);
|
||
final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
|
||
scroll(
|
||
container,
|
||
Direction.DOWN,
|
||
new Rect(
|
||
0,
|
||
containerRect.height() - distance - bottomGestureMarginInContainer,
|
||
0,
|
||
bottomGestureMarginInContainer + bottomPadding),
|
||
/* steps= */ 10,
|
||
/* slowDown= */ true);
|
||
}
|
||
|
||
void scrollLeftByDistance(UiObject2 container, int distance) {
|
||
final Rect containerRect = getVisibleBounds(container);
|
||
final int rightGestureMarginInContainer = getRightGestureMarginInContainer(container);
|
||
final int leftGestureMargin = getTargetInsets().left + getEdgeSensitivityWidth();
|
||
scroll(
|
||
container,
|
||
Direction.LEFT,
|
||
new Rect(leftGestureMargin,
|
||
0,
|
||
Math.max(containerRect.width() - distance - leftGestureMargin,
|
||
rightGestureMarginInContainer),
|
||
0),
|
||
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.DONT_EXPECT_PILFER),
|
||
event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
|
||
() -> "Didn't receive a scroll end message: " + startX + ", " + startY
|
||
+ ", " + endX + ", " + endY,
|
||
"scrolling");
|
||
}
|
||
|
||
void pointerScroll(float pointerX, float pointerY, Direction direction) {
|
||
executeAndWaitForLauncherEvent(
|
||
() -> injectEvent(getPointerMotionEvent(
|
||
ACTION_SCROLL, pointerX, pointerY, direction)),
|
||
event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
|
||
() -> "Didn't receive a scroll end message: " + direction + " scroll from ("
|
||
+ pointerX + ", " + pointerY + ")",
|
||
"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);
|
||
long endTime = downTime;
|
||
sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
|
||
try {
|
||
|
||
if (mTrackpadGestureType != TrackpadGestureType.NONE) {
|
||
sendPointer(downTime, downTime,
|
||
getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1),
|
||
start, gestureScope);
|
||
if (mTrackpadGestureType == TrackpadGestureType.THREE_FINGER
|
||
|| mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
|
||
sendPointer(downTime, downTime,
|
||
getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2),
|
||
start, gestureScope);
|
||
if (mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
|
||
sendPointer(downTime, downTime,
|
||
getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 3),
|
||
start, gestureScope);
|
||
}
|
||
}
|
||
}
|
||
endTime = movePointer(
|
||
start, end, steps, false, downTime, downTime, slowDown, gestureScope);
|
||
} finally {
|
||
if (mTrackpadGestureType != TrackpadGestureType.NONE) {
|
||
for (int i = mPointerCount; i >= 2; i--) {
|
||
sendPointer(downTime, downTime,
|
||
getPointerAction(MotionEvent.ACTION_POINTER_UP, i - 1),
|
||
start, gestureScope);
|
||
}
|
||
}
|
||
sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
|
||
}
|
||
}
|
||
|
||
private static int getPointerAction(int action, int index) {
|
||
return action + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
|
||
}
|
||
|
||
long movePointer(Point start, Point end, int steps, boolean isDecelerating, long downTime,
|
||
long startTime, boolean slowDown, GestureScope gestureScope) {
|
||
long endTime = movePointer(downTime, startTime, 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 getPointerMotionEvent(
|
||
int action, float x, float y, Direction direction) {
|
||
MotionEvent.PointerCoords[] coordinates = new MotionEvent.PointerCoords[1];
|
||
coordinates[0] = new MotionEvent.PointerCoords();
|
||
coordinates[0].x = x;
|
||
coordinates[0].y = y;
|
||
boolean isVertical = direction == Direction.UP || direction == Direction.DOWN;
|
||
boolean isForward = direction == Direction.RIGHT || direction == Direction.DOWN;
|
||
coordinates[0].setAxisValue(
|
||
isVertical ? MotionEvent.AXIS_VSCROLL : MotionEvent.AXIS_HSCROLL,
|
||
isForward ? 1f : -1f);
|
||
|
||
MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1];
|
||
properties[0] = new MotionEvent.PointerProperties();
|
||
properties[0].id = 0;
|
||
properties[0].toolType = MotionEvent.TOOL_TYPE_MOUSE;
|
||
|
||
final long downTime = SystemClock.uptimeMillis();
|
||
return MotionEvent.obtain(
|
||
downTime,
|
||
downTime,
|
||
action,
|
||
/* pointerCount= */ 1,
|
||
properties,
|
||
coordinates,
|
||
/* metaState= */ 0,
|
||
/* buttonState= */ 0,
|
||
/* xPrecision= */ 1f,
|
||
/* yPrecision= */ 1f,
|
||
/* deviceId= */ 0,
|
||
/* edgeFlags= */ 0,
|
||
InputDevice.SOURCE_CLASS_POINTER,
|
||
/* flags= */ 0);
|
||
}
|
||
|
||
private static MotionEvent getTrackpadMotionEvent(long downTime, long eventTime,
|
||
int action, float x, float y, int pointerCount, TrackpadGestureType gestureType) {
|
||
MotionEvent.PointerProperties[] pointerProperties =
|
||
new MotionEvent.PointerProperties[pointerCount];
|
||
MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
|
||
boolean isMultiFingerGesture = gestureType != TrackpadGestureType.TWO_FINGER;
|
||
for (int i = 0; i < pointerCount; i++) {
|
||
pointerProperties[i] = getPointerProperties(i);
|
||
pointerCoords[i] = getPointerCoords(x, y);
|
||
if (isMultiFingerGesture) {
|
||
pointerCoords[i].setAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT,
|
||
gestureType == TrackpadGestureType.THREE_FINGER ? 3 : 4);
|
||
}
|
||
}
|
||
return MotionEvent.obtain(downTime, eventTime, action, pointerCount, pointerProperties,
|
||
pointerCoords, 0, 0, 1.0f, 1.0f, 0, 0,
|
||
InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_CLASS_POINTER, 0, 0,
|
||
isMultiFingerGesture ? MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
|
||
: MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE);
|
||
}
|
||
|
||
private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
|
||
float x, float y, int source) {
|
||
return MotionEvent.obtain(downTime, eventTime, action, 1,
|
||
new MotionEvent.PointerProperties[]{getPointerProperties(0)},
|
||
new MotionEvent.PointerCoords[]{getPointerCoords(x, y)},
|
||
0, 0, 1.0f, 1.0f, 0, 0, source, 0);
|
||
}
|
||
|
||
private static MotionEvent.PointerProperties getPointerProperties(int pointerId) {
|
||
MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
|
||
properties.id = pointerId;
|
||
properties.toolType = Configurator.getInstance().getToolType();
|
||
return properties;
|
||
}
|
||
|
||
private static MotionEvent.PointerCoords getPointerCoords(float x, float y) {
|
||
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
||
coords.pressure = 1;
|
||
coords.size = 1;
|
||
coords.x = x;
|
||
coords.y = y;
|
||
return coords;
|
||
}
|
||
|
||
private boolean hasTIS() {
|
||
return getTestInfo(TestProtocol.REQUEST_HAS_TIS).getBoolean(
|
||
TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
public boolean isGridOnlyOverviewEnabled() {
|
||
return getTestInfo(TestProtocol.REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW).getBoolean(
|
||
TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
public void sendPointer(long downTime, long currentTime, int action, Point point,
|
||
GestureScope gestureScope) {
|
||
sendPointer(downTime, currentTime, action, point, gestureScope,
|
||
InputDevice.SOURCE_TOUCHSCREEN, false);
|
||
}
|
||
|
||
private void injectEvent(InputEvent event) {
|
||
assertTrue("injectInputEvent failed: event=" + event,
|
||
mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
|
||
}
|
||
|
||
public void sendPointer(long downTime, long currentTime, int action, Point point,
|
||
GestureScope gestureScope, int source) {
|
||
sendPointer(downTime, currentTime, action, point, gestureScope, source, false);
|
||
}
|
||
|
||
public void sendPointer(long downTime, long currentTime, int action, Point point,
|
||
GestureScope gestureScope, int source, boolean isRightClick) {
|
||
final boolean hasTIS = hasTIS();
|
||
int pointerCount = mPointerCount;
|
||
|
||
boolean isTrackpadGesture = mTrackpadGestureType != TrackpadGestureType.NONE;
|
||
switch (action & MotionEvent.ACTION_MASK) {
|
||
case MotionEvent.ACTION_DOWN:
|
||
if (isTrackpadGesture) {
|
||
mPointerCount = 1;
|
||
pointerCount = mPointerCount;
|
||
}
|
||
break;
|
||
case MotionEvent.ACTION_UP:
|
||
if (hasTIS && gestureScope == GestureScope.EXPECT_PILFER) {
|
||
expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
|
||
}
|
||
break;
|
||
case MotionEvent.ACTION_POINTER_DOWN:
|
||
mPointerCount++;
|
||
pointerCount = mPointerCount;
|
||
break;
|
||
case MotionEvent.ACTION_POINTER_UP:
|
||
// When the gesture is handled outside, it's cancelled within launcher.
|
||
mPointerCount--;
|
||
break;
|
||
}
|
||
|
||
final MotionEvent event = isTrackpadGesture
|
||
? getTrackpadMotionEvent(
|
||
downTime, currentTime, action, point.x, point.y, pointerCount,
|
||
mTrackpadGestureType)
|
||
: getMotionEvent(downTime, currentTime, action, point.x, point.y, source);
|
||
if (action == MotionEvent.ACTION_BUTTON_PRESS
|
||
|| action == MotionEvent.ACTION_BUTTON_RELEASE) {
|
||
event.setActionButton(MotionEvent.BUTTON_PRIMARY);
|
||
}
|
||
if (isRightClick) {
|
||
event.setButtonState(event.getButtonState() & MotionEvent.BUTTON_SECONDARY);
|
||
}
|
||
injectEvent(event);
|
||
}
|
||
|
||
private KeyEvent createKeyEvent(int keyCode, int metaState, boolean actionDown) {
|
||
long eventTime = SystemClock.uptimeMillis();
|
||
return KeyEvent.obtain(
|
||
eventTime,
|
||
eventTime,
|
||
actionDown ? ACTION_DOWN : ACTION_UP,
|
||
keyCode,
|
||
/* repeat= */ 0,
|
||
metaState,
|
||
KeyCharacterMap.VIRTUAL_KEYBOARD,
|
||
/* scancode= */ 0,
|
||
/* flags= */ 0,
|
||
InputDevice.SOURCE_KEYBOARD,
|
||
/* characters =*/ null);
|
||
}
|
||
|
||
/**
|
||
* Sends a {@link KeyEvent} with {@link ACTION_DOWN} for the given key codes without sending
|
||
* a {@link KeyEvent} with {@link ACTION_UP}.
|
||
*/
|
||
public void pressAndHoldKeyCode(int keyCode, int metaState) {
|
||
injectEvent(createKeyEvent(keyCode, metaState, true));
|
||
}
|
||
|
||
|
||
/**
|
||
* Sends a {@link KeyEvent} with {@link ACTION_UP} for the given key codes.
|
||
*/
|
||
public void unpressKeyCode(int keyCode, int metaState) {
|
||
injectEvent(createKeyEvent(keyCode, metaState, false));
|
||
}
|
||
|
||
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.0 * (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.0 * (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.DONT_EXPECT_PILFER);
|
||
try {
|
||
expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
|
||
final UiObject2 result = waitForLauncherObject(resName);
|
||
return result;
|
||
} finally {
|
||
sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
|
||
GestureScope.DONT_EXPECT_PILFER);
|
||
}
|
||
}
|
||
|
||
@NonNull
|
||
UiObject2 rightClickAndGet(
|
||
@NonNull final UiObject2 target, @NonNull String resName, Pattern rightClickEvent) {
|
||
final Point targetCenter = target.getVisibleCenter();
|
||
final long downTime = SystemClock.uptimeMillis();
|
||
sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter,
|
||
GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE,
|
||
/* isRightClick= */ true);
|
||
try {
|
||
expectEvent(TestProtocol.SEQUENCE_MAIN, rightClickEvent);
|
||
final UiObject2 result = waitForLauncherObject(resName);
|
||
return result;
|
||
} finally {
|
||
sendPointer(downTime, SystemClock.uptimeMillis(), ACTION_UP, targetCenter,
|
||
GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE,
|
||
/* isRightClick= */ true);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/** Returns the bounds of the display as a Point where x is width and y is height. */
|
||
Point getRealDisplaySize() {
|
||
final Rect displayBounds = getContext().getSystemService(WindowManager.class)
|
||
.getMaximumWindowMetrics()
|
||
.getBounds();
|
||
return new Point(displayBounds.width(), displayBounds.height());
|
||
}
|
||
|
||
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 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;
|
||
}
|
||
|
||
/** Reinitializes the workspace to its default layout. */
|
||
public void reinitializeLauncherData() {
|
||
getTestInfo(TestProtocol.REQUEST_REINITIALIZE_DATA);
|
||
}
|
||
|
||
/** Clears the workspace, leaving it empty. */
|
||
public void clearLauncherData() {
|
||
getTestInfo(TestProtocol.REQUEST_CLEAR_DATA);
|
||
}
|
||
|
||
/** Shows the taskbar if it is hidden, otherwise does nothing. */
|
||
public void showTaskbarIfHidden() {
|
||
getTestInfo(TestProtocol.REQUEST_UNSTASH_TASKBAR_IF_STASHED);
|
||
}
|
||
|
||
/** Shows the bubble bar if it is stashed, otherwise this does nothing. */
|
||
public void showBubbleBarIfHidden() {
|
||
getTestInfo(TestProtocol.REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED);
|
||
}
|
||
|
||
/** Blocks the taskbar from automatically stashing based on time. */
|
||
public void enableBlockTimeout(boolean enable) {
|
||
getTestInfo(enable
|
||
? TestProtocol.REQUEST_ENABLE_BLOCK_TIMEOUT
|
||
: TestProtocol.REQUEST_DISABLE_BLOCK_TIMEOUT);
|
||
}
|
||
|
||
public boolean isTransientTaskbar() {
|
||
return getTestInfo(TestProtocol.REQUEST_IS_TRANSIENT_TASKBAR)
|
||
.getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
/** Enables transient taskbar for testing purposes only. */
|
||
public void enableTransientTaskbar(boolean enable) {
|
||
getTestInfo(enable
|
||
? TestProtocol.REQUEST_ENABLE_TRANSIENT_TASKBAR
|
||
: TestProtocol.REQUEST_DISABLE_TRANSIENT_TASKBAR);
|
||
}
|
||
|
||
/**
|
||
* Recreates the taskbar (outside of tests this is done for certain configuration changes).
|
||
* The expected behavior is that the taskbar retains its current state after being recreated.
|
||
* For example, if taskbar is currently stashed, it should still be stashed after recreating.
|
||
*/
|
||
public void recreateTaskbar() {
|
||
getTestInfo(TestProtocol.REQUEST_RECREATE_TASKBAR);
|
||
}
|
||
|
||
// TODO(b/270393900): Remove with ENABLE_ALL_APPS_SEARCH_IN_TASKBAR flag cleanup.
|
||
|
||
/** Refreshes the known overview target in TIS. */
|
||
public void refreshOverviewTarget() {
|
||
getTestInfo(TestProtocol.REQUEST_REFRESH_OVERVIEW_TARGET);
|
||
}
|
||
|
||
public List<String> getHotseatIconNames() {
|
||
return getTestInfo(TestProtocol.REQUEST_HOTSEAT_ICON_NAMES)
|
||
.getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
private String[] getActivities() {
|
||
return getTestInfo(TestProtocol.REQUEST_GET_ACTIVITIES)
|
||
.getStringArray(TestProtocol.TEST_INFO_RESPONSE_FIELD);
|
||
}
|
||
|
||
public String getRootedActivitiesList() {
|
||
return String.join(", ", getActivities());
|
||
}
|
||
|
||
/** Returns whether no leaked activities are detected. */
|
||
public boolean noLeakedActivities(boolean requireOneActiveActivity) {
|
||
final String[] activities = getActivities();
|
||
|
||
for (String activity : activities) {
|
||
if (activity.contains("(destroyed)")) {
|
||
return false;
|
||
}
|
||
}
|
||
return activities.length <= (requireOneActiveActivity ? 1 : 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();
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/** Returns whether the Launcher is a Launcher3 one */
|
||
public 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);
|
||
}
|
||
|
||
/**
|
||
* Taps outside container to dismiss, centered vertically and halfway to the edge of the screen.
|
||
*
|
||
* @param container container to be dismissed
|
||
* @param tapRight tap on the right of the container if true, or left otherwise
|
||
*/
|
||
void touchOutsideContainer(UiObject2 container, boolean tapRight) {
|
||
touchOutsideContainer(container, tapRight, true);
|
||
}
|
||
|
||
/**
|
||
* Taps outside the container, to the right or left, and centered vertically.
|
||
*
|
||
* @param tapRight if true touches to the right of the container, otherwise touches on left
|
||
* @param halfwayToEdge if true touches halfway to the screen edge, if false touches 1 px from
|
||
* container
|
||
*/
|
||
void touchOutsideContainer(UiObject2 container, boolean tapRight, boolean halfwayToEdge) {
|
||
try (LauncherInstrumentation.Closable c = addContextLayer(
|
||
"want to tap outside container on the " + (tapRight ? "right" : "left"))) {
|
||
Rect containerBounds = getVisibleBounds(container);
|
||
|
||
int x;
|
||
if (halfwayToEdge) {
|
||
x = tapRight
|
||
? (containerBounds.right + getRealDisplaySize().x) / 2
|
||
: containerBounds.left / 2;
|
||
} else {
|
||
x = tapRight
|
||
? containerBounds.right + 1
|
||
: containerBounds.left - 1;
|
||
}
|
||
// If IME is visible and overlaps the container bounds, touch above it.
|
||
final Insets systemGestureRegion = getSystemGestureRegion();
|
||
int bottomBound = Math.min(
|
||
containerBounds.bottom,
|
||
getRealDisplaySize().y - systemGestureRegion.bottom);
|
||
int y = (bottomBound - containerBounds.top) / 2;
|
||
// Do not tap in the status bar.
|
||
y = Math.max(y, systemGestureRegion.top);
|
||
|
||
final long downTime = SystemClock.uptimeMillis();
|
||
final Point tapTarget = new Point(x, y);
|
||
sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, tapTarget,
|
||
LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
|
||
sendPointer(downTime, downTime, MotionEvent.ACTION_UP, tapTarget,
|
||
LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Waits until a particular condition is true. Based on WaitMixin.
|
||
*/
|
||
boolean waitAndGet(BooleanSupplier condition, long timeout, long interval) {
|
||
long startTime = SystemClock.uptimeMillis();
|
||
|
||
boolean result = condition.getAsBoolean();
|
||
for (long elapsedTime = 0; !result; elapsedTime = SystemClock.uptimeMillis() - startTime) {
|
||
if (elapsedTime >= timeout) {
|
||
break;
|
||
}
|
||
SystemClock.sleep(interval);
|
||
result = condition.getAsBoolean();
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/** Executes a runnable and waits for the wallpaper-open animation completion. */
|
||
public void executeAndWaitForWallpaperAnimation(Runnable r, String actionName) {
|
||
executeAndWaitForLauncherEvent(
|
||
() -> r.run(),
|
||
event -> TestProtocol.WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE
|
||
.equals(event.getClassName().toString()),
|
||
() -> "Didn't detect finishing wallpaper-open animation",
|
||
actionName);
|
||
}
|
||
}
|