Wednesday, June 15, 2011

Android Monkey Script

Monkey: The Monkey is a program that runs on your emulator or device and generates pseudo-random streams of user events such as clicks, touches, or gestures, as well as a number of system-level events. You can use the Monkey to stress-test applications that you are developing, in a random yet repeatable manner.

Monkey script is unfortunately poorly documented but it is pretty easy to write monkey script after looking at the Monkey Script Java source code itself. The Java Source code is the code in charge of parsing the monkey script and dispatching all monkey script event in a timely manner.

development/cmds/monkey/src/com/android/commands/monkey/MonkeySourceScript.java

/*
* Copyright (C) 2008 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.os.SystemClock;
import android.view.KeyEvent;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.StringTokenizer;

/**
* monkey event queue. It takes a script to produce events
*
* sample script format:
* type= raw events
* count= 10
* speed= 1.0
* start data >>
* captureDispatchPointer(5109520,5109520,0,230.75429,458.1814,0.20784314,
* 0.06666667,0,0.0,0.0,65539,0)
* captureDispatchKey(5113146,5113146,0,20,0,0,0,0)
* captureDispatchFlip(true)
* ...
*/
public class MonkeySourceScript implements MonkeyEventSource {
private int mEventCountInScript = 0; //total number of events in the file
private int mVerbose = 0;
private double mSpeed = 1.0;
private String mScriptFileName;
private MonkeyEventQueue mQ;

private static final String HEADER_TYPE = "type=";
private static final String HEADER_COUNT = "count=";
private static final String HEADER_SPEED = "speed=";

private long mLastRecordedDownTimeKey = 0;
private long mLastRecordedDownTimeMotion = 0;
private long mLastExportDownTimeKey = 0;
private long mLastExportDownTimeMotion = 0;
private long mLastExportEventTime = -1;
private long mLastRecordedEventTime = -1;

private static final boolean THIS_DEBUG = false;
// a parameter that compensates the difference of real elapsed time and
// time in theory
private static final long SLEEP_COMPENSATE_DIFF = 16;

// maximum number of events that we read at one time
private static final int MAX_ONE_TIME_READS = 100;

// number of additional events added to the script
// add HOME_KEY down and up events to make start UI consistent in each round
private static final int POLICY_ADDITIONAL_EVENT_COUNT = 2;

// event key word in the capture log
private static final String EVENT_KEYWORD_POINTER = "DispatchPointer";
private static final String EVENT_KEYWORD_TRACKBALL = "DispatchTrackball";
private static final String EVENT_KEYWORD_KEY = "DispatchKey";
private static final String EVENT_KEYWORD_FLIP = "DispatchFlip";

// a line at the end of the header
private static final String STARTING_DATA_LINE = "start data >>";
private boolean mFileOpened = false;

FileInputStream mFStream;
DataInputStream mInputStream;
BufferedReader mBufferReader;

public MonkeySourceScript(String filename, long throttle) {
mScriptFileName = filename;
mQ = new MonkeyEventQueue(throttle);
}

/**
*
* @return the number of total events that will be generated in a round
*/
public int getOneRoundEventCount() {
//plus one home key down and up event
return mEventCountInScript + POLICY_ADDITIONAL_EVENT_COUNT;
}

private void resetValue() {
mLastRecordedDownTimeKey = 0;
mLastRecordedDownTimeMotion = 0;
mLastExportDownTimeKey = 0;
mLastExportDownTimeMotion = 0;
mLastRecordedEventTime = -1;
mLastExportEventTime = -1;
}

private boolean readScriptHeader() {
mEventCountInScript = -1;
mFileOpened = false;
try {
if (THIS_DEBUG) {
System.out.println("reading script header");
}

mFStream = new FileInputStream(mScriptFileName);
mInputStream = new DataInputStream(mFStream);
mBufferReader = new BufferedReader(
new InputStreamReader(mInputStream));
String sLine;
while ((sLine = mBufferReader.readLine()) != null) {
sLine = sLine.trim();
if (sLine.indexOf(HEADER_TYPE) >= 0) {
// at this point, we only have one type of script
} else if (sLine.indexOf(HEADER_COUNT) >= 0) {
try {
mEventCountInScript = Integer.parseInt(sLine.substring(
HEADER_COUNT.length() + 1).trim());
} catch (NumberFormatException e) {
System.err.println(e);
}
} else if (sLine.indexOf(HEADER_SPEED) >= 0) {
try {
mSpeed = Double.parseDouble(sLine.substring(
HEADER_SPEED.length() + 1).trim());

} catch (NumberFormatException e) {
System.err.println(e);
}
} else if (sLine.indexOf(STARTING_DATA_LINE) >= 0) {
// header ends until we read the start data mark
mFileOpened = true;
if (THIS_DEBUG) {
System.out.println("read script header success");
}
return true;
}
}
} catch (FileNotFoundException e) {
System.err.println(e);
} catch (IOException e) {
System.err.println(e);
}

if (THIS_DEBUG) {
System.out.println("Error in reading script header");
}
return false;
}

private void processLine(String s) {
int index1 = s.indexOf('(');
int index2 = s.indexOf(')');

if (index1 < 0 || index2 < 0) {
return;
}

StringTokenizer st = new StringTokenizer(
s.substring(index1 + 1, index2), ",");

if (s.indexOf(EVENT_KEYWORD_KEY) >= 0) {
// key events
try {
long downTime = Long.parseLong(st.nextToken());
long eventTime = Long.parseLong(st.nextToken());
int action = Integer.parseInt(st.nextToken());
int code = Integer.parseInt(st.nextToken());
int repeat = Integer.parseInt(st.nextToken());
int metaState = Integer.parseInt(st.nextToken());
int device = Integer.parseInt(st.nextToken());
int scancode = Integer.parseInt(st.nextToken());

MonkeyKeyEvent e = new MonkeyKeyEvent(downTime, eventTime,
action, code, repeat, metaState, device, scancode);
mQ.addLast(e);

} catch (NumberFormatException e) {
// something wrong with this line in the script
}
} else if (s.indexOf(EVENT_KEYWORD_POINTER) >= 0 ||
s.indexOf(EVENT_KEYWORD_TRACKBALL) >= 0) {
// trackball/pointer event
try {
long downTime = Long.parseLong(st.nextToken());
long eventTime = Long.parseLong(st.nextToken());
int action = Integer.parseInt(st.nextToken());
float x = Float.parseFloat(st.nextToken());
float y = Float.parseFloat(st.nextToken());
float pressure = Float.parseFloat(st.nextToken());
float size = Float.parseFloat(st.nextToken());
int metaState = Integer.parseInt(st.nextToken());
float xPrecision = Float.parseFloat(st.nextToken());
float yPrecision = Float.parseFloat(st.nextToken());
int device = Integer.parseInt(st.nextToken());
int edgeFlags = Integer.parseInt(st.nextToken());

int type = MonkeyEvent.EVENT_TYPE_TRACKBALL;
if (s.indexOf("Pointer") > 0) {
type = MonkeyEvent.EVENT_TYPE_POINTER;
}
MonkeyMotionEvent e = new MonkeyMotionEvent(type, downTime, eventTime,
action, x, y, pressure, size, metaState, xPrecision, yPrecision,
device, edgeFlags);
mQ.addLast(e);
} catch (NumberFormatException e) {
// we ignore this event
}
} else if (s.indexOf(EVENT_KEYWORD_FLIP) >= 0) {
boolean keyboardOpen = Boolean.parseBoolean(st.nextToken());
MonkeyFlipEvent e = new MonkeyFlipEvent(keyboardOpen);
mQ.addLast(e);
}
}

private void closeFile() {
mFileOpened = false;
if (THIS_DEBUG) {
System.out.println("closing script file");
}

try {
mFStream.close();
mInputStream.close();
} catch (IOException e) {
System.out.println(e);
}
}

/**
* add home key press/release event to the queue
*/
private void addHomeKeyEvent() {
MonkeyKeyEvent e = new MonkeyKeyEvent(KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_HOME);
mQ.addLast(e);
e = new MonkeyKeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HOME);
mQ.addLast(e);
}

