From: Bill Napier Date: Fri, 7 Aug 2009 18:34:12 +0000 (-0700) Subject: Added in simple command scripting to monkey over a TCP socket. X-Git-Tag: android-x86-1.6~4^2~69 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=a68dbdb1c31c486f489f38291eea05b3c621ae36;p=android-x86%2Fdevelopment.git 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. --- diff --git a/cmds/monkey/Android.mk b/cmds/monkey/Android.mk index 6bedc43e..ba9cf041 100644 --- a/cmds/monkey/Android.mk +++ b/cmds/monkey/Android.mk @@ -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) diff --git a/cmds/monkey/README.NETWORK.txt b/cmds/monkey/README.NETWORK.txt new file mode 100644 index 00000000..ccf741b5 --- /dev/null +++ b/cmds/monkey/README.NETWORK.txt @@ -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. diff --git a/cmds/monkey/example_script.txt b/cmds/monkey/example_script.txt new file mode 100644 index 00000000..5c1c61de --- /dev/null +++ b/cmds/monkey/example_script.txt @@ -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 diff --git a/cmds/monkey/src/com/android/commands/monkey/Monkey.java b/cmds/monkey/src/com/android/commands/monkey/Monkey.java index 7ebd7274..6b10147a 100644 --- a/cmds/monkey/src/com/android/commands/monkey/Monkey.java +++ b/cmds/monkey/src/com/android/commands/monkey/Monkey.java @@ -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 mMainCategories = new ArrayList(); /** Applications we can switch to. */ private ArrayList mMainApps = new ArrayList(); - + /** 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 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 newStones = new HashSet(); 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"); } diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeySourceNetwork.java b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceNetwork.java new file mode 100644 index 00000000..de784d0a --- /dev/null +++ b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceNetwork.java @@ -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 command); + } + + /** + * Command to simulate closing and opening the keyboard. + */ + private static class FlipCommand implements MonkeyCommand { + // flip open + // flip closed + public MonkeyEvent translateCommand(List 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 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 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 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 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 COMMAND_MAP = new HashMap(); + + 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 commandLineSplit(String line) { + ArrayList result = new ArrayList(); + 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 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; + } +} diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java index 5f9c10f0..27c8a517 100644 --- a/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java +++ b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java @@ -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 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 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; } }