OSDN Git Service

Extracted ChimpChat from MonkeyRunner
authorMichael Wright <michaelwr@google.com>
Mon, 13 Jun 2011 16:15:08 +0000 (09:15 -0700)
committerMichael Wright <michaelwr@google.com>
Mon, 20 Jun 2011 21:24:05 +0000 (14:24 -0700)
Extracted ChimpChat, the library for communication with Monkey from
MonkeyRunner

Change-Id: Ia9f966549d27abc9f494b2b001099d8130dea376

20 files changed:
chimpchat/src/Android.mk
chimpchat/src/com/android/chimpchat/ChimpChat.java
chimpchat/src/com/android/chimpchat/ChimpManager.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/PhysicalButton.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/AdbBackend.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/AdbChimpDevice.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/AdbChimpImage.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/CommandOutputCapture.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/LinearInterpolator.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/LoggingOutputReceiver.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/image/CaptureRawAndConvertedImage.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/image/ImageUtils.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/image/SixteenBitColorModel.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/adb/image/ThirtyTwoBitColorModel.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/core/ChimpImageBase.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/core/IChimpBackend.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/core/IChimpDevice.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/core/IChimpImage.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/core/TouchPressType.java [new file with mode: 0644]
chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java [new file with mode: 0644]

index 301ec71..82a2052 100644 (file)
@@ -18,6 +18,13 @@ include $(CLEAR_VARS)
 
 LOCAL_SRC_FILES := $(call all-subdir-java-files)
 
+LOCAL_JAVA_LIBRARIES := \
+       ddmlib \
+       hierarchyviewerlib \
+       guavalib \
+       sdklib \
+       swt
+
 LOCAL_MODULE := chimpchat
 
 LOCAL_MODULE_TAGS := eng
index 71684c9..1927535 100644 (file)
 
 package com.android.chimpchat;
 
+import com.android.chimpchat.adb.AdbBackend;
+import com.android.chimpchat.core.IChimpBackend;
+
 import java.util.Map;
 
