blob: dedc1ea4252dca83859d41859e93ea10ae217ece [file] [log] [blame]
/*
* Copyright (C) 2010 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.monkeyrunner.adb;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.InstallException;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.monkeyrunner.MonkeyDevice;
import com.android.monkeyrunner.MonkeyImage;
import com.android.monkeyrunner.MonkeyManager;
import com.android.monkeyrunner.adb.LinearInterpolator.Point;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
public class AdbMonkeyDevice extends MonkeyDevice {
private static final Logger LOG = Logger.getLogger(AdbMonkeyDevice.class.getName());
private static final String[] ZERO_LENGTH_STRING_ARRAY = new String[0];
private static final long MANAGER_CREATE_TIMEOUT_MS = 5 * 1000; // 5 seconds
private final ExecutorService executor = Executors.newCachedThreadPool();
private final IDevice device;
private MonkeyManager manager;
public AdbMonkeyDevice(IDevice device) {
this.device = device;
this.manager = createManager("127.0.0.1", 12345);
Preconditions.checkNotNull(this.manager);
}
@Override
public MonkeyManager getManager() {
return manager;
}
@Override
public void dispose() {
try {
manager.quit();
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error getting the manager to quit", e);
}
manager = null;
}
private void executeAsyncCommand(final String command,
final LoggingOutputReceiver logger) {
executor.submit(new Runnable() {
public void run() {
try {
device.executeShellCommand(command, logger);
} catch (TimeoutException e) {
LOG.log(Level.SEVERE, "Error starting command: " + command, e);
throw new RuntimeException(e);
} catch (AdbCommandRejectedException e) {
LOG.log(Level.SEVERE, "Error starting command: " + command, e);
throw new RuntimeException(e);
} catch (ShellCommandUnresponsiveException e) {
LOG.log(Level.SEVERE, "Error starting command: " + command, e);
throw new RuntimeException(e);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error starting command: " + command, e);
throw new RuntimeException(e);
}
}
});
}
private MonkeyManager createManager(String address, int port) {
try {
device.createForward(port, port);
} catch (TimeoutException e) {
LOG.log(Level.SEVERE, "Timeout creating adb port forwarding", e);
return null;
} catch (AdbCommandRejectedException e) {
LOG.log(Level.SEVERE, "Adb rejected adb port forwarding command: " + e.getMessage(), e);
return null;
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to create adb port forwarding: " + e.getMessage(), e);
return null;
}
String command = "monkey --port " + port;
executeAsyncCommand(command, new LoggingOutputReceiver(LOG, Level.FINE));
// Sleep for a second to give the command time to execute.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
LOG.log(Level.SEVERE, "Unable to sleep", e);
}
InetAddress addr;
try {
addr = InetAddress.getByName(address);
} catch (UnknownHostException e) {
LOG.log(Level.SEVERE, "Unable to convert address into InetAddress: " + address, e);
return null;
}
// We have a tough problem to solve here. "monkey" on the device gives us no indication
// when it has started up and is ready to serve traffic. If you try too soon, commands
// will fail. To remedy this, we will keep trying until a single command (in this case,
// wake) succeeds.
boolean success = false;
MonkeyManager mm = null;
long start = System.currentTimeMillis();
while (!success) {
long now = System.currentTimeMillis();
long diff = now - start;
if (diff > MANAGER_CREATE_TIMEOUT_MS) {
LOG.severe("Timeout while trying to create monkey mananger");
return null;
}
Socket monkeySocket;
try {
monkeySocket = new Socket(addr, port);
} catch (IOException e) {
LOG.log(Level.FINE, "Unable to connect socket", e);
success = false;
continue;
}
mm = new MonkeyManager(monkeySocket);
try {
mm.wake();
} catch (IOException e) {
LOG.log(Level.FINE, "Unable to wake up device", e);
success = false;
continue;
}
success = true;
}
return mm;
}
@Override
public MonkeyImage takeSnapshot() {
try {
return new AdbMonkeyImage(device.getScreenshot());
} catch (TimeoutException e) {
LOG.log(Level.SEVERE, "Unable to take snapshot", e);
return null;
} catch (AdbCommandRejectedException e) {
LOG.log(Level.SEVERE, "Unable to take snapshot", e);
return null;
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to take snapshot", e);
return null;
}
}
@Override
protected String getSystemProperty(String key) {
return device.getProperty(key);
}
@Override
protected String getProperty(String key) {
try {
return manager.getVariable(key);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to get variable: " + key, e);
return null;
}
}
@Override
protected void wake() {
try {
manager.wake();
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to wake device (too sleepy?)", e);
}
}
private String shell(String... args) {
StringBuilder cmd = new StringBuilder();
for (String arg : args) {
cmd.append(arg).append(" ");
}
return shell(cmd.toString());
}
@Override
protected String shell(String cmd) {
CommandOutputCapture capture = new CommandOutputCapture();
try {
device.executeShellCommand(cmd, capture);
} catch (TimeoutException e) {
LOG.log(Level.SEVERE, "Error executing command: " + cmd, e);
return null;
} catch (ShellCommandUnresponsiveException e) {
LOG.log(Level.SEVERE, "Error executing command: " + cmd, e);
return null;
} catch (AdbCommandRejectedException e) {
LOG.log(Level.SEVERE, "Error executing command: " + cmd, e);
return null;
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error executing command: " + cmd, e);
return null;
}
return capture.toString();
}
@Override
protected boolean installPackage(String path) {
try {
String result = device.installPackage(path, true);
if (result != null) {
LOG.log(Level.SEVERE, "Got error installing package: "+ result);
return false;
}
return true;
} catch (InstallException e) {
LOG.log(Level.SEVERE, "Error installing package: " + path, e);
return false;
}
}
@Override
protected boolean removePackage(String packageName) {
try {
String result = device.uninstallPackage(packageName);
if (result != null) {
LOG.log(Level.SEVERE, "Got error uninstalling package "+ packageName + ": " +
result);
return false;
}
return true;
} catch (InstallException e) {
LOG.log(Level.SEVERE, "Error installing package: " + packageName, e);
return false;
}
}
@Override
protected void press(String keyName, TouchPressType type) {
try {
switch (type) {
case DOWN_AND_UP:
manager.press(keyName);
break;
case DOWN:
manager.keyDown(keyName);
break;
case UP:
manager.keyUp(keyName);
break;
}
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending press event: " + keyName + " " + type, e);
}
}
@Override
protected void type(String string) {
try {
manager.type(string);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error Typing: " + string, e);
}
}
@Override
protected void touch(int x, int y, TouchPressType type) {
try {
switch (type) {
case DOWN:
manager.touchDown(x, y);
break;
case UP:
manager.touchUp(x, y);
break;
case DOWN_AND_UP:
manager.tap(x, y);
break;
}
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending touch event: " + x + " " + y + " " + type, e);
}
}
@Override
protected void reboot(String into) {
try {
device.reboot(into);
} catch (TimeoutException e) {
LOG.log(Level.SEVERE, "Unable to reboot device", e);
} catch (AdbCommandRejectedException e) {
LOG.log(Level.SEVERE, "Unable to reboot device", e);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to reboot device", e);
}
}
@Override
protected void startActivity(String uri, String action, String data, String mimetype,
Collection<String> categories, Map<String, Object> extras, String component,
int flags) {
List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories,
extras, component, flags);
shell(Lists.asList("am", "start",
intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY));
}
@Override
protected void broadcastIntent(String uri, String action, String data, String mimetype,
Collection<String> categories, Map<String, Object> extras, String component,
int flags) {
List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories,
extras, component, flags);
shell(Lists.asList("am", "broadcast",
intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY));
}
private static boolean isNullOrEmpty(@Nullable String string) {
return string == null || string.length() == 0;
}
private List<String> buildIntentArgString(String uri, String action, String data, String mimetype,
Collection<String> categories, Map<String, Object> extras, String component,
int flags) {
List<String> parts = Lists.newArrayList();
// from adb docs:
//<INTENT> specifications include these flags:
// [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>]
// [-c <CATEGORY> [-c <CATEGORY>] ...]
// [-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...]
// [--esn <EXTRA_KEY> ...]
// [--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...]
// [-e|--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...]
// [-n <COMPONENT>] [-f <FLAGS>]
// [<URI>]
if (!isNullOrEmpty(action)) {
parts.add("-a");
parts.add(action);
}
if (!isNullOrEmpty(data)) {
parts.add("-d");
parts.add(data);
}
if (!isNullOrEmpty(mimetype)) {
parts.add("-t");
parts.add(mimetype);
}
// Handle categories
for (String category : categories) {
parts.add("-c");
parts.add(category);
}
// Handle extras
for (Entry<String, Object> entry : extras.entrySet()) {
// Extras are either boolean, string, or int. See which we have
Object value = entry.getValue();
String valueString;
String arg;
if (value instanceof Integer) {
valueString = Integer.toString((Integer) value);
arg = "--ei";
} else if (value instanceof Boolean) {
valueString = Boolean.toString((Boolean) value);
arg = "--ez";
} else {
// treat is as a string.
valueString = value.toString();
arg = "--esmake";
}
parts.add(arg);
parts.add(valueString);
}
if (!isNullOrEmpty(component)) {
parts.add("-n");
parts.add(component);
}
if (flags != 0) {
parts.add("-f");
parts.add(Integer.toString(flags));
}
if (!isNullOrEmpty(uri)) {
parts.add(uri);
}
return parts;
}
@Override
protected Map<String, Object> instrument(String packageName, Map<String, Object> args) {
List<String> shellCmd = Lists.newArrayList("am", "instrument", "-w", "-r", packageName);
String result = shell(shellCmd.toArray(ZERO_LENGTH_STRING_ARRAY));
return convertInstrumentResult(result);
}
/**
* Convert the instrumentation result into it's Map representation.
*
* @param result the result string
* @return the new map
*/
@VisibleForTesting
/* package */ static Map<String, Object> convertInstrumentResult(String result) {
Map<String, Object> map = Maps.newHashMap();
Pattern pattern = Pattern.compile("^INSTRUMENTATION_(\\w+): ", Pattern.MULTILINE);
Matcher matcher = pattern.matcher(result);
int previousEnd = 0;
String previousWhich = null;
while (matcher.find()) {
if ("RESULT".equals(previousWhich)) {
String resultLine = result.substring(previousEnd, matcher.start()).trim();
// Look for the = in the value, and split there
int splitIndex = resultLine.indexOf("=");
String key = resultLine.substring(0, splitIndex);
String value = resultLine.substring(splitIndex + 1);
map.put(key, value);
}
previousEnd = matcher.end();
previousWhich = matcher.group(1);
}
if ("RESULT".equals(previousWhich)) {
String resultLine = result.substring(previousEnd, matcher.start()).trim();
// Look for the = in the value, and split there
int splitIndex = resultLine.indexOf("=");
String key = resultLine.substring(0, splitIndex);
String value = resultLine.substring(splitIndex + 1);
map.put(key, value);
}
return map;
}
@Override
protected void drag(int startx, int starty, int endx, int endy, int steps, long ms) {
final long iterationTime = ms / steps;
LinearInterpolator lerp = new LinearInterpolator(steps);
LinearInterpolator.Point start = new LinearInterpolator.Point(startx, starty);
LinearInterpolator.Point end = new LinearInterpolator.Point(endx, endy);
lerp.interpolate(start, end, new LinearInterpolator.Callback() {
public void step(Point point) {
try {
manager.touchMove(point.getX(), point.getY());
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending drag start event", e);
}
try {
Thread.sleep(iterationTime);
} catch (InterruptedException e) {
LOG.log(Level.SEVERE, "Error sleeping", e);
}
}
public void start(Point point) {
try {
manager.touchDown(point.getX(), point.getY());
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending drag start event", e);
}
try {
Thread.sleep(iterationTime);
} catch (InterruptedException e) {
LOG.log(Level.SEVERE, "Error sleeping", e);
}
}
public void end(Point point) {
try {
manager.touchUp(point.getX(), point.getY());
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending drag end event", e);
}
}
});
}
}