/* Copyright (C) 2010 Mobile Sorcery AB
This program is free software; you can redistribute it and/or modify it
under the terms of the Eclipse Public License v1.0.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the Eclipse Public License v1.0 for
more details.
You should have received a copy of the Eclipse Public License v1.0 along
with this program. It is also available at http://www.eclipse.org/legal/epl-v10.html
*/
package com.mobilesorcery.sdk.builder.android.launch;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import com.mobilesorcery.sdk.builder.android.Activator;
import com.mobilesorcery.sdk.builder.android.PropertyInitializer;
import com.mobilesorcery.sdk.core.AbstractTool;
import com.mobilesorcery.sdk.core.CollectingLineHandler;
import com.mobilesorcery.sdk.core.CommandLineExecutor;
import com.mobilesorcery.sdk.core.CoreMoSyncPlugin;
import com.mobilesorcery.sdk.core.LineReader;
import com.mobilesorcery.sdk.core.LineReader.ILineHandler;
import com.mobilesorcery.sdk.core.LineReader.LineAdapter;
import com.mobilesorcery.sdk.core.MoSyncTool;
import com.mobilesorcery.sdk.core.Util;
import com.mobilesorcery.sdk.core.LineReader.LineHandlerList;
/**
* A class representing the Android Debug Bridge.
*
* @author Mattias Bybro
*
*/
public class ADB extends AbstractTool {
private static final String LIB_GDBSERVER = "lib/gdbserver";
public static class ProcessKiller extends LineReader.LineAdapter {
private Process process;
private IProgressMonitor monitor;
public ProcessKiller() {
this(null);
}
public ProcessKiller(IProgressMonitor monitor) {
this.monitor = monitor;
listenToMonitor();
}
private void listenToMonitor() {
Thread listenThread = new Thread(new Runnable() {
@Override
public void run() {
try {
IProgressMonitor monitorRef = monitor;
while (process != null && monitorRef != null && !monitorRef.isCanceled()) {
Thread.sleep(500);
}
if (monitorRef != null && monitorRef.isCanceled()) {
killProcess();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
listenThread.setDaemon(true);
listenThread.start();
}
@Override
public void stop(IOException e) {
process = null;
}
@Override
public void start(Process process) {
this.process = process;
}
public void killProcess() {
if (process != null) {
monitor = null;
process.destroy();
}
}
}
private final class LogcatListener implements IPropertyChangeListener {
private ADB adb;
public LogcatListener(ADB adb) {
this.adb = adb;
}
@Override
public void propertyChange(PropertyChangeEvent event) {
if (PropertyInitializer.ADB_DEBUG_LOG.equals(event.getProperty()) ||
PropertyInitializer.ADB_LOGCAT_ARGS.equals(event.getProperty()) ||
PropertyInitializer.ADB_USE_NDK_STACK.equals(event.getProperty())) {
try {
adb.startLogCat();
} catch (CoreException e) {
CoreMoSyncPlugin.getDefault().log(e);
}
}
}
}
private static class PidFinder extends CollectingLineHandler {
private int lineCount = 0;
private int pidCol = 2; // <== Fallback value
private Map<String, String> pids = new HashMap<String, String>();
public void newLine(String line) {
String[] cols = columns(line);
if (lineCount == 0) {
for (int i = 0; i < cols.length; i++) {
if (cols[i].trim().equalsIgnoreCase("pid")) {
pidCol = i;
}
}
} else {
// Last col is the process name
String name = cols[cols.length - 1].trim();
String pid = cols[pidCol];
pids.put(name, pid);
}
lineCount++;
}
private String[] columns(String line) {
return line.split("\\s+");
}
public Map<String, String> getPids() {
return pids;
}
}
private static ADB instance = new ADB();
private static ADB external;
private boolean logcatStarted;
private IPropertyChangeListener logCatListener = null;
private ProcessKiller logcatProcessHandler;
public static final String ARMEABI = "armeabi";
public static final String ARMEABI_V7A = "armeabi-v7a";
public static final String[] ABIS = new String[] { ARMEABI, ARMEABI_V7A };
private ADB() {
this(MoSyncTool.getDefault().getBinary("android/adb"));
}
public ADB(IPath pathToADB) {
super(pathToADB);
logCatListener = new LogcatListener(this);
logcatProcessHandler = new ProcessKiller();
}
public static ADB getDefault() {
ADB external = getExternal();
return external != null && external.isValid() ? external : instance;
}
public static ADB getExternal() {
IPath sdkPath = Activator.getDefault().getExternalAndroidSDKPath();
if (external == null || !external.getToolPath().equals(sdkPath)) {
ADB oldExternal = external;
if (oldExternal != null) {
oldExternal.dispose();
}
external = findADB(sdkPath);
}
return external;
}
private void dispose() {
stopLogCat();
}
/**
* Tries to locate the proper ADB to use; this location may differ depending
* on which Android SDK is installed.
* @param sdkRootPath The Android SDK root directory
* @return Does not return {@code null} unless {@code sdkRootPath} is {@code null}.
*/
public static ADB findADB(IPath sdkRootPath) {
if (sdkRootPath == null) {
return null;
}
IPath primaryADBPath = sdkRootPath.append("platform-tools/adb" + MoSyncTool.getBinExtension());
ADB primaryADB = new ADB(primaryADBPath);
if (!primaryADB.isValid()) {
// Older Android SDK has the adb tool elsewhere
IPath secondaryADBPath = sdkRootPath.append("tools/adb" + MoSyncTool.getBinExtension());
ADB secondaryADB = new ADB(secondaryADBPath);
if (secondaryADB.isValid()) {
return secondaryADB;
}
}
return primaryADB;
}
/**
* Returns a list of all online android devices (no emulators)
*
* @return
* @throws CoreException
*/
public List<String> listDeviceSerialNumbers(boolean useConsole)
throws CoreException {
return listDevices(false, true, useConsole);
}
/**
* Returns a list of all online android emulators (no real devices)
*
* @return
* @throws CoreException
*/
public List<String> listEmulators(boolean useConsole) throws CoreException {
return listDevices(true, false, useConsole);
}
private List<String> listDevices(boolean emulators, boolean realDevices,
boolean useConsole) throws CoreException {
CollectingLineHandler collectingLineHandler = new CollectingLineHandler();
execute(new String[] { getToolPath().getAbsolutePath(), "devices" },
collectingLineHandler, collectingLineHandler, CoreMoSyncPlugin.LOG_CONSOLE_NAME, false);
ArrayList<String> result = new ArrayList<String>();
for (String line : collectingLineHandler.getLines()) {
line = line.trim();
if (line.length() > 0) {
if (!line.startsWith("*") && !line.startsWith("List")) {
// Then heuristically, we have a device!
String[] device = line.split("\\s+");
if (device.length > 1) {
String serialNumber = device[0];
String state = device[device.length - 1];
if ("device".equals(state)) {
boolean isEmulator = serialNumber
.startsWith("emulator-");
if ((realDevices && !isEmulator)
|| (emulators && isEmulator)) {
// Only include online devices and no emulators
result.add(serialNumber);
}
}
}
}
}
}
return result;
}
public void uninstall(String packageName, String serialNumberOfDevice, ProcessKiller processKiller) throws CoreException {
runAndCollectError(processKiller, new String[] { getToolPath().getAbsolutePath(), "-s",
serialNumberOfDevice, "uninstall", packageName });
}
public void install(File packageToInstall, String packageName, String serialNumberOfDevice, ProcessKiller processKiller) throws CoreException {
IProgressMonitor monitor = processKiller.monitor;
if (packageName != null && Activator.getDefault().getPreferenceStore().getBoolean(PropertyInitializer.ADB_UNINSTALL_FIRST)) {
try {
uninstall(packageName, serialNumberOfDevice, processKiller);
} catch (CoreException e) {
// Ignore -- the apk might not exist.
}
}
if (monitor == null || !monitor.isCanceled()) {
runAndCollectError(processKiller, new String[] { getToolPath().getAbsolutePath(), "-s",
serialNumberOfDevice, "install", "-r",
packageToInstall.getAbsolutePath() });
}
}
private void runAndCollectError(ProcessKiller processKiller, String[] commandLine)
throws CoreException {
LineHandlerList stdout = new LineHandlerList();
CollectingLineHandler collectingLineHandler = new CollectingLineHandler();
stdout.addHandler(collectingLineHandler);
stdout.addHandler(processKiller);
CollectingLineHandler errorLineHandler = new CollectingLineHandler();
int errorCode = execute(commandLine,
stdout, errorLineHandler, false);
String errorMsg = null;
for (String line : collectingLineHandler.getLines()) {
if (line.trim().startsWith("Failure")) {
errorMsg = line;
errorCode = -127;
}
}
List<String> errorLines = errorLineHandler.getLines();
if (errorLines.size() > 0) {
String error = Util.join(errorLines.toArray(), "\n").trim();
if (!Util.isEmpty(error) && errorMsg == null) {
errorMsg = error;
}
}
if (errorCode != 0 && errorMsg != null) {
throw new CoreException(new Status(IStatus.ERROR,
Activator.PLUGIN_ID, MessageFormat.format(
"Could not install on device: {0}", errorMsg)));
}
}
public void launch(String activityName, String serialNumberOfDevice, ProcessKiller processKiller)
throws CoreException {
CollectingLineHandler collectingLineHandler = new CollectingLineHandler();
CollectingLineHandler errorLineHandler = new CollectingLineHandler();
int errorCode = execute(new String[] { getToolPath().getAbsolutePath(),
"-s", serialNumberOfDevice, "shell", "am", "start", "-n",
activityName }, collectingLineHandler, errorLineHandler, false);
}
@Override
protected String getToolName() {
return "ADB";
}
public void awaitBoot(String serialNumberOfDevice, long timeoutInMs) throws CoreException {
long startTime = System.currentTimeMillis();
while (!isBootComplete(serialNumberOfDevice)) {
if (System.currentTimeMillis() - startTime > timeoutInMs) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Connection to Android Emulator timed out"));
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Connection to Android Emulator timed out", e));
}
}
}
public boolean isBootComplete(String serialNumberOfDevice) throws CoreException {
String reply = getProp(serialNumberOfDevice, "dev.bootcomplete");
return "1".equals(reply);
}
public String getProp(String serialNumberOfDevice, String prop) throws CoreException {
CollectingLineHandler cl = new CollectingLineHandler();
execute(new String[] { getToolPath().getAbsolutePath(), "-s",
serialNumberOfDevice, "shell", "getprop", prop
}, cl, cl, CoreMoSyncPlugin.LOG_CONSOLE_NAME, false);
String reply = cl.getFirstLine().trim();
return reply;
}
public Map<String, String> getPids(String serialNumberOfDevice) throws CoreException {
PidFinder pf = new PidFinder();
execute(new String[] { getToolPath().getAbsolutePath(), "-s",
serialNumberOfDevice, "shell", "ps"
}, pf, pf, CoreMoSyncPlugin.LOG_CONSOLE_NAME, false);
return pf.getPids();
}
public void kill9(String serialNumberOfDevice, String packageName, String pid) throws CoreException {
ArrayList<String> cmd = new ArrayList<String>();
cmd.add(getToolPath().getAbsolutePath());
cmd.add("-s");
cmd.add(serialNumberOfDevice);
cmd.add("shell");
if (packageName != null) {
cmd.add("run-as");
cmd.add(packageName);
}
cmd.add("kill");
cmd.add("-9");
cmd.add(pid);
execute(cmd.toArray(new String[cmd.size()]), null, null, CoreMoSyncPlugin.LOG_CONSOLE_NAME, false);
}
public String getDataDirectory(String serialNumberOfDevice, String packageName) throws CoreException {
CollectingLineHandler cl = new CollectingLineHandler();
execute(new String[] { getToolPath().getAbsolutePath(), "-s",
serialNumberOfDevice, "shell", "run-as", packageName,
"/system/bin/sh", "-c", "pwd"
}, cl, cl, CoreMoSyncPlugin.LOG_CONSOLE_NAME, false);
String reply = cl.getFirstLine().trim();
return reply;
}
public void setupGdb(String serialNumberOfDevice, String packageName, int debugPort) throws CoreException {
Map<String, String> pids = getPids(serialNumberOfDevice);
String packagePid = pids.get(packageName);
if (packagePid == null) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
MessageFormat.format("No package with name {0} found on device.", packageName)));
}
String gdbserverPid = pids.get(LIB_GDBSERVER);
if (gdbserverPid != null) {
kill9(serialNumberOfDevice, packageName, gdbserverPid);
}
// Start the debugger.
execute(new String[] { getToolPath().getAbsolutePath(), "-s",
serialNumberOfDevice, "shell", "run-as", packageName,
LIB_GDBSERVER, "+debug-socket", "--attach", packagePid
}, null, null, CoreMoSyncPlugin.LOG_CONSOLE_NAME, true);
// Start forwarding.
String dataDirectory = getDataDirectory(serialNumberOfDevice, packageName);
execute(new String[] { getToolPath().getAbsolutePath(), "-s",
serialNumberOfDevice, "forward", "tcp:" + debugPort,
"localfilesystem:" + dataDirectory + "/debug-socket",
}, null, null, CoreMoSyncPlugin.LOG_CONSOLE_NAME, true);
}
public void setProp(String serialNumberOfDevice, String propKey, String propValue) throws CoreException {
CollectingLineHandler cl = new CollectingLineHandler();
execute(new String[] { getToolPath().getAbsolutePath(), "-s",
serialNumberOfDevice, "shell", "setprop", propKey, propValue
}, cl, cl, CoreMoSyncPlugin.LOG_CONSOLE_NAME, false);
}
public synchronized void startLogCat() throws CoreException {
IPreferenceStore prefs = Activator.getDefault().getPreferenceStore();
if (!logcatStarted) {
logcatStarted = true;
prefs.addPropertyChangeListener(logCatListener);
}
boolean silent = !prefs.getBoolean(PropertyInitializer.ADB_DEBUG_LOG);
logcatProcessHandler.killProcess();
if (!silent) {
// Then restart!
ArrayList<String> commandLine = new ArrayList<String>();
commandLine.add(getToolPath().getAbsolutePath());
commandLine.add("logcat");
String[] args = CommandLineExecutor.parseCommandLine(prefs.getString(PropertyInitializer.ADB_LOGCAT_ARGS));
commandLine.addAll(Arrays.asList(args));
// First clear
clearLogCat();
// We never have more than one logcat process.
execute(commandLine.toArray(new String[0]), logcatProcessHandler, null, true);
}
}
private void clearLogCat() throws CoreException {
ArrayList<String> commandLine = new ArrayList<String>();
commandLine.add(getToolPath().getAbsolutePath());
commandLine.add("logcat");
commandLine.add("-c");
execute(commandLine.toArray(new String[0]), null, null, false);
}
private synchronized void stopLogCat() {
logcatProcessHandler.killProcess();
logcatStarted = false;
IPreferenceStore prefs = Activator.getDefault().getPreferenceStore();
prefs.removePropertyChangeListener(logCatListener);
}
public synchronized void killServer() throws CoreException {
execute(new String[] { getToolPath().getAbsolutePath(),
"kill-server"
}, null, null, CoreMoSyncPlugin.LOG_CONSOLE_NAME, false);
stopLogCat();
}
public String matchAbi(String serialNumberOfDevice, String[] abis) throws CoreException {
String abi1 = getProp(serialNumberOfDevice, "ro.product.cpu.abi");
String abi2 = getProp(serialNumberOfDevice, "ro.product.cpu.abi2");
HashSet<String> abisOnDevice = new HashSet<String>(Arrays.asList(abi1, abi2));
HashSet<String> availableAbis = new HashSet<String>(Arrays.asList(abis));
if (abisOnDevice.contains(ADB.ARMEABI_V7A) &&
availableAbis.contains(ADB.ARMEABI_V7A)) {
return ADB.ARMEABI_V7A;
} if (abisOnDevice.contains(ADB.ARMEABI) &&
availableAbis.contains(ADB.ARMEABI)) {
return ADB.ARMEABI;
} else {
return null;
}
}
public String getModelName(String serialNumber) {
try {
getPids(serialNumber);
} catch (CoreException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
ArrayList<String> commandLine = new ArrayList<String>();
commandLine.add(getToolPath().getAbsolutePath());
commandLine.add("shell");
commandLine.add("cat");
commandLine.add("/system/build.prop");
final String[] model = new String[1];
CollectingLineHandler handler = new CollectingLineHandler() {
private final Pattern REGEX = Pattern.compile("(.*)=(.*)");
@Override
public void newLine(String line) {
Matcher m = REGEX.matcher(line);
if (model[0] == null && m.matches()) {
String property = m.group(1);
String value = m.group(2);
if (property.trim().equals("ro.product.model")) {
model[0] = value;
}
}
}
};
try {
execute(commandLine.toArray(new String[0]), handler, handler, CoreMoSyncPlugin.LOG_CONSOLE_NAME, true);
handler.awaitStopped(5, TimeUnit.SECONDS);
} catch (Exception e) {
// Just ignore.
e.printStackTrace();
return null;
}
return model[0];
}
public void pull(String serialNumberOfDevice, String pathOnDevice, String destination) throws CoreException {
File sourceFile = new File(pathOnDevice);
File destinationFile = new File(destination);
if (destinationFile.isDirectory()) {
destination = new File(destinationFile, sourceFile.getName()).getAbsolutePath();
}
execute(new String[] { getToolPath().getAbsolutePath(),
"pull", pathOnDevice, destination
}, null, null, CoreMoSyncPlugin.LOG_CONSOLE_NAME, false);
}
}