Start refactoring MonkeyRunner to be a bit easier to understand.

- Add in ImageUtils (which is basically a copy from HierarchyViewer)
- Encapsulate ADB communications inside DebugBridge.
- Encapsulate Network Monkey protocol inside MonkeyMananger

Most of this code is currenlty duplicated in MonkeyRunner.java.  Deletions of that code
will come in a subsequent change.

Change-Id: Ia33eeae9b12d97371781c5d2a0292b738130b4d3
diff --git a/tools/monkeyrunner/src/Android.mk b/tools/monkeyrunner/src/Android.mk
index fb6b9c1..614acca 100644
--- a/tools/monkeyrunner/src/Android.mk
+++ b/tools/monkeyrunner/src/Android.mk
@@ -22,7 +22,8 @@
 LOCAL_JAVA_LIBRARIES := \
 	ddmlib \
 	jython \
-	xmlwriter
+	xmlwriter \
+	guavalib
 
 
 LOCAL_MODULE := monkeyrunner
@@ -48,4 +49,3 @@
 LOCAL_MODULE := xmlwriter
 
 include $(BUILD_HOST_JAVA_LIBRARY)
-
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/DebugBridge.java b/tools/monkeyrunner/src/com/android/monkeyrunner/DebugBridge.java
new file mode 100644
index 0000000..6ba1239
--- /dev/null
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/DebugBridge.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+public class DebugBridge {
+    private final Logger log = Logger.getLogger(DebugBridge.class.getName());
+
+    private final List<IDevice> devices = Lists.newArrayList();
+
+    private final AndroidDebugBridge bridge;
+
+    public DebugBridge() {
+        this.bridge = AndroidDebugBridge.createBridge();
+    }
+
+    public DebugBridge(AndroidDebugBridge bridge) {
+        this.bridge = bridge;
+    }
+
+    /* package */ void addDevice(IDevice device) {
+        devices.add(device);
+    }
+
+    /* package */ void removeDevice(IDevice device) {
+        devices.remove(device);
+    }
+
+    public Collection<IDevice> getConnectedDevices() {
+        if (devices.size() > 0) {
+            return ImmutableList.copyOf(devices);
+        }
+        return Collections.emptyList();
+    }
+
+    public IDevice getPreferredDevice() {
+        if (devices.size() > 0) {
+            return devices.get(0);
+        }
+        return null;
+    }
+
+    public static DebugBridge createDebugBridge() {
+        AndroidDebugBridge.init(false);
+
+        final DebugBridge bridge = new DebugBridge(AndroidDebugBridge.createBridge());
+        AndroidDebugBridge.addDeviceChangeListener(new IDeviceChangeListener() {
+            public void deviceDisconnected(IDevice device) {
+                bridge.removeDevice(device);
+            }
+
+            public void deviceConnected(IDevice device) {
+                bridge.addDevice(device);
+            }
+
+            public void deviceChanged(IDevice device, int arg1) {
+                // TODO Auto-generated method stub
+
+            }
+        });
+
+        return bridge;
+    }
+}
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/ImageUtils.java b/tools/monkeyrunner/src/com/android/monkeyrunner/ImageUtils.java
new file mode 100644
index 0000000..870fafa
--- /dev/null
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/ImageUtils.java
@@ -0,0 +1,123 @@
+/*
+ * 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;
+
+import com.android.ddmlib.RawImage;
+
+import java.awt.image.BufferedImage;
+/**
+ * Useful image related functions.
+ */
+public class ImageUtils {
+    // Utility class
+    private ImageUtils() { }
+
+    public static BufferedImage convertImage(RawImage rawImage, BufferedImage image) {
+        if (image == null || rawImage.width != image.getWidth() ||
+                rawImage.height != image.getHeight()) {
+            image = new BufferedImage(rawImage.width, rawImage.height,
+                    BufferedImage.TYPE_INT_ARGB);
+        }
+
+        switch (rawImage.bpp) {
+            case 16:
+                rawImage16toARGB(image, rawImage);
+                break;
+            case 32:
+                rawImage32toARGB(image, rawImage);
+                break;
+        }
+
+        return image;
+    }
+
+    public static BufferedImage convertImage(RawImage rawImage) {
+        return convertImage(rawImage, null);
+    }
+
+    private static int getMask(int length) {
+        int res = 0;
+        for (int i = 0 ; i < length ; i++) {
+            res = (res << 1) + 1;
+        }
+
+        return res;
+    }
+
+    private static void rawImage32toARGB(BufferedImage image, RawImage rawImage) {
+        int[] scanline = new int[rawImage.width];
+        byte[] buffer = rawImage.data;
+        int index = 0;
+
+        final int redOffset = rawImage.red_offset;
+        final int redLength = rawImage.red_length;
+        final int redMask = getMask(redLength);
+        final int greenOffset = rawImage.green_offset;
+        final int greenLength = rawImage.green_length;
+        final int greenMask = getMask(greenLength);
+        final int blueOffset = rawImage.blue_offset;
+        final int blueLength = rawImage.blue_length;
+        final int blueMask = getMask(blueLength);
+        final int alphaLength = rawImage.alpha_length;
+        final int alphaOffset = rawImage.alpha_offset;
+        final int alphaMask = getMask(alphaLength);
+
+        for (int y = 0 ; y < rawImage.height ; y++) {
+            for (int x = 0 ; x < rawImage.width ; x++) {
+                int value = buffer[index++] & 0x00FF;
+                value |= (buffer[index++] & 0x00FF) << 8;
+                value |= (buffer[index++] & 0x00FF) << 16;
+                value |= (buffer[index++] & 0x00FF) << 24;
+
+                int r = ((value >>> redOffset) & redMask) << (8 - redLength);
+                int g = ((value >>> greenOffset) & greenMask) << (8 - greenLength);
+                int b = ((value >>> blueOffset) & blueMask) << (8 - blueLength);
+                int a = 0xFF;
+
+                if (alphaLength != 0) {
+                    a = ((value >>> alphaOffset) & alphaMask) << (8 - alphaLength);
+                }
+
+                scanline[x] = a << 24 | r << 16 | g << 8 | b;
+            }
+
+            image.setRGB(0, y, rawImage.width, 1, scanline,
+                    0, rawImage.width);
+        }
+    }
+
+    private static void rawImage16toARGB(BufferedImage image, RawImage rawImage) {
+        int[] scanline = new int[rawImage.width];
+        byte[] buffer = rawImage.data;
+        int index = 0;
+
+        for (int y = 0 ; y < rawImage.height ; y++) {
+            for (int x = 0 ; x < rawImage.width ; x++) {
+                int value = buffer[index++] & 0x00FF;
+                value |= (buffer[index++] << 8) & 0x0FF00;
+
+                int r = ((value >> 11) & 0x01F) << 3;
+                int g = ((value >> 5) & 0x03F) << 2;
+                int b = ((value     ) & 0x01F) << 3;
+
+                scanline[x] = 0xFF << 24 | r << 16 | g << 8 | b;
+            }
+
+            image.setRGB(0, y, rawImage.width, 1, scanline,
+                    0, rawImage.width);
+        }
+    }
+}
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/LoggingOutputReceiver.java b/tools/monkeyrunner/src/com/android/monkeyrunner/LoggingOutputReceiver.java
new file mode 100644
index 0000000..5b8eb45
--- /dev/null
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/LoggingOutputReceiver.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+import com.android.ddmlib.IShellOutputReceiver;
+
+import java.util.logging.Logger;
+
+public class LoggingOutputReceiver implements IShellOutputReceiver {
+    private final Logger log;
+
+    public LoggingOutputReceiver(Logger log) {
+        this.log = log;
+    }
+
+    public void addOutput(byte[] data, int offset, int length) {
+        log.info(new String(data, offset, length));
+    }
+
+    public void flush() { }
+
+    public boolean isCancelled() {
+        return false;
+    }
+}
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java
new file mode 100644
index 0000000..d9fa7be
--- /dev/null
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java
@@ -0,0 +1,349 @@
+/*
+ * 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;
+
+import com.google.common.collect.Lists;
+
+import com.android.ddmlib.IDevice;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.StringTokenizer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Provides a nicer interface to interacting with the low-level network access protocol for talking
+ * to the monkey.
+ *
+ * This class is thread-safe and can handle being called from multiple threads.
+ */
+public class MonkeyManager {
+    private static String DEFAULT_MONKEY_SERVER_ADDRESS = "127.0.0.1";
+    private static int DEFAULT_MONKEY_PORT = 12345;
+
+    private static Logger LOG = Logger.getLogger(MonkeyManager.class.getName());
+
+    private Socket monkeySocket;
+    private BufferedWriter monkeyWriter;
+    private BufferedReader monkeyReader;
+    private final IDevice device;
+
+    /**
+     * Create a new MonkeyMananger to talk to the specified device.
+     *
+     * @param device the device to talk to
+     * @param address the address on which to talk to the device
+     * @param port the port on which to talk to the device
+     */
+    public MonkeyManager(IDevice device, String address, int port) {
+        this.device = device;
+        device.createForward(port, port);
+        String command = "monkey --port " + port + "&";
+        try {
+            device.executeShellCommand(command, new LoggingOutputReceiver(LOG));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        try {
+            InetAddress addr = InetAddress.getByName(address);
+            monkeySocket = new Socket(addr, port);
+            monkeyWriter = new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream()));
+            monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream()));
+        } catch (UnknownHostException e) {
+            throw new RuntimeException(e);
+        } catch(IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Create a new MonkeyMananger to talk to the specified device.
+     *
+     * @param device the device to talk to
+     */
+    public MonkeyManager(IDevice device) {
+        this(device, DEFAULT_MONKEY_SERVER_ADDRESS, DEFAULT_MONKEY_PORT);
+    }
+
+    /**
+     * Send a touch down event at the specified location.
+     *
+     * @param x the x coordinate of where to click
+     * @param y the y coordinate of where to click
+     * @return success or not
+     * @throws IOException on error communicating with the device
+     */
+    public boolean touchDown(int x, int y) throws IOException {
+        return sendMonkeyEvent("touch down " + x + " " + y);
+    }
+
+    /**
+     * Send a touch down event at the specified location.
+     *
+     * @param x the x coordinate of where to click
+     * @param y the y coordinate of where to click
+     * @return success or not
+     * @throws IOException on error communicating with the device
+     */
+    public boolean touchUp(int x, int y) throws IOException {
+        return sendMonkeyEvent("touch up " + x + " " + y);
+    }
+
+    /**
+     * Send a touch (down and then up) event at the specified location.
+     *
+     * @param x the x coordinate of where to click
+     * @param y the y coordinate of where to click
+     * @return success or not
+     * @throws IOException on error communicating with the device
+     */
+    public boolean touch(int x, int y) throws IOException {
+        return sendMonkeyEvent("tap " + x + " " + y);
+    }
+
+    /**
+     * Press a physical button on the device.
+     *
+     * @param name the name of the button (As specified in the protocol)
+     * @return success or not
+     * @throws IOException on error communicating with the device
+     */
+    public boolean press(String name) throws IOException {
+        return sendMonkeyEvent("press " + name);
+    }
+
+    /**
+     * Press a physical button on the device.
+     *
+     * @param button the button to press
+     * @return success or not
+     * @throws IOException on error communicating with the device
+     */
+    public boolean press(PhysicalButton button) throws IOException {
+        return press(button.getKeyName());
+    }
+
+    /**
+     * This function allows the communication bridge between the host and the device
+     * to be invisible to the script for internal needs.
+     * It splits a command into monkey events and waits for responses for each over an adb tcp socket.
+     * Returns on an error, else continues and sets up last response.
+     *
+     * @param command the monkey command to send to the device
+     * @return the (unparsed) response returned from the monkey.
+     */
+    private String sendMonkeyEventAndGetResponse(String command) throws IOException {
+        command = command.trim();
+        LOG.info("Monkey Command: " + command + ".");
+
+        // send a single command and get the response
+        monkeyWriter.write(command + "\n");
+        monkeyWriter.flush();
+        return monkeyReader.readLine();
+    }
+
+    /**
+     * Parse a monkey response string to see if the command succeeded or not.
+     *
+     * @param monkeyResponse the response
+     * @return true if response code indicated success.
+     */
+    private boolean parseResponseForSuccess(String monkeyResponse) {
+        if (monkeyResponse == null) {
+            return false;
+        }
+        // return on ok
+        if(monkeyResponse.startsWith("OK")) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Parse a monkey response string to get the extra data returned.
+     *
+     * @param monkeyResponse the response
+     * @return any extra data that was returned, or empty string if there was nothing.
+     */
+    private String parseResponseForExtra(String monkeyResponse) {
+        int offset = monkeyResponse.indexOf(':');
+        if (offset < 0) {
+            return "";
+        }
+        return monkeyResponse.substring(offset + 1);
+    }
+
+    /**
+     * This function allows the communication bridge between the host and the device
+     * to be invisible to the script for internal needs.
+     * It splits a command into monkey events and waits for responses for each over an
+     * adb tcp socket.
+     *
+     * @param command the monkey command to send to the device
+     * @return true on success.
+     */
+    private boolean sendMonkeyEvent(String command) throws IOException {
+        synchronized (this) {
+            String monkeyResponse = sendMonkeyEventAndGetResponse(command);
+            return parseResponseForSuccess(monkeyResponse);
+        }
+    }
+
+    /**
+     * Close all open resources related to this device.
+     */
+    public void close() {
+        try {
+            monkeySocket.close();
+        } catch (IOException e) {
+            LOG.log(Level.SEVERE, "Unable to close monkeySocket", e);
+        }
+        try {
+            monkeyReader.close();
+        } catch (IOException e) {
+            LOG.log(Level.SEVERE, "Unable to close monkeyReader", e);
+        }
+        try {
+            monkeyWriter.close();
+        } catch (IOException e) {
+            LOG.log(Level.SEVERE, "Unable to close monkeyWriter", e);
+        }
+    }
+
+    /**
+     * Function to get a static variable from the device
+     *
+     * @param name name of static variable to get
+     * @return the value of the variable, or empty string if there was an error
+     */
+    public String getVariable(String name) throws IOException {
+        synchronized (this) {
+            String response = sendMonkeyEventAndGetResponse("getvar " + name);
+            if (!parseResponseForSuccess(response)) {
+                return "";
+            }
+            return parseResponseForExtra(response);
+        }
+    }
+
+    /**
+     * Function to get the list of static variables from the device
+     */
+    public Collection<String> listVariable() throws IOException {
+        synchronized (this) {
+            String response = sendMonkeyEventAndGetResponse("listvar");
+            if (!parseResponseForSuccess(response)) {
+                Collections.emptyList();
+            }
+            String extras = parseResponseForExtra(response);
+            return Lists.newArrayList(extras.split(" "));
+        }
+    }
+
+    /**
+     * Tells the monkey that we are done for this session.
+     * @throws IOException
+     */
+    public void done() throws IOException {
+        // this command just drops the connection, so handle it here
+        synchronized (this) {
+            sendMonkeyEventAndGetResponse("done");
+        }
+    }
+
+    /**
+     * Send a tap event at the specified location.
+     *
+     * @param x the x coordinate of where to click
+     * @param y the y coordinate of where to click
+     * @return success or not
+     * @throws IOException
+     * @throws IOException on error communicating with the device
+     */
+    public boolean tap(int x, int y) throws IOException {
+        return sendMonkeyEvent("tap " + x + " " + y);
+    }
+
+    /**
+     * Type the following string to the monkey.
+     *
+     * @param text the string to type
+     * @return success
+     * @throws IOException
+     */
+    public boolean type(String text) throws IOException {
+        // The network protocol can't handle embedded line breaks, so we have to handle it
+        // here instead
+        StringTokenizer tok = new StringTokenizer(text, "\n", true);
+        while (tok.hasMoreTokens()) {
+            String line = tok.nextToken();
+            if ("\n".equals(line)) {
+                boolean success = press(PhysicalButton.ENTER);
+                if (!success) {
+                    return false;
+                }
+            } else {
+                boolean success = sendMonkeyEvent("type " + line);
+                if (!success) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Type the character to the monkey.
+     *
+     * @param keyChar the character to type.
+     * @return success
+     * @throws IOException
+     */
+    public boolean type(char keyChar) throws IOException {
+        return type(Character.toString(keyChar));
+    }
+
+    /**
+     * Gets the underlying device so low-level commands can be executed.
+     *
+     * NOTE: using this method doesn't provide any thread safety.  If needed, the MonkeyMananger
+     * itself should be used as the lock for synchronization.  For Example:
+     *
+     * <code>
+     * MonkeyMananger mgr;
+     * IDevice device = mgr.getDevice();
+     * synchronized (mgr) {
+     *   /// Do stuff with the device
+     * }
+     * </code>
+     *
+     * @return the device.
+     */
+    public IDevice getDevice() {
+        return device;
+    }
+}
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java b/tools/monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java
new file mode 100644
index 0000000..f0525a0
--- /dev/null
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+public enum PhysicalButton {
+    HOME("home"),
+    SEARCH("search"),
+    MENU("menu"),
+    BACK("back"),
+    DPAD_UP("DPAD_UP"),
+    DPAD_DOWN("DPAD_DOWN"),
+    DPAD_LEFT("DPAD_LEFT"),
+    DPAD_RIGHT("DPAD_RIGHT"),
+    DPAD_CENTER("DPAD_CENTER"),
+    ENTER("enter");
+
+    private String keyName;
+
+    private PhysicalButton(String keyName) {
+        this.keyName = keyName;
+    }
+
+    public String getKeyName() {
+        return keyName;
+    }
+}