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