+/**
+ * ChimpChat is a host-side library that provides an API for communication with
+ * an instance of Monkey on a device. This class provides an entry point to
+ * setting up communication with a device. Currently it only supports communciation
+ * over ADB, however.
+ */
 public class ChimpChat {
-    private final Map<String, String> options;
+    private final IChimpBackend mBackend;
+
+    private ChimpChat(IChimpBackend backend) {
+        this.mBackend = backend;
+    }
+
+    /**
+     * Generates a new instance of ChimpChat based on the options passed.
+     * @param options a map of settings for the new ChimpChat instance
+     * @return a new instance of ChimpChat or null if there was an issue setting up the backend
+     */
+    public static ChimpChat getInstance(Map<String, String> options) {
+        IChimpBackend backend = ChimpChat.createBackendByName(options.get("backend"));
+        if (backend == null) {
+            return null;
+        }
+        ChimpChat chimpchat = new ChimpChat(backend);
+        return chimpchat;
+    }
+
 
-    public ChimpChat(Map<String, String> options) {
-        this.options = options;
+    public static IChimpBackend createBackendByName(String backendName) {
+        if ("adb".equals(backendName)) {
+            return new AdbBackend();
+        } else {
+            return null;
+        }
     }
 }
diff --git a/chimpchat/src/com/android/chimpchat/ChimpManager.java b/chimpchat/src/com/android/chimpchat/ChimpManager.java
new file mode 100644 (file)
index 0000000..a858e6a
--- /dev/null
@@ -0,0 +1,350 @@
+
+/*
+ * 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.chimpchat;
+
+import com.google.common.collect.Lists;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+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 ChimpManager {
+    private static Logger LOG = Logger.getLogger(ChimpManager.class.getName());
+
+    private Socket monkeySocket;
+    private BufferedWriter monkeyWriter;
+    private BufferedReader monkeyReader;
+
+    /**
+     * Create a new ChimpMananger to talk to the specified device.
+     *
+     * @param monkeySocket the already connected socket on which to send protocol messages.
+     * @throws IOException if there is an issue setting up the sockets
+     */
+    public ChimpManager(Socket monkeySocket) throws IOException {
+        this.monkeySocket = monkeySocket;
+        monkeyWriter =
+                new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream()));
+        monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream()));
+    }
+
+    /**
+     * 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 move 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 touchMove(int x, int y) throws IOException {
+        return sendMonkeyEvent("touch move " + 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);
+    }
+
+    /**
+     * Send a Key Down event for the specified button.
+     *
+     * @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 keyDown(String name) throws IOException {
+        return sendMonkeyEvent("key down " + name);
+    }
+
+    /**
+     * Send a Key Up event for the specified button.
+     *
+     * @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 keyUp(String name) throws IOException {
+        return sendMonkeyEvent("key up " + 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 null if there was an error
+     */
+    public String getVariable(String name) throws IOException {
+        synchronized (this) {
+            String response = sendMonkeyEventAndGetResponse("getvar " + name);
+            if (!parseResponseForSuccess(response)) {
+                return null;
+            }
+            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");
+        }
+    }
+
+    /**
+     * Tells the monkey that we are done forever.
+     * @throws IOException
+     */
+    public void quit() throws IOException {
+        // this command drops the connection, so handle it here
+        synchronized (this) {
+            sendMonkeyEventAndGetResponse("quit");
+        }
+    }
+
+    /**
+     * 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));
+    }
+
+    /**
+     * Wake the device up from sleep.
+     * @throws IOException
+     */
+    public void wake() throws IOException {
+        sendMonkeyEvent("wake");
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/PhysicalButton.java b/chimpchat/src/com/android/chimpchat/PhysicalButton.java
new file mode 100644 (file)
index 0000000..9363c08
--- /dev/null
@@ -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.chimpchat;
+
+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;
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/AdbBackend.java b/chimpchat/src/com/android/chimpchat/adb/AdbBackend.java
new file mode 100644 (file)
index 0000000..e2f9ca0
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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.chimpchat.adb;
+
+import com.google.common.collect.Lists;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.IDevice;
+import com.android.chimpchat.core.IChimpBackend;
+import com.android.chimpchat.core.IChimpDevice;
+import com.android.sdklib.SdkConstants;
+
+import java.io.File;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/**
+ * Backend implementation that works over ADB to talk to the device.
+ */
+public class AdbBackend implements IChimpBackend {
+    private static Logger LOG = Logger.getLogger(AdbBackend.class.getCanonicalName());
+    // How long to wait each time we check for the device to be connected.
+    private static final int CONNECTION_ITERATION_TIMEOUT_MS = 200;
+    private final List<IChimpDevice> devices = Lists.newArrayList();
+    private final AndroidDebugBridge bridge;
+
+    public AdbBackend() {
+        // [try to] ensure ADB is running
+        String adbLocation = findAdb();
+
+        AndroidDebugBridge.init(false /* debugger support */);
+
+        bridge = AndroidDebugBridge.createBridge(
+                adbLocation, true /* forceNewBridge */);
+    }
+
+    private String findAdb() {
+        File location =
+            new File(AdbBackend.class.getProtectionDomain().getCodeSource().getLocation().getPath());
+        String mrParentLocation = new File(location.getParent()).getParent();
+
+        // in the new SDK, adb is in the platform-tools, but when run from the command line
+        // in the Android source tree, then adb is next to monkeyrunner.
+        if (mrParentLocation != null && mrParentLocation.length() != 0) {
+            // check if there's a platform-tools folder
+            File platformTools = new File(new File(mrParentLocation).getParent(),
+                    SdkConstants.FD_PLATFORM_TOOLS);
+            if (platformTools.isDirectory()) {
+                return platformTools.getAbsolutePath() + File.separator + SdkConstants.FN_ADB;
+            }
+
+            return mrParentLocation + File.separator + SdkConstants.FD_OUTPUT +
+                    File.separator + SdkConstants.FN_ADB;
+        }
+
+        return SdkConstants.FN_ADB;
+    }
+
+    /**
+     * Checks the attached devices looking for one whose device id matches the specified regex.
+     *
+     * @param deviceIdRegex the regular expression to match against
+     * @return the Device (if found), or null (if not found).
+     */
+    private IDevice findAttachedDevice(String deviceIdRegex) {
+        Pattern pattern = Pattern.compile(deviceIdRegex);
+        for (IDevice device : bridge.getDevices()) {
+            String serialNumber = device.getSerialNumber();
+            if (pattern.matcher(serialNumber).matches()) {
+                return device;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public IChimpDevice waitForConnection() {
+        return waitForConnection(Integer.MAX_VALUE, ".*");
+    }
+
+    @Override
+    public IChimpDevice waitForConnection(long timeoutMs, String deviceIdRegex) {
+        do {
+            IDevice device = findAttachedDevice(deviceIdRegex);
+            // Only return the device when it is online
+            if (device != null && device.getState() == IDevice.DeviceState.ONLINE) {
+                IChimpDevice chimpDevice = new AdbChimpDevice(device);
+                devices.add(chimpDevice);
+                return chimpDevice;
+            }
+
+            try {
+                Thread.sleep(CONNECTION_ITERATION_TIMEOUT_MS);
+            } catch (InterruptedException e) {
+                LOG.log(Level.SEVERE, "Error sleeping", e);
+            }
+            timeoutMs -= CONNECTION_ITERATION_TIMEOUT_MS;
+        } while (timeoutMs > 0);
+
+        // Timeout.  Give up.
+        return null;
+    }
+
+    @Override
+    public void shutdown() {
+        for (IChimpDevice device : devices) {
+            device.dispose();
+        }
+        AndroidDebugBridge.terminate();
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/AdbChimpDevice.java b/chimpchat/src/com/android/chimpchat/adb/AdbChimpDevice.java
new file mode 100644 (file)
index 0000000..5b70148
--- /dev/null
@@ -0,0 +1,557 @@
+/*
+ * 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.chimpchat.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.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.InstallException;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.chimpchat.ChimpManager;
+import com.android.chimpchat.adb.LinearInterpolator.Point;
+import com.android.chimpchat.core.IChimpImage;
+import com.android.chimpchat.core.IChimpDevice;
+import com.android.chimpchat.core.TouchPressType;
+import com.android.chimpchat.hierarchyviewer.HierarchyViewer;
+
+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 AdbChimpDevice implements IChimpDevice {
+    private static final Logger LOG = Logger.getLogger(AdbChimpDevice.class.getName());
+
+    private static final String[] ZERO_LENGTH_STRING_ARRAY = new String[0];
+    private static final long MANAGER_CREATE_TIMEOUT_MS = 30 * 1000; // 30 seconds
+    private static final long MANAGER_CREATE_WAIT_TIME_MS = 1000; // wait 1 second
+
+    private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    private final IDevice device;
+    private ChimpManager manager;
+
+    public AdbChimpDevice(IDevice device) {
+        this.device = device;
+        this.manager = createManager("127.0.0.1", 12345);
+
+        Preconditions.checkNotNull(this.manager);
+    }
+
+    @Override
+    public ChimpManager 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;
+    }
+
+    @Override
+    public HierarchyViewer getHierarchyViewer() {
+        return new HierarchyViewer(device);
+    }
+
+    private void executeAsyncCommand(final String command,
+            final LoggingOutputReceiver logger) {
+        executor.submit(new Runnable() {
+            @Override
+            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) {
+                    // This happens a lot
+                    LOG.log(Level.INFO, "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 ChimpManager 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;
+        ChimpManager 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 chimp mananger");
+                return null;
+            }
+
+            try {
+                Thread.sleep(MANAGER_CREATE_WAIT_TIME_MS);
+            } catch (InterruptedException e) {
+                LOG.log(Level.SEVERE, "Unable to sleep", e);
+            }
+
+            Socket monkeySocket;
+            try {
+                monkeySocket = new Socket(addr, port);
+            } catch (IOException e) {
+                LOG.log(Level.FINE, "Unable to connect socket", e);
+                success = false;
+                continue;
+            }
+
+            try {
+                mm = new ChimpManager(monkeySocket);
+            } catch (IOException e) {
+                LOG.log(Level.SEVERE, "Unable to open writer and reader to socket");
+                continue;
+            }
+
+            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 IChimpImage takeSnapshot() {
+        try {
+            return new AdbChimpImage(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
+    public String getSystemProperty(String key) {
+        return device.getProperty(key);
+    }
+
+    @Override
+    public 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
+    public 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
+    public 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
+    public 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
+    public 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
+    public 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
+    public void type(String string) {
+        try {
+            manager.type(string);
+        } catch (IOException e) {
+            LOG.log(Level.SEVERE, "Error Typing: " + string, e);
+        }
+    }
+
+    @Override
+    public 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
+    public 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
+    public 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
+    public 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 = "--es";
+            }
+            parts.add(arg);
+            parts.add(entry.getKey());
+            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
+    public 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
+    public 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() {
+            @Override
+            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);
+                }
+            }
+
+            @Override
+            public void start(Point point) {
+                try {
+                    manager.touchDown(point.getX(), point.getY());
+                    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);
+                }
+            }
+
+            @Override
+            public void end(Point point) {
+                try {
+                    manager.touchMove(point.getX(), point.getY());
+                    manager.touchUp(point.getX(), point.getY());
+                } catch (IOException e) {
+                    LOG.log(Level.SEVERE, "Error sending drag end event", e);
+                }
+            }
+        });
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/AdbChimpImage.java b/chimpchat/src/com/android/chimpchat/adb/AdbChimpImage.java
new file mode 100644 (file)
index 0000000..2d41600
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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.chimpchat.adb;
+
+import com.android.ddmlib.RawImage;
+import com.android.chimpchat.adb.image.ImageUtils;
+import com.android.chimpchat.core.ChimpImageBase;
+
+import java.awt.image.BufferedImage;
+
+/**
+ * ADB implementation of the ChimpImage class.
+ */
+public class AdbChimpImage extends ChimpImageBase {
+    private final RawImage image;
+
+    /**
+     * Create a new AdbMonkeyImage.
+     *
+     * @param image the image from adb.
+     */
+    AdbChimpImage(RawImage image) {
+        this.image = image;
+    }
+
+    @Override
+    public BufferedImage createBufferedImage() {
+        return ImageUtils.convertImage(image);
+    }
+
+    public RawImage getRawImage() {
+        return image;
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/CommandOutputCapture.java b/chimpchat/src/com/android/chimpchat/adb/CommandOutputCapture.java
new file mode 100644 (file)
index 0000000..eadd697
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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.chimpchat.adb;
+
+import com.android.ddmlib.IShellOutputReceiver;
+
+/**
+ * Shell Output Receiver that captures shell output into a String for
+ * later retrieval.
+ */
+public class CommandOutputCapture implements IShellOutputReceiver {
+    private final StringBuilder builder = new StringBuilder();
+
+    public void flush() { }
+
+    public boolean isCancelled() {
+        return false;
+    }
+
+    public void addOutput(byte[] data, int offset, int length) {
+        String message = new String(data, offset, length);
+        builder.append(message);
+    }
+
+    @Override
+    public String toString() {
+        return builder.toString();
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/LinearInterpolator.java b/chimpchat/src/com/android/chimpchat/adb/LinearInterpolator.java
new file mode 100644 (file)
index 0000000..708007d
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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.chimpchat.adb;
+
+
+
+/**
+ * Linear Interpolation class.
+ */
+public class LinearInterpolator {
+    private final int steps;
+
+    /**
+     * Use our own Point class so we don't pull in java.awt.* just for this simple class.
+     */
+    public static class Point {
+        private final int x;
+        private final int y;
+
+        public Point(int x, int y) {
+            this.x = x;
+            this.y = y;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder().
+                append("(").
+                append(x).
+                append(",").
+                append(y).
+                append(")").toString();
+        }
+
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof Point) {
+                Point that = (Point) obj;
+                return this.x == that.x && this.y == that.y;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return 0x43125315 + x + y;
+        }
+
+        public int getX() {
+            return x;
+        }
+
+        public int getY() {
+            return y;
+        }
+    }
+
+    /**
+     * Callback interface to recieve interpolated points.
+     */
+    public interface Callback {
+        /**
+         * Called once to inform of the start point.
+         */
+        void start(Point point);
+        /**
+         * Called once to inform of the end point.
+         */
+        void end(Point point);
+        /**
+         * Called at every step in-between start and end.
+         */
+        void step(Point point);
+    }
+
+    /**
+     * Create a new linear Interpolator.
+     *
+     * @param steps How many steps should be in a single run.  This counts the intervals
+     *              in-between points, so the actual number of points generated will be steps + 1.
+     */
+    public LinearInterpolator(int steps) {
+        this.steps = steps;
+    }
+
+    // Copied from android.util.MathUtils since we couldn't link it in on the host.
+    private static float lerp(float start, float stop, float amount) {
+        return start + (stop - start) * amount;
+    }
+
+    /**
+     * Calculate the interpolated points.
+     *
+     * @param start The starting point
+     * @param end The ending point
+     * @param callback the callback to call with each calculated points.
+     */
+    public void interpolate(Point start, Point end, Callback callback) {
+        int xDistance = Math.abs(end.getX() - start.getX());
+        int yDistance = Math.abs(end.getY() - start.getY());
+        float amount = (float) (1.0 / steps);
+
+
+        callback.start(start);
+        for (int i = 1; i < steps; i++) {
+            float newX = lerp(start.getX(), end.getX(), amount * i);
+            float newY = lerp(start.getY(), end.getY(), amount * i);
+
+            callback.step(new Point(Math.round(newX), Math.round(newY)));
+        }
+        // Generate final point
+        callback.end(end);
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/LoggingOutputReceiver.java b/chimpchat/src/com/android/chimpchat/adb/LoggingOutputReceiver.java
new file mode 100644 (file)
index 0000000..e318a01
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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.chimpchat.adb;
+
+import com.android.ddmlib.IShellOutputReceiver;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Shell Output Receiver that sends shell output to a Logger.
+ */
+public class LoggingOutputReceiver implements IShellOutputReceiver {
+    private final Logger log;
+    private final Level level;
+
+    public LoggingOutputReceiver(Logger log, Level level) {
+        this.log = log;
+        this.level = level;
+    }
+
+    public void addOutput(byte[] data, int offset, int length) {
+        String message = new String(data, offset, length);
+        for (String line : message.split("\n")) {
+            log.log(level, line);
+        }
+    }
+
+    public void flush() { }
+
+    public boolean isCancelled() {
+        return false;
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/image/CaptureRawAndConvertedImage.java b/chimpchat/src/com/android/chimpchat/adb/image/CaptureRawAndConvertedImage.java
new file mode 100644 (file)
index 0000000..2b700ea
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * 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.chimpchat.adb.image;
+
+import com.android.ddmlib.RawImage;
+import com.android.chimpchat.adb.AdbBackend;
+import com.android.chimpchat.adb.AdbChimpImage;
+import com.android.chimpchat.core.IChimpBackend;
+import com.android.chimpchat.core.IChimpImage;
+import com.android.chimpchat.core.IChimpDevice;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+/**
+ * Utility program to capture raw and converted images from a device and write them to a file.
+ * This is used to generate the test data for ImageUtilsTest.
+ */
+public class CaptureRawAndConvertedImage {
+    public static class ChimpRunnerRawImage implements Serializable {
+        public int version;
+        public int bpp;
+        public int size;
+        public int width;
+        public int height;
+        public int red_offset;
+        public int red_length;
+        public int blue_offset;
+        public int blue_length;
+        public int green_offset;
+        public int green_length;
+        public int alpha_offset;
+        public int alpha_length;
+
+        public byte[] data;
+
+        public ChimpRunnerRawImage(RawImage rawImage) {
+            version = rawImage.version;
+            bpp = rawImage.bpp;
+            size = rawImage.size;
+            width = rawImage.width;
+            height = rawImage.height;
+            red_offset = rawImage.red_offset;
+            red_length = rawImage.red_length;
+            blue_offset = rawImage.blue_offset;
+            blue_length = rawImage.blue_length;
+            green_offset = rawImage.green_offset;
+            green_length = rawImage.green_length;
+            alpha_offset = rawImage.alpha_offset;
+            alpha_length = rawImage.alpha_length;
+
+            data = rawImage.data;
+        }
+
+        public RawImage toRawImage() {
+            RawImage rawImage = new RawImage();
+
+            rawImage.version = version;
+            rawImage.bpp = bpp;
+            rawImage.size = size;
+            rawImage.width = width;
+            rawImage.height = height;
+            rawImage.red_offset = red_offset;
+            rawImage.red_length = red_length;
+            rawImage.blue_offset = blue_offset;
+            rawImage.blue_length = blue_length;
+            rawImage.green_offset = green_offset;
+            rawImage.green_length = green_length;
+            rawImage.alpha_offset = alpha_offset;
+            rawImage.alpha_length = alpha_length;
+
+            rawImage.data = data;
+            return rawImage;
+        }
+    }
+
+    private static void writeOutImage(RawImage screenshot, String name) throws IOException {
+        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(name));
+        out.writeObject(new ChimpRunnerRawImage(screenshot));
+        out.close();
+    }
+
+    public static void main(String[] args) throws IOException {
+        IChimpBackend backend = new AdbBackend();
+        IChimpDevice device = backend.waitForConnection();
+        IChimpImage snapshot = (IChimpImage) device.takeSnapshot();
+
+        // write out to a file
+        snapshot.writeToFile("output.png", "png");
+        writeOutImage(((AdbChimpImage)snapshot).getRawImage(), "output.raw");
+        System.exit(0);
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/image/ImageUtils.java b/chimpchat/src/com/android/chimpchat/adb/image/ImageUtils.java
new file mode 100644 (file)
index 0000000..39ec533
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 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.chimpchat.adb.image;
+
+import com.android.ddmlib.RawImage;
+
+import java.awt.Point;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.util.Hashtable;
+/**
+ * Useful image related functions.
+ */
+public class ImageUtils {
+    // Utility class
+    private ImageUtils() { }
+
+    private static Hashtable<?,?> EMPTY_HASH = new Hashtable();
+    private static int[] BAND_OFFSETS_32 = { 0, 1, 2, 3 };
+    private static int[] BAND_OFFSETS_16 = { 0, 1 };
+
+    /**
+     * Convert a raw image into a buffered image.
+     *
+     * @param rawImage the raw image to convert
+     * @param image the old image to (possibly) recycle
+     * @return the converted image
+     */
+    public static BufferedImage convertImage(RawImage rawImage, BufferedImage image) {
+        switch (rawImage.bpp) {
+            case 16:
+                return rawImage16toARGB(image, rawImage);
+            case 32:
+                return rawImage32toARGB(rawImage);
+        }
+        return null;
+    }
+
+    /**
+     * Convert a raw image into a buffered image.
+     *
+     * @param rawImage the image to convert.
+     * @return the converted image.
+     */
+    public static BufferedImage convertImage(RawImage rawImage) {
+        return convertImage(rawImage, null);
+    }
+
+    static int getMask(int length) {
+        int res = 0;
+        for (int i = 0 ; i < length ; i++) {
+            res = (res << 1) + 1;
+        }
+
+        return res;
+    }
+
+    private static BufferedImage rawImage32toARGB(RawImage rawImage) {
+        // Do as much as we can to not make an extra copy of the data.  This is just a bunch of
+        // classes that wrap's the raw byte array of the image data.
+        DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size);
+
+        PixelInterleavedSampleModel sampleModel =
+            new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height,
+                    4, rawImage.width * 4, BAND_OFFSETS_32);
+        WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer,
+                new Point(0, 0));
+        return new BufferedImage(new ThirtyTwoBitColorModel(rawImage), raster, false, EMPTY_HASH);
+    }
+
+    private static BufferedImage rawImage16toARGB(BufferedImage image, RawImage rawImage) {
+        // Do as much as we can to not make an extra copy of the data.  This is just a bunch of
+        // classes that wrap's the raw byte array of the image data.
+        DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size);
+
+        PixelInterleavedSampleModel sampleModel =
+            new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height,
+                    2, rawImage.width * 2, BAND_OFFSETS_16);
+        WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer,
+                new Point(0, 0));
+        return new BufferedImage(new SixteenBitColorModel(rawImage), raster, false, EMPTY_HASH);
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/image/SixteenBitColorModel.java b/chimpchat/src/com/android/chimpchat/adb/image/SixteenBitColorModel.java
new file mode 100644 (file)
index 0000000..1a1fbd9
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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.chimpchat.adb.image;
+
+import com.android.ddmlib.RawImage;
+
+import java.awt.Transparency;
+import java.awt.color.ColorSpace;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+
+/**
+ * Internal color model used to do conversion of 16bpp RawImages.
+ */
+class SixteenBitColorModel extends ColorModel {
+    private static final int[] BITS = {
+        8, 8, 8, 8
+    };
+    public SixteenBitColorModel(RawImage rawImage) {
+        super(32
+                , BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB),
+                true, false, Transparency.TRANSLUCENT,
+                DataBuffer.TYPE_BYTE);
+    }
+
+    @Override
+    public boolean isCompatibleRaster(Raster raster) {
+        return true;
+    }
+
+    private int getPixel(Object inData) {
+        byte[] data = (byte[]) inData;
+        int value = data[0] & 0x00FF;
+        value |= (data[1] << 8) & 0x0FF00;
+
+        return value;
+    }
+
+    @Override
+    public int getAlpha(Object inData) {
+        return 0xff;
+    }
+
+    @Override
+    public int getBlue(Object inData) {
+        int pixel = getPixel(inData);
+        return ((pixel >> 0) & 0x01F) << 3;
+    }
+
+    @Override
+    public int getGreen(Object inData) {
+        int pixel = getPixel(inData);
+        return ((pixel >> 5) & 0x03F) << 2;
+    }
+
+    @Override
+    public int getRed(Object inData) {
+        int pixel = getPixel(inData);
+        return ((pixel >> 11) & 0x01F) << 3;
+    }
+
+    @Override
+    public int getAlpha(int pixel) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getBlue(int pixel) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getGreen(int pixel) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getRed(int pixel) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/adb/image/ThirtyTwoBitColorModel.java b/chimpchat/src/com/android/chimpchat/adb/image/ThirtyTwoBitColorModel.java
new file mode 100644 (file)
index 0000000..dda43dc
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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.chimpchat.adb.image;
+
+import com.android.ddmlib.RawImage;
+
+import java.awt.Transparency;
+import java.awt.color.ColorSpace;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+
+/**
+ * Internal color model used to do conversion of 32bpp RawImages.
+ */
+class ThirtyTwoBitColorModel extends ColorModel {
+    private static final int[] BITS = {
+        8, 8, 8, 8,
+    };
+    private final int alphaLength;
+    private final int alphaMask;
+    private final int alphaOffset;
+    private final int blueMask;
+    private final int blueLength;
+    private final int blueOffset;
+    private final int greenMask;
+    private final int greenLength;
+    private final int greenOffset;
+    private final int redMask;
+    private final int redLength;
+    private final int redOffset;
+
+    public ThirtyTwoBitColorModel(RawImage rawImage) {
+        super(32, BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB),
+                true, false, Transparency.TRANSLUCENT,
+                DataBuffer.TYPE_BYTE);
+
+        redOffset = rawImage.red_offset;
+        redLength = rawImage.red_length;
+        redMask = ImageUtils.getMask(redLength);
+        greenOffset = rawImage.green_offset;
+        greenLength = rawImage.green_length;
+        greenMask = ImageUtils.getMask(greenLength);
+        blueOffset = rawImage.blue_offset;
+        blueLength = rawImage.blue_length;
+        blueMask = ImageUtils.getMask(blueLength);
+        alphaLength = rawImage.alpha_length;
+        alphaOffset = rawImage.alpha_offset;
+        alphaMask = ImageUtils.getMask(alphaLength);
+    }
+
+    @Override
+    public boolean isCompatibleRaster(Raster raster) {
+        return true;
+    }
+
+    private int getPixel(Object inData) {
+        byte[] data = (byte[]) inData;
+        int value = data[0] & 0x00FF;
+        value |= (data[1] & 0x00FF) << 8;
+        value |= (data[2] & 0x00FF) << 16;
+        value |= (data[3] & 0x00FF) << 24;
+
+        return value;
+    }
+
+    @Override
+    public int getAlpha(Object inData) {
+        int pixel = getPixel(inData);
+        if(alphaLength == 0) {
+            return 0xff;
+        }
+        return ((pixel >>> alphaOffset) & alphaMask) << (8 - alphaLength);
+    }
+
+    @Override
+    public int getBlue(Object inData) {
+        int pixel = getPixel(inData);
+        return ((pixel >>> blueOffset) & blueMask) << (8 - blueLength);
+    }
+
+    @Override
+    public int getGreen(Object inData) {
+        int pixel = getPixel(inData);
+        return ((pixel >>> greenOffset) & greenMask) << (8 - greenLength);
+    }
+
+    @Override
+    public int getRed(Object inData) {
+        int pixel = getPixel(inData);
+        return ((pixel >>> redOffset) & redMask) << (8 - redLength);
+    }
+
+    @Override
+    public int getAlpha(int pixel) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getBlue(int pixel) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getGreen(int pixel) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getRed(int pixel) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/core/ChimpImageBase.java b/chimpchat/src/com/android/chimpchat/core/ChimpImageBase.java
new file mode 100644 (file)
index 0000000..e1ec29f
--- /dev/null
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2011 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.chimpchat.core;
+
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.Iterator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageWriter;
+import javax.imageio.stream.ImageOutputStream;
+
+/**
+ * Base class with basic functionality for ChimpImage implementations.
+ */
+public abstract class ChimpImageBase implements IChimpImage {
+    private static Logger LOG = Logger.getLogger(ChimpImageBase.class.getCanonicalName());
+
+    /**
+     * Convert the ChimpImage to a BufferedImage.
+     *
+     * @return a BufferedImage for this ChimpImage.
+     */
+    @Override
+    public abstract BufferedImage createBufferedImage();
+
+    // Cache the BufferedImage so we don't have to generate it every time.
+    private WeakReference<BufferedImage> cachedBufferedImage = null;
+
+    /**
+     * Utility method to handle getting the BufferedImage and managing the cache.
+     *
+     * @return the BufferedImage for this image.
+     */
+    @Override
+    public BufferedImage getBufferedImage() {
+        // Check the cache first
+        if (cachedBufferedImage != null) {
+            BufferedImage img = cachedBufferedImage.get();
+            if (img != null) {
+                return img;
+            }
+        }
+
+        // Not in the cache, so create it and cache it.
+        BufferedImage img = createBufferedImage();
+        cachedBufferedImage = new WeakReference<BufferedImage>(img);
+        return img;
+    }
+
+    @Override
+    public byte[] convertToBytes(String format) {
+      BufferedImage argb = convertSnapshot();
+
+      ByteArrayOutputStream os = new ByteArrayOutputStream();
+      try {
+          ImageIO.write(argb, format, os);
+      } catch (IOException e) {
+          return new byte[0];
+      }
+      return os.toByteArray();
+    }
+
+    @Override
+    public boolean writeToFile(String path, String format) {
+        if (format != null) {
+            return writeToFileHelper(path, format);
+        }
+        int offset = path.lastIndexOf('.');
+        if (offset < 0) {
+            return writeToFileHelper(path, "png");
+        }
+        String ext = path.substring(offset + 1);
+        Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix(ext);
+        if (!writers.hasNext()) {
+            return writeToFileHelper(path, "png");
+        }
+        ImageWriter writer = writers.next();
+        BufferedImage image = convertSnapshot();
+        try {
+            File f = new File(path);
+            f.delete();
+
+            ImageOutputStream outputStream = ImageIO.createImageOutputStream(f);
+            writer.setOutput(outputStream);
+
+            try {
+                writer.write(image);
+            } finally {
+                writer.dispose();
+                outputStream.flush();
+            }
+        } catch (IOException e) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int getPixel(int x, int y) {
+        BufferedImage image = getBufferedImage();
+        return image.getRGB(x, y);
+    }
+
+    private BufferedImage convertSnapshot() {
+        BufferedImage image = getBufferedImage();
+
+        // Convert the image to ARGB so ImageIO writes it out nicely
+        BufferedImage argb = new BufferedImage(image.getWidth(), image.getHeight(),
+                BufferedImage.TYPE_INT_ARGB);
+        Graphics g = argb.createGraphics();
+        g.drawImage(image, 0, 0, null);
+        g.dispose();
+        return argb;
+    }
+
+    private boolean writeToFileHelper(String path, String format) {
+        BufferedImage argb = convertSnapshot();
+
+        try {
+            ImageIO.write(argb, format, new File(path));
+        } catch (IOException e) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean sameAs(IChimpImage other, double percent) {
+        BufferedImage otherImage = other.getBufferedImage();
+        BufferedImage myImage = getBufferedImage();
+
+        // Easy size check
+        if (otherImage.getWidth() != myImage.getWidth()) {
+            return false;
+        }
+        if (otherImage.getHeight() != myImage.getHeight()) {
+            return false;
+        }
+
+        int[] otherPixel = new int[1];
+        int[] myPixel = new int[1];
+
+        int width = myImage.getWidth();
+        int height = myImage.getHeight();
+
+        int numDiffPixels = 0;
+        // Now, go through pixel-by-pixel and check that the images are the same;
+        for (int y = 0; y < height; y++) {
+            for (int x = 0; x < width; x++) {
+                if (myImage.getRGB(x, y) != otherImage.getRGB(x, y)) {
+                    numDiffPixels++;
+                }
+            }
+        }
+        double numberPixels = (height * width);
+        double diffPercent = numDiffPixels / numberPixels;
+        return percent <= 1.0 - diffPercent;
+    }
+
+    // TODO: figure out the location of this class and is superclasses
+    private static class BufferedImageChimpImage extends ChimpImageBase {
+        private final BufferedImage image;
+
+        public BufferedImageChimpImage(BufferedImage image) {
+            this.image = image;
+        }
+
+        @Override
+        public BufferedImage createBufferedImage() {
+            return image;
+        }
+    }
+
+    public static IChimpImage loadImageFromFile(String path) {
+        File f = new File(path);
+        if (f.exists() && f.canRead()) {
+            try {
+                BufferedImage bufferedImage = ImageIO.read(new File(path));
+                if (bufferedImage == null) {
+                    LOG.log(Level.WARNING, "Cannot decode file %s", path);
+                    return null;
+                }
+                return new BufferedImageChimpImage(bufferedImage);
+            } catch (IOException e) {
+                LOG.log(Level.WARNING, "Exception trying to decode image", e);
+                return null;
+            }
+        } else {
+            LOG.log(Level.WARNING, "Cannot read file %s", path);
+            return null;
+        }
+    }
+
+    @Override
+    public IChimpImage getSubImage(int x, int y, int w, int h) {
+        BufferedImage image = getBufferedImage();
+        return new BufferedImageChimpImage(image.getSubimage(x, y, w, h));
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/core/IChimpBackend.java b/chimpchat/src/com/android/chimpchat/core/IChimpBackend.java
new file mode 100644 (file)
index 0000000..092b849
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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.chimpchat.core;
+
+import com.android.chimpchat.core.IChimpDevice;
+
+/**
+ * Interface between the ChimpChat API and the ChimpChat backend that communicates
+ * with Monkey.
+ */
+public interface IChimpBackend {
+    /**
+     * Wait for a default device to connect to the backend.
+     *
+     * @return the connected device (or null if timeout);
+     */
+    IChimpDevice waitForConnection();
+
+    /**
+     * Wait for a device to connect to the backend.
+     *
+     * @param timeoutMs how long (in ms) to wait
+     * @param deviceIdRegex the regular expression to specify which device to wait for.
+     * @return the connected device (or null if timeout);
+     */
+    IChimpDevice waitForConnection(long timeoutMs, String deviceIdRegex);
+
+    /**
+     * Shutdown the backend and cleanup any resources it was using.
+     */
+    void shutdown();
+}
diff --git a/chimpchat/src/com/android/chimpchat/core/IChimpDevice.java b/chimpchat/src/com/android/chimpchat/core/IChimpDevice.java
new file mode 100644 (file)
index 0000000..7ba09c8
--- /dev/null
@@ -0,0 +1,197 @@
+
+/*
+ * Copyright (C) 2011 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.chimpchat.core;
+
+import com.android.chimpchat.ChimpManager;
+import com.android.chimpchat.hierarchyviewer.HierarchyViewer;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * ChimpDevice interface.
+ */
+public interface IChimpDevice {
+    /**
+     * Create a ChimpManager for talking to this device.
+     *
+     * @return the ChimpManager
+     */
+    ChimpManager getManager();
+
+    /**
+     * Dispose of any native resources this device may have taken hold of.
+     */
+    void dispose();
+
+    /**
+     * @return hierarchy viewer implementation for querying state of the view
+     * hierarchy.
+     */
+    HierarchyViewer getHierarchyViewer();
+
+    /**
+     * Take the current screen's snapshot.
+     * @return the snapshot image
+     */
+    IChimpImage takeSnapshot();
+
+    /**
+     * Reboot the device.
+     *
+     * @param into which bootloader to boot into.  Null means default reboot.
+     */
+    void reboot(@Nullable String into);
+
+    /**
+     * Get device's property.
+     *
+     * @param key the property name
+     * @return the property value
+     */
+    String getProperty(String key);
+
+    /**
+     * Get system property.
+     *
+     * @param key the name of the system property
+     * @return  the property value
+     */
+    String getSystemProperty(String key);
+
+    /**
+     * Perform a touch of the given type at (x,y).
+     *
+     * @param x the x coordinate
+     * @param y the y coordinate
+     * @param type the touch type
+     */
+    void touch(int x, int y, TouchPressType type);
+
+    /**
+     * Perform a press of a given type using a given key.
+     *
+     * TODO: define standard key names in a separate class or enum
+     *
+     * @param keyName the name of the key to use
+     * @param type the type of press to perform
+     */
+    void press(String keyName, TouchPressType type);
+
+    /**
+     * Perform a drag from one one location to another
+     *
+     * @param startx the x coordinate of the drag's starting point
+     * @param starty the y coordinate of the drag's starting point
+     * @param endx the x coordinate of the drag's end point
+     * @param endy the y coordinate of the drag's end point
+     * @param steps the number of steps to take when interpolating points
+     * @param ms the duration of the drag
+     */
+    void drag(int startx, int starty, int endx, int endy, int steps, long ms);
+
+    /**
+     * Type a given string.
+     *
+     * @param string the string to type
+     */
+    void type(String string);
+
+    /**
+     * Execute a shell command.
+     *
+     * @param cmd the command to execute
+     * @return the output of the command
+     */
+    String shell(String cmd);
+
+    /**
+     * Install a given package.
+     *
+     * @param path the path to the installation package
+     * @return true if success
+     */
+    boolean installPackage(String path);
+
+    /**
+     * Uninstall a given package.
+     *
+     * @param packageName the name of the package
+     * @return true if success
+     */
+    boolean removePackage(String packageName);
+
+    /**
+     * Start an activity.
+     *
+     * @param uri the URI for the Intent
+     * @param action the action for the Intent
+     * @param data the data URI for the Intent
+     * @param mimeType the mime type for the Intent
+     * @param categories the category names for the Intent
+     * @param extras the extras to add to the Intent
+     * @param component the component of the Intent
+     * @param flags the flags for the Intent
+     */
+    void startActivity(@Nullable String uri, @Nullable String action,
+            @Nullable String data, @Nullable String mimeType,
+            Collection<String> categories, Map<String, Object> extras, @Nullable String component,
+            int flags);
+
+    /**
+     * Send a broadcast intent to the device.
+     *
+     * @param uri the URI for the Intent
+     * @param action the action for the Intent
+     * @param data the data URI for the Intent
+     * @param mimeType the mime type for the Intent
+     * @param categories the category names for the Intent
+     * @param extras the extras to add to the Intent
+     * @param component the component of the Intent
+     * @param flags the flags for the Intent
+     */
+    void broadcastIntent(@Nullable String uri, @Nullable String action,
+            @Nullable String data, @Nullable String mimeType,
+            Collection<String> categories, Map<String, Object> extras, @Nullable String component,
+            int flags);
+
+    /**
+     * Run the specified package with instrumentation and return the output it
+     * generates.
+     *
+     * Use this to run a test package using InstrumentationTestRunner.
+     *
+     * @param packageName The class to run with instrumentation. The format is
+     * packageName/className. Use packageName to specify the Android package to
+     * run, and className to specify the class to run within that package. For
+     * test packages, this is usually testPackageName/InstrumentationTestRunner
+     * @param args a map of strings to objects containing the arguments to pass
+     * to this instrumentation.
+     * @return A map of strings to objects for the output from the package.
+     * For a test package, contains a single key-value pair: the key is 'stream'
+     * and the value is a string containing the test output.
+     */
+    Map<String, Object> instrument(String packageName,
+            Map<String, Object> args);
+
+    /**
+     * Wake up the screen on the device.
+     */
+    void wake();
+}
diff --git a/chimpchat/src/com/android/chimpchat/core/IChimpImage.java b/chimpchat/src/com/android/chimpchat/core/IChimpImage.java
new file mode 100644 (file)
index 0000000..6cd8f53
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 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.chimpchat.core;
+
+import java.awt.image.BufferedImage;
+
+/**
+ * ChimpImage interface.
+ *
+ * This interface defines an image representing a screen snapshot.
+ */
+public interface IChimpImage {
+    // TODO: add java docs
+    BufferedImage createBufferedImage();
+    BufferedImage getBufferedImage();
+
+    IChimpImage getSubImage(int x, int y, int w, int h);
+
+    byte[] convertToBytes(String format);
+    boolean writeToFile(String path, String format);
+    int getPixel(int x, int y);
+    boolean sameAs(IChimpImage other, double percent);
+}
diff --git a/chimpchat/src/com/android/chimpchat/core/TouchPressType.java b/chimpchat/src/com/android/chimpchat/core/TouchPressType.java
new file mode 100644 (file)
index 0000000..e5b92b7
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2011 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.chimpchat.core;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * TouchPressType enum contains valid input for the "touch" Monkey command.
+ * When passed as a string, the "identifier" value is used.
+ */
+public enum TouchPressType {
+    DOWN("down"), UP("up"), DOWN_AND_UP("downAndUp");
+
+    private static final Map<String,TouchPressType> identifierToEnum =
+        new HashMap<String,TouchPressType>();
+    static {
+        for (TouchPressType type : values()) {
+            identifierToEnum.put(type.identifier, type);
+        }
+    }
+
+    private String identifier;
+
+    TouchPressType(String identifier) {
+        this.identifier = identifier;
+    }
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public static TouchPressType fromIdentifier(String name) {
+        return identifierToEnum.get(name);
+    }
+}
diff --git a/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java b/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java
new file mode 100644 (file)
index 0000000..6ad98ad
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2011 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.chimpchat.hierarchyviewer;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.hierarchyviewerlib.device.DeviceBridge;
+import com.android.hierarchyviewerlib.device.ViewNode;
+import com.android.hierarchyviewerlib.device.Window;
+
+import org.eclipse.swt.graphics.Point;
+
+/**
+ * Class for querying the view hierarchy of the device.
+ */
+public class HierarchyViewer {
+    public static final String TAG = "hierarchyviewer";
+
+    private IDevice mDevice;
+
+    /**
+     * Constructs the hierarchy viewer for the specified device.
+     *
+     * @param device The Android device to connect to.
+     */
+    public HierarchyViewer(IDevice device) {
+        this.mDevice = device;
+        setupViewServer();
+    }
+
+    private void setupViewServer() {
+        DeviceBridge.setupDeviceForward(mDevice);
+        if (!DeviceBridge.isViewServerRunning(mDevice)) {
+            if (!DeviceBridge.startViewServer(mDevice)) {
+                // TODO: Get rid of this delay.
+                try {
+                    Thread.sleep(2000);
+                } catch (InterruptedException e) {
+                }
+                if (!DeviceBridge.startViewServer(mDevice)) {
+                    Log.e(TAG, "Unable to debug device " + mDevice);
+                    throw new RuntimeException("Could not connect to the view server");
+                }
+                return;
+            }
+        }
+        DeviceBridge.loadViewServerInfo(mDevice);
+    }
+
+    /**
+     * Find a view by id.
+     *
+     * @param id id for the view.
+     * @return view with the specified ID, or {@code null} if no view found.
+     */
+
+    public ViewNode findViewById(String id) {
+        ViewNode rootNode = DeviceBridge.loadWindowData(
+                new Window(mDevice, "", 0xffffffff));
+        if (rootNode == null) {
+            throw new RuntimeException("Could not dump view");
+        }
+        return findViewById(id, rootNode);
+    }
+
+    /**
+     * Find a view by ID, starting from the given root node
+     * @param id ID of the view you're looking for
+     * @param rootNode the ViewNode at which to begin the traversal
+     * @return view with the specified ID, or {@code null} if no view found.
+     */
+
+    public ViewNode findViewById(String id, ViewNode rootNode) {
+        if (rootNode.id.equals(id)) {
+            return rootNode;
+        }
+
+        for (ViewNode child : rootNode.children) {
+            ViewNode found = findViewById(id,child);
+            if (found != null) {
+                return found;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets the window that currently receives the focus.
+     *
+     * @return name of the window that currently receives the focus.
+     */
+    public String getFocusedWindowName() {
+        int id = DeviceBridge.getFocusedWindow(mDevice);
+        Window[] windows = DeviceBridge.loadWindows(mDevice);
+        for (Window w : windows) {
+            if (w.getHashCode() == id)
+                return w.getTitle();
+        }
+        return null;
+    }
+
+    /**
+     * Gets the absolute x/y position of the view node.
+     *
+     * @param node view node to find position of.
+     * @return point specifying the x/y position of the node.
+     */
+    public static Point getAbsolutePositionOfView(ViewNode node) {
+        int x = node.left;
+        int y = node.top;
+        ViewNode p = node.parent;
+        while (p != null) {
+            x += p.left - p.scrollX;
+            y += p.top - p.scrollY;
+            p = p.parent;
+        }
+        return new Point(x, y);
+    }
+
+    /**
+     * Gets the absolute x/y center of the specified view node.
+     *
+     * @param node view node to find position of.
+     * @return absolute x/y center of the specified view node.
+     */
+    public static Point getAbsoluteCenterOfView(ViewNode node) {
+        Point point = getAbsolutePositionOfView(node);
+        return new Point(
+                point.x + (node.width / 2), point.y + (node.height / 2));
+    }
+
+    /**
+     * Gets the visibility of a given element.
+     *
+     * @param selector selector for the view.
+     * @return True if the element is visible.
+     */
+    public boolean visible(ViewNode node) {
+        boolean ret = (node != null)
+                && node.namedProperties.containsKey("getVisibility()")
+                && "VISIBLE".equalsIgnoreCase(
+                        node.namedProperties.get("getVisibility()").value);
+        return ret;
+
+    }
+
+    /**
+     * Gets the text of a given element.
+     *
+     * @param selector selector for the view.
+     * @return the text of the given element.
+     */
+    public String getText(ViewNode node) {
+        if (node == null) {
+            throw new RuntimeException("Node not found");
+        }
+        ViewNode.Property textProperty = node.namedProperties.get("text:mText");
+        if (textProperty == null) {
+            throw new RuntimeException("No text property on node");
+        }
+        return textProperty.value;
+    }
+}