/**
* read next batch of events from the provided script file
* @return true if success
*/
private boolean readNextBatch() {
String sLine = null;
int readCount = 0;

if (THIS_DEBUG) {
System.out.println("readNextBatch(): reading next batch of events");
}

if (!mFileOpened) {
if (!readScriptHeader()) {
closeFile();
return false;
}
resetValue();

/*
* In order to allow the Monkey to replay captured events multiple times
* we need to define a default start UI, which is the home screen
* Otherwise, it won't be accurate since the captured events
* could end anywhere
*/
addHomeKeyEvent();
}

try {
while (readCount++ < MAX_ONE_TIME_READS &&
(sLine = mBufferReader.readLine()) != null) {
sLine = sLine.trim();
processLine(sLine);
}
} catch (IOException e) {
System.err.println(e);
return false;
}

if (sLine == null) {
// to the end of the file
if (THIS_DEBUG) {
System.out.println("readNextBatch(): to the end of file");
}
closeFile();
}
return true;
}

/**
* sleep for a period of given time, introducing latency among events
* @param time to sleep in millisecond
*/
private void needSleep(long time) {
if (time < 1) {
return;
}
try {
Thread.sleep(time);
} catch (InterruptedException e) {
}
}

/**
* check whether we can successfully read the header of the script file
*/
public boolean validate() {
boolean b = readNextBatch();
if (mVerbose > 0) {
System.out.println("Replaying " + mEventCountInScript +
" events with speed " + mSpeed);
}
return b;
}

