Now have working AndroidEnvironment, with AndroidDevices that let you listen to stdout and stderr from specific apps.

This commit is contained in:
jdf
2010-02-25 20:46:46 +00:00
parent f3e6ea4b72
commit 1a76fa7742
15 changed files with 605 additions and 21 deletions
@@ -0,0 +1,180 @@
package processing.app.tools.android;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import processing.app.tools.android.LogEntry.Severity;
public class AndroidDevice implements AndroidDeviceProperties {
private final AndroidEnvironment env;
private final String id;
private final AndroidProcesses processes;
private final Map<String, List<ProcessOutputListener>> outputListeners = new HashMap<String, List<ProcessOutputListener>>();
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
// mutable state
private Process logcat;
public AndroidDevice(final AndroidEnvironment env, final String id) {
this.env = env;
this.id = id;
this.processes = new AndroidProcesses(this);
}
// adb shell am start -n com.android.launcher2/.Launcher
private class LogLineProcessor implements LineProcessor {
@Override
public void processLine(final String line) {
final LogEntry entry = new LogEntry(line);
final String src = entry.source;
final String msg = entry.message;
final Severity sev = entry.severity;
if (src.equals("ActivityManager") && msg.startsWith("Start proc")) {
handleStartProcEntry(entry);
} else {
if ((src.equals("AndroidRuntime") && sev == Severity.Error)
|| src.equals("System.out") || src.equals("System.err")) {
final List<ProcessOutputListener> listeners = getListeners(entry.sourcePid);
if (listeners != null) {
for (final ProcessOutputListener listener : listeners) {
if (sev == Severity.Warning || sev == Severity.Error
|| sev == Severity.Fatal) {
listener.handleStderr(msg);
} else {
listener.handleStdout(msg);
}
}
}
}
}
//System.err.println(entry.source + "/" + entry.message);
}
}
public void initialize() throws IOException, InterruptedException {
processes.refresh();
new ProcessHelper(generateAdbCommand("logcat", "-c")).execute();
logcat = Runtime.getRuntime().exec(generateAdbCommand("logcat"));
new StreamPump(logcat.getInputStream()).addTarget(new LogLineProcessor())
.start();
new StreamPump(logcat.getErrorStream()).addTarget(System.err).start();
}
public void shutdown() {
for (final PropertyChangeListener pcl : Arrays.asList(pcs
.getPropertyChangeListeners())) {
pcs.removePropertyChangeListener(pcl);
}
outputListeners.clear();
if (logcat != null) {
logcat.destroy();
}
env.deviceRemoved(this);
}
public String getId() {
return id;
}
public AndroidEnvironment getEnv() {
return env;
}
public List<AndroidProcess> ps() {
return processes.getProcesses();
}
public void addOutputListener(final String processName,
final ProcessOutputListener listener) {
if (!outputListeners.containsKey(processName)) {
outputListeners.put(processName, new ArrayList<ProcessOutputListener>());
}
outputListeners.get(processName).add(listener);
}
public void removeOutputListener(final String processName,
final ProcessOutputListener listener) {
final List<ProcessOutputListener> listeners = outputListeners
.get(processName);
if (listeners != null) {
listeners.remove(listener);
}
}
private static final Pattern START_PROC = Pattern
.compile("^Start proc (\\S+) for \\S+ [^:]+: pid=(\\d+).+$");
protected List<ProcessOutputListener> getListeners(final String pid) {
final AndroidProcess process = processes.byPid(pid);
if (process == null) {
System.err.println("I don't recognize process id " + pid + "!");
return null;
}
return outputListeners.get(process.name);
}
private void handleStartProcEntry(final LogEntry entry) {
final Matcher m = START_PROC.matcher(entry.message);
if (m.matches()) {
startProc(m.group(1), m.group(2));
} else {
System.err.println("I don't recognize this start proc message:\n"
+ entry.message);
}
}
private void startProc(final String name, final String pid) {
final AndroidProcess proc = new AndroidProcess(pid, name);
processes.refresh();
firePropertyChange(APP_STARTED, null, proc);
}
private void endProc(final String pid) {
final AndroidProcess proc = processes.byPid(pid);
if (proc == null) {
System.err.println("Process " + pid
+ " ended, but I hadn't known about it.");
} else {
processes.refresh();
firePropertyChange(APP_ENDED, proc, null);
}
}
String[] generateAdbCommand(final String... cmd) {
final String[] adbCmd = new String[3 + cmd.length];
adbCmd[0] = "adb";
adbCmd[1] = "-s";
adbCmd[2] = getId();
System.arraycopy(cmd, 0, adbCmd, 3, cmd.length);
return adbCmd;
}
@Override
public String toString() {
return "[AndroidDevice " + getId() + "]";
}
public void addPropertyChangeListener(final PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
private void firePropertyChange(final String propertyName,
final Object oldValue, final Object newValue) {
pcs.firePropertyChange(propertyName, oldValue, newValue);
}
public void removePropertyChangeListener(final PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
}
@@ -0,0 +1,6 @@
package processing.app.tools.android;
public interface AndroidDeviceProperties {
public static final String APP_STARTED = "android.device.app.started";
public static final String APP_ENDED = "android.device.app.ended";
}
@@ -0,0 +1,189 @@
package processing.app.tools.android;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* <p>An AndroidEnvironment is an object that periodically polls for the existence
* of running Android devices, both emulated and hardware. You can register to
* be notified when devices are added to or removed from the environment. You
* can also ask for an emulator or a hardware device specifically.
*
* <pre> AndroidEnvironment env = new AndroidEnvironment();
* env.addPropertyChangeListener(...);
* env.initialize();
*
* AndroidDevice n1 = env.getHardware();</pre>
*
* @author Jonathan Feinberg &lt;jdf@us.ibm.com&gt;
*
*/
public class AndroidEnvironment implements AndroidEnvironmentProperties {
private final Map<String, AndroidDevice> devices = new ConcurrentHashMap<String, AndroidDevice>();
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private final Timer refreshTimer = new Timer("AndroidEnvironment refresher");
public AndroidEnvironment() {
}
/**
* Cause this model of the Android environment to begin discovering
* devices. If you wish to be informed about device discovery, make
* sure you register yourself as a listener before calling this
* method.
*
* This method starts up a thread that must be nuked via the
* shutdown() method in order for your app to exit cleanly.
*/
public void initialize() {
for (final String deviceId : listDevices()) {
addDevice(new AndroidDevice(this, deviceId));
}
refreshTimer.schedule(new TimerTask() {
@Override
public void run() {
refresh();
}
}, TimeUnit.SECONDS.toMillis(2), TimeUnit.SECONDS.toMillis(2));
}
/**
* Turn off the environment's device-discovery polling.
*/
public void shutdown() {
refreshTimer.cancel();
}
/**
* @return the first Android emulator known to be running, or null if there are none.
*/
public AndroidDevice getEmulator() {
for (final AndroidDevice device : devices.values()) {
if (device.getId().contains("emulator")) {
return device;
}
}
return null;
}
/**
* @return the first Android hardware device known to be running, or null if there are none.
*/
public AndroidDevice getHardware() {
for (final AndroidDevice device : devices.values()) {
if (!device.getId().contains("emulator")) {
return device;
}
}
return null;
}
private void refresh() {
final List<String> activeDevices = listDevices();
for (final String deviceId : activeDevices) {
if (!devices.containsKey(deviceId)) {
addDevice(new AndroidDevice(this, deviceId));
}
}
for (final String deviceId : new ArrayList<String>(devices.keySet())) {
if (!activeDevices.contains(deviceId)) {
devices.get(deviceId).shutdown();
}
}
}
private void addDevice(final AndroidDevice device) {
if (devices.put(device.getId(), device) != null) {
throw new IllegalStateException("Adding " + device
+ ", which already exists!");
}
try {
device.initialize();
} catch (final Exception e) {
System.err.println("Cannot initialize " + device + ": " + e);
devices.remove(device.getId());
}
firePropertyChange(DEVICE_ADDED, null, device);
}
void deviceRemoved(final AndroidDevice device) {
if (devices.remove(device.getId()) == null) {
throw new IllegalStateException("I didn't know about device "
+ device.getId() + "!");
}
firePropertyChange(DEVICE_REMOVED, device, null);
}
public void addPropertyChangeListener(final PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void firePropertyChange(final String propertyName,
final Object oldValue, final Object newValue) {
pcs.firePropertyChange(propertyName, oldValue, newValue);
}
public void removePropertyChangeListener(final PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
static final String ADB_DEVICES_ERROR = "Received unfamiliar output from “adb devices”.\n"
+ "The device list may have errors.";
/**
* <p>First line starts "List of devices"
<p>When an emulator is started with a debug port, then it shows up
in the list of devices.
<p>List of devices attached
<br>HT91MLC00031 device
<br>emulator-5554 offline
<p>List of devices attached
<br>HT91MLC00031 device
<br>emulator-5554 device
* @return list of device identifiers
* @throws IOException
*/
private static List<String> listDevices() {
ProcessResult result;
try {
result = new ProcessHelper("adb", "devices").execute();
} catch (final InterruptedException e) {
return Collections.emptyList();
} catch (final IOException e) {
System.err.println(e);
return Collections.emptyList();
}
if (!result.succeeded()) {
System.err.println(result);
return Collections.emptyList();
}
// might read "List of devices attached"
if (!result.getStdout().startsWith("List of devices")) {
System.err.println(ADB_DEVICES_ERROR);
return Collections.emptyList();
}
final List<String> devices = new ArrayList<String>();
for (final String line : result) {
if (!line.contains("\t")) {
continue;
}
devices.add(line.split("\t")[0]);
}
return devices;
}
}
@@ -0,0 +1,6 @@
package processing.app.tools.android;
public interface AndroidEnvironmentProperties {
public static final String DEVICE_REMOVED = "android.device.removed";
public static final String DEVICE_ADDED = "android.device.added";
}
@@ -0,0 +1,36 @@
package processing.app.tools.android;
public class AndroidProcess {
public final String pid;
public final String name;
public AndroidProcess(final String pid, final String name) {
if (pid == null) {
throw new IllegalArgumentException("null pid");
}
if (name == null) {
throw new IllegalArgumentException("null name");
}
this.pid = pid;
this.name = name;
}
@Override
public String toString() {
return name + " (" + pid + ")";
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof AndroidProcess)) {
return false;
}
final AndroidProcess o = (AndroidProcess) obj;
return pid.equals(o.pid) && name.equals(o.name);
}
@Override
public int hashCode() {
return pid.hashCode() * 17 + name.hashCode();
}
}
@@ -0,0 +1,59 @@
package processing.app.tools.android;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class AndroidProcesses {
private final List<AndroidProcess> processes = new ArrayList<AndroidProcess>();
private final AndroidDevice device;
public AndroidProcesses(final AndroidDevice device) {
this.device = device;
}
void refresh() {
processes.clear();
try {
for (final String line : new ProcessHelper(this.device
.generateAdbCommand("shell", "ps")).execute()) {
final String[] fields = line.split("\\s+");
if (fields.length != 9 || fields[0].equals("USER")) {
continue;
}
final String pid = fields[1];
final String name = fields[8];
processes.add(new AndroidProcess(pid, name));
}
} catch (final Exception e) {
System.err.println("Unable to refresh processes on " + device + ":");
e.printStackTrace(System.err);
}
}
List<AndroidProcess> getProcesses() {
return Collections.unmodifiableList(processes);
}
public AndroidProcess byPid(final String pid) {
for (final AndroidProcess p : processes) {
if (p.pid.equals(pid)) {
return p;
}
}
return null;
}
public AndroidProcess byName(final String name) {
for (final AndroidProcess p : processes) {
if (p.name.equals(name)) {
return p;
}
}
return null;
}
public void add(final AndroidProcess androidProcess) {
processes.add(androidProcess);
}
}
@@ -133,19 +133,6 @@ public class AndroidRunner extends Runner {
@Override
protected void generateTrace(final PrintWriter writer) {
final Thread logcatter = new Thread(new Runnable() {
public void run() {
try {
new SketchLogCatter().start();
} catch (final InterruptedException e) {
System.err.println("logcat interrupted");
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
}, "logcatter");
logcatter.start();
// At this point, disable the run button.
// This happens when the sketch is exited by hitting ESC,
// or the user manually closes the sketch window.
@@ -716,7 +716,6 @@ public class AndroidTool implements Tool {
final boolean emu = device.startsWith("emulator");
editor.statusNotice("Sketch started on the "
+ (emu ? "emulator" : "phone") + ".");
new AndroidRunner(editor, editor.getSketch()).launch(ADB_SOCKET_PORT);
} else {
editor.statusError("Could not start the sketch.");
System.err.println(result);
@@ -205,8 +205,6 @@ public class Device {
// }
if (lines.length == 0) {
// result was 0, so we're ok, but this is odd.
System.out.println("No devices found.");
return new String[] {};
}
@@ -0,0 +1,5 @@
package processing.app.tools.android;
public class EmulatorController {
}
@@ -0,0 +1,57 @@
package processing.app.tools.android;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
public class Harness implements AndroidEnvironmentProperties,
AndroidDeviceProperties {
public static void main(final String[] args) throws Exception {
final AndroidEnvironment env = new AndroidEnvironment();
env.addPropertyChangeListener(PCD);
env.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(final PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(DEVICE_ADDED)) {
final AndroidDevice device = (AndroidDevice) evt.getNewValue();
device.addOutputListener("processing.android.test.testconsole",
new ProcessOutputListener() {
@Override
public void handleStdout(final String line) {
System.out.println("testconsole: " + line);
}
@Override
public void handleStderr(final String line) {
System.err.println("testconsole: " + line);
}
});
device.addOutputListener("processing.android.test.testmouse",
new ProcessOutputListener() {
@Override
public void handleStdout(final String line) {
System.out.println("testmouse: " + line);
}
@Override
public void handleStderr(final String line) {
System.err.println("testmouse: " + line);
}
});
}
}
});
env.initialize();
}
private static final PropertyChangeListener PCD = new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
System.err.println(evt.getSource());
System.err.println(" " + evt.getPropertyName());
System.err.println(" old: " + evt.getOldValue());
System.err.println(" new: " + evt.getNewValue());
System.err.println();
}
};
}
@@ -0,0 +1,52 @@
package processing.app.tools.android;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LogEntry {
public static enum Severity {
Verbose, Debug, Info, Warning, Error, Fatal;
static Severity fromChar(final char c) {
if (c == 'V') {
return Verbose;
} else if (c == 'D') {
return Debug;
} else if (c == 'I') {
return Info;
} else if (c == 'W') {
return Warning;
} else if (c == 'E') {
return Error;
} else if (c == 'F') {
return Fatal;
} else {
throw new IllegalArgumentException("I don't know how to interpret '"
+ c + "' as a log severity");
}
}
}
public final Severity severity;
public final String source;
public final String sourcePid;
public final String message;
private static final Pattern PARSER = Pattern
.compile("^([VDIWEF])/([^\\(]+)\\(\\s*(\\d+)\\):\\s*(.+)$");
public LogEntry(final String line) {
final Matcher m = PARSER.matcher(line);
if (!m.matches()) {
throw new RuntimeException("I can't understand log entry\n" + line);
}
this.severity = Severity.fromChar(m.group(1).charAt(0));
this.source = m.group(2);
this.sourcePid = m.group(3);
this.message = m.group(4);
}
@Override
public String toString() {
return severity + "/" + source + "(" + sourcePid + "): " + message;
}
}
@@ -91,9 +91,13 @@ class ProcessHelper {
errpump.addTarget(System.err);
}
errpump.start();
return new ProcessResult(getCommand(), process.waitFor(), outWriter
.toString(), errWriter.toString(), System.currentTimeMillis()
- startTime);
try {
return new ProcessResult(getCommand(), process.waitFor(), outWriter
.toString(), errWriter.toString(), System.currentTimeMillis()
- startTime);
} catch (final InterruptedException e) {
System.err.println("Interrupted: " + getCommand());
throw e;
}
}
}
@@ -0,0 +1,7 @@
package processing.app.tools.android;
public interface ProcessOutputListener {
public void handleStdout(final String line);
public void handleStderr(final String line);
}
@@ -96,6 +96,5 @@ class StreamPump implements Runnable {
public void processLine(final String line) {
writer.println(line);
}
}
}