OSDN Git Service

Added in simple command scripting to monkey over a TCP socket.
authorBill Napier <napier@google.com>
Fri, 7 Aug 2009 18:34:12 +0000 (11:34 -0700)
committerBill Napier <napier@google.com>
Fri, 7 Aug 2009 18:34:12 +0000 (11:34 -0700)
This allows a host program to talk to the monkey over TCP (via adb) and script up specific commands to run.

cmds/monkey/Android.mk
cmds/monkey/README.NETWORK.txt [new file with mode: 0644]
cmds/monkey/example_script.txt [new file with mode: 0644]
cmds/monkey/src/com/android/commands/monkey/Monkey.java
cmds/monkey/src/com/android/commands/monkey/MonkeySourceNetwork.java [new file with mode: 0644]
cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java

index 6bedc43..ba9cf04 100644 (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)
diff --git a/cmds/monkey/README.NETWORK.txt b/cmds/monkey/README.NETWORK.txt
new file mode 100644 (file)
index 0000000..ccf741b
--- /dev/null
@@ -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 (file)
index 0000000..5c1c61d
--- /dev/null
@@ -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
index 7ebd727..6b10147 100644 (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");
   }
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 (file)
index 0000000..de784d0
--- /dev/null
@@ -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;
+    }
+}
index 5f9c10f..27c8a51 100644 (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;
     }
 }