diff --git a/android/tool/src/processing/app/tools/android/AndroidDevice.java b/android/tool/src/processing/app/tools/android/AndroidDevice.java new file mode 100644 index 000000000..8275586e5 --- /dev/null +++ b/android/tool/src/processing/app/tools/android/AndroidDevice.java @@ -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> outputListeners = new HashMap>(); + + 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 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 ps() { + return processes.getProcesses(); + } + + public void addOutputListener(final String processName, + final ProcessOutputListener listener) { + if (!outputListeners.containsKey(processName)) { + outputListeners.put(processName, new ArrayList()); + } + outputListeners.get(processName).add(listener); + } + + public void removeOutputListener(final String processName, + final ProcessOutputListener listener) { + final List 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 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); + } + +} diff --git a/android/tool/src/processing/app/tools/android/AndroidDeviceProperties.java b/android/tool/src/processing/app/tools/android/AndroidDeviceProperties.java new file mode 100644 index 000000000..7aa19f3d1 --- /dev/null +++ b/android/tool/src/processing/app/tools/android/AndroidDeviceProperties.java @@ -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"; +} diff --git a/android/tool/src/processing/app/tools/android/AndroidEnvironment.java b/android/tool/src/processing/app/tools/android/AndroidEnvironment.java new file mode 100644 index 000000000..fb9088fb9 --- /dev/null +++ b/android/tool/src/processing/app/tools/android/AndroidEnvironment.java @@ -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; + +/** + *

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. + * + *

 AndroidEnvironment env = new AndroidEnvironment();
+ * env.addPropertyChangeListener(...);
+ * env.initialize();
+ * 
+ * AndroidDevice n1 = env.getHardware();
+ * + * @author Jonathan Feinberg <jdf@us.ibm.com> + * + */ +public class AndroidEnvironment implements AndroidEnvironmentProperties { + private final Map devices = new ConcurrentHashMap(); + 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 activeDevices = listDevices(); + for (final String deviceId : activeDevices) { + if (!devices.containsKey(deviceId)) { + addDevice(new AndroidDevice(this, deviceId)); + } + } + for (final String deviceId : new ArrayList(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."; + + /** + *

First line starts "List of devices" + +

When an emulator is started with a debug port, then it shows up + in the list of devices. + +

List of devices attached +
HT91MLC00031 device +
emulator-5554 offline + +

List of devices attached +
HT91MLC00031 device +
emulator-5554 device + + * @return list of device identifiers + * @throws IOException + */ + private static List 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 devices = new ArrayList(); + for (final String line : result) { + if (!line.contains("\t")) { + continue; + } + devices.add(line.split("\t")[0]); + } + return devices; + } +} diff --git a/android/tool/src/processing/app/tools/android/AndroidEnvironmentProperties.java b/android/tool/src/processing/app/tools/android/AndroidEnvironmentProperties.java new file mode 100644 index 000000000..babc4a0cc --- /dev/null +++ b/android/tool/src/processing/app/tools/android/AndroidEnvironmentProperties.java @@ -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"; +} diff --git a/android/tool/src/processing/app/tools/android/AndroidProcess.java b/android/tool/src/processing/app/tools/android/AndroidProcess.java new file mode 100644 index 000000000..bfcfa7c62 --- /dev/null +++ b/android/tool/src/processing/app/tools/android/AndroidProcess.java @@ -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(); + } +} diff --git a/android/tool/src/processing/app/tools/android/AndroidProcesses.java b/android/tool/src/processing/app/tools/android/AndroidProcesses.java new file mode 100644 index 000000000..00d9d485c --- /dev/null +++ b/android/tool/src/processing/app/tools/android/AndroidProcesses.java @@ -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 processes = new ArrayList(); + 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 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); + } +} diff --git a/android/tool/src/processing/app/tools/android/AndroidRunner.java b/android/tool/src/processing/app/tools/android/AndroidRunner.java index cd22c3474..0f2c5805c 100644 --- a/android/tool/src/processing/app/tools/android/AndroidRunner.java +++ b/android/tool/src/processing/app/tools/android/AndroidRunner.java @@ -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. diff --git a/android/tool/src/processing/app/tools/android/AndroidTool.java b/android/tool/src/processing/app/tools/android/AndroidTool.java index fb1af6ce4..1496c87de 100644 --- a/android/tool/src/processing/app/tools/android/AndroidTool.java +++ b/android/tool/src/processing/app/tools/android/AndroidTool.java @@ -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); diff --git a/android/tool/src/processing/app/tools/android/Device.java b/android/tool/src/processing/app/tools/android/Device.java index 119f0dd22..2de372d94 100644 --- a/android/tool/src/processing/app/tools/android/Device.java +++ b/android/tool/src/processing/app/tools/android/Device.java @@ -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[] {}; } diff --git a/android/tool/src/processing/app/tools/android/EmulatorController.java b/android/tool/src/processing/app/tools/android/EmulatorController.java new file mode 100644 index 000000000..eab35840d --- /dev/null +++ b/android/tool/src/processing/app/tools/android/EmulatorController.java @@ -0,0 +1,5 @@ +package processing.app.tools.android; + +public class EmulatorController { + +} diff --git a/android/tool/src/processing/app/tools/android/Harness.java b/android/tool/src/processing/app/tools/android/Harness.java new file mode 100644 index 000000000..93049a371 --- /dev/null +++ b/android/tool/src/processing/app/tools/android/Harness.java @@ -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(); + } + }; +} diff --git a/android/tool/src/processing/app/tools/android/LogEntry.java b/android/tool/src/processing/app/tools/android/LogEntry.java new file mode 100644 index 000000000..b16c73c23 --- /dev/null +++ b/android/tool/src/processing/app/tools/android/LogEntry.java @@ -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; + } +} diff --git a/android/tool/src/processing/app/tools/android/ProcessHelper.java b/android/tool/src/processing/app/tools/android/ProcessHelper.java index 245a134b0..783f6f8f8 100644 --- a/android/tool/src/processing/app/tools/android/ProcessHelper.java +++ b/android/tool/src/processing/app/tools/android/ProcessHelper.java @@ -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; + } } } \ No newline at end of file diff --git a/android/tool/src/processing/app/tools/android/ProcessOutputListener.java b/android/tool/src/processing/app/tools/android/ProcessOutputListener.java new file mode 100644 index 000000000..5c0b7abf3 --- /dev/null +++ b/android/tool/src/processing/app/tools/android/ProcessOutputListener.java @@ -0,0 +1,7 @@ +package processing.app.tools.android; + +public interface ProcessOutputListener { + public void handleStdout(final String line); + + public void handleStderr(final String line); +} diff --git a/android/tool/src/processing/app/tools/android/StreamPump.java b/android/tool/src/processing/app/tools/android/StreamPump.java index e30981d83..53dcc7c38 100644 --- a/android/tool/src/processing/app/tools/android/StreamPump.java +++ b/android/tool/src/processing/app/tools/android/StreamPump.java @@ -96,6 +96,5 @@ class StreamPump implements Runnable { public void processLine(final String line) { writer.println(line); } - } }