1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.commands.monkey;
18
19import android.content.ComponentName;
20import android.graphics.PointF;
21import android.hardware.display.DisplayManagerGlobal;
22import android.os.SystemClock;
23import android.view.Display;
24import android.view.KeyCharacterMap;
25import android.view.KeyEvent;
26import android.view.MotionEvent;
27import android.view.Surface;
28
29import java.util.ArrayList;
30import java.util.Random;
31
32/**
33 * monkey event queue
34 */
35public class MonkeySourceRandom implements MonkeyEventSource {
36    /** Key events that move around the UI. */
37    private static final int[] NAV_KEYS = {
38        KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN,
39        KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
40    };
41    /**
42     * Key events that perform major navigation options (so shouldn't be sent
43     * as much).
44     */
45    private static final int[] MAJOR_NAV_KEYS = {
46        KeyEvent.KEYCODE_MENU, /*KeyEvent.KEYCODE_SOFT_RIGHT,*/
47        KeyEvent.KEYCODE_DPAD_CENTER,
48    };
49    /** Key events that perform system operations. */
50    private static final int[] SYS_KEYS = {
51        KeyEvent.KEYCODE_HOME, KeyEvent.KEYCODE_BACK,
52        KeyEvent.KEYCODE_CALL, KeyEvent.KEYCODE_ENDCALL,
53        KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE,
54        KeyEvent.KEYCODE_MUTE,
55    };
56    /** If a physical key exists? */
57    private static final boolean[] PHYSICAL_KEY_EXISTS = new boolean[KeyEvent.getMaxKeyCode() + 1];
58    static {
59        for (int i = 0; i < PHYSICAL_KEY_EXISTS.length; ++i) {
60            PHYSICAL_KEY_EXISTS[i] = true;
61        }
62        // Only examine SYS_KEYS
63        for (int i = 0; i < SYS_KEYS.length; ++i) {
64            PHYSICAL_KEY_EXISTS[SYS_KEYS[i]] = KeyCharacterMap.deviceHasKey(SYS_KEYS[i]);
65        }
66    }
67    /** Possible screen rotation degrees **/
68    private static final int[] SCREEN_ROTATION_DEGREES = {
69      Surface.ROTATION_0,
70      Surface.ROTATION_90,
71      Surface.ROTATION_180,
72      Surface.ROTATION_270,
73    };
74
75    public static final int FACTOR_TOUCH        = 0;
76    public static final int FACTOR_MOTION       = 1;
77    public static final int FACTOR_PINCHZOOM    = 2;
78    public static final int FACTOR_TRACKBALL    = 3;
79    public static final int FACTOR_ROTATION     = 4;
80    public static final int FACTOR_NAV          = 5;
81    public static final int FACTOR_MAJORNAV     = 6;
82    public static final int FACTOR_SYSOPS       = 7;
83    public static final int FACTOR_APPSWITCH    = 8;
84    public static final int FACTOR_FLIP         = 9;
85    public static final int FACTOR_ANYTHING     = 10;
86    public static final int FACTORZ_COUNT       = 11;    // should be last+1
87
88    private static final int GESTURE_TAP = 0;
89    private static final int GESTURE_DRAG = 1;
90    private static final int GESTURE_PINCH_OR_ZOOM = 2;
91
92    /** percentages for each type of event.  These will be remapped to working
93     * values after we read any optional values.
94     **/
95    private float[] mFactors = new float[FACTORZ_COUNT];
96    private ArrayList<ComponentName> mMainApps;
97    private int mEventCount = 0;  //total number of events generated so far
98    private MonkeyEventQueue mQ;
99    private Random mRandom;
100    private int mVerbose = 0;
101    private long mThrottle = 0;
102
103    private boolean mKeyboardOpen = false;
104
105    public static String getKeyName(int keycode) {
106        return KeyEvent.keyCodeToString(keycode);
107    }
108
109    /**
110     * Looks up the keyCode from a given KEYCODE_NAME.  NOTE: This may
111     * be an expensive operation.
112     *
113     * @param keyName the name of the KEYCODE_VALUE to lookup.
114     * @returns the intenger keyCode value, or KeyEvent.KEYCODE_UNKNOWN if not found
115     */
116    public static int getKeyCode(String keyName) {
117        return KeyEvent.keyCodeFromString(keyName);
118    }
119
120    public MonkeySourceRandom(Random random, ArrayList<ComponentName> MainApps,
121            long throttle, boolean randomizeThrottle) {
122        // default values for random distributions
123        // note, these are straight percentages, to match user input (cmd line args)
124        // but they will be converted to 0..1 values before the main loop runs.
125        mFactors[FACTOR_TOUCH] = 15.0f;
126        mFactors[FACTOR_MOTION] = 10.0f;
127        mFactors[FACTOR_TRACKBALL] = 15.0f;
128        // Adjust the values if we want to enable rotation by default.
129        mFactors[FACTOR_ROTATION] = 0.0f;
130        mFactors[FACTOR_NAV] = 25.0f;
131        mFactors[FACTOR_MAJORNAV] = 15.0f;
132        mFactors[FACTOR_SYSOPS] = 2.0f;
133        mFactors[FACTOR_APPSWITCH] = 2.0f;
134        mFactors[FACTOR_FLIP] = 1.0f;
135        mFactors[FACTOR_ANYTHING] = 13.0f;
136        mFactors[FACTOR_PINCHZOOM] = 2.0f;
137
138        mRandom = random;
139        mMainApps = MainApps;
140        mQ = new MonkeyEventQueue(random, throttle, randomizeThrottle);
141    }
142
143    /**
144     * Adjust the percentages (after applying user values) and then normalize to a 0..1 scale.
145     */
146    private boolean adjustEventFactors() {
147        // go through all values and compute totals for user & default values
148        float userSum = 0.0f;
149        float defaultSum = 0.0f;
150        int defaultCount = 0;
151        for (int i = 0; i < FACTORZ_COUNT; ++i) {
152            if (mFactors[i] <= 0.0f) {   // user values are zero or negative
153                userSum -= mFactors[i];
154            } else {
155                defaultSum += mFactors[i];
156                ++defaultCount;
157            }
158        }
159
160        // if the user request was > 100%, reject it
161        if (userSum > 100.0f) {
162            System.err.println("** Event weights > 100%");
163            return false;
164        }
165
166        // if the user specified all of the weights, then they need to be 100%
167        if (defaultCount == 0 && (userSum < 99.9f || userSum > 100.1f)) {
168            System.err.println("** Event weights != 100%");
169            return false;
170        }
171
172        // compute the adjustment necessary
173        float defaultsTarget = (100.0f - userSum);
174        float defaultsAdjustment = defaultsTarget / defaultSum;
175
176        // fix all values, by adjusting defaults, or flipping user values back to >0
177        for (int i = 0; i < FACTORZ_COUNT; ++i) {
178            if (mFactors[i] <= 0.0f) {   // user values are zero or negative
179                mFactors[i] = -mFactors[i];
180            } else {
181                mFactors[i] *= defaultsAdjustment;
182            }
183        }
184
185        // if verbose, show factors
186        if (mVerbose > 0) {
187            System.out.println("// Event percentages:");
188            for (int i = 0; i < FACTORZ_COUNT; ++i) {
189                System.out.println("//   " + i + ": " + mFactors[i] + "%");
190            }
191        }
192
193        if (!validateKeys()) {
194            return false;
195        }
196
197        // finally, normalize and convert to running sum
198        float sum = 0.0f;
199        for (int i = 0; i < FACTORZ_COUNT; ++i) {
200            sum += mFactors[i] / 100.0f;
201            mFactors[i] = sum;
202        }
203        return true;
204    }
205
206    private static boolean validateKeyCategory(String catName, int[] keys, float factor) {
207        if (factor < 0.1f) {
208            return true;
209        }
210        for (int i = 0; i < keys.length; ++i) {
211            if (PHYSICAL_KEY_EXISTS[keys[i]]) {
212                return true;
213            }
214        }
215        System.err.println("** " + catName + " has no physical keys but with factor " + factor + "%.");
216        return false;
217    }
218
219    /**
220     * See if any key exists for non-zero factors.
221     */
222    private boolean validateKeys() {
223        return validateKeyCategory("NAV_KEYS", NAV_KEYS, mFactors[FACTOR_NAV])
224            && validateKeyCategory("MAJOR_NAV_KEYS", MAJOR_NAV_KEYS, mFactors[FACTOR_MAJORNAV])
225            && validateKeyCategory("SYS_KEYS", SYS_KEYS, mFactors[FACTOR_SYSOPS]);
226    }
227
228    /**
229     * set the factors
230     *
231     * @param factors percentages for each type of event
232     */
233    public void setFactors(float factors[]) {
234        int c = FACTORZ_COUNT;
235        if (factors.length < c) {
236            c = factors.length;
237        }
238        for (int i = 0; i < c; i++)
239            mFactors[i] = factors[i];
240    }
241
242    public void setFactors(int index, float v) {
243        mFactors[index] = v;
244    }
245
246    /**
247     * Generates a random motion event. This method counts a down, move, and up as multiple events.
248     *
249     * TODO:  Test & fix the selectors when non-zero percentages
250     * TODO:  Longpress.
251     * TODO:  Fling.
252     * TODO:  Meta state
253     * TODO:  More useful than the random walk here would be to pick a single random direction
254     * and distance, and divvy it up into a random number of segments.  (This would serve to
255     * generate fling gestures, which are important).
256     *
257     * @param random Random number source for positioning
258     * @param gesture The gesture to perform.
259     *
260     */
261    private void generatePointerEvent(Random random, int gesture) {
262        Display display = DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
263
264        PointF p1 = randomPoint(random, display);
265        PointF v1 = randomVector(random);
266
267        long downAt = SystemClock.uptimeMillis();
268
269        mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_DOWN)
270                .setDownTime(downAt)
271                .addPointer(0, p1.x, p1.y)
272                .setIntermediateNote(false));
273
274        // sometimes we'll move during the touch
275        if (gesture == GESTURE_DRAG) {
276            int count = random.nextInt(10);
277            for (int i = 0; i < count; i++) {
278                randomWalk(random, display, p1, v1);
279
280                mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_MOVE)
281                        .setDownTime(downAt)
282                        .addPointer(0, p1.x, p1.y)
283                        .setIntermediateNote(true));
284            }
285        } else if (gesture == GESTURE_PINCH_OR_ZOOM) {
286            PointF p2 = randomPoint(random, display);
287            PointF v2 = randomVector(random);
288
289            randomWalk(random, display, p1, v1);
290            mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_POINTER_DOWN
291                            | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT))
292                    .setDownTime(downAt)
293                    .addPointer(0, p1.x, p1.y).addPointer(1, p2.x, p2.y)
294                    .setIntermediateNote(true));
295
296            int count = random.nextInt(10);
297            for (int i = 0; i < count; i++) {
298                randomWalk(random, display, p1, v1);
299                randomWalk(random, display, p2, v2);
300
301                mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_MOVE)
302                        .setDownTime(downAt)
303                        .addPointer(0, p1.x, p1.y).addPointer(1, p2.x, p2.y)
304                        .setIntermediateNote(true));
305            }
306
307            randomWalk(random, display, p1, v1);
308            randomWalk(random, display, p2, v2);
309            mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_POINTER_UP
310                            | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT))
311                    .setDownTime(downAt)
312                    .addPointer(0, p1.x, p1.y).addPointer(1, p2.x, p2.y)
313                    .setIntermediateNote(true));
314        }
315
316        randomWalk(random, display, p1, v1);
317        mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_UP)
318                .setDownTime(downAt)
319                .addPointer(0, p1.x, p1.y)
320                .setIntermediateNote(false));
321    }
322
323    private PointF randomPoint(Random random, Display display) {
324        return new PointF(random.nextInt(display.getWidth()), random.nextInt(display.getHeight()));
325    }
326
327    private PointF randomVector(Random random) {
328        return new PointF((random.nextFloat() - 0.5f) * 50, (random.nextFloat() - 0.5f) * 50);
329    }
330
331    private void randomWalk(Random random, Display display, PointF point, PointF vector) {
332        point.x = (float) Math.max(Math.min(point.x + random.nextFloat() * vector.x,
333                display.getWidth()), 0);
334        point.y = (float) Math.max(Math.min(point.y + random.nextFloat() * vector.y,
335                display.getHeight()), 0);
336    }
337
338    /**
339     * Generates a random trackball event. This consists of a sequence of small moves, followed by
340     * an optional single click.
341     *
342     * TODO:  Longpress.
343     * TODO:  Meta state
344     * TODO:  Parameterize the % clicked
345     * TODO:  More useful than the random walk here would be to pick a single random direction
346     * and distance, and divvy it up into a random number of segments.  (This would serve to
347     * generate fling gestures, which are important).
348     *
349     * @param random Random number source for positioning
350     *
351     */
352    private void generateTrackballEvent(Random random) {
353        for (int i = 0; i < 10; ++i) {
354            // generate a small random step
355            int dX = random.nextInt(10) - 5;
356            int dY = random.nextInt(10) - 5;
357
358            mQ.addLast(new MonkeyTrackballEvent(MotionEvent.ACTION_MOVE)
359                    .addPointer(0, dX, dY)
360                    .setIntermediateNote(i > 0));
361        }
362
363        // 10% of trackball moves end with a click
364        if (0 == random.nextInt(10)) {
365            long downAt = SystemClock.uptimeMillis();
366
367            mQ.addLast(new MonkeyTrackballEvent(MotionEvent.ACTION_DOWN)
368                    .setDownTime(downAt)
369                    .addPointer(0, 0, 0)
370                    .setIntermediateNote(true));
371
372            mQ.addLast(new MonkeyTrackballEvent(MotionEvent.ACTION_UP)
373                    .setDownTime(downAt)
374                    .addPointer(0, 0, 0)
375                    .setIntermediateNote(false));
376        }
377    }
378
379    /**
380     * Generates a random screen rotation event.
381     *
382     * @param random Random number source for rotation degree.
383     */
384    private void generateRotationEvent(Random random) {
385        mQ.addLast(new MonkeyRotationEvent(
386                SCREEN_ROTATION_DEGREES[random.nextInt(
387                        SCREEN_ROTATION_DEGREES.length)],
388                random.nextBoolean()));
389    }
390
391    /**
392     * generate a random event based on mFactor
393     */
394    private void generateEvents() {
395        float cls = mRandom.nextFloat();
396        int lastKey = 0;
397
398        if (cls < mFactors[FACTOR_TOUCH]) {
399            generatePointerEvent(mRandom, GESTURE_TAP);
400            return;
401        } else if (cls < mFactors[FACTOR_MOTION]) {
402            generatePointerEvent(mRandom, GESTURE_DRAG);
403            return;
404        } else if (cls < mFactors[FACTOR_PINCHZOOM]) {
405            generatePointerEvent(mRandom, GESTURE_PINCH_OR_ZOOM);
406            return;
407        } else if (cls < mFactors[FACTOR_TRACKBALL]) {
408            generateTrackballEvent(mRandom);
409            return;
410        } else if (cls < mFactors[FACTOR_ROTATION]) {
411            generateRotationEvent(mRandom);
412            return;
413        }
414
415        // The remaining event categories are injected as key events
416        for (;;) {
417            if (cls < mFactors[FACTOR_NAV]) {
418                lastKey = NAV_KEYS[mRandom.nextInt(NAV_KEYS.length)];
419            } else if (cls < mFactors[FACTOR_MAJORNAV]) {
420                lastKey = MAJOR_NAV_KEYS[mRandom.nextInt(MAJOR_NAV_KEYS.length)];
421            } else if (cls < mFactors[FACTOR_SYSOPS]) {
422                lastKey = SYS_KEYS[mRandom.nextInt(SYS_KEYS.length)];
423            } else if (cls < mFactors[FACTOR_APPSWITCH]) {
424                MonkeyActivityEvent e = new MonkeyActivityEvent(mMainApps.get(
425                        mRandom.nextInt(mMainApps.size())));
426                mQ.addLast(e);
427                return;
428            } else if (cls < mFactors[FACTOR_FLIP]) {
429                MonkeyFlipEvent e = new MonkeyFlipEvent(mKeyboardOpen);
430                mKeyboardOpen = !mKeyboardOpen;
431                mQ.addLast(e);
432                return;
433            } else {
434                lastKey = 1 + mRandom.nextInt(KeyEvent.getMaxKeyCode() - 1);
435            }
436
437            if (lastKey != KeyEvent.KEYCODE_POWER
438                    && lastKey != KeyEvent.KEYCODE_ENDCALL
439                    && lastKey != KeyEvent.KEYCODE_SLEEP
440                    && PHYSICAL_KEY_EXISTS[lastKey]) {
441                break;
442            }
443        }
444
445        MonkeyKeyEvent e = new MonkeyKeyEvent(KeyEvent.ACTION_DOWN, lastKey);
446        mQ.addLast(e);
447
448        e = new MonkeyKeyEvent(KeyEvent.ACTION_UP, lastKey);
449        mQ.addLast(e);
450    }
451
452    public boolean validate() {
453        //check factors
454        return adjustEventFactors();
455    }
456
457    public void setVerbose(int verbose) {
458        mVerbose = verbose;
459    }
460
461    /**
462     * generate an activity event
463     */
464    public void generateActivity() {
465        MonkeyActivityEvent e = new MonkeyActivityEvent(mMainApps.get(
466                mRandom.nextInt(mMainApps.size())));
467        mQ.addLast(e);
468    }
469
470    /**
471     * if the queue is empty, we generate events first
472     * @return the first event in the queue
473     */
474    public MonkeyEvent getNextEvent() {
475        if (mQ.isEmpty()) {
476            generateEvents();
477        }
478        mEventCount++;
479        MonkeyEvent e = mQ.getFirst();
480        mQ.removeFirst();
481        return e;
482    }
483}
484