/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.launcher3.ui; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static androidx.test.InstrumentationRegistry.getInstrumentation; import android.app.Instrumentation; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherActivityInfo; import android.graphics.Point; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; import android.view.Surface; import androidx.test.InstrumentationRegistry; import androidx.test.uiautomator.By; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.Direction; import androidx.test.uiautomator.UiDevice; import androidx.test.uiautomator.UiObject2; import androidx.test.uiautomator.Until; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherState; import com.android.launcher3.MainThreadExecutor; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.testcomponent.AppWidgetNoConfig; import com.android.launcher3.testcomponent.AppWidgetWithConfig; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.LauncherActivityRule; import com.android.launcher3.util.rule.ShellCommandRule; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.runners.model.Statement; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; /** * Base class for all instrumentation tests providing various utility methods. */ public abstract class AbstractLauncherUiTest { public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10); public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5; public static final long SHORT_UI_TIMEOUT= 300; public static final long DEFAULT_UI_TIMEOUT = 10000; public static final long DEFAULT_WORKER_TIMEOUT_SECS = 5; protected MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); protected final UiDevice mDevice; protected final LauncherInstrumentation mLauncher; protected Context mTargetContext; protected String mTargetPackage; private static final String TAG = "AbstractLauncherUiTest"; protected AbstractLauncherUiTest() { final Instrumentation instrumentation = getInstrumentation(); mDevice = UiDevice.getInstance(instrumentation); try { mDevice.setOrientationNatural(); } catch (RemoteException e) { throw new RuntimeException(e); } if (TestHelpers.isInLauncherProcess()) Utilities.enableRunningInTestHarnessForTests(); mLauncher = new LauncherInstrumentation(instrumentation); } @Rule public LauncherActivityRule mActivityMonitor = new LauncherActivityRule(); @Rule public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher(); @Rule public ShellCommandRule mDisableHeadsUpNotification = ShellCommandRule.disableHeadsUpNotification(); // Annotation for tests that need to be run in portrait and landscape modes. @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) protected @interface PortraitLandscape { } @Rule public TestRule mPortraitLandscapeExecutor = (base, description) -> false && description.getAnnotation(PortraitLandscape.class) != null ? new Statement() { @Override public void evaluate() throws Throwable { try { // Create launcher activity if necessary and bring it to the front. mDevice.pressHome(); waitForLauncherCondition(launcher -> launcher != null); executeOnLauncher(launcher -> launcher.getRotationHelper().forceAllowRotationForTesting(true)); evaluateInPortrait(); evaluateInLandscape(); } finally { mDevice.setOrientationNatural(); executeOnLauncher(launcher -> launcher.getRotationHelper().forceAllowRotationForTesting(false)); mLauncher.setExpectedRotation(Surface.ROTATION_0); } } private void evaluateInPortrait() throws Throwable { mDevice.setOrientationNatural(); mLauncher.setExpectedRotation(Surface.ROTATION_0); base.evaluate(); } private void evaluateInLandscape() throws Throwable { mDevice.setOrientationLeft(); mLauncher.setExpectedRotation(Surface.ROTATION_90); base.evaluate(); } } : base; @Before public void setUp() throws Exception { mTargetContext = InstrumentationRegistry.getTargetContext(); mTargetPackage = mTargetContext.getPackageName(); } @After public void tearDown() throws Exception { // Limits UI tests affecting tests running after them. resetLoaderState(); } protected void lockRotation(boolean naturalOrientation) throws RemoteException { if (naturalOrientation) { mDevice.setOrientationNatural(); } else { mDevice.setOrientationRight(); } } /** * Opens all apps and returns the recycler view */ protected UiObject2 openAllApps() { mDevice.waitForIdle(); UiObject2 hotseat = mDevice.wait( Until.findObject(getSelectorForId(R.id.hotseat)), 2500); Point start = hotseat.getVisibleCenter(); int endY = (int) (mDevice.getDisplayHeight() * 0.1f); // 100 px/step mDevice.swipe(start.x, start.y, start.x, endY, (start.y - endY) / 100); return findViewById(R.id.apps_list_view); } /** * Opens widget tray and returns the recycler view. */ protected UiObject2 openWidgetsTray() { mDevice.pressMenu(); // Enter overview mode. mDevice.wait(Until.findObject( By.text(mTargetContext.getString(R.string.widget_button_text))), DEFAULT_UI_TIMEOUT).click(); return findViewById(R.id.widgets_list_view); } /** * Scrolls the {@param container} until it finds an object matching {@param condition}. * @return the matching object. */ protected UiObject2 scrollAndFind(UiObject2 container, BySelector condition) { container.setGestureMargins(0, 0, 0, 200); int i = 0; for (; ; ) { // findObject can only execute after spring settles. mDevice.wait(Until.findObject(condition), SHORT_UI_TIMEOUT); UiObject2 widget = container.findObject(condition); if (widget != null && widget.getVisibleBounds().intersects( 0, 0, mDevice.getDisplayWidth(), mDevice.getDisplayHeight() - 200)) { return widget; } if (++i > 40) fail("Too many attempts"); container.scroll(Direction.DOWN, 1f); } } /** * Drags an icon to the center of homescreen. * @param icon object that is either app icon or shortcut icon */ protected void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) { Point center = icon.getVisibleCenter(); // Action Down sendPointer(MotionEvent.ACTION_DOWN, center); UiObject2 dragLayer = findViewById(R.id.drag_layer); if (expectedToShowShortcuts) { // Make sure shortcuts show up, and then move a bit to hide them. assertNotNull(findViewById(R.id.deep_shortcuts_container)); Point moveLocation = new Point(center); int distanceToMove = mTargetContext.getResources().getDimensionPixelSize( R.dimen.deep_shortcuts_start_drag_threshold) + 50; if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) { moveLocation.y -= distanceToMove; } else { moveLocation.y += distanceToMove; } movePointer(center, moveLocation); assertNull(findViewById(R.id.deep_shortcuts_container)); } // Wait until Remove/Delete target is visible assertNotNull(findViewById(R.id.delete_target_text)); Point moveLocation = dragLayer.getVisibleCenter(); // Move to center movePointer(center, moveLocation); sendPointer(MotionEvent.ACTION_UP, center); // Wait until remove target is gone. mDevice.wait(Until.gone(getSelectorForId(R.id.delete_target_text)), DEFAULT_UI_TIMEOUT); } private void movePointer(Point from, Point to) { while(!from.equals(to)) { from.x = getNextMoveValue(to.x, from.x); from.y = getNextMoveValue(to.y, from.y); sendPointer(MotionEvent.ACTION_MOVE, from); } } private int getNextMoveValue(int targetValue, int oldValue) { if (targetValue - oldValue > 10) { return oldValue + 10; } else if (targetValue - oldValue < -10) { return oldValue - 10; } else { return targetValue; } } protected void sendPointer(int action, Point point) { MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), action, point.x, point.y, 0); getInstrumentation().sendPointerSync(event); event.recycle(); } /** * Removes all icons from homescreen and hotseat. */ public void clearHomescreen() throws Throwable { LauncherSettings.Settings.call(mTargetContext.getContentResolver(), LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); LauncherSettings.Settings.call(mTargetContext.getContentResolver(), LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG); resetLoaderState(); } protected void resetLoaderState() { if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { android.util.Log.d("b/117332845", "START " + android.util.Log.getStackTraceString(new Throwable())); } try { mMainThreadExecutor.execute(new Runnable() { @Override public void run() { LauncherAppState.getInstance(mTargetContext).getModel().forceReload(); } }); } catch (Throwable t) { throw new IllegalArgumentException(t); } waitForLauncherCondition(launcher -> LauncherAppState.getInstance(mTargetContext).getModel().isModelLoaded()); if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { android.util.Log.d("b/117332845", "FINISH " + android.util.Log.getStackTraceString(new Throwable())); } } /** * Runs the callback on the UI thread and returns the result. */ protected T getOnUiThread(final Callable callback) { try { return mMainThreadExecutor.submit(callback).get(); } catch (Exception e) { throw new RuntimeException(e); } } protected T getFromLauncher(Function f) { if (!TestHelpers.isInLauncherProcess()) return null; return getOnUiThread(() -> f.apply(mActivityMonitor.getActivity())); } protected void executeOnLauncher(Consumer f) { getFromLauncher(launcher -> { f.accept(launcher); return null; }); } // Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call expecting // the results of that gesture because the wait can hide flakeness. protected boolean waitForState(LauncherState state) { return waitForLauncherCondition(launcher -> launcher.getStateManager().getState() == state); } // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide // flakiness. protected boolean waitForLauncherCondition(Function condition) { return waitForLauncherCondition(condition, DEFAULT_ACTIVITY_TIMEOUT); } // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide // flakiness. protected boolean waitForLauncherCondition( Function condition, long timeout) { if (!TestHelpers.isInLauncherProcess()) return true; return Wait.atMost(() -> getFromLauncher(condition), timeout); } /** * Finds a widget provider which can fit on the home screen. * @param hasConfigureScreen if true, a provider with a config screen is returned. */ protected LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) { LauncherAppWidgetProviderInfo info = getOnUiThread(new Callable() { @Override public LauncherAppWidgetProviderInfo call() throws Exception { ComponentName cn = new ComponentName(getInstrumentation().getContext(), hasConfigureScreen ? AppWidgetWithConfig.class : AppWidgetNoConfig.class); Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString()); return AppWidgetManagerCompat.getInstance(mTargetContext) .findProvider(cn, Process.myUserHandle()); } }); if (info == null) { throw new IllegalArgumentException("No valid widget provider"); } return info; } protected UiObject2 findViewById(int id) { return mDevice.wait(Until.findObject(getSelectorForId(id)), DEFAULT_UI_TIMEOUT); } protected BySelector getSelectorForId(int id) { String name = mTargetContext.getResources().getResourceEntryName(id); return By.res(mTargetPackage, name); } protected LauncherActivityInfo getSettingsApp() { return LauncherAppsCompat.getInstance(mTargetContext) .getActivityList("com.android.settings", Process.myUserHandle()).get(0); } protected LauncherActivityInfo getChromeApp() { return LauncherAppsCompat.getInstance(mTargetContext) .getActivityList("com.android.chrome", Process.myUserHandle()).get(0); } /** * Broadcast receiver which blocks until the result is received. */ public class BlockingBroadcastReceiver extends BroadcastReceiver { private final CountDownLatch latch = new CountDownLatch(1); private Intent mIntent; public BlockingBroadcastReceiver(String action) { mTargetContext.registerReceiver(this, new IntentFilter(action)); } @Override public void onReceive(Context context, Intent intent) { mIntent = intent; latch.countDown(); } public Intent blockingGetIntent() throws InterruptedException { latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS); mTargetContext.unregisterReceiver(this); return mIntent; } public Intent blockingGetExtraIntent() throws InterruptedException { Intent intent = blockingGetIntent(); return intent == null ? null : (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT); } } }