1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.monkeyrunner; 18 19 import com.android.ddmlib.AndroidDebugBridge; 20 import com.android.ddmlib.IDevice; 21 import com.android.ddmlib.Log; 22 import com.android.ddmlib.NullOutputReceiver; 23 import com.android.ddmlib.RawImage; 24 import com.android.ddmlib.Log.ILogOutput; 25 import com.android.ddmlib.Log.LogLevel; 26 27 import java.awt.image.BufferedImage; 28 29 import java.io.BufferedReader; 30 import java.io.BufferedWriter; 31 import java.io.File; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.InputStreamReader; 35 import java.io.OutputStreamWriter; 36 import java.net.InetAddress; 37 import java.net.Socket; 38 import java.net.UnknownHostException; 39 import java.util.logging.Level; 40 import java.util.logging.Logger; 41 42 import javax.imageio.ImageIO; 43 44 /** 45 * MonkeyRunner is a host side application to control a monkey instance on a 46 * device. MonkeyRunner provides some useful helper functions to control the 47 * device as well as various other methods to help script tests. 48 */ 49 public class MonkeyRunner { 50 51 static String monkeyServer = "127.0.0.1"; 52 static int monkeyPort = 1080; 53 static Socket monkeySocket = null; 54 55 static IDevice monkeyDevice; 56 57 static BufferedReader monkeyReader; 58 static BufferedWriter monkeyWriter; 59 static String monkeyResponse; 60 61 static MonkeyRecorder monkeyRecorder; 62 63 static String scriptName = null; 64 65 // Obtain a suitable logger. 66 private static Logger logger = Logger.getLogger("com.android.monkeyrunner"); 67 68 // delay between key events 69 final static int KEY_INPUT_DELAY = 1000; 70 71 // version of monkey runner 72 final static String monkeyRunnerVersion = "0.4"; 73 74 // TODO: interface cmd; class xml tags; fix logger; test class/script 75 76 public static void main(String[] args) throws IOException { 77 78 // haven't figure out how to get below INFO...bad parent. Pass -v INFO to turn on logging 79 logger.setLevel(Level.parse("WARNING")); 80 processOptions(args); 81 82 logger.info("initAdb"); 83 initAdbConnection(); 84 logger.info("openMonkeyConnection"); 85 openMonkeyConnection(); 86 87 logger.info("start_script"); 88 start_script(); 89 90 logger.info("ScriptRunner.run"); 91 ScriptRunner.run(scriptName); 92 93 logger.info("end_script"); 94 end_script(); 95 logger.info("closeMonkeyConnection"); 96 closeMonkeyConnection(); 97 } 98 99 /** 100 * Initialize an adb session with a device connected to the host 101 * 102 */ 103 public static void initAdbConnection() { 104 String adbLocation = "adb"; 105 boolean device = false; 106 boolean emulator = false; 107 String serial = null; 108 109 AndroidDebugBridge.init(false /* debugger support */); 110 111 try { 112 AndroidDebugBridge bridge = AndroidDebugBridge.createBridge( 113 adbLocation, true /* forceNewBridge */); 114 115 // we can't just ask for the device list right away, as the internal thread getting 116 // them from ADB may not be done getting the first list. 117 // Since we don't really want getDevices() to be blocking, we wait here manually. 118 int count = 0; 119 while (bridge.hasInitialDeviceList() == false) { 120 try { 121 Thread.sleep(100); 122 count++; 123 } catch (InterruptedException e) { 124 // pass 125 } 126 127 // let's not wait > 10 sec. 128 if (count > 100) { 129 System.err.println("Timeout getting device list!"); 130 return; 131 } 132 } 133 134 // now get the devices 135 IDevice[] devices = bridge.getDevices(); 136 137 if (devices.length == 0) { 138 printAndExit("No devices found!", true /* terminate */); 139 } 140 141 monkeyDevice = null; 142 143 if (emulator || device) { 144 for (IDevice d : devices) { 145 // this test works because emulator and device can't both be true at the same 146 // time. 147 if (d.isEmulator() == emulator) { 148 // if we already found a valid target, we print an error and return. 149 if (monkeyDevice != null) { 150 if (emulator) { 151 printAndExit("Error: more than one emulator launched!", 152 true /* terminate */); 153 } else { 154 printAndExit("Error: more than one device connected!",true /* terminate */); 155 } 156 } 157 monkeyDevice = d; 158 } 159 } 160 } else if (serial != null) { 161 for (IDevice d : devices) { 162 if (serial.equals(d.getSerialNumber())) { 163 monkeyDevice = d; 164 break; 165 } 166 } 167 } else { 168 if (devices.length > 1) { 169 printAndExit("Error: more than one emulator or device available!", 170 true /* terminate */); 171 } 172 monkeyDevice = devices[0]; 173 } 174 175 monkeyDevice.createForward(monkeyPort, monkeyPort); 176 String command = "monkey --port " + monkeyPort; 177 monkeyDevice.executeShellCommand(command, new NullOutputReceiver()); 178 179 } catch(IOException e) { 180 e.printStackTrace(); 181 } 182 } 183 184 /** 185 * Open a tcp session over adb with the device to communicate monkey commands 186 */ 187 public static void openMonkeyConnection() { 188 try { 189 InetAddress addr = InetAddress.getByName(monkeyServer); 190 monkeySocket = new Socket(addr, monkeyPort); 191 monkeyWriter = new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream())); 192 monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream())); 193 } catch (UnknownHostException e) { 194 e.printStackTrace(); 195 } catch(IOException e) { 196 e.printStackTrace(); 197 } 198 } 199 200 /** 201 * Close tcp session with the monkey on the device 202 * 203 */ 204 public static void closeMonkeyConnection() { 205 try { 206 monkeyReader.close(); 207 monkeyWriter.close(); 208 monkeySocket.close(); 209 AndroidDebugBridge.terminate(); 210 } catch(IOException e) { 211 e.printStackTrace(); 212 } 213 } 214 215 /** 216 * This is a house cleaning routine to run before starting a script. Puts 217 * the device in a known state and starts recording interesting info. 218 */ 219 public static void start_script() throws IOException { 220 press("menu", false); 221 press("menu", false); 222 press("home", false); 223 224 // Start recording the script output, might want md5 signature of file for completeness 225 monkeyRecorder = new MonkeyRecorder(scriptName, monkeyRunnerVersion); 226 227 // Record what device we are running on 228 addDeviceVars(); 229 monkeyRecorder.addComment("Script commands"); 230 } 231 232 /** 233 * This is a house cleaning routine to run after finishing a script. 234 * Puts the monkey server in a known state and closes the recording. 235 */ 236 public static void end_script() throws IOException { 237 String command = "done"; 238 sendMonkeyEvent(command, false, false); 239 240 // Stop the recording and zip up the results 241 monkeyRecorder.close(); 242 } 243 244 /** This is a method for scripts to launch an activity on the device 245 * 246 * @param name The name of the activity to launch 247 */ 248 public static void launch_activity(String name) throws IOException { 249 System.out.println("Launching: " + name); 250 recordCommand("Launching: " + name); 251 monkeyDevice.executeShellCommand("am start -a android.intent.action.MAIN -n " 252 + name, new NullOutputReceiver()); 253 // void return, so no response given, just close the command element in the xml file. 254 monkeyRecorder.endCommand(); 255 } 256 257 /** 258 * Grabs the current state of the screen stores it as a png 259 * 260 * @param tag filename or tag descriptor of the screenshot 261 */ 262 public static void grabscreen(String tag) throws IOException { 263 tag += ".png"; 264 265 try { 266 Thread.sleep(1000); 267 getDeviceImage(monkeyDevice, tag, false); 268 } catch (InterruptedException e) { 269 } 270 } 271 272 /** 273 * Sleeper method for script to call 274 * 275 * @param msec msecs to sleep for 276 */ 277 public static void sleep(int msec) throws IOException { 278 try { 279 recordCommand("sleep: " + msec); 280 Thread.sleep(msec); 281 recordResponse("OK"); 282 } catch (InterruptedException e) { 283 e.printStackTrace(); 284 } 285 } 286 287 /** 288 * Tap function for scripts to call at a particular x and y location 289 * 290 * @param x x-coordinate 291 * @param y y-coordinate 292 */ 293 public static boolean tap(int x, int y) throws IOException { 294 String command = "tap " + x + " " + y; 295 boolean result = sendMonkeyEvent(command); 296 return result; 297 } 298 299 /** 300 * Press function for scripts to call on a particular button or key 301 * 302 * @param key key to press 303 */ 304 public static boolean press(String key) throws IOException { 305 return press(key, true); 306 } 307 308 /** 309 * Press function for scripts to call on a particular button or key 310 * 311 * @param key key to press 312 * @param print whether to send output to user 313 */ 314 private static boolean press(String key, boolean print) throws IOException { 315 String command = "press " + key; 316 boolean result = sendMonkeyEvent(command, print, true); 317 return result; 318 } 319 320 /** 321 * dpad down function 322 */ 323 public static boolean down() throws IOException { 324 return press("dpad_down"); 325 } 326 327 /** 328 * dpad up function 329 */ 330 public static boolean up() throws IOException { 331 return press("dpad_up"); 332 } 333 334 /** 335 * Function to type text on the device 336 * 337 * @param text text to type 338 */ 339 public static boolean type(String text) throws IOException { 340 boolean result = false; 341 // text might have line ends, which signal new monkey command, so we have to eat and reissue 342 String[] lines = text.split("[\\r\\n]+"); 343 for (String line: lines) { 344 result = sendMonkeyEvent("type " + line + "\n"); 345 } 346 // return last result. Should never fail..? 347 return result; 348 } 349 350 /** 351 * Function to get a static variable from the device 352 * 353 * @param name name of static variable to get 354 */ 355 public static boolean getvar(String name) throws IOException { 356 return sendMonkeyEvent("getvar " + name + "\n"); 357 } 358 359 /** 360 * Function to get the list of static variables from the device 361 */ 362 public static boolean listvar() throws IOException { 363 return sendMonkeyEvent("listvar \n"); 364 } 365 366 /** 367 * This function is the communication bridge between the host and the device. 368 * It sends monkey events and waits for responses over the adb tcp socket. 369 * This version if for all scripted events so that they get recorded and reported to user. 370 * 371 * @param command the monkey command to send to the device 372 */ 373 private static boolean sendMonkeyEvent(String command) throws IOException { 374 return sendMonkeyEvent(command, true, true); 375 } 376 377 /** 378 * This function allows the communication bridge between the host and the device 379 * to be invisible to the script for internal needs. 380 * It splits a command into monkey events and waits for responses for each over an adb tcp socket. 381 * Returns on an error, else continues and sets up last response. 382 * 383 * @param command the monkey command to send to the device 384 * @param print whether to print out the responses to the user 385 * @param record whether to put the command in the xml file that stores test outputs 386 */ 387 private static boolean sendMonkeyEvent(String command, Boolean print, Boolean record) throws IOException { 388 command = command.trim(); 389 if (print) 390 System.out.println("MonkeyCommand: " + command); 391 if (record) 392 recordCommand(command); 393 logger.info("Monkey Command: " + command + "."); 394 395 // send a single command and get the response 396 monkeyWriter.write(command + "\n"); 397 monkeyWriter.flush(); 398 monkeyResponse = monkeyReader.readLine(); 399 400 if(monkeyResponse != null) { 401 // if a command returns with a response 402 if (print) 403 System.out.println("MonkeyServer: " + monkeyResponse); 404 if (record) 405 recordResponse(monkeyResponse); 406 logger.info("Monkey Response: " + monkeyResponse + "."); 407 408 // return on error 409 if (monkeyResponse.startsWith("ERROR")) 410 return false; 411 412 // return on ok 413 if(monkeyResponse.startsWith("OK")) 414 return true; 415 416 // return on something else? 417 return false; 418 } 419 // didn't get a response... 420 if (print) 421 System.out.println("MonkeyServer: ??no response"); 422 if (record) 423 recordResponse("??no response"); 424 logger.info("Monkey Response: ??no response."); 425 426 //return on no response 427 return false; 428 } 429 430 /** 431 * Record the command in the xml file 432 * 433 * @param command the command sent to the monkey server 434 */ 435 private static void recordCommand(String command) throws IOException { 436 if (monkeyRecorder != null) { // don't record setup junk 437 monkeyRecorder.startCommand(); 438 monkeyRecorder.addInput(command); 439 } 440 } 441 442 /** 443 * Record the response in the xml file 444 * 445 * @param response the response sent by the monkey server 446 */ 447 private static void recordResponse(String response) throws IOException { 448 recordResponse(response, ""); 449 } 450 451 /** 452 * Record the response and the filename in the xml file, store the file (to be zipped up later) 453 * 454 * @param response the response sent by the monkey server 455 * @param filename the filename of a file to be time stamped, recorded in the xml file and stored 456 */ 457 private static void recordResponse(String response, String filename) throws IOException { 458 if (monkeyRecorder != null) { // don't record setup junk 459 monkeyRecorder.addResult(response, filename); // ignores file if filename empty 460 monkeyRecorder.endCommand(); 461 } 462 } 463 464 /** 465 * Add the device variables to the xml file in monkeyRecorder. 466 * The results get added as device_var tags in the script_run tag 467 */ 468 private static void addDeviceVars() throws IOException { 469 monkeyRecorder.addComment("Device specific variables"); 470 sendMonkeyEvent("listvar \n", false, false); 471 if (monkeyResponse.startsWith("OK:")) { 472 // peel off "OK:" string and get the individual var names 473 String[] varNames = monkeyResponse.substring(3).split("\\s+"); 474 // grab all the individual var values 475 for (String name: varNames) { 476 sendMonkeyEvent("getvar " + name, false, false); 477 if(monkeyResponse != null) { 478 if (monkeyResponse.startsWith("OK") ) { 479 if (monkeyResponse.length() > 2) { 480 monkeyRecorder.addDeviceVar(name, monkeyResponse.substring(3)); 481 } else { 482 // only got OK - good variable but no value 483 monkeyRecorder.addDeviceVar(name, "null"); 484 } 485 } else { 486 // error returned - couldn't get var value for name... include error return 487 monkeyRecorder.addDeviceVar(name, monkeyResponse); 488 } 489 } else { 490 // no monkeyResponse - bad variable with no value 491 monkeyRecorder.addDeviceVar(name, "null"); 492 } 493 } 494 } else { 495 // it's an error, can't find variable names... 496 monkeyRecorder.addAttribute("listvar", monkeyResponse); 497 } 498 } 499 500 /** 501 * Process the command-line options 502 * 503 * @return Returns true if options were parsed with no apparent errors. 504 */ 505 private static void processOptions(String[] args) { 506 // parse command line parameters. 507 int index = 0; 508 509 do { 510 String argument = args[index++]; 511 512 if ("-s".equals(argument)) { 513 if(index == args.length) { 514 printUsageAndQuit("Missing Server after -s"); 515 } 516 517 monkeyServer = args[index++]; 518 519 } else if ("-p".equals(argument)) { 520 // quick check on the next argument. 521 if (index == args.length) { 522 printUsageAndQuit("Missing Server port after -p"); 523 } 524 525 monkeyPort = Integer.parseInt(args[index++]); 526 527 } else if ("-v".equals(argument)) { 528 // quick check on the next argument. 529 if (index == args.length) { 530 printUsageAndQuit("Missing Log Level after -v"); 531 } 532 533 Level level = Level.parse(args[index++]); 534 logger.setLevel(level); 535 level = logger.getLevel(); 536 System.out.println("Log level set to: " + level + "(" + level.intValue() + ")."); 537 System.out.println("Warning: Log levels below INFO(800) not working currently... parent issues"); 538 539 } else if (argument.startsWith("-")) { 540 // we have an unrecognized argument. 541 printUsageAndQuit("Unrecognized argument: " + argument + "."); 542 543 monkeyPort = Integer.parseInt(args[index++]); 544 545 } else { 546 // get the filepath of the script to run. This will be the last undashed argument. 547 scriptName = argument; 548 } 549 } while (index < args.length); 550 } 551 552 /* 553 * Grab an image from an ADB-connected device. 554 */ 555 private static void getDeviceImage(IDevice device, String filepath, boolean landscape) 556 throws IOException { 557 RawImage rawImage; 558 recordCommand("grabscreen"); 559 System.out.println("Grabbing Screeshot: " + filepath + "."); 560 561 try { 562 rawImage = device.getScreenshot(); 563 } 564 catch (IOException ioe) { 565 recordResponse("No frame buffer", ""); 566 printAndExit("Unable to get frame buffer: " + ioe.getMessage(), true /* terminate */); 567 return; 568 } 569 570 // device/adb not available? 571 if (rawImage == null) { 572 recordResponse("No image", ""); 573 return; 574 } 575 576 assert rawImage.bpp == 16; 577 578 BufferedImage image; 579 580 logger.info("Raw Image - height: " + rawImage.height + ", width: " + rawImage.width); 581 582 if (landscape) { 583 // convert raw data to an Image 584 image = new BufferedImage(rawImage.height, rawImage.width, 585 BufferedImage.TYPE_INT_ARGB); 586 587 byte[] buffer = rawImage.data; 588 int index = 0; 589 for (int y = 0 ; y < rawImage.height ; y++) { 590 for (int x = 0 ; x < rawImage.width ; x++) { 591 592 int value = buffer[index++] & 0x00FF; 593 value |= (buffer[index++] << 8) & 0x0FF00; 594 595 int r = ((value >> 11) & 0x01F) << 3; 596 int g = ((value >> 5) & 0x03F) << 2; 597 int b = ((value >> 0) & 0x01F) << 3; 598 599 value = 0xFF << 24 | r << 16 | g << 8 | b; 600 601 image.setRGB(y, rawImage.width - x - 1, value); 602 } 603 } 604 } else { 605 // convert raw data to an Image 606 image = new BufferedImage(rawImage.width, rawImage.height, 607 BufferedImage.TYPE_INT_ARGB); 608 609 byte[] buffer = rawImage.data; 610 int index = 0; 611 for (int y = 0 ; y < rawImage.height ; y++) { 612 for (int x = 0 ; x < rawImage.width ; x++) { 613 614 int value = buffer[index++] & 0x00FF; 615 value |= (buffer[index++] << 8) & 0x0FF00; 616 617 int r = ((value >> 11) & 0x01F) << 3; 618 int g = ((value >> 5) & 0x03F) << 2; 619 int b = ((value >> 0) & 0x01F) << 3; 620 621 value = 0xFF << 24 | r << 16 | g << 8 | b; 622 623 image.setRGB(x, y, value); 624 } 625 } 626 } 627 628 if (!ImageIO.write(image, "png", new File(filepath))) { 629 recordResponse("No png writer", ""); 630 throw new IOException("Failed to find png writer"); 631 } 632 recordResponse("OK", filepath); 633 } 634 635 private static void printUsageAndQuit(String message) { 636 // 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789 637 System.out.println(message); 638 System.out.println("Usage: monkeyrunner [options] SCRIPT_FILE"); 639 System.out.println(""); 640 System.out.println(" -s MonkeyServer IP Address."); 641 System.out.println(" -p MonkeyServer TCP Port."); 642 System.out.println(" -v MonkeyServer Logging level (ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, OFF)"); 643 System.out.println(""); 644 System.out.println(""); 645 646 System.exit(1); 647 } 648 649 private static void printAndExit(String message, boolean terminate) { 650 System.out.println(message); 651 if (terminate) { 652 AndroidDebugBridge.terminate(); 653 } 654 System.exit(1); 655 } 656 } 657