| /* |
| * Copyright (C) 2009 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.AndroidDebugBridge; |
| import com.android.ddmlib.IDevice; |
| import com.android.ddmlib.Log; |
| import com.android.ddmlib.NullOutputReceiver; |
| import com.android.ddmlib.RawImage; |
| import com.android.ddmlib.Log.ILogOutput; |
| import com.android.ddmlib.Log.LogLevel; |
| |
| import java.awt.image.BufferedImage; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.net.InetAddress; |
| import java.net.Socket; |
| import java.net.UnknownHostException; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| import javax.imageio.ImageIO; |
| |
| /** |
| * MonkeyRunner is a host side application to control a monkey instance on a |
| * device. MonkeyRunner provides some useful helper functions to control the |
| * device as well as various other methods to help script tests. |
| */ |
| public class MonkeyRunner { |
| |
| static String monkeyServer = "127.0.0.1"; |
| static int monkeyPort = 1080; |
| static Socket monkeySocket = null; |
| |
| static IDevice monkeyDevice; |
| |
| static BufferedReader monkeyReader; |
| static BufferedWriter monkeyWriter; |
| static String monkeyResponse; |
| |
| static MonkeyRecorder monkeyRecorder; |
| |
| static String scriptName = null; |
| |
| // Obtain a suitable logger. |
| private static Logger logger = Logger.getLogger("com.android.monkeyrunner"); |
| |
| // delay between key events |
| final static int KEY_INPUT_DELAY = 1000; |
| |
| // version of monkey runner |
| final static String monkeyRunnerVersion = "0.31"; |
| |
| // TODO: interface cmd; class xml tags; fix logger; test class/script |
| |
| public static void main(String[] args) throws IOException { |
| |
| // haven't figure out how to get below INFO...bad parent. Pass -v INFO to turn on logging |
| logger.setLevel(Level.parse("WARNING")); |
| processOptions(args); |
| |
| logger.info("initAdb"); |
| initAdbConnection(); |
| logger.info("openMonkeyConnection"); |
| openMonkeyConnection(); |
| |
| logger.info("start_script"); |
| start_script(); |
| |
| logger.info("ScriptRunner.run"); |
| ScriptRunner.run(scriptName); |
| |
| logger.info("end_script"); |
| end_script(); |
| logger.info("closeMonkeyConnection"); |
| closeMonkeyConnection(); |
| } |
| |
| /** |
| * Initialize an adb session with a device connected to the host |
| * |
| */ |
| public static void initAdbConnection() { |
| String adbLocation = "adb"; |
| boolean device = false; |
| boolean emulator = false; |
| String serial = null; |
| |
| AndroidDebugBridge.init(false /* debugger support */); |
| |
| try { |
| AndroidDebugBridge bridge = AndroidDebugBridge.createBridge( |
| adbLocation, true /* forceNewBridge */); |
| |
| // we can't just ask for the device list right away, as the internal thread getting |
| // them from ADB may not be done getting the first list. |
| // Since we don't really want getDevices() to be blocking, we wait here manually. |
| int count = 0; |
| while (bridge.hasInitialDeviceList() == false) { |
| try { |
| Thread.sleep(100); |
| count++; |
| } catch (InterruptedException e) { |
| // pass |
| } |
| |
| // let's not wait > 10 sec. |
| if (count > 100) { |
| System.err.println("Timeout getting device list!"); |
| return; |
| } |
| } |
| |
| // now get the devices |
| IDevice[] devices = bridge.getDevices(); |
| |
| if (devices.length == 0) { |
| printAndExit("No devices found!", true /* terminate */); |
| } |
| |
| monkeyDevice = null; |
| |
| if (emulator || device) { |
| for (IDevice d : devices) { |
| // this test works because emulator and device can't both be true at the same |
| // time. |
| if (d.isEmulator() == emulator) { |
| // if we already found a valid target, we print an error and return. |
| if (monkeyDevice != null) { |
| if (emulator) { |
| printAndExit("Error: more than one emulator launched!", |
| true /* terminate */); |
| } else { |
| printAndExit("Error: more than one device connected!",true /* terminate */); |
| } |
| } |
| monkeyDevice = d; |
| } |
| } |
| } else if (serial != null) { |
| for (IDevice d : devices) { |
| if (serial.equals(d.getSerialNumber())) { |
| monkeyDevice = d; |
| break; |
| } |
| } |
| } else { |
| if (devices.length > 1) { |
| printAndExit("Error: more than one emulator or device available!", |
| true /* terminate */); |
| } |
| monkeyDevice = devices[0]; |
| } |
| |
| monkeyDevice.createForward(monkeyPort, monkeyPort); |
| String command = "monkey --port " + monkeyPort; |
| monkeyDevice.executeShellCommand(command, new NullOutputReceiver()); |
| |
| } catch(IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| /** |
| * Open a tcp session over adb with the device to communicate monkey commands |
| */ |
| public static void openMonkeyConnection() { |
| try { |
| InetAddress addr = InetAddress.getByName(monkeyServer); |
| monkeySocket = new Socket(addr, monkeyPort); |
| monkeyWriter = new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream())); |
| monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream())); |
| } catch (UnknownHostException e) { |
| e.printStackTrace(); |
| } catch(IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| /** |
| * Close tcp session with the monkey on the device |
| * |
| */ |
| public static void closeMonkeyConnection() { |
| try { |
| monkeyReader.close(); |
| monkeyWriter.close(); |
| monkeySocket.close(); |
| AndroidDebugBridge.terminate(); |
| } catch(IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| /** |
| * This is a house cleaning routine to run before starting a script. Puts |
| * the device in a known state and starts recording interesting info. |
| */ |
| public static void start_script() throws IOException { |
| press("menu", false); |
| press("menu", false); |
| press("home", false); |
| |
| // Start recording the script output, might want md5 signature of file for completeness |
| monkeyRecorder = new MonkeyRecorder(scriptName); |
| |
| // Record what device and version of software we are running on |
| monkeyRecorder.addAttribute("monkeyRunnerVersion", monkeyRunnerVersion); |
| addDeviceVars(); |
| monkeyRecorder.addComment("Script commands"); |
| } |
| |
| /** |
| * This is a house cleaning routine to run after finishing a script. |
| * Puts the monkey server in a known state and closes the recording. |
| */ |
| public static void end_script() throws IOException { |
| String command = "done"; |
| sendMonkeyEvent(command, false, false); |
| |
| // Stop the recording and zip up the results |
| monkeyRecorder.close(); |
| } |
| |
| /** This is a method for scripts to launch an activity on the device |
| * |
| * @param name The name of the activity to launch |
| */ |
| public static void launch_activity(String name) throws IOException { |
| System.out.println("Launching: " + name); |
| recordCommand("Launching: " + name); |
| monkeyDevice.executeShellCommand("am start -a android.intent.action.MAIN -n " |
| + name, new NullOutputReceiver()); |
| // void return, so no response given, just close the command element in the xml file. |
| monkeyRecorder.endCommand(); |
| } |
| |
| /** |
| * Grabs the current state of the screen stores it as a png |
| * |
| * @param tag filename or tag descriptor of the screenshot |
| */ |
| public static void grabscreen(String tag) throws IOException { |
| tag += ".png"; |
| |
| try { |
| Thread.sleep(1000); |
| getDeviceImage(monkeyDevice, tag, false); |
| } catch (InterruptedException e) { |
| } |
| } |
| |
| /** |
| * Sleeper method for script to call |
| * |
| * @param msec msecs to sleep for |
| */ |
| public static void sleep(int msec) throws IOException { |
| try { |
| recordCommand("sleep: " + msec); |
| Thread.sleep(msec); |
| recordResponse("OK"); |
| } catch (InterruptedException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| /** |
| * Tap function for scripts to call at a particular x and y location |
| * |
| * @param x x-coordinate |
| * @param y y-coordinate |
| */ |
| public static boolean tap(int x, int y) throws IOException { |
| String command = "tap " + x + " " + y; |
| boolean result = sendMonkeyEvent(command); |
| return result; |
| } |
| |
| /** |
| * Press function for scripts to call on a particular button or key |
| * |
| * @param key key to press |
| */ |
| public static boolean press(String key) throws IOException { |
| return press(key, true); |
| } |
| |
| /** |
| * Press function for scripts to call on a particular button or key |
| * |
| * @param key key to press |
| * @param print whether to send output to user |
| */ |
| private static boolean press(String key, boolean print) throws IOException { |
| String command = "press " + key; |
| boolean result = sendMonkeyEvent(command, print, true); |
| return result; |
| } |
| |
| /** |
| * dpad down function |
| */ |
| public static boolean down() throws IOException { |
| return press("dpad_down"); |
| } |
| |
| /** |
| * dpad up function |
| */ |
| public static boolean up() throws IOException { |
| return press("dpad_up"); |
| } |
| |
| /** |
| * Function to type text on the device |
| * |
| * @param text text to type |
| */ |
| public static boolean type(String text) throws IOException { |
| boolean result = false; |
| // text might have line ends, which signal new monkey command, so we have to eat and reissue |
| String[] lines = text.split("[\\r\\n]+"); |
| for (String line: lines) { |
| result = sendMonkeyEvent("type " + line + "\n"); |
| } |
| // return last result. Should never fail..? |
| return result; |
| } |
| |
| /** |
| * Function to get a static variable from the device |
| * |
| * @param name name of static variable to get |
| */ |
| public static boolean getvar(String name) throws IOException { |
| return sendMonkeyEvent("getvar " + name + "\n"); |
| } |
| |
| /** |
| * Function to get the list of static variables from the device |
| */ |
| public static boolean listvar() throws IOException { |
| return sendMonkeyEvent("listvar \n"); |
| } |
| |
| /** |
| * This function is the communication bridge between the host and the device. |
| * It sends monkey events and waits for responses over the adb tcp socket. |
| * This version if for all scripted events so that they get recorded and reported to user. |
| * |
| * @param command the monkey command to send to the device |
| */ |
| private static boolean sendMonkeyEvent(String command) throws IOException { |
| return sendMonkeyEvent(command, true, true); |
| } |
| |
| /** |
| * 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 |
| * @param print whether to print out the responses to the user |
| * @param record whether to put the command in the xml file that stores test outputs |
| */ |
| private static boolean sendMonkeyEvent(String command, Boolean print, Boolean record) throws IOException { |
| command = command.trim(); |
| if (print) |
| System.out.println("MonkeyCommand: " + command); |
| if (record) |
| recordCommand(command); |
| logger.info("Monkey Command: " + command + "."); |
| |
| // send a single command and get the response |
| monkeyWriter.write(command + "\n"); |
| monkeyWriter.flush(); |
| monkeyResponse = monkeyReader.readLine(); |
| |
| if(monkeyResponse != null) { |
| // if a command returns with a response |
| if (print) |
| System.out.println("MonkeyServer: " + monkeyResponse); |
| if (record) |
| recordResponse(monkeyResponse); |
| logger.info("Monkey Response: " + monkeyResponse + "."); |
| |
| // return on error |
| if (monkeyResponse.startsWith("ERROR")) |
| return false; |
| |
| // return on ok |
| if(monkeyResponse.startsWith("OK")) |
| return true; |
| |
| // return on something else? |
| return false; |
| } |
| // didn't get a response... |
| if (print) |
| System.out.println("MonkeyServer: ??no response"); |
| if (record) |
| recordResponse("??no response"); |
| logger.info("Monkey Response: ??no response."); |
| |
| //return on no response |
| return false; |
| } |
| |
| /** |
| * Record the command in the xml file |
| * |
| * @param command the command sent to the monkey server |
| */ |
| private static void recordCommand(String command) throws IOException { |
| if (monkeyRecorder != null) { // don't record setup junk |
| monkeyRecorder.startCommand(); |
| monkeyRecorder.addInput(command); |
| } |
| } |
| |
| /** |
| * Record the response in the xml file |
| * |
| * @param response the response sent by the monkey server |
| */ |
| private static void recordResponse(String response) throws IOException { |
| recordResponse(response, ""); |
| } |
| |
| /** |
| * Record the response and the filename in the xml file, store the file (to be zipped up later) |
| * |
| * @param response the response sent by the monkey server |
| * @param filename the filename of a file to be time stamped, recorded in the xml file and stored |
| */ |
| private static void recordResponse(String response, String filename) throws IOException { |
| if (monkeyRecorder != null) { // don't record setup junk |
| monkeyRecorder.addResult(response, filename); // ignores file if filename empty |
| monkeyRecorder.endCommand(); |
| } |
| } |
| |
| /** |
| * Add the device variables to the xml file in monkeyRecorder. |
| * The results get added as device_var tags in the script_run tag |
| */ |
| private static void addDeviceVars() throws IOException { |
| monkeyRecorder.addComment("Device specific variables"); |
| sendMonkeyEvent("listvar \n", false, false); |
| if (monkeyResponse.startsWith("OK:")) { |
| // peel off "OK:" string and get the individual var names |
| String[] varNames = monkeyResponse.substring(3).split("\\s+"); |
| // grab all the individual var values |
| for (String name: varNames) { |
| sendMonkeyEvent("getvar " + name, false, false); |
| if(monkeyResponse != null) { |
| if (monkeyResponse.startsWith("OK") ) { |
| if (monkeyResponse.length() > 2) { |
| monkeyRecorder.addDeviceVar(name, monkeyResponse.substring(3)); |
| } else { |
| // only got OK - good variable but no value |
| monkeyRecorder.addDeviceVar(name, "null"); |
| } |
| } else { |
| // error returned - couldn't get var value for name... include error return |
| monkeyRecorder.addDeviceVar(name, monkeyResponse); |
| } |
| } else { |
| // no monkeyResponse - bad variable with no value |
| monkeyRecorder.addDeviceVar(name, "null"); |
| } |
| } |
| } else { |
| // it's an error, can't find variable names... |
| monkeyRecorder.addAttribute("listvar", monkeyResponse); |
| } |
| } |
| |
| /** |
| * Process the command-line options |
| * |
| * @return Returns true if options were parsed with no apparent errors. |
| */ |
| private static void processOptions(String[] args) { |
| // parse command line parameters. |
| int index = 0; |
| |
| do { |
| String argument = args[index++]; |
| |
| if ("-s".equals(argument)) { |
| if(index == args.length) { |
| printUsageAndQuit("Missing Server after -s"); |
| } |
| |
| monkeyServer = args[index++]; |
| |
| } else if ("-p".equals(argument)) { |
| // quick check on the next argument. |
| if (index == args.length) { |
| printUsageAndQuit("Missing Server port after -p"); |
| } |
| |
| monkeyPort = Integer.parseInt(args[index++]); |
| |
| } else if ("-v".equals(argument)) { |
| // quick check on the next argument. |
| if (index == args.length) { |
| printUsageAndQuit("Missing Log Level after -v"); |
| } |
| |
| Level level = Level.parse(args[index++]); |
| logger.setLevel(level); |
| level = logger.getLevel(); |
| System.out.println("Log level set to: " + level + "(" + level.intValue() + ")."); |
| System.out.println("Warning: Log levels below INFO(800) not working currently... parent issues"); |
| |
| } else if (argument.startsWith("-")) { |
| // we have an unrecognized argument. |
| printUsageAndQuit("Unrecognized argument: " + argument + "."); |
| |
| monkeyPort = Integer.parseInt(args[index++]); |
| |
| } else { |
| // get the filepath of the script to run. This will be the last undashed argument. |
| scriptName = argument; |
| } |
| } while (index < args.length); |
| } |
| |
| /* |
| * Grab an image from an ADB-connected device. |
| */ |
| private static void getDeviceImage(IDevice device, String filepath, boolean landscape) |
| throws IOException { |
| RawImage rawImage; |
| recordCommand("grabscreen"); |
| System.out.println("Grabbing Screeshot: " + filepath + "."); |
| |
| try { |
| rawImage = device.getScreenshot(); |
| } |
| catch (IOException ioe) { |
| recordResponse("No frame buffer", ""); |
| printAndExit("Unable to get frame buffer: " + ioe.getMessage(), true /* terminate */); |
| return; |
| } |
| |
| // device/adb not available? |
| if (rawImage == null) { |
| recordResponse("No image", ""); |
| return; |
| } |
| |
| assert rawImage.bpp == 16; |
| |
| BufferedImage image; |
| |
| logger.info("Raw Image - height: " + rawImage.height + ", width: " + rawImage.width); |
| |
| if (landscape) { |
| // convert raw data to an Image |
| image = new BufferedImage(rawImage.height, rawImage.width, |
| BufferedImage.TYPE_INT_ARGB); |
| |
| 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 >> 0) & 0x01F) << 3; |
| |
| value = 0xFF << 24 | r << 16 | g << 8 | b; |
| |
| image.setRGB(y, rawImage.width - x - 1, value); |
| } |
| } |
| } else { |
| // convert raw data to an Image |
| image = new BufferedImage(rawImage.width, rawImage.height, |
| BufferedImage.TYPE_INT_ARGB); |
| |
| 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 >> 0) & 0x01F) << 3; |
| |
| value = 0xFF << 24 | r << 16 | g << 8 | b; |
| |
| image.setRGB(x, y, value); |
| } |
| } |
| } |
| |
| if (!ImageIO.write(image, "png", new File(filepath))) { |
| recordResponse("No png writer", ""); |
| throw new IOException("Failed to find png writer"); |
| } |
| recordResponse("OK", filepath); |
| } |
| |
| private static void printUsageAndQuit(String message) { |
| // 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789 |
| System.out.println(message); |
| System.out.println("Usage: monkeyrunner [options] SCRIPT_FILE"); |
| System.out.println(""); |
| System.out.println(" -s MonkeyServer IP Address."); |
| System.out.println(" -p MonkeyServer TCP Port."); |
| System.out.println(" -v MonkeyServer Logging level (ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, OFF)"); |
| System.out.println(""); |
| System.out.println(""); |
| |
| System.exit(1); |
| } |
| |
| private static void printAndExit(String message, boolean terminate) { |
| System.out.println(message); |
| if (terminate) { |
| AndroidDebugBridge.terminate(); |
| } |
| System.exit(1); |
| } |
| } |