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