Android 12 Monkey Stress Testing: Source Code Execution Flow Analysis
Monkey is a testing utility provided by Android for automated application testing and stress testing. Its source code (Android 12) is located at:
/development/cmds/monkey/
The deployment format is a Java binary:
// development/cmds/monkey/Android.bp
package {
default_applicable_licenses: ["development_cmds_monkey_license"],
}
license {
name: "development_cmds_monkey_license",
visibility: [":__subpackages__"],
license_kinds: [
"SPDX-license-identifier-Apache-2.0",
],
license_text: [
"NOTICE",
],
}
java_binary {
name: "monkey",
srcs: ["**/*.java"],
wrapper: "monkey",
}
Monkey enables simulation of user interactions including touch events (single-finger, multi-finger, gestures), key events, and more. It detects ANR and Crash occurrences while collecting relevant debugging information.
Example command for testing application com.package.linduo:
adb shell monkey -p com.package.linduo --pct-touch 10 --pct-motion 20 10000
# Executes 10000 test events with Touch events at 10% and Motion events at 20%
# Or enter android terminal via adb shell and use monkey directly
Monkey Command Options
private void showUsage() {
StringBuilder helpText = new StringBuilder();
helpText.append("usage: monkey [-p ALLOWED_PACKAGE [-p ALLOWED_PACKAGE] ...]\n");
helpText.append(" [-c MAIN_CATEGORY [-c MAIN_CATEGORY] ...]\n");
helpText.append(" [--ignore-crashes] [--ignore-timeouts]\n");
helpText.append(" [--ignore-security-exceptions]\n");
helpText.append(" [--monitor-native-crashes] [--ignore-native-crashes]\n");
helpText.append(" [--kill-process-after-error] [--hprof]\n");
helpText.append(" [--match-description TEXT]\n");
helpText.append(" [--pct-touch PERCENT] [--pct-motion PERCENT]\n");
helpText.append(" [--pct-trackball PERCENT] [--pct-syskeys PERCENT]\n");
helpText.append(" [--pct-nav PERCENT] [--pct-majornav PERCENT]\n");
helpText.append(" [--pct-appswitch PERCENT] [--pct-flip PERCENT]\n");
helpText.append(" [--pct-anyevent PERCENT] [--pct-pinchzoom PERCENT]\n");
helpText.append(" [--pct-permission PERCENT]\n");
helpText.append(" [--pkg-blacklist-file PACKAGE_BLACKLIST_FILE]\n");
helpText.append(" [--pkg-whitelist-file PACKAGE_WHITELIST_FILE]\n");
helpText.append(" [--wait-dbg] [--dbg-no-events]\n");
helpText.append(" [--setup scriptfile] [-f scriptfile [-f scriptfile] ...]\n");
helpText.append(" [--port port]\n");
helpText.append(" [-s SEED] [-v [-v] ...]\n");
helpText.append(" [--throttle MILLISEC] [--randomize-throttle]\n");
helpText.append(" [--profile-wait MILLISEC]\n");
helpText.append(" [--device-sleep-time MILLISEC]\n");
helpText.append(" [--randomize-script]\n");
helpText.append(" [--script-log]\n");
helpText.append(" [--bugreport]\n");
helpText.append(" [--periodic-bugreport]\n");
helpText.append(" [--permission-target-system]\n");
helpText.append(" COUNT\n");
Logger.err.println(helpText.toString());
}
Source Code Analysis of Monkey Test Execution
The following analysis focuses on the execution flow of random events:
- Monkey initialization
- Monkey event generation
- Monkey event dispatch to system
Monkey Initialization
The Monkey.java file contains the program entry point main, which initiates the Monkey framwork:
// development/cmds/monkey/src/com/android/commands/monkey/Monkey.java
public static void main(String[] args) {
Process.setArgV0("com.android.commands.monkey");
Logger.err.println("args: " + Arrays.toString(args));
int resultCode = (new Monkey()).run(args);
System.exit(resultCode);
}
// development/cmds/monkey/src/com/android/commands/monkey/Monkey.java
/**
* Execute the command!
*
* @param args Command-line arguments
* @return POSIX-style result code. 0 indicates success.
*/
private int run(String[] args) {
mVerbose = 0;
mCount = 1000;
mSeed = 0;
mThrottle = 0;
mArgs = args;
if (!processOptions()) {
return -1;
}
if (!loadPackageLists()) {
return -1;
}
if (mMainCategories.size() == 0) {
mMainCategories.add(Intent.CATEGORY_LAUNCHER);
mMainCategories.add(Intent.CATEGORY_MONKEY);
}
if (mSeed == 0) {
mSeed = System.currentTimeMillis() + System.identityHashCode(this);
}
if (!getSystemInterfaces()) {
return -3;
}
if (!getMainApps()) {
return -4;
}
if (mScriptFileNames != null && mScriptFileNames.size() == 1) {
// script mode, ignore other options
} else if (mScriptFileNames != null && mScriptFileNames.size() > 1) {
} else if (mServerPort != -1) {
} else {
mEventSource = new MonkeySourceRandom(mRandom, mMainApps,
mThrottle, mRandomizeThrottle, mPermissionTargetSystem);
mEventSource.setVerbose(mVerbose);
for (int i = 0; i < MonkeySourceRandom.FACTORZ_COUNT; i++) {
if (mFactors[i] <= 0.0f) {
((MonkeySourceRandom) mEventSource).setFactors(i, mFactors[i]);
}
}
((MonkeySourceRandom) mEventSource).generateActivity();
}
try {
crashedAtCycle = runMonkeyCycles();
} finally {
new MonkeyRotationEvent(Surface.ROTATION_0, false).injectEvent(
mWm, mAm, mVerbose);
}
}
Monkey Parameter Parsing
The processOptions method parses command-line arguments and configures corresponding class member variables:
// development/cmds/monkey/src/com/android/commands/monkey/Monkey.java
private boolean processOptions() {
if (mArgs.length < 1) {
showUsage();
return false;
}
try {
String option;
Set<String> validPackages = new HashSet<>();
while ((option = nextOption()) != null) {
if (option.equals("-s")) {
mSeed = nextOptionLong("Seed");
} else if (option.equals("-p")) {
validPackages.add(nextOptionData());
} else if (option.equals("-c")) {
// implementation details omitted
} else {
Logger.err.println("** Error: Unknown option: " + option);
showUsage();
return false;
}
}
MonkeyUtils.getPackageFilter().addValidPackages(validPackages);
} catch (RuntimeException ex) {
Logger.err.println("** Error: " + ex.toString());
showUsage();
return false;
}
if (mServerPort == -1) {
// implementation details omitted
}
return true;
}
Monkey System Service Acquisition
The getSystemInterfaces method retrieves Android system services including AMS, PMS, and WMS. It calls AMS's setActivityController interface to register an IActivityController.Stub object for monitoring application ANR and Crash events:
/**
* Connect to required system interfaces.
*
* @return true if all system interfaces were successfully obtained.
*/
private boolean getSystemInterfaces() {
mAm = ActivityManager.getService();
if (mAm == null) {
Logger.err.println("** Error: Unable to connect to activity manager; is the system "
+ "running?");
return false;
}
mWm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
if (mWm == null) {
Logger.err.println("** Error: Unable to connect to window manager; is the system "
+ "running?");
return false;
}
mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
if (mPm == null) {
Logger.err.println("** Error: Unable to connect to package manager; is the system "
+ "running?");
return false;
}
try {
mAm.setActivityController(new ActivityController(), true);
mNetworkMonitor.register(mAm);
} catch (RemoteException e) {
Logger.err.println("** Failed talking with activity manager!");
return false;
}
return true;
}
/**
* Monitor system operations.
*/
private class ActivityController extends IActivityController.Stub {
public boolean activityStarting(Intent intent, String pkg) {
// implementation details omitted
}
private boolean isActivityStartingAllowed(Intent intent, String pkg) {
// implementation details omitted
}
public boolean activityResuming(String pkg) {
// implementation details omitted
}
public boolean appCrashed(String processName, int pid,
String shortMsg, String longMsg,
long timeMillis, String stackTrace) {
// implementation details omitted
}
public int appEarlyNotResponding(String processName, int pid, String annotation) {
return 0;
}
public int appNotResponding(String processName, int pid, String processStats) {
// implementation details omitted
}
public int systemNotResponding(String message) {
// implementation details omitted
}
}
Monkey Activity Acquisition for Testing
Monkey queries activities with Intent.CATEGORY_LAUNCHER and Intent.CATEGORY_MONKEY categories using PackageManager's queryIntentActivities interface. It filters activities belonging to target applications and adds them to the mMainApps variable:
// development/cmds/monkey/src/com/android/commands/monkey/Monkey.java
/**
* Generate a list of activities that can be switched to based on
* provided restrictions (categories and packages).
*
* @return true if the target activity list was successfully built.
*/
private boolean getMainApps() {
try {
final int categoryCount = mMainCategories.size();
for (int i = 0; i < categoryCount; i++) {
Intent intent = new Intent(Intent.ACTION_MAIN);
String category = mMainCategories.get(i);
if (category.length() > 0) {
intent.addCategory(category);
}
List<ResolveInfo> mainApps = mPm.queryIntentActivities(intent, null, 0,
ActivityManager.getCurrentUser()).getList();
final int appCount = mainApps.size();
for (int a = 0; a < appCount; a++) {
ResolveInfo info = mainApps.get(a);
String packageName = info.activityInfo.applicationInfo.packageName;
if (MonkeyUtils.getPackageFilter().checkEnteringPackage(packageName)) {
mMainApps.add(new ComponentName(packageName, info.activityInfo.name));
}
}
}
} catch (RemoteException e) {
Logger.err.println("** Failed talking with package manager!");
return false;
}
if (mMainApps.size() == 0) {
Logger.out.println("** No activities found to run, monkey aborted.");
return false;
}
return true;
}
Monkey Event Generation and Execution
// development/cmds/monkey/src/com/android/commands/monkey/Monkey.java
private int run(String[] args) {
mEventSource = new MonkeySourceRandom(mRandom, mMainApps,
mThrottle, mRandomizeThrottle, mPermissionTargetSystem);
for (int i = 0; i < MonkeySourceRandom.FACTORZ_COUNT; i++) {
if (mFactors[i] <= 0.0f) {
((MonkeySourceRandom) mEventSource).setFactors(i, mFactors[i]);
}
}
try {
crashedAtCycle = runMonkeyCycles();
} finally {
new MonkeyRotationEvent(Surface.ROTATION_0, false).injectEvent(
mWm, mAm, mVerbose);
}
}
The runMonkeyCycles method calls MonkeySourceRandom.getNextEvent() to generate simulated test events (MonkeyEvent), then invokes MonkeyEvent.injectEvent() to execute them:
private int runMonkeyCycles() {
int eventCounter = 0;
int cycleCounter = 0;
boolean shouldReportAnrTraces = false;
boolean shouldReportDumpsysMemInfo = false;
boolean shouldAbort = false;
boolean systemCrashed = false;
try {
while (!systemCrashed && cycleCounter < mCount) {
synchronized (this) {
MonkeyEvent ev = mEventSource.getNextEvent();
if (ev != null) {
int injectCode = ev.injectEvent(mWm, mAm, mVerbose);
eventCounter++;
}
}
cycleCounter++;
}
} catch (RuntimeException e) {
Logger.error("** Error: A RuntimeException occurred:", e);
}
Logger.out.println("Events injected: " + eventCounter);
return eventCounter;
}
The MonkeySourceRandom.getNextEvent method generates test events when the event queue is empty. It generates a random number and creates different event types based on their configured percentages:
// development/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java
/**
* Generate an activity event.
*/
public void generateActivity() {
MonkeyActivityEvent event = new MonkeyActivityEvent(mMainApps.get(
mRandom.nextInt(mMainApps.size())));
mQ.addLast(event);
}
/**
* Generate events when queue is empty.
* @return the first event from the queue.
*/
public MonkeyEvent getNextEvent() {
if (mQ.isEmpty()) {
generateEvents();
}
mEventCount++;
MonkeyEvent event = mQ.getFirst();
mQ.removeFirst();
return event;
}
/**
* Generate random events based on mFactor weights.
*/
private void generateEvents() {
float randomValue = mRandom.nextFloat();
int selectedKey = 0;
if (randomValue < mFactors[FACTOR_TOUCH]) {
generatePointerEvent(mRandom, GESTURE_TAP);
return;
} else if (randomValue < mFactors[FACTOR_MOTION]) {
generatePointerEvent(mRandom, GESTURE_DRAG);
return;
} else if (randomValue < mFactors[FACTOR_PINCHZOOM]) {
generatePointerEvent(mRandom, GESTURE_PINCH_OR_ZOOM);
return;
} else if (randomValue < mFactors[FACTOR_TRACKBALL]) {
generateTrackballEvent(mRandom);
return;
} else if (randomValue < mFactors[FACTOR_ROTATION]) {
generateRotationEvent(mRandom);
return;
} else if (randomValue < mFactors[FACTOR_PERMISSION]) {
mQ.add(mPermissionUtil.generateRandomPermissionEvent(mRandom));
return;
}
for (;;) {
if (randomValue < mFactors[FACTOR_NAV]) {
selectedKey = NAV_KEYS[mRandom.nextInt(NAV_KEYS.length)];
} else if (randomValue < mFactors[FACTOR_MAJORNAV]) {
selectedKey = MAJOR_NAV_KEYS[mRandom.nextInt(MAJOR_NAV_KEYS.length)];
} else if (randomValue < mFactors[FACTOR_SYSOPS]) {
selectedKey = SYS_KEYS[mRandom.nextInt(SYS_KEYS.length)];
} else if (randomValue < mFactors[FACTOR_APPSWITCH]) {
MonkeyActivityEvent event = new MonkeyActivityEvent(mMainApps.get(
mRandom.nextInt(mMainApps.size())));
mQ.addLast(event);
return;
} else if (randomValue < mFactors[FACTOR_FLIP]) {
MonkeyFlipEvent event = new MonkeyFlipEvent(mKeyboardOpen);
mKeyboardOpen = !mKeyboardOpen;
mQ.addLast(event);
return;
} else {
selectedKey = 1 + mRandom.nextInt(KeyEvent.getMaxKeyCode() - 1);
}
if (selectedKey != KeyEvent.KEYCODE_POWER
&& selectedKey != KeyEvent.KEYCODE_ENDCALL
&& selectedKey != KeyEvent.KEYCODE_SLEEP
&& selectedKey != KeyEvent.KEYCODE_SOFT_SLEEP
&& PHYSICAL_KEY_EXISTS[selectedKey]) {
break;
}
}
MonkeyKeyEvent event = new MonkeyKeyEvent(KeyEvent.ACTION_DOWN, selectedKey);
mQ.addLast(event);
event = new MonkeyKeyEvent(KeyEvent.ACTION_UP, selectedKey);
mQ.addLast(event);
}
Taking touch events as an example, the generatePointerEvent method obtains a Display object via DMS (to determine screen dimensions) and creates a MonkeyTouchEvent object:
// development/cmds/monkey/src/com/android/commands/monkey/MonkeySourceRandom.java
private void generatePointerEvent(Random random, int gesture) {
Display display = DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
PointF p1 = randomPoint(random, display);
PointF v1 = randomVector(random);
long downAt = SystemClock.uptimeMillis();
mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_DOWN)
.setDownTime(downAt)
.addPointer(0, p1.x, p1.y)
.setIntermediateNote(false));
randomWalk(random, display, p1, v1);
mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_UP)
.setDownTime(downAt)
.addPointer(0, p1.x, p1.y)
.setIntermediateNote(false));
}
The MonkeyTouchEvent.injectEvent method dispatches touch events to the system using InputManager:
// development/cmds/monkey/src/com/android/commands/monkey/MonkeyMotionEvent.java
@Override
public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) {
MotionEvent motionEvent = getEvent();
if ((verbose > 0 && !mIntermediateNote) || verbose > 1) {
StringBuilder msg = new StringBuilder(":Sending ");
msg.append(getTypeLabel()).append(" (");
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
msg.append("ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
msg.append("ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
msg.append("ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
msg.append("ACTION_CANCEL");
break;
case MotionEvent.ACTION_POINTER_DOWN:
msg.append("ACTION_POINTER_DOWN ").append(motionEvent.getPointerId(motionEvent.getActionIndex()));
break;
case MotionEvent.ACTION_POINTER_UP:
msg.append("ACTION_POINTER_UP ").append(motionEvent.getPointerId(motionEvent.getActionIndex()));
break;
default:
msg.append(motionEvent.getAction());
break;
}
msg.append("):");
int pointerCount = motionEvent.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
msg.append(" ").append(motionEvent.getPointerId(i));
msg.append(":(").append(motionEvent.getX(i)).append(",").append(motionEvent.getY(i)).append(")");
}
Logger.out.println(msg.toString());
}
try {
if (!InputManager.getInstance().injectInputEvent(motionEvent,
InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT)) {
return MonkeyEvent.INJECT_FAIL;
}
} finally {
motionEvent.recycle();
}
return MonkeyEvent.INJECT_SUCCESS;
}