public void setVerbose(int verbose) {
mVerbose = verbose;
}

/**
* adjust key downtime and eventtime according to both
* recorded values and current system time
* @param e KeyEvent
*/
private void adjustKeyEventTime(MonkeyKeyEvent e) {
if (e.getEventTime() < 0) {
return;
}
long thisDownTime = 0;
long thisEventTime = 0;
long expectedDelay = 0;

if (mLastRecordedEventTime <= 0) {
// first time event
thisDownTime = SystemClock.uptimeMillis();
thisEventTime = thisDownTime;
} else {
if (e.getDownTime() != mLastRecordedDownTimeKey) {
thisDownTime = e.getDownTime();
} else {
thisDownTime = mLastExportDownTimeKey;
}
expectedDelay = (long) ((e.getEventTime() -
mLastRecordedEventTime) * mSpeed);
thisEventTime = mLastExportEventTime + expectedDelay;
// add sleep to simulate everything in recording
needSleep(expectedDelay - SLEEP_COMPENSATE_DIFF);
}
mLastRecordedDownTimeKey = e.getDownTime();
mLastRecordedEventTime = e.getEventTime();
e.setDownTime(thisDownTime);
e.setEventTime(thisEventTime);
mLastExportDownTimeKey = thisDownTime;
mLastExportEventTime = thisEventTime;
}

/**
* adjust motion downtime and eventtime according to both
* recorded values and current system time
* @param e KeyEvent
*/
private void adjustMotionEventTime(MonkeyMotionEvent e) {
if (e.getEventTime() < 0) {
return;
}
long thisDownTime = 0;
long thisEventTime = 0;
long expectedDelay = 0;

if (mLastRecordedEventTime <= 0) {
// first time event
thisDownTime = SystemClock.uptimeMillis();
thisEventTime = thisDownTime;
} else {
if (e.getDownTime() != mLastRecordedDownTimeMotion) {
thisDownTime = e.getDownTime();
} else {
thisDownTime = mLastExportDownTimeMotion;
}
expectedDelay = (long) ((e.getEventTime() -
mLastRecordedEventTime) * mSpeed);
thisEventTime = mLastExportEventTime + expectedDelay;
// add sleep to simulate everything in recording
needSleep(expectedDelay - SLEEP_COMPENSATE_DIFF);
}

mLastRecordedDownTimeMotion = e.getDownTime();
mLastRecordedEventTime = e.getEventTime();
e.setDownTime(thisDownTime);
e.setEventTime(thisEventTime);
mLastExportDownTimeMotion = thisDownTime;
mLastExportEventTime = thisEventTime;
}

/**
* if the queue is empty, we generate events first
* @return the first event in the queue, if null, indicating the system crashes
*/
public MonkeyEvent getNextEvent() {
long recordedEventTime = -1;

if (mQ.isEmpty()) {
readNextBatch();
}
MonkeyEvent e = mQ.getFirst();
mQ.removeFirst();

if (e.getEventType() == MonkeyEvent.EVENT_TYPE_KEY) {
adjustKeyEventTime((MonkeyKeyEvent) e);
} else if (e.getEventType() == MonkeyEvent.EVENT_TYPE_POINTER ||
e.getEventType() == MonkeyEvent.EVENT_TYPE_TRACKBALL) {
adjustMotionEventTime((MonkeyMotionEvent) e);
}
return e;
}
}

To write monkey script the first thing you have to do is write a small JAVA application. You can use hello world sample application. You can build it using Eclipse Java IDE and Android Windows SDK. Inside your sample application open the main activity JAVA file and add some code to enumerate the activity of an APK of your choice assuming this application is not presently running. If the application you'd like to automate is running, then use getPackageInfo(), instead of getPackageArchiveInfo().

See code bellow. Run the "rever engineering" application in the debugger with the device attached to your PC/Laptop with USB cable so that you can step into the code.


import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;

