/* * Copyright 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.commands.monkey; import android.content.Context; import android.os.IPowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.lang.Integer; import java.lang.NumberFormatException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.StringTokenizer; /** * An Event source for getting Monkey Network Script commands from * over the network. */ public class MonkeySourceNetwork implements MonkeyEventSource { private static final String TAG = "MonkeyStub"; /* The version of the monkey network protocol */ public static final int MONKEY_NETWORK_VERSION = 2; private static DeferredReturn deferredReturn; /** * ReturnValue from the MonkeyCommand that indicates whether the * command was sucessful or not. */ public static class MonkeyCommandReturn { private final boolean success; private final String message; public MonkeyCommandReturn(boolean success) { this.success = success; this.message = null; } public MonkeyCommandReturn(boolean success, String message) { this.success = success; this.message = message; } boolean hasMessage() { return message != null; } String getMessage() { return message; } boolean wasSuccessful() { return success; } } public final static MonkeyCommandReturn OK = new MonkeyCommandReturn(true); public final static MonkeyCommandReturn ERROR = new MonkeyCommandReturn(false); public final static MonkeyCommandReturn EARG = new MonkeyCommandReturn(false, "Invalid Argument"); /** * Interface that MonkeyCommands must implement. */ public interface MonkeyCommand { /** * Translate the command line into a sequence of MonkeyEvents. * * @param command the command line. * @param queue the command queue. * @return MonkeyCommandReturn indicating what happened. */ MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue); } /** * Command to simulate closing and opening the keyboard. */ private static class FlipCommand implements MonkeyCommand { // flip open // flip closed public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() > 1) { String direction = command.get(1); if ("open".equals(direction)) { queue.enqueueEvent(new MonkeyFlipEvent(true)); return OK; } else if ("close".equals(direction)) { queue.enqueueEvent(new MonkeyFlipEvent(false)); return OK; } } return EARG; } } /** * Command to send touch events to the input system. */ private static class TouchCommand implements MonkeyCommand { // touch [down|up|move] [x] [y] // touch down 120 120 // touch move 140 140 // touch up 140 140 public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() == 4) { String actionName = command.get(1); int x = 0; int y = 0; try { x = Integer.parseInt(command.get(2)); y = Integer.parseInt(command.get(3)); } catch (NumberFormatException e) { // Ok, it wasn't a number Log.e(TAG, "Got something that wasn't a number", e); return EARG; } // figure out the action int action = -1; if ("down".equals(actionName)) { action = MotionEvent.ACTION_DOWN; } else if ("up".equals(actionName)) { action = MotionEvent.ACTION_UP; } else if ("move".equals(actionName)) { action = MotionEvent.ACTION_MOVE; } if (action == -1) { Log.e(TAG, "Got a bad action: " + actionName); return EARG; } queue.enqueueEvent(new MonkeyTouchEvent(action) .addPointer(0, x, y)); return OK; } return EARG; } } /** * Command to send Trackball events to the input system. */ private static class TrackballCommand implements MonkeyCommand { // trackball [dx] [dy] // trackball 1 0 -- move right // trackball -1 0 -- move left public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() == 3) { int dx = 0; int dy = 0; try { dx = Integer.parseInt(command.get(1)); dy = Integer.parseInt(command.get(2)); } catch (NumberFormatException e) { // Ok, it wasn't a number Log.e(TAG, "Got something that wasn't a number", e); return EARG; } queue.enqueueEvent(new MonkeyTrackballEvent(MotionEvent.ACTION_MOVE) .addPointer(0, dx, dy)); return OK; } return EARG; } } /** * Command to send Key events to the input system. */ private static class KeyCommand implements MonkeyCommand { // key [down|up] [keycode] // key down 82 // key up 82 public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() == 3) { int keyCode = getKeyCode(command.get(2)); if (keyCode < 0) { // Ok, you gave us something bad. Log.e(TAG, "Can't find keyname: " + command.get(2)); return EARG; } Log.d(TAG, "keycode: " + keyCode); int action = -1; if ("down".equals(command.get(1))) { action = KeyEvent.ACTION_DOWN; } else if ("up".equals(command.get(1))) { action = KeyEvent.ACTION_UP; } if (action == -1) { Log.e(TAG, "got unknown action."); return EARG; } queue.enqueueEvent(new MonkeyKeyEvent(action, keyCode)); return OK; } return EARG; } } /** * Get an integer keycode value from a given keyname. * * @param keyName the key name to get the code for * @return the integer keycode value, or -1 on error. */ private static int getKeyCode(String keyName) { int keyCode = -1; try { keyCode = Integer.parseInt(keyName); } catch (NumberFormatException e) { // Ok, it wasn't a number, see if we have a // keycode name for it keyCode = MonkeySourceRandom.getKeyCode(keyName); if (keyCode == -1) { // OK, one last ditch effort to find a match. // Build the KEYCODE_STRING from the string // we've been given and see if that key // exists. This would allow you to do "key // down menu", for example. keyCode = MonkeySourceRandom.getKeyCode("KEYCODE_" + keyName.toUpperCase()); } } return keyCode; } /** * Command to put the Monkey to sleep. */ private static class SleepCommand implements MonkeyCommand { // sleep 2000 public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() == 2) { int sleep = -1; String sleepStr = command.get(1); try { sleep = Integer.parseInt(sleepStr); } catch (NumberFormatException e) { Log.e(TAG, "Not a number: " + sleepStr, e); return EARG; } queue.enqueueEvent(new MonkeyThrottleEvent(sleep)); return OK; } return EARG; } } /** * Command to type a string */ private static class TypeCommand implements MonkeyCommand { // wake public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() == 2) { String str = command.get(1); char[] chars = str.toString().toCharArray(); // Convert the string to an array of KeyEvent's for // the built in keymap. KeyCharacterMap keyCharacterMap = KeyCharacterMap. load(KeyCharacterMap.VIRTUAL_KEYBOARD); KeyEvent[] events = keyCharacterMap.getEvents(chars); // enqueue all the events we just got. for (KeyEvent event : events) { queue.enqueueEvent(new MonkeyKeyEvent(event)); } return OK; } return EARG; } } /** * Command to wake the device up */ private static class WakeCommand implements MonkeyCommand { // wake public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (!wake()) { return ERROR; } return OK; } } /** * Command to "tap" at a location (Sends a down and up touch * event). */ private static class TapCommand implements MonkeyCommand { // tap x y public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() == 3) { int x = 0; int y = 0; try { x = Integer.parseInt(command.get(1)); y = Integer.parseInt(command.get(2)); } catch (NumberFormatException e) { // Ok, it wasn't a number Log.e(TAG, "Got something that wasn't a number", e); return EARG; } queue.enqueueEvent(new MonkeyTouchEvent(MotionEvent.ACTION_DOWN) .addPointer(0, x, y)); queue.enqueueEvent(new MonkeyTouchEvent(MotionEvent.ACTION_UP) .addPointer(0, x, y)); return OK; } return EARG; } } /** * Command to "press" a buttons (Sends an up and down key event.) */ private static class PressCommand implements MonkeyCommand { // press keycode public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() == 2) { int keyCode = getKeyCode(command.get(1)); if (keyCode < 0) { // Ok, you gave us something bad. Log.e(TAG, "Can't find keyname: " + command.get(1)); return EARG; } queue.enqueueEvent(new MonkeyKeyEvent(KeyEvent.ACTION_DOWN, keyCode)); queue.enqueueEvent(new MonkeyKeyEvent(KeyEvent.ACTION_UP, keyCode)); return OK; } return EARG; } } /** * Command to defer the return of another command until the given event occurs. * deferreturn takes three arguments. It takes an event to wait for (e.g. waiting for the * device to display a different activity would the "screenchange" event), a * timeout, which is the number of microseconds to wait for the event to occur, and it takes * a command. The command can be any other Monkey command that can be issued over the network * (e.g. press KEYCODE_HOME). deferreturn will then run this command, return an OK, wait for * the event to occur and return the deferred return value when either the event occurs or * when the timeout is reached (whichever occurs first). Note that there is no difference * between an event occurring and the timeout being reached; the client will have to verify * that the change actually occured. * * Example: * deferreturn screenchange 1000 press KEYCODE_HOME * This command will press the home key on the device and then wait for the screen to change * for up to one second. Either the screen will change, and the results fo the key press will * be returned to the client, or the timeout will be reached, and the results for the key * press will be returned to the client. */ private static class DeferReturnCommand implements MonkeyCommand { // deferreturn [event] [timeout (ms)] [command] // deferreturn screenchange 100 tap 10 10 public MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue) { if (command.size() > 3) { String event = command.get(1); int eventId; if (event.equals("screenchange")) { eventId = DeferredReturn.ON_WINDOW_STATE_CHANGE; } else { return EARG; } long timeout = Long.parseLong(command.get(2)); MonkeyCommand deferredCommand = COMMAND_MAP.get(command.get(3)); if (deferredCommand != null) { List<String> parts = command.subList(3, command.size()); MonkeyCommandReturn ret = deferredCommand.translateCommand(parts, queue); deferredReturn = new DeferredReturn(eventId, ret, timeout); return OK; } } return EARG; } } /** * Force the device to wake up. * * @return true if woken up OK. */ private static final boolean wake() { IPowerManager pm = IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE)); try { pm.userActivityWithForce(SystemClock.uptimeMillis(), true, true); } catch (RemoteException e) { Log.e(TAG, "Got remote exception", e); return false; } return true; } // This maps from command names to command implementations. private static final Map<String, MonkeyCommand> COMMAND_MAP = new HashMap<String, MonkeyCommand>(); static { // Add in all the commands we support COMMAND_MAP.put("flip", new FlipCommand()); COMMAND_MAP.put("touch", new TouchCommand()); COMMAND_MAP.put("trackball", new TrackballCommand()); COMMAND_MAP.put("key", new KeyCommand()); COMMAND_MAP.put("sleep", new SleepCommand()); COMMAND_MAP.put("wake", new WakeCommand()); COMMAND_MAP.put("tap", new TapCommand()); COMMAND_MAP.put("press", new PressCommand()); COMMAND_MAP.put("type", new TypeCommand()); COMMAND_MAP.put("listvar", new MonkeySourceNetworkVars.ListVarCommand()); COMMAND_MAP.put("getvar", new MonkeySourceNetworkVars.GetVarCommand()); COMMAND_MAP.put("listviews", new MonkeySourceNetworkViews.ListViewsCommand()); COMMAND_MAP.put("queryview", new MonkeySourceNetworkViews.QueryViewCommand()); COMMAND_MAP.put("getrootview", new MonkeySourceNetworkViews.GetRootViewCommand()); COMMAND_MAP.put("getviewswithtext", new MonkeySourceNetworkViews.GetViewsWithTextCommand()); COMMAND_MAP.put("deferreturn", new DeferReturnCommand()); } // QUIT command private static final String QUIT = "quit"; // DONE command private static final String DONE = "done"; // command response strings private static final String OK_STR = "OK"; private static final String ERROR_STR = "ERROR"; public static interface CommandQueue { /** * Enqueue an event to be returned later. This allows a * command to return multiple events. Commands using the * command queue still have to return a valid event from their * translateCommand method. The returned command will be * executed before anything put into the queue. * * @param e the event to be enqueued. */ public void enqueueEvent(MonkeyEvent e); }; // Queue of Events to be processed. This allows commands to push // multiple events into the queue to be processed. private static class CommandQueueImpl implements CommandQueue{ private final Queue<MonkeyEvent> queuedEvents = new LinkedList<MonkeyEvent>(); public void enqueueEvent(MonkeyEvent e) { queuedEvents.offer(e); } /** * Get the next queued event to excecute. * * @return the next event, or null if there aren't any more. */ public MonkeyEvent getNextQueuedEvent() { return queuedEvents.poll(); } }; // A holder class for a deferred return value. This allows us to defer returning the success of // a call until a given event has occurred. private static class DeferredReturn { public static final int ON_WINDOW_STATE_CHANGE = 1; private int event; private MonkeyCommandReturn deferredReturn; private long timeout; public DeferredReturn(int event, MonkeyCommandReturn deferredReturn, long timeout) { this.event = event; this.deferredReturn = deferredReturn; this.timeout = timeout; } /** * Wait until the given event has occurred before returning the value. * @return The MonkeyCommandReturn from the command that was deferred. */ public MonkeyCommandReturn waitForEvent() { switch(event) { case ON_WINDOW_STATE_CHANGE: try { synchronized(MonkeySourceNetworkViews.class) { MonkeySourceNetworkViews.class.wait(timeout); } } catch(InterruptedException e) { Log.d(TAG, "Deferral interrupted: " + e.getMessage()); } } return deferredReturn; } }; private final CommandQueueImpl commandQueue = new CommandQueueImpl(); private BufferedReader input; private PrintWriter output; private boolean started = false; private ServerSocket serverSocket; private Socket clientSocket; public MonkeySourceNetwork(int port) throws IOException { // Only bind this to local host. This means that you can only // talk to the monkey locally, or though adb port forwarding. serverSocket = new ServerSocket(port, 0, // default backlog InetAddress.getLocalHost()); } /** * Start a network server listening on the specified port. The * network protocol is a line oriented protocol, where each line * is a different command that can be run. * * @param port the port to listen on */ private void startServer() throws IOException { clientSocket = serverSocket.accept(); // At this point, we have a client connected. // Attach the accessibility listeners so that we can start receiving // view events. Do this before wake so we can catch the wake event // if possible. MonkeySourceNetworkViews.setup(); // Wake the device up in preparation for doing some commands. wake(); input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); // auto-flush output = new PrintWriter(clientSocket.getOutputStream(), true); } /** * Stop the server from running so it can reconnect a new client. */ private void stopServer() throws IOException { clientSocket.close(); input.close(); output.close(); started = false; } /** * Helper function for commandLineSplit that replaces quoted * charaters with their real values. * * @param input the string to do replacement on. * @return the results with the characters replaced. */ private static String replaceQuotedChars(String input) { return input.replace("\\\"", "\""); } /** * This function splits the given line into String parts. It obey's quoted * strings and returns them as a single part. * * "This is a test" -> returns only one element * This is a test -> returns four elements * * @param line the line to parse * @return the List of elements */ private static List<String> commandLineSplit(String line) { ArrayList<String> result = new ArrayList<String>(); StringTokenizer tok = new StringTokenizer(line); boolean insideQuote = false; StringBuffer quotedWord = new StringBuffer(); while (tok.hasMoreTokens()) { String cur = tok.nextToken(); if (!insideQuote && cur.startsWith("\"")) { // begin quote quotedWord.append(replaceQuotedChars(cur)); insideQuote = true; } else if (insideQuote) { // end quote if (cur.endsWith("\"")) { insideQuote = false; quotedWord.append(" ").append(replaceQuotedChars(cur)); String word = quotedWord.toString(); // trim off the quotes result.add(word.substring(1, word.length() - 1)); } else { quotedWord.append(" ").append(replaceQuotedChars(cur)); } } else { result.add(replaceQuotedChars(cur)); } } return result; } /** * Translate the given command line into a MonkeyEvent. * * @param commandLine the full command line given. */ private void translateCommand(String commandLine) { Log.d(TAG, "translateCommand: " + commandLine); List<String> parts = commandLineSplit(commandLine); if (parts.size() > 0) { MonkeyCommand command = COMMAND_MAP.get(parts.get(0)); if (command != null) { MonkeyCommandReturn ret = command.translateCommand(parts, commandQueue); handleReturn(ret); } } } private void handleReturn(MonkeyCommandReturn ret) { if (ret.wasSuccessful()) { if (ret.hasMessage()) { returnOk(ret.getMessage()); } else { returnOk(); } } else { if (ret.hasMessage()) { returnError(ret.getMessage()); } else { returnError(); } } } public MonkeyEvent getNextEvent() { if (!started) { try { startServer(); } catch (IOException e) { Log.e(TAG, "Got IOException from server", e); return null; } started = true; } // Now, get the next command. This call may block, but that's OK try { while (true) { // Check to see if we have any events queued up. If // we do, use those until we have no more. Then get // more input from the user. MonkeyEvent queuedEvent = commandQueue.getNextQueuedEvent(); if (queuedEvent != null) { // dispatch the event return queuedEvent; } // Check to see if we have any returns that have been deferred. If so, now that // we've run the queued commands, wait for the given event to happen (or the timeout // to be reached), and handle the deferred MonkeyCommandReturn. if (deferredReturn != null) { Log.d(TAG, "Waiting for event"); MonkeyCommandReturn ret = deferredReturn.waitForEvent(); deferredReturn = null; handleReturn(ret); } String command = input.readLine(); if (command == null) { Log.d(TAG, "Connection dropped."); // Treat this exactly the same as if the user had // ended the session cleanly with a done commant. command = DONE; } if (DONE.equals(command)) { // stop the server so it can accept new connections try { stopServer(); } catch (IOException e) { Log.e(TAG, "Got IOException shutting down!", e); return null; } // return a noop event so we keep executing the main // loop return new MonkeyNoopEvent(); } // Do quit checking here if (QUIT.equals(command)) { // then we're done Log.d(TAG, "Quit requested"); // let the host know the command ran OK returnOk(); return null; } // Do comment checking here. Comments aren't a // command, so we don't echo anything back to the // user. if (command.startsWith("#")) { // keep going continue; } // Translate the command line. This will handle returning error/ok to the user translateCommand(command); } } catch (IOException e) { Log.e(TAG, "Exception: ", e); return null; } } /** * Returns ERROR to the user. */ private void returnError() { output.println(ERROR_STR); } /** * Returns ERROR to the user. * * @param msg the error message to include */ private void returnError(String msg) { output.print(ERROR_STR); output.print(":"); output.println(msg); } /** * Returns OK to the user. */ private void returnOk() { output.println(OK_STR); } /** * Returns OK to the user. * * @param returnValue the value to return from this command. */ private void returnOk(String returnValue) { output.print(OK_STR); output.print(":"); output.println(returnValue); } public void setVerbose(int verbose) { // We're not particualy verbose } public boolean validate() { // we have no pre-conditions to validate return true; } }