LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := \
+ ddmlib \
+ hierarchyviewerlib \
+ guavalib \
+ sdklib \
+ swt
+
LOCAL_MODULE := chimpchat
LOCAL_MODULE_TAGS := eng
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;
+ }
}
}
--- /dev/null
+
+/*
+ * 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");
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+ });
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}