com.tascape.qa.th.android.driver.UiAutomatorDevice Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2015 - 2016 Nebula Bay.
*
* 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.tascape.qa.th.android.driver;
import com.android.uiautomator.stub.IUiCollection;
import com.android.uiautomator.stub.IUiDevice;
import com.android.uiautomator.stub.IUiObject;
import com.android.uiautomator.stub.IUiScrollable;
import com.android.uiautomator.stub.Point;
import com.android.uiautomator.stub.UiSelector;
import com.android.uiautomator.stub.UiWatcher;
import com.google.common.collect.Lists;
import com.tascape.qa.th.Utils;
import com.tascape.qa.th.android.comm.Adb;
import com.tascape.qa.th.android.model.UIA;
import com.tascape.qa.th.android.model.UIAException;
import com.tascape.qa.th.android.model.WindowHierarchy;
import com.tascape.qa.th.exception.EntityCommunicationException;
import java.awt.Dimension;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import net.sf.lipermi.exception.LipeRMIException;
import net.sf.lipermi.handler.CallHandler;
import net.sf.lipermi.net.Client;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author linsong wang
*/
public class UiAutomatorDevice extends AdbDevice implements IUiDevice {
private static final Logger LOG = LoggerFactory.getLogger(UiAutomatorDevice.class);
private static final long serialVersionUID = 1L;
public static final String UIA_SERVER_JAR = "uia-server.jar";
public static final String UIA_BUNDLE_JAR = "bundle.jar";
private static final String UIA_SERVER_PATH;
private static final String UIA_BUNDLE_PATH;
public static final long WAIT_FOR_EXISTS = 30000;
private static final AtomicInteger LOCAL_RMI_PORT = new AtomicInteger(IUiDevice.UIAUTOMATOR_RMI_PORT + 10000);
private static final List DEVICES = new ArrayList<>();
static {
try {
File server = Paths.get(File.createTempFile("uias", ".jar").getParent(), UIA_SERVER_JAR).toFile();
File bundle = Paths.get(File.createTempFile("uias", ".jar").getParent(), UIA_BUNDLE_JAR).toFile();
UIA_SERVER_PATH = server.getAbsolutePath();
UIA_BUNDLE_PATH = bundle.getAbsolutePath();
LOG.debug("uia server {}", UIA_SERVER_PATH);
LOG.debug("uia bundle {}", UIA_BUNDLE_PATH);
server.createNewFile();
bundle.createNewFile();
OutputStream out = new FileOutputStream(server);
IOUtils.copy(UiAutomatorDevice.class.getResourceAsStream("/uias/" + UIA_SERVER_JAR), out);
out = new FileOutputStream(bundle);
IOUtils.copy(UiAutomatorDevice.class.getResourceAsStream("/uias/" + UIA_BUNDLE_JAR), out);
} catch (IOException ex) {
throw new RuntimeException("Cannot get uia server/bundle jar files", ex);
}
}
public static synchronized List getAllDevices() {
if (DEVICES.isEmpty()) {
Adb.getSerialProduct().entrySet().stream().forEach((serial) -> {
try {
Adb adb = new Adb(serial.getKey());
UiAutomatorDevice device = new UiAutomatorDevice();
device.setAdb(adb);
device.setProductDetail(serial.getValue());
DEVICES.add(device);
} catch (IOException | EntityCommunicationException ex) {
LOG.warn("Cannnot debug device {}", serial, ex);
}
});
if (DEVICES.isEmpty()) {
throw new UIAException("Cannot debug any attached device");
}
}
return DEVICES;
}
private String productDetail;
private ExecuteWatchdog uiautomatorDog;
private Client client;
private IUiDevice uiDevice;
private IUiObject uiObject;
private IUiCollection uiCollection;
private IUiScrollable uiScrollable;
private final Set apps = new HashSet<>();
private final Dimension screenDimension = new Dimension(0, 0);
public void start() throws IOException, InterruptedException {
uiautomatorDog = this.setupUiAutomatorRmiServer();
int port = this.setupRmiPortForward();
CallHandler callHandler = new CallHandler();
this.client = new Client("localhost", port, callHandler);
this.uiDevice = IUiDevice.class.cast(client.getGlobal(IUiDevice.class));
this.uiObject = IUiObject.class.cast(client.getGlobal(IUiObject.class));
this.uiCollection = IUiCollection.class.cast(client.getGlobal(IUiCollection.class));
this.uiScrollable = IUiScrollable.class.cast(client.getGlobal(IUiScrollable.class));
LOG.debug("Device product name '{}'", this.uiDevice.getProductName());
screenDimension.width = uiDevice.getDisplayWidth();
screenDimension.height = uiDevice.getDisplayHeight();
LOG.debug("Device screen dimension '{}'", screenDimension);
apps.forEach(app -> app.fetchUiaStubs());
}
public void stop() {
try {
this.killUiAutomatorProcess();
} catch (IOException ex) {
LOG.warn("{}", ex.getMessage());
}
if (uiautomatorDog != null) {
uiautomatorDog.destroyProcess();
}
try {
client.close();
} catch (IOException ex) {
LOG.warn("{}", ex.getMessage());
}
}
public void install(App app) {
app.setDevice(this);
apps.add(app);
app.fetchUiaStubs();
}
@Override
public String getName() {
return UiAutomatorDevice.class.getSimpleName();
}
/**
* Throws UnsupportedOperationException.
*/
@Override
public void reset() {
throw new UnsupportedOperationException();
}
public IUiDevice getUiDevice() {
return uiDevice;
}
public IUiObject getUiObject() {
return uiObject;
}
public IUiCollection getUiCollection() {
return uiCollection;
}
public IUiScrollable getUiScrollable() {
return uiScrollable;
}
public UiAutomatorDevice install(String apkPath) throws IOException, InterruptedException {
this.backToHome();
ExecuteWatchdog dog = this.getAdb().adbAsync(Lists.newArrayList("install", "-rg", apkPath), 60000);
Utils.sleep(10000, "wait for app push");
this.takeDeviceScreenshot();
String pkg = uiDevice.getCurrentPackageName();
if (pkg.equals("com.android.packageinstaller")) {
Utils.sleep(10000, "wait for allow");
if (this.resourceIdExists("android:id/button1")) {
this.clickByResourceId("android:id/button1");
}
}
pkg = uiDevice.getCurrentPackageName();
if (pkg.equals("com.android.packageinstaller")) {
Utils.sleep(10000, "wait for OK");
if (this.resourceIdExists("com.android.packageinstaller:id/ok_button")) {
this.clickByResourceId("com.android.packageinstaller:id/ok_button");
}
}
if (dog.isWatching()) {
dog.killedProcess();
}
return this;
}
public Dimension getScreenDimension() {
return screenDimension;
}
public UiAutomatorDevice home() {
pressHome();
return this;
}
public UiAutomatorDevice back() {
pressBack();
return this;
}
public UiAutomatorDevice enter() {
pressEnter();
return this;
}
public UiAutomatorDevice backToHome() {
int i = 0;
while (pressBack() && i++ < 10) {
}
i = 0;
while (pressHome() && i++ < 5) {
}
return this;
}
/**
* Drags screen vertically from center.
*
* @param size positive to drag down, negative to drag up
*
* @return this
*/
public UiAutomatorDevice dragVertically(int size) {
LOG.debug("drag, from center, vertically");
Dimension dimension = this.getScreenDimension();
this.swipe(dimension.width / 2, dimension.height / 2, dimension.width / 2, dimension.height / 2 + size,
Math.abs(size / 20 + 5));
return this;
}
/**
* Drags screen horizontally from center.
*
* @param size positive to drag right, negative to drag left
*
* @return this
*/
public UiAutomatorDevice dragHorizontally(int size) {
LOG.debug("drag, from center, horizontally");
Dimension dimension = this.getScreenDimension();
this.swipe(dimension.width / 2, dimension.height / 2, dimension.width / 2 + size, dimension.height / 2,
Math.abs(size / 20 + 5));
return this;
}
public UiAutomatorDevice dragHalfScreenUp() {
LOG.debug("drag, from center, half screen up");
Dimension dimension = this.getScreenDimension();
this.swipe(dimension.width / 2, dimension.height / 2, dimension.width / 2, 0, 38);
return this;
}
public UiAutomatorDevice dragHalfScreenDown() {
LOG.debug("drag, from center, half screen down");
Dimension dimension = this.getScreenDimension();
this.swipe(dimension.width / 2, dimension.height / 2, dimension.width / 2, dimension.height, 38);
return this;
}
public boolean resourceIdExists(String resouceId) {
LOG.debug("look for {}", resouceId);
uiObject.useUiObjectSelector(new UiSelector().resourceId(resouceId));
return uiObject.exists();
}
public boolean textExists(String text) {
LOG.debug("look for {}", text);
uiObject.useUiObjectSelector(new UiSelector().text(text));
return uiObject.exists();
}
public boolean descriptionExists(String text) {
LOG.debug("look for {}", text);
uiObject.useUiObjectSelector(new UiSelector().description(text));
return uiObject.exists();
}
public boolean waitForResourceId(String resouceId) {
LOG.debug("wait {} for {} ms", resouceId, WAIT_FOR_EXISTS);
uiObject.useUiObjectSelector(new UiSelector().resourceId(resouceId));
uiObject.waitForExists(WAIT_FOR_EXISTS);
return uiObject.exists();
}
public boolean waitForText(String text) {
LOG.debug("wait {} for {} ms", text, WAIT_FOR_EXISTS);
uiObject.useUiObjectSelector(new UiSelector().text(text));
uiObject.waitForExists(WAIT_FOR_EXISTS);
return uiObject.exists();
}
public boolean waitForTextContains(String text) {
LOG.debug("wait {} for {} ms", text, WAIT_FOR_EXISTS);
uiObject.useUiObjectSelector(new UiSelector().textContains(text));
uiObject.waitForExists(WAIT_FOR_EXISTS);
return uiObject.exists();
}
public void clickByResourceId(String resouceId) {
LOG.debug("click {}", resouceId);
uiObject.useUiObjectSelector(new UiSelector().resourceId(resouceId));
uiObject.click();
uiDevice.waitForIdle();
}
public UiAutomatorDevice clickByText(String text) {
LOG.debug("click {}", text);
uiObject.useUiObjectSelector(new UiSelector().text(text));
uiObject.click();
uiDevice.waitForIdle();
return this;
}
public UiAutomatorDevice clickByTextContains(String text) {
LOG.debug("click {}", text);
uiObject.useUiObjectSelector(new UiSelector().textContains(text));
uiObject.click();
uiDevice.waitForIdle();
return this;
}
public UiAutomatorDevice clickByDescription(String text) {
LOG.debug("click {}", text);
uiObject.useUiObjectSelector(new UiSelector().description(text));
uiObject.click();
uiDevice.waitForIdle();
return this;
}
public UiAutomatorDevice clearTextByResourceId(String resouceId) {
LOG.debug("clear {}", resouceId);
uiObject.useUiObjectSelector(new UiSelector().resourceId(resouceId));
uiObject.clearTextField();
String text = uiObject.getText();
if (text.isEmpty()) {
return this;
}
uiObject.clickBottomRight();
for (int i = 0; i < text.length(); i++) {
uiDevice.pressDPadRight();
}
for (int i = 0; i < text.length(); i++) {
uiDevice.pressDelete();
}
return this;
}
public UiAutomatorDevice setTextByResourceId(String resouceId, String text) {
LOG.debug("type {} into {}", text, resouceId);
uiObject.useUiObjectSelector(new UiSelector().resourceId(resouceId));
uiObject.setText(text);
uiObject.click();
this.back();
return this;
}
public String getTextByResourceId(String resouceId) {
if (resourceIdExists(resouceId)) {
uiObject.useUiObjectSelector(new UiSelector().resourceId(resouceId));
return uiObject.getText();
}
return null;
}
public File takeDeviceScreenshot() throws IOException {
try {
return ss();
} catch (ExecuteException ex) {
LOG.warn(ex.getMessage());
try {
Utils.sleep(5000, "wait for device");
} catch (InterruptedException ex1) {
LOG.warn(ex.getMessage());
}
return ss();
}
}
/**
* Loads window hierarchy as an in-memory node tree.
*
* @return UI view hierarchy node tree
*
* @throws Exception cannot dump window hierarchy
*/
public WindowHierarchy loadWindowHierarchy() throws Exception {
try {
return wh();
} catch (Exception ex) {
Utils.sleep(1000, "wait to retry");
return wh();
}
}
@Override
public void clearLastTraversedText() {
this.uiDevice.clearLastTraversedText();
}
@Override
public boolean click(int x, int y) {
LOG.debug("click {}, {}", x, y);
boolean ok = this.uiDevice.click(x, y);
this.waitForIdle();
return ok;
}
@Override
public void dumpWindowHierarchy(String fileName) {
this.uiDevice.dumpWindowHierarchy(fileName);
}
@Override
public void freezeRotation() throws LipeRMIException {
this.uiDevice.freezeRotation();
}
@Override
public String getCurrentActivityName() {
return this.uiDevice.getCurrentActivityName();
}
@Override
public String getCurrentPackageName() {
return this.uiDevice.getCurrentPackageName();
}
@Override
public int getDisplayHeight() {
return this.uiDevice.getDisplayHeight();
}
@Override
public int getDisplayRotation() {
return this.uiDevice.getDisplayRotation();
}
@Override
public Point getDisplaySizeDp() {
return this.uiDevice.getDisplaySizeDp();
}
@Override
public int getDisplayWidthDp() {
return this.uiDevice.getDisplayWidthDp();
}
@Override
public int getDisplayHeightDp() {
return this.uiDevice.getDisplayHeightDp();
}
@Override
public int getDisplayWidth() {
return this.uiDevice.getDisplayWidth();
}
@Override
public String getLastTraversedText() {
return this.uiDevice.getLastTraversedText();
}
@Override
public String getProductName() {
return this.uiDevice.getProductName();
}
@Override
public boolean hasAnyWatcherTriggered() {
return this.uiDevice.hasAnyWatcherTriggered();
}
@Override
public boolean hasWatcherTriggered(String watcherName) {
return this.uiDevice.hasWatcherTriggered(watcherName);
}
@Override
public boolean isNaturalOrientation() {
return this.uiDevice.isNaturalOrientation();
}
@Override
public boolean isScreenOn() throws LipeRMIException {
return this.uiDevice.isScreenOn();
}
@Override
public boolean pressBack() {
LOG.debug("press back");
boolean ok = this.uiDevice.pressBack();
this.waitForIdle();
return ok;
}
@Override
public boolean pressDPadCenter() {
return this.uiDevice.pressDPadCenter();
}
@Override
public boolean pressDPadDown() {
return this.uiDevice.pressDPadDown();
}
@Override
public boolean pressDPadLeft() {
return this.uiDevice.pressDPadLeft();
}
@Override
public boolean pressDPadRight() {
return this.uiDevice.pressDPadRight();
}
@Override
public boolean pressDPadUp() {
return this.uiDevice.pressDPadUp();
}
@Override
public boolean pressDelete() {
LOG.debug("press delete");
boolean ok = this.uiDevice.pressDelete();
this.waitForIdle();
return ok;
}
@Override
public boolean pressEnter() {
LOG.debug("press enter");
boolean ok = this.uiDevice.pressEnter();
this.waitForIdle();
return ok;
}
@Override
public boolean pressHome() {
LOG.debug("press home");
boolean ok = this.uiDevice.pressHome();
this.waitForIdle();
return ok;
}
@Override
public boolean pressKeyCode(int keyCode) {
LOG.debug("press key {}", keyCode);
boolean ok = this.uiDevice.pressKeyCode(keyCode);
this.waitForIdle();
return ok;
}
@Override
public boolean pressKeyCode(int keyCode, int metaState) {
return this.uiDevice.pressKeyCode(keyCode, metaState);
}
@Override
public boolean pressMenu() {
LOG.debug("press menu");
boolean ok = this.uiDevice.pressMenu();
this.waitForIdle();
return ok;
}
@Override
public boolean pressRecentApps() throws LipeRMIException {
LOG.debug("press recent apps");
boolean ok = this.uiDevice.pressRecentApps();
this.waitForIdle();
return ok;
}
@Override
public boolean pressSearch() {
LOG.debug("press search");
boolean ok = this.uiDevice.pressSearch();
this.waitForIdle();
return ok;
}
@Override
public void registerWatcher(String name, UiWatcher watcher) {
this.uiDevice.registerWatcher(name, watcher);
}
@Override
public void removeWatcher(String name) {
this.uiDevice.removeWatcher(name);
}
@Override
public void resetWatcherTriggers() {
this.uiDevice.resetWatcherTriggers();
}
@Override
public void runWatchers() {
this.uiDevice.runWatchers();
}
@Override
public void setOrientationLeft() throws LipeRMIException {
this.uiDevice.setOrientationLeft();
}
@Override
public void setOrientationNatural() throws LipeRMIException {
this.uiDevice.setOrientationNatural();
}
@Override
public void setOrientationRight() throws LipeRMIException {
this.uiDevice.setOrientationRight();
}
@Override
public void sleep() throws LipeRMIException {
LOG.debug("put device to sleep");
this.uiDevice.sleep();
}
@Override
public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
boolean ok = this.uiDevice.swipe(startX, startY, endX, endY, steps);
this.waitForIdle();
return ok;
}
@Override
public boolean swipe(Point[] segments, int segmentSteps) {
boolean ok = this.uiDevice.swipe(segments, segmentSteps);
this.waitForIdle();
return ok;
}
@Override
public boolean takeScreenshot(String name) {
return this.uiDevice.takeScreenshot(name);
}
@Override
public boolean takeScreenshot(String name, float scale, int quality) {
return this.uiDevice.takeScreenshot(name, scale, quality);
}
@Override
public void unfreezeRotation() throws LipeRMIException {
this.uiDevice.unfreezeRotation();
}
@Override
public void waitForIdle() {
this.uiDevice.waitForIdle();
}
@Override
public void waitForIdle(long time) {
this.uiDevice.waitForIdle(time);
}
@Override
public boolean waitForWindowUpdate(String packageName, long timeout) {
return this.uiDevice.waitForWindowUpdate(packageName, timeout);
}
@Override
public void wakeUp() throws LipeRMIException {
this.uiDevice.wakeUp();
this.waitForIdle();
}
@Override
public void setCompressedLayoutHeirarchy(boolean compressed) {
uiDevice.setCompressedLayoutHeirarchy(compressed);
}
@Override
public boolean openNotification() {
LOG.debug("open notification");
return uiDevice.openNotification();
}
@Override
public boolean openQuickSettings() {
LOG.debug("open quick settings");
return uiDevice.openQuickSettings();
}
@Override
public boolean drag(int startX, int startY, int endX, int endY, int steps) {
LOG.debug("drag from {},{} to {},{}", startX, startY, endX, endY);
return uiDevice.drag(startX, startY, endX, endY, steps);
}
public String getProductDetail() {
return productDetail;
}
public void setProductDetail(String productDetail) {
this.productDetail = productDetail;
}
private File ss() throws IOException {
String name = "ss-" + UUID.randomUUID() + ".png";
this.uiDevice.takeScreenshot(name, 1.0f, 68);
File png = this.getLogPath().resolve(name).toFile();
this.getAdb().pull(IUiDevice.TMP_DIR + name, png);
LOG.debug("Save screenshot as {}", png.getAbsolutePath());
return png;
}
private ExecuteWatchdog setupUiAutomatorRmiServer() throws IOException, InterruptedException {
List