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

@@ -47,10 +47,10 @@ import java.util.List;
* Application that injects random key events and other actions into the system.
*/
public class Monkey {
/**
* Monkey Debugging/Dev Support
*
*
* All values should be zero when checking in.
*/
private final static int DEBUG_ALLOW_ANY_STARTS = 0;
@@ -74,20 +74,20 @@ public class Monkey {
/** Ignore any not responding timeouts while running? */
private boolean mIgnoreTimeouts;
/** Ignore security exceptions when launching activities */
/** (The activity launch still fails, but we keep pluggin' away) */
private boolean mIgnoreSecurityExceptions;
/** Monitor /data/tombstones and stop the monkey if new files appear. */
private boolean mMonitorNativeCrashes;
/** Send no events. Use with long throttle-time to watch user operations */
private boolean mSendNoEvents;
/** This is set when we would like to abort the running of the monkey. */
private boolean mAbort;
/** This is set by the ActivityController thread to request collection of ANR trace files */
private boolean mRequestAnrTraces = false;
@@ -96,7 +96,7 @@ public class Monkey {
/** Kill the process after a timeout or crash. */
private boolean mKillProcessAfterError;
/** Generate hprof reports before/after monkey runs */
private boolean mGenerateHprof;
@@ -106,16 +106,16 @@ public class Monkey {
ArrayList<String> mMainCategories = new ArrayList<String>();
/** Applications we can switch to. */
private ArrayList<ComponentName> mMainApps = new ArrayList<ComponentName>();
/** The delay between event inputs **/
long mThrottle = 0;
/** The number of iterations **/
int mCount = 1000;
/** The random number seed **/
long mSeed = 0;
/** Dropped-event statistics **/
long mDroppedKeyEvents = 0;
long mDroppedPointerEvents = 0;
@@ -124,14 +124,17 @@ 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;
float[] mFactors = new float[MonkeySourceRandom.FACTORZ_COUNT];
float[] mFactors = new float[MonkeySourceRandom.FACTORZ_COUNT];
MonkeyEventSource mEventSource;
private MonkeyNetworkMonitor mNetworkMonitor = new MonkeyNetworkMonitor();
/**
* Monitor operations happening in the system.
*/
@@ -144,7 +147,7 @@ public class Monkey {
}
return allow;
}
public boolean activityResuming(String pkg) {
System.out.println(" // activityResuming(" + pkg + ")");
boolean allow = checkEnteringPackage(pkg) || (DEBUG_ALLOW_ANY_RESTARTS != 0);
@@ -156,7 +159,7 @@ public class Monkey {
}
return allow;
}
private boolean checkEnteringPackage(String pkg) {
if (pkg == null) {
return true;
@@ -168,7 +171,7 @@ public class Monkey {
return mValidPackages.contains(pkg);
}
}
public boolean appCrashed(String processName, int pid, String shortMsg,
String longMsg, byte[] crashData) {
System.err.println("// CRASH: " + processName + " (pid " + pid
@@ -223,14 +226,14 @@ public class Monkey {
return 1;
}
}
/**
* Run the procrank tool to insert system status information into the debug report.
*/
private void reportProcRank() {
commandLineReport("procrank", "procrank");
}
/**
* Run "cat /data/anr/traces.txt". Wait about 5 seconds first, to let the asynchronous
* report writing complete.
@@ -238,21 +241,21 @@ public class Monkey {
private void reportAnrTraces() {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
} catch (InterruptedException e) {
}
commandLineReport("anr traces", "cat /data/anr/traces.txt");
}
/**
* Run "dumpsys meminfo"
*
*
* NOTE: You cannot perform a dumpsys call from the ActivityController callback, as it will
* deadlock. This should only be called from the main loop of the monkey.
*/
private void reportDumpsysMemInfo() {
commandLineReport("meminfo", "dumpsys meminfo");
}
/**
* Print report from a single command line.
* @param reportName Simple tag that will print before the report and in various annotations.
@@ -266,7 +269,7 @@ public class Monkey {
try {
// Process must be fully qualified here because android.os.Process is used elsewhere
java.lang.Process p = Runtime.getRuntime().exec(command);
// pipe everything from process stdout -> System.err
InputStream inStream = p.getInputStream();
InputStreamReader inReader = new InputStreamReader(inStream);
@@ -275,7 +278,7 @@ public class Monkey {
while ((s = inBuffer.readLine()) != null) {
System.err.println(s);
}
int status = p.waitFor();
System.err.println("// " + reportName + " status was " + status);
} catch (Exception e) {
@@ -307,26 +310,26 @@ public class Monkey {
Debug.waitForDebugger();
}
}
// Default values for some command-line options
mVerbose = 0;
mCount = 1000;
mSeed = 0;
mThrottle = 0;
// prepare for command-line processing
mArgs = args;
mNextArg = 0;
//set a positive value, indicating none of the factors is provided yet
for (int i = 0; i < MonkeySourceRandom.FACTORZ_COUNT; i++) {
mFactors[i] = 1.0f;
}
if (!processOptions()) {
return -1;
}
// now set up additional data in preparation for launch
if (mMainCategories.size() == 0) {
mMainCategories.add(Intent.CATEGORY_LAUNCHER);
@@ -348,11 +351,11 @@ public class Monkey {
}
}
}
if (!checkInternalConfiguration()) {
return -2;
}
if (!getSystemInterfaces()) {
return -3;
}
@@ -360,11 +363,14 @@ public class Monkey {
if (!getMainApps()) {
return -4;
}
if (mScriptFileName != null) {
// 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
@@ -378,7 +384,7 @@ public class Monkey {
((MonkeySourceRandom) mEventSource).setFactors(i, mFactors[i]);
}
}
//in random mode, we start with a random activity
((MonkeySourceRandom) mEventSource).generateActivity();
}
@@ -387,7 +393,7 @@ public class Monkey {
if (!mEventSource.validate()) {
return -5;
}
if (mScriptFileName != null) {
// in random mode, count is the number of single events
// while in script mode, count is the number of repetition
@@ -396,12 +402,12 @@ public class Monkey {
mCount = mCount * ((MonkeySourceScript) mEventSource)
.getOneRoundEventCount();
}
// If we're profiling, do it immediately before/after the main monkey loop
if (mGenerateHprof) {
signalPersistentProcesses();
}
mNetworkMonitor.start();
int crashedAtCycle = runMonkeyCycles();
mNetworkMonitor.stop();
@@ -423,7 +429,7 @@ public class Monkey {
System.out.println("// Generated profiling reports in /data/misc");
}
}
try {
mAm.setActivityController(null);
mNetworkMonitor.unregister(mAm);
@@ -434,7 +440,7 @@ public class Monkey {
crashedAtCycle = mCount - 1;
}
}
// report dropped event stats
if (mVerbose > 0) {
System.out.print(":Dropped: keys=");
@@ -446,7 +452,7 @@ public class Monkey {
System.out.print(" flips=");
System.out.println(mDroppedFlipEvents);
}
// report network stats
mNetworkMonitor.dump();
@@ -461,10 +467,10 @@ public class Monkey {
return 0;
}
}
/**
* Process the command-line options
*
*
* @return Returns true if options were parsed with no apparent errors.
*/
private boolean processOptions() {
@@ -498,28 +504,28 @@ public class Monkey {
} else if (opt.equals("--hprof")) {
mGenerateHprof = true;
} else if (opt.equals("--pct-touch")) {
mFactors[MonkeySourceRandom.FACTOR_TOUCH] =
mFactors[MonkeySourceRandom.FACTOR_TOUCH] =
-nextOptionLong("touch events percentage");
} else if (opt.equals("--pct-motion")) {
mFactors[MonkeySourceRandom.FACTOR_MOTION] =
mFactors[MonkeySourceRandom.FACTOR_MOTION] =
-nextOptionLong("motion events percentage");
} else if (opt.equals("--pct-trackball")) {
mFactors[MonkeySourceRandom.FACTOR_TRACKBALL] =
mFactors[MonkeySourceRandom.FACTOR_TRACKBALL] =
-nextOptionLong("trackball events percentage");
} else if (opt.equals("--pct-nav")) {
mFactors[MonkeySourceRandom.FACTOR_NAV] =
mFactors[MonkeySourceRandom.FACTOR_NAV] =
-nextOptionLong("nav events percentage");
} else if (opt.equals("--pct-majornav")) {
mFactors[MonkeySourceRandom.FACTOR_MAJORNAV] =
mFactors[MonkeySourceRandom.FACTOR_MAJORNAV] =
-nextOptionLong("major nav events percentage");
} else if (opt.equals("--pct-appswitch")) {
mFactors[MonkeySourceRandom.FACTOR_APPSWITCH] =
mFactors[MonkeySourceRandom.FACTOR_APPSWITCH] =
-nextOptionLong("app switch events percentage");
} else if (opt.equals("--pct-flip")) {
mFactors[MonkeySourceRandom.FACTOR_FLIP] =
-nextOptionLong("keyboard flip percentage");
} else if (opt.equals("--pct-anyevent")) {
mFactors[MonkeySourceRandom.FACTOR_ANYTHING] =
mFactors[MonkeySourceRandom.FACTOR_ANYTHING] =
-nextOptionLong("any events percentage");
} else if (opt.equals("--throttle")) {
mThrottle = nextOptionLong("delay (in milliseconds) to wait between events");
@@ -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;
@@ -564,7 +576,7 @@ public class Monkey {
/**
* Check for any internal configuration (primarily build-time) errors.
*
*
* @return Returns true if ready to rock.
*/
private boolean checkInternalConfiguration() {
@@ -585,7 +597,7 @@ public class Monkey {
/**
* Attach to the required system interfaces.
*
*
* @return Returns true if all system interfaces were available.
*/
private boolean getSystemInterfaces() {
@@ -621,7 +633,7 @@ public class Monkey {
/**
* Using the restrictions provided (categories & packages), generate a list of activities
* that we can actually switch to.
*
*
* @return Returns true if it could successfully build a list of target activities
*/
private boolean getMainApps() {
@@ -644,7 +656,7 @@ public class Monkey {
final int NA = mainApps.size();
for (int a = 0; a < NA; a++) {
ResolveInfo r = mainApps.get(a);
if (mValidPackages.size() == 0 ||
if (mValidPackages.size() == 0 ||
mValidPackages.contains(r.activityInfo.applicationInfo.packageName)) {
if (mVerbose >= 2) { // very verbose
System.out.println("// + Using main activity "
@@ -676,15 +688,15 @@ public class Monkey {
System.out.println("** No activities found to run, monkey aborted.");
return false;
}
return true;
}
/**
* Run mCount cycles and see if we hit any crashers.
*
*
* TODO: Meta state on keys
*
*
* @return Returns the last cycle which executed. If the value == mCount, no errors detected.
*/
private int runMonkeyCycles() {
@@ -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;
}
@@ -775,18 +789,18 @@ public class Monkey {
/**
* Watch for appearance of new tombstone files, which indicate native crashes.
*
*
* @return Returns true if new files have appeared in the list
*/
private boolean checkNativeCrashes() {
String[] tombstones = TOMBSTONES_PATH.list();
// shortcut path for usually empty directory, so we don't waste even more objects
if ((tombstones == null) || (tombstones.length == 0)) {
mTombstones = null;
return false;
}
// use set logic to look for new files
HashSet<String> newStones = new HashSet<String>();
for (String x : tombstones) {
@@ -804,14 +818,14 @@ public class Monkey {
/**
* Return the next command line option. This has a number of special cases which
* closely, but not exactly, follow the POSIX command line options patterns:
*
*
* -- means to stop processing additional options
* -z means option z
* -z ARGS means option z with (non-optional) arguments ARGS
* -zARGS means option z with (optional) arguments ARGS
* --zz means option zz
* --zz ARGS means option zz with (non-optional) arguments ARGS
*
*
* Note that you cannot combine single letter options; -abc != -a -b -c
*
* @return Returns the option string, or null if there are no more options.
@@ -857,10 +871,10 @@ public class Monkey {
mNextArg++;
return data;
}
/**
* Returns a long converted from the next data argument, with error handling if not available.
*
*
* @param opt The name of the option.
* @return Returns a long converted from the argument.
*/
@@ -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

@@ -31,7 +31,7 @@ import java.util.Random;
/**
* monkey event queue
*/
public class MonkeySourceRandom implements MonkeyEventSource {
public class MonkeySourceRandom implements MonkeyEventSource {
/** Key events that move around the UI. */
private static final int[] NAV_KEYS = {
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN,
@@ -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",
@@ -146,7 +146,7 @@ public class MonkeySourceRandom implements MonkeyEventSource {
"KEYCODE_REWIND",
"KEYCODE_FORWARD",
"KEYCODE_MUTE",
"TAG_LAST_KEYCODE" // EOL. used to keep the lists in sync
};
@@ -158,34 +158,50 @@ public class MonkeySourceRandom implements MonkeyEventSource {
public static final int FACTOR_SYSOPS = 5;
public static final int FACTOR_APPSWITCH = 6;
public static final int FACTOR_FLIP = 7;
public static final int FACTOR_ANYTHING = 8;
public static final int FACTOR_ANYTHING = 8;
public static final int FACTORZ_COUNT = 9; // should be last+1
/** percentages for each type of event. These will be remapped to working
* values after we read any optional values.
**/
**/
private float[] mFactors = new float[FACTORZ_COUNT];
private ArrayList<ComponentName> mMainApps;
private int mEventCount = 0; //total number of events generated so far
private MonkeyEventQueue mQ;
private Random mRandom;
private Random mRandom;
private int mVerbose = 0;
private long mThrottle = 0;
private boolean mKeyboardOpen = false;
/**
/**
* @return the last name in the key list
*/
public static String getLastKeyName() {
return KEY_NAMES[KeyEvent.getMaxKeyCode() + 1];
}
public static String getKeyName(int keycode) {
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)
@@ -199,7 +215,7 @@ public class MonkeySourceRandom implements MonkeyEventSource {
mFactors[FACTOR_APPSWITCH] = 2.0f;
mFactors[FACTOR_FLIP] = 1.0f;
mFactors[FACTOR_ANYTHING] = 15.0f;
mRandom = new SecureRandom();
mRandom.setSeed((seed == 0) ? -1 : seed);
mMainApps = MainApps;
@@ -220,25 +236,25 @@ public class MonkeySourceRandom implements MonkeyEventSource {
} else {
defaultSum += mFactors[i];
++defaultCount;
}
}
}
// if the user request was > 100%, reject it
if (userSum > 100.0f) {
System.err.println("** Event weights > 100%");
return false;
}
// if the user specified all of the weights, then they need to be 100%
if (defaultCount == 0 && (userSum < 99.9f || userSum > 100.1f)) {
System.err.println("** Event weights != 100%");
return false;
}
// compute the adjustment necessary
float defaultsTarget = (100.0f - userSum);
float defaultsAdjustment = defaultsTarget / defaultSum;
// fix all values, by adjusting defaults, or flipping user values back to >0
for (int i = 0; i < FACTORZ_COUNT; ++i) {
if (mFactors[i] <= 0.0f) { // user values are zero or negative
@@ -247,46 +263,46 @@ public class MonkeySourceRandom implements MonkeyEventSource {
mFactors[i] *= defaultsAdjustment;
}
}
// if verbose, show factors
if (mVerbose > 0) {
System.out.println("// Event percentages:");
for (int i = 0; i < FACTORZ_COUNT; ++i) {
System.out.println("// " + i + ": " + mFactors[i] + "%");
}
}
}
// finally, normalize and convert to running sum
float sum = 0.0f;
for (int i = 0; i < FACTORZ_COUNT; ++i) {
sum += mFactors[i] / 100.0f;
mFactors[i] = sum;
}
}
return true;
}
/**
* set the factors
*
*
* @param factors: percentages for each type of event
*/
public void setFactors(float factors[]) {
int c = FACTORZ_COUNT;
if (factors.length < c) {
c = factors.length;
}
}
for (int i = 0; i < c; i++)
mFactors[i] = factors[i];
}
public void setFactors(int index, float v) {
mFactors[index] = v;
}
/**
* Generates a random motion event. This method counts a down, move, and up as multiple events.
*
*
* TODO: Test & fix the selectors when non-zero percentages
* TODO: Longpress.
* TODO: Fling.
@@ -294,13 +310,13 @@ public class MonkeySourceRandom implements MonkeyEventSource {
* TODO: More useful than the random walk here would be to pick a single random direction
* and distance, and divvy it up into a random number of segments. (This would serve to
* generate fling gestures, which are important).
*
*
* @param random Random number source for positioning
* @param motionEvent If false, touch/release. If true, touch/move/release.
*
* @param motionEvent If false, touch/release. If true, touch/move/release.
*
*/
private void generateMotionEvent(Random random, boolean motionEvent){
Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
float x = Math.abs(random.nextInt() % display.getWidth());
@@ -310,12 +326,12 @@ public class MonkeySourceRandom implements MonkeyEventSource {
if (downAt == -1) {
downAt = eventTime;
}
MonkeyMotionEvent e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
downAt, MotionEvent.ACTION_DOWN, x, y, 0);
e.setIntermediateNote(false);
MonkeyMotionEvent e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
downAt, MotionEvent.ACTION_DOWN, x, y, 0);
e.setIntermediateNote(false);
mQ.addLast(e);
// sometimes we'll move during the touch
if (motionEvent) {
int count = random.nextInt(10);
@@ -323,34 +339,34 @@ public class MonkeySourceRandom implements MonkeyEventSource {
// generate some slop in the up event
x = (x + (random.nextInt() % 10)) % display.getWidth();
y = (y + (random.nextInt() % 10)) % display.getHeight();
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
downAt, MotionEvent.ACTION_MOVE, x, y, 0);
e.setIntermediateNote(true);
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
downAt, MotionEvent.ACTION_MOVE, x, y, 0);
e.setIntermediateNote(true);
mQ.addLast(e);
}
}
// TODO generate some slop in the up event
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
downAt, MotionEvent.ACTION_UP, x, y, 0);
e.setIntermediateNote(false);
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_POINTER,
downAt, MotionEvent.ACTION_UP, x, y, 0);
e.setIntermediateNote(false);
mQ.addLast(e);
}
/**
* Generates a random trackball event. This consists of a sequence of small moves, followed by
* an optional single click.
*
*
* TODO: Longpress.
* TODO: Meta state
* TODO: Parameterize the % clicked
* TODO: More useful than the random walk here would be to pick a single random direction
* and distance, and divvy it up into a random number of segments. (This would serve to
* generate fling gestures, which are important).
*
*
* @param random Random number source for positioning
*
*
*/
private void generateTrackballEvent(Random random) {
Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
@@ -362,47 +378,47 @@ public class MonkeySourceRandom implements MonkeyEventSource {
// generate a small random step
int dX = random.nextInt(10) - 5;
int dY = random.nextInt(10) - 5;
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, -1,
MotionEvent.ACTION_MOVE, dX, dY, 0);
e.setIntermediateNote(i > 0);
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, -1,
MotionEvent.ACTION_MOVE, dX, dY, 0);
e.setIntermediateNote(i > 0);
mQ.addLast(e);
}
// 10% of trackball moves end with a click
if (0 == random.nextInt(10)) {
long downAt = SystemClock.uptimeMillis();
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, downAt,
MotionEvent.ACTION_DOWN, 0, 0, 0);
e.setIntermediateNote(true);
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, downAt,
MotionEvent.ACTION_DOWN, 0, 0, 0);
e.setIntermediateNote(true);
mQ.addLast(e);
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, downAt,
MotionEvent.ACTION_UP, 0, 0, 0);
e.setIntermediateNote(false);
e = new MonkeyMotionEvent(MonkeyEvent.EVENT_TYPE_TRACKBALL, downAt,
MotionEvent.ACTION_UP, 0, 0, 0);
e.setIntermediateNote(false);
mQ.addLast(e);
}
}
}
/**
/**
* generate a random event based on mFactor
*/
private void generateEvents() {
private void generateEvents() {
float cls = mRandom.nextFloat();
int lastKey = 0;
boolean touchEvent = cls < mFactors[FACTOR_TOUCH];
boolean motionEvent = !touchEvent && (cls < mFactors[FACTOR_MOTION]);
if (touchEvent || motionEvent) {
if (touchEvent || motionEvent) {
generateMotionEvent(mRandom, motionEvent);
return;
}
if (cls < mFactors[FACTOR_TRACKBALL]) {
if (cls < mFactors[FACTOR_TRACKBALL]) {
generateTrackballEvent(mRandom);
return;
}
@@ -427,23 +443,23 @@ public class MonkeySourceRandom implements MonkeyEventSource {
} else {
lastKey = 1 + mRandom.nextInt(KeyEvent.getMaxKeyCode() - 1);
}
MonkeyKeyEvent e = new MonkeyKeyEvent(KeyEvent.ACTION_DOWN, lastKey);
mQ.addLast(e);
e = new MonkeyKeyEvent(KeyEvent.ACTION_UP, lastKey);
mQ.addLast(e);
}
public boolean validate() {
//check factors
return adjustEventFactors();
}
public void setVerbose(int verbose) {
mVerbose = verbose;
}
/**
* generate an activity event
*/
@@ -452,18 +468,18 @@ public class MonkeySourceRandom implements MonkeyEventSource {
mRandom.nextInt(mMainApps.size())));
mQ.addLast(e);
}
/**
* if the queue is empty, we generate events first
* @return the first event in the queue
* @return the first event in the queue
*/
public MonkeyEvent getNextEvent() {
if (mQ.isEmpty()) {
generateEvents();
}
mEventCount++;
MonkeyEvent e = mQ.getFirst();
mQ.removeFirst();
}
mEventCount++;
MonkeyEvent e = mQ.getFirst();
mQ.removeFirst();
return e;
}
}