Files
lawnchair/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java

424 lines
16 KiB
Java
Raw Normal View History

/*
* 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 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.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.LauncherAppsCompat;
import com.android.launcher3.tapl.LauncherInstrumentation;
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 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 =TestHelpers.getInstrumentation();
mDevice = UiDevice.getInstance(instrumentation);
try {
mDevice.setOrientationNatural();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
mLauncher = new LauncherInstrumentation(instrumentation);
}
@Rule
public LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
// 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();
mDevice.executeShellCommand("settings put global heads_up_notifications_enabled 0");
}
@After
public void tearDown() throws Exception {
mDevice.executeShellCommand("settings put global heads_up_notifications_enabled 1");
}
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);
TestHelpers.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() {
try {
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
LauncherAppState.getInstance(mTargetContext).getModel().forceReload();
}
});
} catch (Throwable t) {
throw new IllegalArgumentException(t);
}
}
/**
* Runs the callback on the UI thread and returns the result.
*/
protected <T> T getOnUiThread(final Callable<T> callback) {
try {
return mMainThreadExecutor.submit(callback).get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected <T> T getFromLauncher(Function<Launcher, T> f) {
if (!TestHelpers.isInLauncherProcess()) return null;
return getOnUiThread(() -> f.apply(mActivityMonitor.getActivity()));
}
protected void executeOnLauncher(Consumer<Launcher> 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<Launcher, Boolean> 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<Launcher, Boolean> 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<LauncherAppWidgetProviderInfo>() {
@Override
public LauncherAppWidgetProviderInfo call() throws Exception {
ComponentName cn = new ComponentName(TestHelpers.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);
}
}
}