Added in simple command scripting to monkey over a TCP socket.

This allows a host program to talk to the monkey over TCP (via adb) and script up specific commands to run.
This commit is contained in:
Bill Napier
2009-08-07 11:34:12 -07:00
parent 36e0ac795e
commit a68dbdb1c3
6 changed files with 711 additions and 160 deletions

View File

@@ -7,6 +7,7 @@ LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_MODULE := monkey
include $(BUILD_JAVA_LIBRARY)
################################################################
include $(CLEAR_VARS)
ALL_PREBUILT += $(TARGET_OUT)/bin/monkey
$(TARGET_OUT)/bin/monkey : $(LOCAL_PATH)/monkey | $(ACP)

View File

@@ -0,0 +1,86 @@
MONKEY NETWORK SCRIPT
The Monkey Network Script was designed to be a low-level way to
programmability inject KeyEvents and MotionEvents into the input
system. The idea is that a process will run on a host computer that
will support higher-level operations (like conditionals, etc.) and
will talk (via TCP over ADB) to the device in Monkey Network Script.
For security reasons, the Monkey only binds to localhost, so you will
need to use adb to setup port forwarding to actually talk to the
device.
INITIAL SETUP
Setup port forwarding from a local port on your machine to a port on
the device:
$ adb forward tcp:1080 tcp:1080
Start the monkey server
$ adb shell monkey --port 1080
Now you're ready to run commands
COMMAND LIST
Individual commands are separated by newlines. The Monkey will
respond to every command with a line starting with OK for commands
that executed without a problem, or a line starting with ERROR for
commands that had problems being run. The Monkey may decide to return
more information about command execution. That information would come
on the same line after the OK or ERROR. A possible example:
key down menu
OK
touch monkey
ERROR: monkey not a number
The complete list of commands follows:
key [down|up] keycode
This command injects KeyEvent's into the input system. The keycode
parameter refers to the KEYCODE list in the KeyEvent class
(http://developer.android.com/reference/android/view/KeyEvent.html).
The format of that parameter is quite flexible. Using the menu key as
an example, it can be 82 (the integer value of the keycode),
KEYCODE_MENU (the name of the keycode), or just menu (and the Monkey
will add the KEYCODE part). Do note that this last part doesn't work
for things like KEYCODE_1 for obvious reasons.
Note that sending a full button press requires sending both the down
and the up event for that key
touch [down|up|move] x y
This command injects a MotionEvent into the input system that
simulates a user touching the touchscreen (or a pointer event). x and
y specify coordinates on the display (0 0 being the upper left) for
the touch event to happen. Just like key events, touch events at a
single location require both a down and an up. To simulate dragging,
send a "touch down", then a series of "touch move" events (to simulate
the drag), followed by a "touch up" at the final location.
trackball dx dy
This command injects a MotionEvent into the input system that
simulates a user using the trackball. dx and dy indicates the amount
of change in the trackball location (as opposed to exact coordinates
that the touch events use)
flip [open|close]
This simulates the opening or closing the keyboard (like on dream).
OTHER NOTES
There are some convenience features added to allow running without
needing a host process.
Lines starting with a # character are considered comments. The Monkey
eats them and returns no indication that it did anything (no ERROR and
no OK).
You can put the Monkey to sleep by using the "sleep" command with a
single argument, how many ms to sleep.

View File

@@ -0,0 +1,57 @@
# Touch the android
touch down 160 200
touch up 160 200
sleep 1000
# Hit Next
touch down 300 450
touch up 300 450
sleep 1000
# Hit Next
touch down 300 450
touch up 300 450
sleep 1000
# Hit Next
touch down 300 450
touch up 300 450
sleep 1000
# Go down and select the account username
key down dpad_down
key up dpad_down
key down dpad_down
key up dpad_down
key down dpad_center
key up dpad_center
# account name: bill
key down b
key up b
key down i
key up i
key down l
key up l
key down l
key up l
# Go down to the password field
key down dpad_down
key up dpad_down
# password: bill
key down b
key up b
key down i
key up i
key down l
key up l
key down l
key up l
# Select next
touch down 300 450
touch up 300 450
# quit
quit

View File

@@ -125,6 +125,9 @@ public class Monkey {
/** a filename to the script (if any) **/
private String mScriptFileName = null;
/** a TCP port to listen on for remote commands. */
private int mServerPort = -1;
private static final File TOMBSTONES_PATH = new File("/data/tombstones");
private HashSet<String> mTombstones = null;
@@ -365,6 +368,9 @@ public class Monkey {
// script mode, ignore other options
mEventSource = new MonkeySourceScript(mScriptFileName, mThrottle);
mEventSource.setVerbose(mVerbose);
} else if (mServerPort != -1) {
mEventSource = new MonkeySourceNetwork(mServerPort);
mCount = Integer.MAX_VALUE;
} else {
// random source by default
if (mVerbose >= 2) { // check seeding performance
@@ -527,7 +533,9 @@ public class Monkey {
// do nothing - it's caught at the very start of run()
} else if (opt.equals("--dbg-no-events")) {
mSendNoEvents = true;
} else if (opt.equals("-f")) {
} else if (opt.equals("--port")) {
mServerPort = (int) nextOptionLong("Server port to listen on for commands");
} else if (opt.equals("-f")) {
mScriptFileName = nextOptionData();
} else if (opt.equals("-h")) {
showUsage();
@@ -544,19 +552,23 @@ public class Monkey {
return false;
}
String countStr = nextArg();
if (countStr == null) {
System.err.println("** Error: Count not specified");
showUsage();
return false;
}
// If a server port hasn't been specified, we need to specify
// a count
if (mServerPort == -1) {
String countStr = nextArg();
if (countStr == null) {
System.err.println("** Error: Count not specified");
showUsage();
return false;
}
try {
mCount = Integer.parseInt(countStr);
} catch (NumberFormatException e) {
System.err.println("** Error: Count is not a number");
showUsage();
return false;
try {
mCount = Integer.parseInt(countStr);
} catch (NumberFormatException e) {
System.err.println("** Error: Count is not a number");
showUsage();
return false;
}
}
return true;
@@ -749,9 +761,11 @@ public class Monkey {
} else if (injectCode == MonkeyEvent.INJECT_ERROR_SECURITY_EXCEPTION) {
systemCrashed = !mIgnoreSecurityExceptions;
}
} else {
// Event Source has signaled that we have no more events to process
break;
}
}
// If we got this far, we succeeded!
return mCount;
}
@@ -904,6 +918,7 @@ public class Monkey {
System.err.println(" [--pct-appswitch PERCENT] [--pct-flip PERCENT]");
System.err.println(" [--pct-anyevent PERCENT]");
System.err.println(" [--wait-dbg] [--dbg-no-events] [-f scriptfile]");
System.err.println(" [--port port]");
System.err.println(" [-s SEED] [-v [-v] ...] [--throttle MILLISEC]");
System.err.println(" COUNT");
}

View File

@@ -0,0 +1,376 @@
/*
* 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.util.Log;
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.List;
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";
private interface MonkeyCommand {
MonkeyEvent translateCommand(List<String> command);
}
/**
* Command to simulate closing and opening the keyboard.
*/
private static class FlipCommand implements MonkeyCommand {
// flip open
// flip closed
public MonkeyEvent translateCommand(List<String> command) {
if (command.size() > 1) {
String direction = command.get(1);
if ("open".equals(direction)) {
return new MonkeyFlipEvent(true);
} else if ("close".equals(direction)) {
return new MonkeyFlipEvent(false);
}
}
return null;
}
}
/**
* 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 MonkeyEvent translateCommand(List<String> command) {
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 null;
}
// 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 null;
}
return new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
-1, action, x, y, 0);
}
return null;
}
}
/**
* 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 MonkeyEvent translateCommand(List<String> command) {
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 null;
}
return new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, -1,
MotionEvent.ACTION_MOVE, dx, dy, 0);
}
return null;
}
}
/**
* 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 MonkeyEvent translateCommand(List<String> command) {
if (command.size() == 3) {
int keyCode = -1;
String keyName = command.get(2);
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());
if (keyCode == -1) {
// Ok, you gave us something bad.
Log.e(TAG, "Can't find keyname: " + keyName);
return null;
}
}
}
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 null;
}
return new MonkeyKeyEvent(action, keyCode);
}
return null;
}
}
/**
* Command to put the Monkey to sleep.
*/
private static class SleepCommand implements MonkeyCommand {
// sleep 2000
public MonkeyEvent translateCommand(List<String> command) {
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 new MonkeyThrottleEvent(sleep);
}
return null;
}
}
// 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());
}
// QUIT command
private static final String QUIT = "quit";
// command response strings
private static final String OK = "OK";
private static final String ERROR = "ERROR";
private final int port;
private BufferedReader input;
private PrintWriter output;
private boolean started = false;
public MonkeySourceNetwork(int port) {
this.port = port;
}
/**
* 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 {
// Only bind this to local host. This means that you can only
// talk to the monkey locally, or though adb port forwarding.
ServerSocket server = new ServerSocket(port,
0, // default backlog
InetAddress.getLocalHost());
Socket s = server.accept();
input = new BufferedReader(new InputStreamReader(s.getInputStream()));
// auto-flush
output = new PrintWriter(s.getOutputStream(), true);
}
/**
* 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(cur);
insideQuote = true;
} else if (insideQuote) {
// end quote
if (cur.endsWith("\"")) {
insideQuote = false;
quotedWord.append(cur);
String word = quotedWord.toString();
// trim off the quotes
result.add(word.substring(1, word.length() - 1));
} else {
quotedWord.append(cur);
}
} else {
result.add(cur);
}
}
return result;
}
/**
* Translate the given command line into a MonkeyEvent.
*
* @param commandLine the full command line given.
* @returns the MonkeyEvent corresponding to the command, or null
* if there was an issue.
*/
private MonkeyEvent 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) {
return command.translateCommand(parts);
}
return null;
}
return null;
}
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) {
String command = input.readLine();
if (command == null) {
Log.d(TAG, "Connection dropped.");
return null;
}
// 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
output.println(OK);
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
MonkeyEvent event = translateCommand(command);
if (event != null) {
// let the host know the command ran OK
output.println(OK);
return event;
}
// keep going. maybe the next command will make more sense
Log.e(TAG, "Got unknown command! \"" + command + "\"");
output.println(ERROR);
}
} catch (IOException e) {
Log.e(TAG, "Exception: ", e);
return null;
}
}
public void setVerbose(int verbose) {
// We're not particualy verbose
}
public boolean validate() {
// we have no pre-conditions to validate
return true;
}
}

View File

@@ -55,7 +55,7 @@ public class MonkeySourceRandom implements MonkeyEventSource {
/** Nice names for all key events. */
private static final String[] KEY_NAMES = {
"KEYCODE_UNKNOWN",
"KEYCODE_MENU",
"KEYCODE_SOFT_LEFT",
"KEYCODE_SOFT_RIGHT",
"KEYCODE_HOME",
"KEYCODE_BACK",
@@ -186,6 +186,22 @@ public class MonkeySourceRandom implements MonkeyEventSource {
return KEY_NAMES[keycode];
}
/**
* Looks up the keyCode from a given KEYCODE_NAME. NOTE: This may
* be an expensive operation.
*
* @param keyName the name of the KEYCODE_VALUE to lookup.
* @returns the intenger keyCode value, or -1 if not found
*/
public static int getKeyCode(String keyName) {
for (int x = 0; x < KEY_NAMES.length; x++) {
if (KEY_NAMES[x].equals(keyName)) {
return x;
}
}
return -1;
}
public MonkeySourceRandom(long seed, ArrayList<ComponentName> MainApps, long throttle) {
// default values for random distributions
// note, these are straight percentages, to match user input (cmd line args)