Home | History | Annotate | Download | only in monkeyrunner
      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