public class ReverseEngineering extends Activity
{
static final String TAG = "ReverseEngineering:Log";

/** onStart() is called when the application is on the foreground.
* We shouldn't use onCreate() which is called only once in the phone's process life cycle.
* This function will override what is available in the parent Activity class. */
@Override
public void onStart()
{
// Keep on starting.
super.onStart();
setContentView(R.layout.main);

PackageManager pm = getPackageManager();

PackageInfo info = pm.getPackageArchiveInfo("/data/app/com.company.application-1.apk", PackageManager.GET_ACTIVITIES);
if(info != null){
ApplicationInfo appInfo = info.applicationInfo;
String appName = pm.getApplicationLabel(appInfo).toString();
String packageName = appInfo.packageName;
Log.d(TAG, " appName =" + appName);
Log.d(TAG, " packageName =" + packageName);
}

ActivityInfo[] list = pm.getPackageArchiveInfo("/data/app/com.company.application-1.apk", PackageManager.GET_ACTIVITIES).activities;
for(int i = 0;i< list.length; i++){
String strName = list[i].getClass().getName();
Log.d(TAG, " ActivityInfo =" + strName);
}


Once you know the name of the activity you can invoke, write a small script.
The following script named monkey_script.txt will execute a package and
its activity.

captureDispatchPointer is a tap down action followed by a tap up action at the coordinate x and y EG X 146 and Y 165 as if you were using the touch screen with your finger. To find out the coordinate for a particular screen on the device you need to capture the screen using ddms (android tool) and open the screen with your favorites image editor. Mine is PhotoShop CS5 or Gimp if you don't have that much $ to spend. Development on Android is free after all.

# KEYEVENT http://developer.android.com/reference/android/view/KeyEvent.html# Start of Script
type= user
count= 49
speed= 1.0
start data >>
LaunchActivity(com.company.application.Activity, com.company.application.Activity)
UserWait(2000)
# first screen
captureDispatchPointer(5109520,5109520,0,146,165,0,0,0,0,0,0,0);
captureDispatchPointer(5109521,5109521,1,146,165,0,0,0,0,0,0,0);
UserWait(5000)
# second screen
captureDispatchPointer(5109520,5109520,0,117,268,0,0,0,0,0,0,0);
captureDispatchPointer(5109521,5109521,1,117,268,0,0,0,0,0,0,0);
# second screen selection dialog autoclose on selection
UserWait(2000)
# now access an other screen
captureDispatchPointer(5109520,5109520,0,117,358,0,0,0,0,0,0,0);
captureDispatchPointer(5109521,5109521,1,117,358,0,0,0,0,0,0,0);
UserWait(2000)
# access a selection dialog with only a cancel button. Dialog will autoclose on tap down
captureDispatchPointer(5109520,5109520,0,117,177,0,0,0,0,0,0,0);
captureDispatchPointer(5109521,5109521,1,117,177,0,0,0,0,0,0,0);
UserWait(2000)

A more complex script with android keypad input

# KEYEVENT http://developer.android.com/reference/android/view/KeyEvent.html
# Start of Script
type= user
count= 49
speed= 1.0
start data >>
LaunchActivity(com.mpowerlabs.coin.android, com.mpowerlabs.coin.android.LoginActivity)
# 3120021258
DispatchPress(KEYCODE_3)
UserWait(200)
DispatchPress(KEYCODE_1)
UserWait(200)
DispatchPress(KEYCODE_3)
UserWait(200)
DispatchPress(KEYCODE_5)
UserWait(200)
DispatchPress(KEYCODE_0)
UserWait(200)
DispatchPress(KEYCODE_2)
UserWait(200)
DispatchPress(KEYCODE_1)
UserWait(200)
DispatchPress(KEYCODE_2)
UserWait(200)
DispatchPress(KEYCODE_5)
UserWait(200)
DispatchPress(KEYCODE_8)
UserWait(200)
# Pin 12345
DispatchPress(KEYCODE_DPAD_DOWN)
UserWait(250)
DispatchPress(KEYCODE_1)
UserWait(200)
DispatchPress(KEYCODE_2)
UserWait(200)
DispatchPress(KEYCODE_3)
UserWait(200)
DispatchPress(KEYCODE_4)
UserWait(200)
DispatchPress(KEYCODE_5)
UserWait(200)
# Down and enter
DispatchPress(KEYCODE_DPAD_DOWN)
UserWait(250)
DispatchPress(KEYCODE_ENTER)

To execute the script on the device you need to create a batch file.

1 to make sure the application is deploy on the device
2 push the script
3 execute the script

adb root
adb install com.company.application.apk
adb push monkey_script.txt /sdcard/monkey_script.txt
adb shell monkey -p com.company.application -v -v -v -f /sdcard/monkey_script.txt 1

Hope you'll find this tutorial usefull !

No comments: