/*
 * Copyright 2020 Advanced Software Technologies Lab at ETH Zurich, Switzerland
 *
 * 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 static com.android.commands.monkey.ape.utils.Config.defaultGUIThrottle;
import static com.android.commands.monkey.ape.utils.Config.doFuzzing;
import static com.android.commands.monkey.ape.utils.Config.fuzzingRate;
import static com.android.commands.monkey.ape.utils.Config.refectchInfoCount;
import static com.android.commands.monkey.ape.utils.Config.refectchInfoWaitingInterval;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeoutException;

import com.android.commands.monkey.ape.Agent;
import com.android.commands.monkey.ape.AndroidDevice;
import com.android.commands.monkey.ape.StopTestingException;
import com.android.commands.monkey.ape.agent.ApeAgent;
import com.android.commands.monkey.ape.events.ApeEvent;
import com.android.commands.monkey.ape.model.Action;
import com.android.commands.monkey.ape.model.ActionType;
import com.android.commands.monkey.ape.model.FuzzAction;
import com.android.commands.monkey.ape.model.ModelAction;
import com.android.commands.monkey.ape.events.ApeTouchGeometry;
import com.android.commands.monkey.ape.model.StartAction;
import com.android.commands.monkey.ape.tree.GUITreeNode;
import com.android.commands.monkey.ape.utils.Config;
import com.android.commands.monkey.ape.utils.Logger;
import com.android.commands.monkey.ape.utils.RandomHelper;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.UiAutomation;
import android.app.UiAutomationConnection;
import android.content.ComponentName;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Build;
import android.os.HandlerThread;
import android.os.RemoteException;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Surface;
import android.view.accessibility.AccessibilityNodeInfo;

/**
 * monkey event queue
 */
public class MonkeySourceApe implements MonkeyEventSource {

    private static long CLICK_WAIT_TIME = 0L;
    private static long LONG_CLICK_WAIT_TIME = 1000L;

    private static final boolean useRandomClick = false;

    static KeyCharacterMap CharMap;
    static {
        if (Build.VERSION.SDK_INT >= 11) // My soft runs until API 5
            CharMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
        else
            CharMap = KeyCharacterMap.load(KeyCharacterMap.ALPHA);
    }

    private List<ComponentName> mMainApps;
    private Map<String, String[]> packagePermissions;
    private int mEventCount = 0; // total number of events generated so far
    private MonkeyEventQueue mQ;
    private int mVerbose = 0;
    private long mThrottle = defaultGUIThrottle;
    private boolean mRandomizeThrottle = false;
    private MonkeyPermissionUtil mPermissionUtil;
    private Random mRandom;

    // private boolean mKeyboardOpen = false;
    private Agent mAgent;
    private MonkeySourceApeEvents mEvents = new MonkeySourceApeEvents();
    private MonkeySourceApeFuzzer mFuzzer = new MonkeySourceApeFuzzer();
    private MonkeySourceApeInput mInput = new MonkeySourceApeInput();
    private int mEventId = 0;
    private int statusBarHeight = -1;
    private File mOutputDirectory;
    private int mGeneratedFuzzActions = 0;
    private int mGeneratedFuzzEvents = 0;

    // Counter

    int nullInfoCounter = 0;
    int lostFocusedCounter = 0;

    /**
     * UiAutomation client and connection
     */
    protected final HandlerThread mHandlerThread = new HandlerThread("MonkeySourceApe");
    protected UiAutomation mUiAutomation;

    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 static boolean hasKey(int key) {
        return KeyCharacterMap.deviceHasKey(key);
    }

    public int getStatusBarHeight() {
        if (this.statusBarHeight == -1) {
            Display display = DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
            DisplayMetrics dm = new DisplayMetrics();
            display.getMetrics(dm);
            this.statusBarHeight = (int) (24 * dm.density);
        }
        return this.statusBarHeight;
    }

    /**
     * Connect to AccessibilityService
     */
    public void connect() {
        if (mHandlerThread.isAlive()) {
            throw new IllegalStateException("Already connected!");
        }
        mHandlerThread.start();
        mUiAutomation = new UiAutomation(mHandlerThread.getLooper(), new UiAutomationConnection());
        mUiAutomation.connect();
        MonkeyMotionEvent.setUiAutomation(mUiAutomation);

        AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
        // Compress this node
        info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;

        mUiAutomation.setServiceInfo(info);

    }

    public int getEventCount() {
        return mEventCount;
    }

    /**
     * Disconnect to AccessibilityService
     */
    public void disconnect() {
        if (!mHandlerThread.isAlive()) {
            throw new IllegalStateException("Already disconnected!");
        }
        mUiAutomation.disconnect();
        MonkeyMotionEvent.setUiAutomation(null);
        mHandlerThread.quit();
    }

    public void tearDown() {
        Logger.iformat("Fuzzing summary: actions=%d events=%d", mGeneratedFuzzActions, mGeneratedFuzzEvents);
        this.disconnect();
        this.mAgent.tearDown();
    }

    public MonkeySourceApe(Random random,
            List<ComponentName> MainApps, long throttle, boolean randomizeThrottle, boolean permissionTargetSystem,
            File outputDirectory) {
        mRandom = random;
        RandomHelper.setRandom(random);
        mMainApps = MainApps;
        packagePermissions = new HashMap<>();
        for (ComponentName app : MainApps) {
            packagePermissions.put(app.getPackageName(), AndroidDevice.getGrantedPermissions(app.getPackageName()));
        }
        mThrottle = throttle;
        if (mThrottle == 0) {
            mThrottle = defaultGUIThrottle;
        }
        mRandomizeThrottle = randomizeThrottle;
        mQ = new MonkeyEventQueue(random, 0, false); // we manage throttle
        mPermissionUtil = new MonkeyPermissionUtil();
        mPermissionUtil.setTargetSystemPackages(permissionTargetSystem);
        // mPermissionUtil.populatePermissionsMapping();
        mOutputDirectory = outputDirectory;

        mAgent = ApeAgent.createAgent(this);
        connect();
    }

    public Agent getAgent() {
        return mAgent;
    }

    public File getOutputDirectory() {
        if (!this.mOutputDirectory.exists()) {
            if (!this.mOutputDirectory.mkdirs()) {
                Logger.wprintln("Fail to create output directory at " + this.mOutputDirectory);
                Logger.wprintln("Use /data/local/tmp instead.");
                return new File("/data/local/tmp");
            }
        }
        return this.mOutputDirectory;
    }

    public String getTopActivityPackageName() {
        ComponentName cn = getTopActivityComponentName();
        if (cn != null) {
            return cn.getPackageName();
        }
        return Monkey.currentPackage;
    }

    public String getTopActivityClassName() {
        ComponentName cn = getTopActivityComponentName();
        if (cn != null) {
            return cn.getClassName();
        }
        return Monkey.currentPackage;
    }

    public ComponentName getTopActivityComponentName() {
        return AndroidDevice.getTopActivityComponentName();
    }

    /**
     * Get visible bounds of a given node.
     */
    public Rect getVisibleBounds(Rect nodeRect) {
        return ApeTouchGeometry.visibleBounds(mAgent.getCurrentRootNodeBounds(), AndroidDevice.getDisplayBounds(), nodeRect);
    }

    /**
     * Always return a fresh rect
     * 
     * @return
     */
    protected Rect getVisibleBounds() {
        return ApeTouchGeometry.visibleRootBounds(mAgent.getCurrentRootNodeBounds(), AndroidDevice.getDisplayBounds());
    }



    int outOfBoundsCounter = 0;

    private boolean waitForActivity;
    private int waitForActivityRetryCount;
    private static final int WAIT_FOR_ACTIVITY_MAX_RETRIES = 100; // 100 * 100ms = 10s

    private boolean clearPackageOnGeneratingActivity;

    private int lastStartTimestamp = -1;

    private boolean waitForActivityFromClean;

    protected void generateClickEventAt(Rect nodeRect, long waitTime) {
        generateClickEventAt(nodeRect, waitTime, useRandomClick ? ClickPoint.RANDOM : ClickPoint.CENTER);
    }

    enum ClickPoint {
        CENTER,
        LEFT,
        TOP,
        RIGHT,
        BOTTOM,
        TOP_LEFT,
        TOP_RIGHT,
        BOTTOM_LEFT,
        BOTTOM_RIGHT,
        RANDOM
    }

    protected void generateClickEventAt(Rect nodeRect, long waitTime, ClickPoint clickPoint) {
        mEvents.generateClickEventAt(this, nodeRect, waitTime, clickPoint);
    }

    protected void generateKeyBackEvent() {
        generateKeyEvent(KeyEvent.KEYCODE_BACK);
    }

    protected void generateKeyMenuEvent() {
        generateKeyEvent(KeyEvent.KEYCODE_MENU);
        generateThrottleEvent(mThrottle);
    }

    protected void generateKeyEvent(int key) {
        generateKeyEvent(key, KeyEvent.KEYCODE_UNKNOWN);
    }

    /**
     * Generate a key event at specific key.
     */
    protected void generateKeyEvent(int key, int additionalKey) {
        mEvents.generateKeyEvent(this, key, additionalKey);
    }

    /**
     * ActiveWindow may not belong to activity package.
     * 
     * @return
     */
    public AccessibilityNodeInfo getRootInActiveWindow() {
        return mUiAutomation.getRootInActiveWindow();
    }

    public AccessibilityNodeInfo getRootInActiveWindowSlow() {
        try {
            mUiAutomation.waitForIdle(1000, 1000 * 10);
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        return mUiAutomation.getRootInActiveWindow();
    }

    private boolean validateClickAction(ModelAction action) {
        return validateBounds(action);
    }

    private boolean validateBounds(ModelAction action) {
        GUITreeNode node = action.getResolvedNode();
        return !node.isEmpty() && !node.isOutOfRoot();
    }

    private boolean validateScrollAction(ModelAction action) {
        return validateBounds(action);
    }

    public boolean validateResolvedAction(ModelAction action) {
        switch (action.getType()) {
        case EVENT_START:
        case EVENT_RESTART:
        case EVENT_CLEAN_RESTART:
        case EVENT_NOP:
        case EVENT_ACTIVATE:
            return true;
        case MODEL_BACK:
            return true;
        case MODEL_CLICK:
        case MODEL_LONG_CLICK:
            return validateClickAction(action);
        case MODEL_SCROLL_BOTTOM_UP:
        case MODEL_SCROLL_TOP_DOWN:
        case MODEL_SCROLL_LEFT_RIGHT:
        case MODEL_SCROLL_RIGHT_LEFT:
            return validateScrollAction(action);
        default:
            throw new RuntimeException("Should not reach here");
        }
    }

    protected void generateActivateEvent() {
        Logger.iprintln("Activating: generate app switch events.");
        mFuzzer.generateAppSwitchEvent(this);
        startEpisode("activate");
    }

    void generateRotationEvent(Random random) {
        mFuzzer.generateRotationEvent(this, random);
    }

    void resetRotation() {
        mFuzzer.resetRotation(this);
    }

    private final boolean hasEvent() {
        return !mQ.isEmpty();
    }

    final void addEvent(MonkeyEvent event) {
        mQ.addLast(event);
        event.setEventId(mEventId++);
    }

    private final void clearEvent() {
        if(mAgent instanceof com.android.commands.monkey.ape.agent.StatefulAgent)
            ((com.android.commands.monkey.ape.agent.StatefulAgent)mAgent).jevEventsDiscarded(mQ);
        while (!mQ.isEmpty()) {
            mQ.removeFirst();
        }
    }

    private final MonkeyEvent popEvent() {
        return mQ.removeFirst();
    }

    protected void generateScrollEventAt(Action action) {
        mInput.generateScrollEventAt(this, action);
    }

    void sleep(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * generate a random event based on mFactor
     */
    protected void generateEvents() {
        if (hasEvent()) {
            return;
        }

        ComponentName topComp = null;
        AccessibilityNodeInfo info = null;
        int repeat = refectchInfoCount;

        Action action = null;
        while (repeat-- > 0) {
            topComp = this.getTopActivityComponentName();
            info = getRootInActiveWindow();
            // this two operations may not be the same
            if (info == null) {
                sleep(refectchInfoWaitingInterval);
                continue;
            }
            if (info != null) {
                nullInfoCounter = 0;
                action = mAgent.updateState(topComp, info);
                if (action == null) {
                    throw new NullPointerException("Resolved action should not be null");
                }
                break;
            }
        }
        if (info == null) {
            Logger.wprintln("Null info root node returned by UiTestAutomationBridge, generate activate action...");
            nullInfoCounter++;
            if (nullInfoCounter > 10) {
                stopTopActivity();
                nullInfoCounter = 0;
            }
            mAgent.onVoidGUITree(nullInfoCounter);
            AndroidDevice.checkInteractive();
            action = Action.ACTIVATE;
        }
        if (action == null) {
            action = Action.NOP;
        }
        generateEventsForAction(action);
        if (doFuzzing && RandomHelper.toss(fuzzingRate)) {
            if (mAgent.canFuzzing()) {
                Action fuzzingAction = mAgent.generateFuzzingAction();
                generateEventsForAction(fuzzingAction);
            }
        }
        mAgent.notifyActionConsumed();
    }

    protected void startRandomMainApp() {
        generateEventsForAction(Action.getStartAction(ActionType.EVENT_START, randomlyPickMainApp()));
    }

    private void generateEventsForActionInternal(Action action) {
        switch (action.getType()) {
        case FUZZ:
            generateFuzzingEvents((FuzzAction) action);
            break;
        case EVENT_START:
            generateActivityEvents(((StartAction) action).getActivity(), false);
            break;
        case EVENT_RESTART:
            restartPackage(((StartAction) action).getActivity(), false, "start action");
            break;
        case EVENT_CLEAN_RESTART:
            restartPackage(((StartAction) action).getActivity(), true, "start action");
            break;
        case EVENT_NOP:
            generateThrottleEvent(action.getThrottle());
            break;
        case EVENT_ACTIVATE:
            generateActivateEvent();
            break;
        case MODEL_BACK:
            generateKeyBackEvent();
            break;
        case MODEL_CLICK:
            GUITreeNode node = action.getResolvedNode();
            generateClickEventAt(action.getResolvedNode().getBoundsInScreen(), CLICK_WAIT_TIME);
            if (node != null && node.getInputText() != null) {
                generateThrottleEvent(200); // Let the click event to be scheduled
                doInput(action, node);
            }
            break;
        case MODEL_LONG_CLICK:
            generateClickEventAt(action.getResolvedNode().getBoundsInScreen(), LONG_CLICK_WAIT_TIME);
            break;
        case MODEL_SCROLL_BOTTOM_UP:
        case MODEL_SCROLL_TOP_DOWN:
        case MODEL_SCROLL_LEFT_RIGHT:
        case MODEL_SCROLL_RIGHT_LEFT:
            generateScrollEventAt(action);
            break;
        default:
            throw new RuntimeException("Should not reach here");
        }
    }

    private void generateFuzzingEvents(FuzzAction action) {
        List<ApeEvent> events = action.getFuzzingEvents();
        mGeneratedFuzzActions++;
        mGeneratedFuzzEvents += events.size();
        Logger.iformat("Fuzzing: action #%d events=%d total_events=%d",
                mGeneratedFuzzActions, events.size(), mGeneratedFuzzEvents);
        long throttle = mThrottle + action.getThrottle();
        for (ApeEvent event : events) {
            List<MonkeyEvent> monkeyEvents = event.generateMonkeyEvents();
            for (MonkeyEvent me : monkeyEvents) {
                if (me == null) {
                    throw new RuntimeException();
                }
                addEvent(me);
            }
            generateThrottleEvent(throttle);
        }
        Logger.iprintln("Fuzzing: reset rotation.");
        resetRotation();
        // avoid non-deterministic actions
        startEpisode("fuzz");
    }

    private void generateEventsForAction(Action action) {
        if(mAgent instanceof com.android.commands.monkey.ape.agent.StatefulAgent)
            ((com.android.commands.monkey.ape.agent.StatefulAgent)mAgent).jevBeforeInput(action);
        int jevFirstEvent=mQ.size();
        long clockTimestamp = System.currentTimeMillis();
        if (action instanceof ModelAction) {
            ModelAction modelAction = (ModelAction) action;
            if (modelAction.requireTarget() && modelAction.getResolvedNode() != null) {
                Logger.iformat("EXECUTE action_fp=%s target_fp=%s action=%s",
                        modelAction.getActionFingerprint(), modelAction.getTargetFingerprint(), modelAction);
            }
        }
        mAgent.appendToActionHistory(clockTimestamp, action);
        generateEventsForActionInternal(action);
        long throttle = mThrottle + action.getThrottle();
        generateThrottleEvent(throttle);
        if(mAgent instanceof com.android.commands.monkey.ape.agent.StatefulAgent)
            ((com.android.commands.monkey.ape.agent.StatefulAgent)mAgent).jevEventsGenerated(action,mQ,jevFirstEvent);
    }

    private void startEpisode(String reason) {
        if(mAgent instanceof com.android.commands.monkey.ape.agent.StatefulAgent)
            ((com.android.commands.monkey.ape.agent.StatefulAgent)mAgent).jevTraceReset(reason);
        mAgent.startNewEpisode();
    }
    public void jevEventInjected(MonkeyEvent event,int code) {
        if(mAgent instanceof com.android.commands.monkey.ape.agent.StatefulAgent)
            ((com.android.commands.monkey.ape.agent.StatefulAgent)mAgent).jevEventInjected(event,code);
    }

    protected boolean checkPackage(ComponentName topComp, AccessibilityNodeInfo info) {
        String packageName = topComp.getPackageName();
        String infoPkg = info.getPackageName().toString();
        if (!infoPkg.equals(packageName)) {
            Logger.wformat("Different packages: top(%s) v.s. info(%s).", packageName, infoPkg);
            return false;
        }
        if (!MonkeyUtils.getPackageFilter().checkEnteringPackage(packageName)) {
            Logger.format("Disallowed package: %s", packageName);
            return false;
        }
        return true;
    }

    public void clearPackage(String packageName) {
        String[] permissions = this.packagePermissions.get(packageName);
        if (permissions == null) {
            Logger.wprintln("Stop clearing untracked package: " + packageName);
            return;
        }
        AndroidDevice.clearPackage(packageName, permissions);
    }

    public void grantRuntimePermissions(String packageName, String reason) {
        String[] permissions = this.packagePermissions.get(packageName);
        if (permissions == null) {
            Logger.wprintln("Stop granting permissions to untracked package: " + packageName);
            return;
        }
        AndroidDevice.grantRuntimePermissions(packageName, permissions, reason);
    }

    public void grantRuntimePermissions(String reason) {
        for (ComponentName cn : mMainApps) {
            grantRuntimePermissions(cn.getPackageName(), reason);
        }
    }

    protected void restartPackage(ComponentName cn, boolean clearPackage, String reason) {
        String packageName = cn.getPackageName();
        Logger.iprintln("Try to restart package " + packageName + " for " + reason);
        stopPackage(cn.getPackageName());
        generateActivityEvents(cn, clearPackage);
    }

    /**
     * Stop the foreground activity
     */
    public void stopTopActivity() {
        boolean killed = false;
        try {
            List<RunningAppProcessInfo> processes = AndroidDevice.iActivityManager.getRunningAppProcesses();
            if (!processes.isEmpty()) {
                RunningAppProcessInfo process = processes.get(0);
                Logger.format("Try to stop process %s(%d) ", process.processName, process.pid);
                // mDevice.mAm.killPids(new int[] {process.pid}, "Killed by
                // ape", true);
                if (AndroidDevice.killPids(process.pid) == 0) { // only when you are
                    // rooted
                    killed = true;
                    Logger.format("Process %s(%d) is killed", process.processName, process.pid);
                }
            }
            mAgent.onActivityStopped();
        } catch (RemoteException e1) {
            e1.printStackTrace();
            killed = false;
        }

        if (!killed) {
            stopPackages();
        }
        AndroidDevice.checkInteractive();
    }

    void stopPackages() {
        for (ComponentName cn : mMainApps) {
            stopPackage(cn.getPackageName());
        }
    }

    void stopPackage(String packageName) {
        if (AndroidDevice.stopPackage(packageName)) {
            Logger.iformat("Package %s has been stopped", packageName);
        } else {
            Logger.wformat("Package %s has NOT been stopped", packageName);
        }
    }

    protected void generateFuzzingEvents() {
        mFuzzer.generateFuzzingEvents(this);
    }

    protected void generateThrottleEvent(long base) {
        mEvents.generateThrottleEvent(this, base);
    }

    public boolean validate() {
        return mHandlerThread.isAlive();
    }

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

    public synchronized void requestClearPackage() {
        this.clearPackageOnGeneratingActivity = true;
    }


    public ComponentName randomlyPickMainApp() {
        int total = mMainApps.size();
        int index = mRandom.nextInt(total);
        ComponentName app = mMainApps.get(index);
        return app;
    }

    /**
     * generate an activity event
     */
    protected void generateActivityEvents(ComponentName app, boolean clearPackage) {
        int timestamp = this.mAgent.getTimestamp();
        boolean cleared = false;
        if (clearPackage) {
            clearPackage(app.getPackageName());
            cleared = true;
        } else {
            synchronized (this) {
                if (this.clearPackageOnGeneratingActivity) {
                    clearPackage(app.getPackageName());
                    cleared = true;
                    clearPackageOnGeneratingActivity = false;
                }
            }
        }
        if (timestamp == lastStartTimestamp && !cleared) {
            Logger.wformat("lastStartTimestamp [%d] is not updated. Try to clear package.", lastStartTimestamp);
            clearPackage(app.getPackageName());
            cleared = true;
        }
        if (Config.getBoolean("ape.forcePortrait", false)) {
            addEvent(new MonkeyRotationEvent(Surface.ROTATION_0, true));
            generateThrottleEvent(500);
        }
        MonkeyActivityEvent e = new MonkeyActivityEvent(app);
        addEvent(e);
        generateThrottleEvent(2000); // waiting for the loading of apps
        startEpisode("activity_start");
        Logger.iprintln("Let's wait for activity loading...");
        this.waitForActivity = true;
        this.waitForActivityRetryCount = 0;
        if (mEventCount == 0) {
            this.waitForActivityFromClean = true;  // TODO: assume that the first activity is cleared
        } else {
            this.waitForActivityFromClean = cleared;
        }
        this.lastStartTimestamp = timestamp;
    }

    protected void checkAppActivity() {
        ComponentName cn = getTopActivityComponentName();
        if (cn == null) {
            clearEvent();
            startRandomMainApp();
            return;
        }
        String pkg = cn.getPackageName();
        boolean allow = MonkeyUtils.getPackageFilter().isPackageValid(pkg);
        if (allow) {
            if (this.waitForActivity) {
                Logger.iformat("Expected activity package [%s] is loaded...", pkg);
                // needed.
                mAgent.onAppActivityStarted(cn, this.waitForActivityFromClean);
                this.waitForActivity = false; // we found the activity we
                this.waitForActivityFromClean = false;
            }
            return;
        }
        if (this.waitForActivity) {
            this.waitForActivityRetryCount++;
            if (this.waitForActivityRetryCount > WAIT_FOR_ACTIVITY_MAX_RETRIES) {
                Logger.wformat("Timed out waiting for target activity after %d retries (top=%s). Force restarting target app.",
                        this.waitForActivityRetryCount, pkg);
                this.waitForActivity = false;
                this.waitForActivityFromClean = false;
                mAgent.onActivityBlocked(cn);
                clearEvent();
                restartPackage(randomlyPickMainApp(), false, "activity load timeout");
                return;
            } else {
                Logger.iprintln("We are still waiting for activity loading. Let's wait for another 100ms...");
                generateThrottleEvent(100);
                return;
            }
        }
        if (cn.getPackageName().equals("com.android.systemui")
                && cn.getClassName().equals("com.android.systemui.recents.RecentsActivity")) {
            if (hasEvent()) {
                Logger.dformat("The top component [%s] belongs to systemui.", cn);
                return;
            }
        } else {
            Logger.dformat("The top activity package %s is not allowed.", pkg);
        }
        mAgent.onActivityBlocked(cn);
        clearEvent();
        startRandomMainApp();
        return;
    }

    private void doInput(Action action, GUITreeNode node) {
        mInput.doInput(this, action, node);
    }

    /**
     * if the queue is empty, we generate events first
     * 
     * @return the first event in the queue
     */
    public MonkeyEvent getNextEvent() {
        if (!hasEvent()) com.android.commands.monkey.ape.utils.ApeSnapshot.boundary(this);
        if (com.android.commands.monkey.ape.utils.ApeSnapshot.expired()) return null;
        checkAppActivity();
        if (!hasEvent()) {
            try {
                generateEvents();
            } catch (StopTestingException e) {
                clearEvent();
                return null;
            }
        }
        mEventCount++;
        return popEvent();
    }

    public Random getRandom() {
        return mRandom;
    }

    public int snapshotPendingEvents() { return mQ.size(); }

    int getVerbose() {
        return mVerbose;
    }

    boolean isRandomizeThrottle() {
        return mRandomizeThrottle;
    }

    public long getThrottle() {
        return this.mThrottle;
    }

    public Bitmap captureBitmap() {
        Bitmap map = mUiAutomation.takeScreenshot();
        return map;
    }
}
