| /* |
| * 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.content.ComponentName; |
| import android.graphics.PointF; |
| import android.hardware.display.DisplayManagerGlobal; |
| import android.os.SystemClock; |
| import android.view.Display; |
| import android.view.KeyCharacterMap; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.Surface; |
| |
| import java.util.ArrayList; |
| import java.util.Random; |
| |
| /** |
| * monkey event queue |
| */ |
| 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, |
| KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT, |
| }; |
| /** |
| * Key events that perform major navigation options (so shouldn't be sent |
| * as much). |
| */ |
| private static final int[] MAJOR_NAV_KEYS = { |
| KeyEvent.KEYCODE_MENU, /*KeyEvent.KEYCODE_SOFT_RIGHT,*/ |
| KeyEvent.KEYCODE_DPAD_CENTER, |
| }; |
| /** Key events that perform system operations. */ |
| private static final int[] SYS_KEYS = { |
| KeyEvent.KEYCODE_HOME, KeyEvent.KEYCODE_BACK, |
| KeyEvent.KEYCODE_CALL, KeyEvent.KEYCODE_ENDCALL, |
| KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE, |
| KeyEvent.KEYCODE_MUTE, |
| }; |
| /** If a physical key exists? */ |
| private static final boolean[] PHYSICAL_KEY_EXISTS = new boolean[KeyEvent.getMaxKeyCode() + 1]; |
| static { |
| for (int i = 0; i < PHYSICAL_KEY_EXISTS.length; ++i) { |
| PHYSICAL_KEY_EXISTS[i] = true; |
| } |
| // Only examine SYS_KEYS |
| for (int i = 0; i < SYS_KEYS.length; ++i) { |
| PHYSICAL_KEY_EXISTS[SYS_KEYS[i]] = KeyCharacterMap.deviceHasKey(SYS_KEYS[i]); |
| } |
| } |
| /** Possible screen rotation degrees **/ |
| private static final int[] SCREEN_ROTATION_DEGREES = { |
| Surface.ROTATION_0, |
| Surface.ROTATION_90, |
| Surface.ROTATION_180, |
| Surface.ROTATION_270, |
| }; |
| |
| public static final int FACTOR_TOUCH = 0; |
| public static final int FACTOR_MOTION = 1; |
| public static final int FACTOR_PINCHZOOM = 2; |
| public static final int FACTOR_TRACKBALL = 3; |
| public static final int FACTOR_ROTATION = 4; |
| public static final int FACTOR_NAV = 5; |
| public static final int FACTOR_MAJORNAV = 6; |
| public static final int FACTOR_SYSOPS = 7; |
| public static final int FACTOR_APPSWITCH = 8; |
| public static final int FACTOR_FLIP = 9; |
| public static final int FACTOR_ANYTHING = 10; |
| public static final int FACTORZ_COUNT = 11; // should be last+1 |
| |
| private static final int GESTURE_TAP = 0; |
| private static final int GESTURE_DRAG = 1; |
| private static final int GESTURE_PINCH_OR_ZOOM = 2; |
| |
| /** 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 int mVerbose = 0; |
| private long mThrottle = 0; |
| |
| private boolean mKeyboardOpen = false; |
| |
| public static String getKeyName(int keycode) { |
| return KeyEvent.keyCodeToString(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 KeyEvent.KEYCODE_UNKNOWN if not found |
| */ |
| public static int getKeyCode(String keyName) { |
| return KeyEvent.keyCodeFromString(keyName); |
| } |
| |
| public MonkeySourceRandom(Random random, ArrayList<ComponentName> MainApps, |
| long throttle, boolean randomizeThrottle) { |
| // default values for random distributions |
| // note, these are straight percentages, to match user input (cmd line args) |
| // but they will be converted to 0..1 values before the main loop runs. |
| mFactors[FACTOR_TOUCH] = 15.0f; |
| mFactors[FACTOR_MOTION] = 10.0f; |
| mFactors[FACTOR_TRACKBALL] = 15.0f; |
| // Adjust the values if we want to enable rotation by default. |
| mFactors[FACTOR_ROTATION] = 0.0f; |
| mFactors[FACTOR_NAV] = 25.0f; |
| mFactors[FACTOR_MAJORNAV] = 15.0f; |
| mFactors[FACTOR_SYSOPS] = 2.0f; |
| mFactors[FACTOR_APPSWITCH] = 2.0f; |
| mFactors[FACTOR_FLIP] = 1.0f; |
| mFactors[FACTOR_ANYTHING] = 13.0f; |
| mFactors[FACTOR_PINCHZOOM] = 2.0f; |
| |
| mRandom = random; |
| mMainApps = MainApps; |
| mQ = new MonkeyEventQueue(random, throttle, randomizeThrottle); |
| } |
| |
| /** |
| * Adjust the percentages (after applying user values) and then normalize to a 0..1 scale. |
| */ |
| private boolean adjustEventFactors() { |
| // go through all values and compute totals for user & default values |
| float userSum = 0.0f; |
| float defaultSum = 0.0f; |
| int defaultCount = 0; |
| for (int i = 0; i < FACTORZ_COUNT; ++i) { |
| if (mFactors[i] <= 0.0f) { // user values are zero or negative |
| userSum -= mFactors[i]; |
| } 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 |
| mFactors[i] = -mFactors[i]; |
| } else { |
| 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] + "%"); |
| } |
| } |
| |
| if (!validateKeys()) { |
| return false; |
| } |
| |
| // 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; |
| } |
| |
| private static boolean validateKeyCategory(String catName, int[] keys, float factor) { |
| if (factor < 0.1f) { |
| return true; |
| } |
| for (int i = 0; i < keys.length; ++i) { |
| if (PHYSICAL_KEY_EXISTS[keys[i]]) { |
| return true; |
| } |
| } |
| System.err.println("** " + catName + " has no physical keys but with factor " + factor + "%."); |
| return false; |
| } |
| |
| /** |
| * See if any key exists for non-zero factors. |
| */ |
| private boolean validateKeys() { |
| return validateKeyCategory("NAV_KEYS", NAV_KEYS, mFactors[FACTOR_NAV]) |
| && validateKeyCategory("MAJOR_NAV_KEYS", MAJOR_NAV_KEYS, mFactors[FACTOR_MAJORNAV]) |
| && validateKeyCategory("SYS_KEYS", SYS_KEYS, mFactors[FACTOR_SYSOPS]); |
| } |
| |
| /** |
| * 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. |
| * TODO: Meta state |
| * 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 gesture The gesture to perform. |
| * |
| */ |
| 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)); |
| |
| // sometimes we'll move during the touch |
| if (gesture == GESTURE_DRAG) { |
| int count = random.nextInt(10); |
| for (int i = 0; i < count; i++) { |
| randomWalk(random, display, p1, v1); |
| |
| mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_MOVE) |
| .setDownTime(downAt) |
| .addPointer(0, p1.x, p1.y) |
| .setIntermediateNote(true)); |
| } |
| } else if (gesture == GESTURE_PINCH_OR_ZOOM) { |
| PointF p2 = randomPoint(random, display); |
| PointF v2 = randomVector(random); |
| |
| randomWalk(random, display, p1, v1); |
| mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_POINTER_DOWN |
| | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT)) |
| .setDownTime(downAt) |
| .addPointer(0, p1.x, p1.y).addPointer(1, p2.x, p2.y) |
| .setIntermediateNote(true)); |
| |
| int count = random.nextInt(10); |
| for (int i = 0; i < count; i++) { |
| randomWalk(random, display, p1, v1); |
| randomWalk(random, display, p2, v2); |
| |
| mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_MOVE) |
| .setDownTime(downAt) |
| .addPointer(0, p1.x, p1.y).addPointer(1, p2.x, p2.y) |
| .setIntermediateNote(true)); |
| } |
| |
| randomWalk(random, display, p1, v1); |
| randomWalk(random, display, p2, v2); |
| mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_POINTER_UP |
| | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT)) |
| .setDownTime(downAt) |
| .addPointer(0, p1.x, p1.y).addPointer(1, p2.x, p2.y) |
| .setIntermediateNote(true)); |
| } |
| |
| randomWalk(random, display, p1, v1); |
| mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_UP) |
| .setDownTime(downAt) |
| .addPointer(0, p1.x, p1.y) |
| .setIntermediateNote(false)); |
| } |
| |
| private PointF randomPoint(Random random, Display display) { |
| return new PointF(random.nextInt(display.getWidth()), random.nextInt(display.getHeight())); |
| } |
| |
| private PointF randomVector(Random random) { |
| return new PointF((random.nextFloat() - 0.5f) * 50, (random.nextFloat() - 0.5f) * 50); |
| } |
| |
| private void randomWalk(Random random, Display display, PointF point, PointF vector) { |
| point.x = (float) Math.max(Math.min(point.x + random.nextFloat() * vector.x, |
| display.getWidth()), 0); |
| point.y = (float) Math.max(Math.min(point.y + random.nextFloat() * vector.y, |
| display.getHeight()), 0); |
| } |
| |
| /** |
| * 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) { |
| for (int i = 0; i < 10; ++i) { |
| // generate a small random step |
| int dX = random.nextInt(10) - 5; |
| int dY = random.nextInt(10) - 5; |
| |
| mQ.addLast(new MonkeyTrackballEvent(MotionEvent.ACTION_MOVE) |
| .addPointer(0, dX, dY) |
| .setIntermediateNote(i > 0)); |
| } |
| |
| // 10% of trackball moves end with a click |
| if (0 == random.nextInt(10)) { |
| long downAt = SystemClock.uptimeMillis(); |
| |
| mQ.addLast(new MonkeyTrackballEvent(MotionEvent.ACTION_DOWN) |
| .setDownTime(downAt) |
| .addPointer(0, 0, 0) |
| .setIntermediateNote(true)); |
| |
| mQ.addLast(new MonkeyTrackballEvent(MotionEvent.ACTION_UP) |
| .setDownTime(downAt) |
| .addPointer(0, 0, 0) |
| .setIntermediateNote(false)); |
| } |
| } |
| |
| /** |
| * Generates a random screen rotation event. |
| * |
| * @param random Random number source for rotation degree. |
| */ |
| private void generateRotationEvent(Random random) { |
| mQ.addLast(new MonkeyRotationEvent( |
| SCREEN_ROTATION_DEGREES[random.nextInt( |
| SCREEN_ROTATION_DEGREES.length)], |
| random.nextBoolean())); |
| } |
| |
| /** |
| * generate a random event based on mFactor |
| */ |
| private void generateEvents() { |
| float cls = mRandom.nextFloat(); |
| int lastKey = 0; |
| |
| if (cls < mFactors[FACTOR_TOUCH]) { |
| generatePointerEvent(mRandom, GESTURE_TAP); |
| return; |
| } else if (cls < mFactors[FACTOR_MOTION]) { |
| generatePointerEvent(mRandom, GESTURE_DRAG); |
| return; |
| } else if (cls < mFactors[FACTOR_PINCHZOOM]) { |
| generatePointerEvent(mRandom, GESTURE_PINCH_OR_ZOOM); |
| return; |
| } else if (cls < mFactors[FACTOR_TRACKBALL]) { |
| generateTrackballEvent(mRandom); |
| return; |
| } else if (cls < mFactors[FACTOR_ROTATION]) { |
| generateRotationEvent(mRandom); |
| return; |
| } |
| |
| // The remaining event categories are injected as key events |
| for (;;) { |
| if (cls < mFactors[FACTOR_NAV]) { |
| lastKey = NAV_KEYS[mRandom.nextInt(NAV_KEYS.length)]; |
| } else if (cls < mFactors[FACTOR_MAJORNAV]) { |
| lastKey = MAJOR_NAV_KEYS[mRandom.nextInt(MAJOR_NAV_KEYS.length)]; |
| } else if (cls < mFactors[FACTOR_SYSOPS]) { |
| lastKey = SYS_KEYS[mRandom.nextInt(SYS_KEYS.length)]; |
| } else if (cls < mFactors[FACTOR_APPSWITCH]) { |
| MonkeyActivityEvent e = new MonkeyActivityEvent(mMainApps.get( |
| mRandom.nextInt(mMainApps.size()))); |
| mQ.addLast(e); |
| return; |
| } else if (cls < mFactors[FACTOR_FLIP]) { |
| MonkeyFlipEvent e = new MonkeyFlipEvent(mKeyboardOpen); |
| mKeyboardOpen = !mKeyboardOpen; |
| mQ.addLast(e); |
| return; |
| } else { |
| lastKey = 1 + mRandom.nextInt(KeyEvent.getMaxKeyCode() - 1); |
| } |
| |
| if (lastKey != KeyEvent.KEYCODE_POWER |
| && lastKey != KeyEvent.KEYCODE_ENDCALL |
| && PHYSICAL_KEY_EXISTS[lastKey]) { |
| break; |
| } |
| } |
| |
| 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 |
| */ |
| public void generateActivity() { |
| MonkeyActivityEvent e = new MonkeyActivityEvent(mMainApps.get( |
| mRandom.nextInt(mMainApps.size()))); |
| mQ.addLast(e); |
| } |
| |
| /** |
| * if the queue is empty, we generate events first |
| * @return the first event in the queue |
| */ |
| public MonkeyEvent getNextEvent() { |
| if (mQ.isEmpty()) { |
| generateEvents(); |
| } |
| mEventCount++; |
| MonkeyEvent e = mQ.getFirst(); |
| mQ.removeFirst(); |
| return e; |
| } |
| } |