Uiautomator1.0与Uiautomator2.0测试项目搭建与运行原理

Uiautomator是Android原生测试框架,可以用于白盒接口测试也可以用于UI自动化测试,Uiautomator分1.0版本与2.0版本,它们都是基于UiAutomation的测试框架,都是通过UiAutomation的接口操作设备, 1.0权限更高,运行更灵活,2.0针对app定制化更高,在同进程内运行,可以获取被测app的运行数据,具体选用哪个框架还是根据业务场景灵活选择。

本文介绍内容: 1、AndroidStudio基于ant编译Uiautomator1.0jar包 2、Uiautomator1.0的便捷构建 3、Uiautomator1.0执行流程解析 4、模拟Uiautomator实现测试服务

uiautomator1.0 uiautomator2.0
工程差异 基于Java的测试工程 基于app的测试框架,运行在目标app的进程内
构建差异 项目基于ant编译jar包,需要借助sdk/tools/工具生成build文件 AndroidStudio gradle进行管理,内置编译及执行任务
运行差异 通过uiautomator命令执行,测试脚本进程为shell进程,进程名为uiautomator 项目通过am命令启动执行,脚本进程为app进程,进程名为包名
权限差异 测试脚本拥有shell权限 与目标app具有相同权限
测试目标 可跨进程测试 可跨进程测试
运行条件 shell执行,可独立运行 shell执行,必须安装脚本应用和被测应用
数据 无法获取被测应用内运行数据 非跨进程测试情况下,可以获取被测app的运行数据

一、AndroidStudio基于ant编译Uiautomator1.0 jar包

uiautomator1.0是基于Java的测试框架,它通过junit和uiautomator框架执行Java测试用例,但是自从uiautomator2.0发布以后,官方不再维护uiautomator1.0的构建。目前无法直接通过Android Studio创建uiautomator1.0测试项目,本节介绍通过AndroidStudio工具构建Uiautomator1.0 jar文件。

构建项目工程

1、AndroidStudio新构建一普通项目 2、导入依赖包

dependencies {
    //uiautomator & junit测试框架,系统内部已集成,不需打包到程序。
    compileOnly 'uiautomator:uiautomator:1.0'
    compileOnly 'junit:junit:3.8'
}

3、创建测试类

package com.android.demo.uiautomator1;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
public class MainTest extends UiAutomatorTestCase {
    public void testMain(){
        System.out.println("=======testMain=======>>>");
    }
    public void testMain2(){
        System.out.println("=======testMain2=======>>>");
    }
}

SDK支持ant编译配置

Android最新SDK已去除ant编译工具支持,如果需要通过ant构建uiautomator1.0测试包那么就需要下载以前的sdk-tools,并将该文件复制到本机sdk中。

各平台下载地址: Windows sdkTools Mac sdkTools Linux sdkTools

根据系统版本下载对应的sdkTools,下载后解压获取tools文件夹,并将tools/文件夹下所有内容复制到本机/sdk/tools/路径下。

根据项目生成build.xml

1、打开控制台,切换到/sdk/tools/路径下 2、执行命令

//projectPath 为项目的绝对路径
//uiautomatorTest为项目名,打包后的jar包名字
 ./android create uitest-project -n uiautomatorTest -t "android-29" -p projectPath

3、打开项目,在项目路径下已生成 [build.xml]文件,打开[build.xml]文件,找到如下代码,并注视。

<!--    <property file="local.properties" />-->

AndroidStudio 导入ant任务,并构建

1、打开当前module的Gradle文件,写入如下代码,然后构建一下gradle,在moduleName->Tasks->other下会列出ant任务

ant.importBuild('build.xml') {
    antTargetName ->
        'ant-' + antTargetName
}

2、点击[ant-build]即可执行构建任务,等待执行结束后在[module/bin]路径下会生成对应的Apk文件。

⚠️ 如果构建过程中报 check-env 错误,是因为sdk-tools 不支持ant编译导致的,需要同步一下sdk-tools

执行测试用例

adb push jarFile /data/local/tmp/

jarFile=data/local/tmp/test.jar
mainClass=com.uiautomator.test.MainTest
uiautomator runtest jarFile -c mainClass

二、Uiautomator1.0 的便捷构建

基于ant的Uiautomator1.0 jar包构建是比较麻烦的,并且sdk-tools必须要支持ant编译。那么jar包的组成部分是什么?

通过解压Uiautomator1.0 jar文件,发现jar包主要由.dex文件和META-INF构成,Android 应用apk文件内也包含着两个文件,所以是可以把Uiautomator1.0源码写在普通Android项目中,并用.apk文件替换.jar文件执行测试脚本。

jar包文件构成 Uiautomator1.0与Uiautomator2.0测试项目搭建与运行原理

apk文件构成 Uiautomator1.0与Uiautomator2.0测试项目搭建与运行原理

构建apk

执行下图Gradle任务,获取打包apk文件 Uiautomator1.0与Uiautomator2.0测试项目搭建与运行原理

任务执行完毕可以得到apk打包文件 Uiautomator1.0与Uiautomator2.0测试项目搭建与运行原理

执行脚本

//将构建的apk文件推送到设备中
adb push apkFile /data/local/tmp/test.apk

//定义脚本执行文件和入口类
jarFile=data/local/tmp/test.apk
mainClass=com.uiautomator.test.MainTest

//通过命令执行测试脚本
uiautomator runtest jarFile -c mainClass

三、Uiautomator1.0执行流程解析

下面通过uiautomator 命令为入口了解uiautomator1.0测试服务是如何运行的,uiautomator进程是如何初始化的。

Uiautomator1.0 执行命令结构如下:

uiautomator runtest jarFile -c mainClass

uiautomator脚本源码则是一个shell脚本,通过 app_process命令加载类com.android.commands.uiautomator.Launcher

CLASSPATH=${CLASSPATH}:${jars}
export CLASSPATH
exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}

Launcher作为uiautomator进程的入口,解析参数并调用RunTestCommand的run方法

   public class Launcher {
    //uiautomator进程真正执行入口
    public static void main(String[] args) {
        Process.setArgV0("uiautomator");//ps查看的进程名
        if (args.length >= 1) {
            //args[0] 对应的命令行参数为 “runtest”,查找名为“runtest”的服务est“
            Command command = findCommand(args[0]);
            if (command != null) {
                String[] args2 = {};
                if (args.length > 1) {
                    // consume the first arg
                    args2 = Arrays.copyOfRange(args, 1, args.length);
                }
                command.run(args2);
                return;
            }
        }
        HELP_COMMAND.run(args);
    }

    private static Command findCommand(String name) {
        for (Command command : COMMANDS) {
            if (command.name().equals(name)) {
                return command;
            }
        }
        return null;
    }
    //测试服务集合
    private static Command[] COMMANDS = new Command[] {
        HELP_COMMAND,
        //RunTestCommand命名为“runt
        new RunTestCommand(),
        new DumpCommand(),
        new EventsCommand(),
    };
}
    

RunTestCommand.run()方法将具体的执行业务递交给UiAutomatorTestRunner类的run方法执行

   package com.android.commands.uiautomator;

public class RunTestCommand extends Command {
    public RunTestCommand() {
        //设置name
        super("runtest");
    }
    //被调用入口
    @Override
    public void run(String[] args) {
        int ret = parseArgs(args);
        ...
        if (mTestClasses.isEmpty()) {
            addTestClassesFromJars();
            if (mTestClasses.isEmpty()) {
                System.err.println("No test classes found.");
                System.exit(ARG_FAIL_NO_CLASS);
            }
        }
        //调用UiAutomatorTestRunner类的run方法
        getRunner().run(mTestClasses, mParams, mDebug, mMonkey);
    }
    protected UiAutomatorTestRunner getRunner() {
        if (mRunner != null) {
            return mRunner;
        }
        if (mRunnerClassName == null) {
            mRunner = new UiAutomatorTestRunner();
            return mRunner;
        }
        ...
        // won't reach here
        return null;
    }
}

在UiAutomatorTestRunner的run方法中初始化执行参数,然后调用start方法中初始化UiAutomationShellWrapper并执行测试任务。UiAutomationShellWrapper的connect方法连接系统测试服务。

 
package com.android.uiautomator.testrunner;

/**
 * @hide
 */
public class UiAutomatorTestRunner {
    public void run(List<String> testClasses, Bundle params, boolean debug, boolean monkey) {
        //全局异常捕获
        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {...});
        mTestClasses = testClasses;
        mParams = params;
        mDebug = debug;
        mMonkey = monkey;
        start();
        System.exit(EXIT_OK);
    }
    protected void start() {
        ...
        //创建Handler线程对象
        mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
        mHandlerThread.setDaemon(true);
        mHandlerThread.start();
        //创建UiAutomationShellWrapper对象
        UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
        //创建测试连接,测试服务初始化完毕
        automationWrapper.connect();
        ...
        try {
            ...
            //执行测试用例
            // run tests for realz!
            for (TestCase testCase : testCases) {
                prepareTestCase(testCase);
                testCase.run(testRunResult);
            }
        } catch (Throwable t) {...
        } finally { ...
        }
    }
}

UiAutomationShellWrapper 类的connect方法初始化了UiAutomation对象,UiAutomation提供测试所需功能API。

public class UiAutomationShellWrapper {
    private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread";
    //构建HandlerThrea对象
    private final HandlerThread mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
    private UiAutomation mUiAutomation;
    public void connect() {
        if (mHandlerThread.isAlive()) {
            throw new IllegalStateException("Already connected!");
        }
        //调用start方法启动线程
        mHandlerThread.start();
        //创建UiAutomation对象
        mUiAutomation = new UiAutomation(mHandlerThread.getLooper(),
                new UiAutomationConnection());
        //创建UiAutomation测试连接
        mUiAutomation.connect();
    }
    //断开连接
    public void disconnect() {
        if (!mHandlerThread.isAlive()) {
            throw new IllegalStateException("Already disconnected!");
        }
        mUiAutomation.disconnect();
        mHandlerThread.quit();
    }
    public UiAutomation getUiAutomation() {
        return mUiAutomation;
    }
}

到此uiautomator进程已经获取UiAutomation对象,可以通过UiAutomation对象获取屏幕元素信息、发送按键事件、滑动屏幕、点击屏幕等。以下代码展示部分重要API,无论是Uiautomator1.0还是Uiautomator2.0最终都是通过UiAutomation对象来执行测试动作的。

public final class UiAutomation {
    private static final String LOG_TAG = UiAutomation.class.getSimpleName();
    //Listener for observing the {@link AccessibilityEvent} stream.
    public static interface OnAccessibilityEventListener {
        public void onAccessibilityEvent(AccessibilityEvent event);
    }
   
    public UiAutomation(Looper looper, IUiAutomationConnection connection) {
        ...
        mLocalCallbackHandler = new Handler(looper);
        mUiAutomationConnection = connection;
    }
    public void connect() {
        connect(0);
    }
    public void connect(int flags) {
        synchronized (mLock) {
            ...
            mClient = new IAccessibilityServiceClientImpl(mRemoteCallbackThread.getLooper());
        }
        try {
            // 连接测试服务,关联IAccessibilityServiceClientImpl,UI变化会回调到mClient接口
            mUiAutomationConnection.connect(mClient, flags);
            mFlags = flags;
        } catch (RemoteException re) {
            throw new RuntimeException("Error while connecting UiAutomation", re);
        }
        ...
    }
    public void disconnect() {
        ...
            //断开测试连接
            mUiAutomationConnection.disconnect();
       ...
    }
    /**
     * Get the flags used to connect the service.
     *
     * @return The flags used to connect
     *
     * @hide
     */
    public int getFlags() {
        return mFlags;
    }

   

    /**
     * The id of the {@link IAccessibilityInteractionConnection} for querying
     * the screen content. This is here for legacy purposes since some tools use
     * hidden APIs to introspect the screen.
     *
     * @hide
     */
    public int getConnectionId() {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
            return mConnectionId;
        }
    }

    /**
     * Reports if the object has been destroyed
     *
     * @return {code true} if the object has been destroyed.
     *
     * @hide
     */
    public boolean isDestroyed() {
        return mIsDestroyed;
    }

    /**
     * Sets a callback for observing the stream of {@link AccessibilityEvent}s.
     * The callbacks are delivered on the main application thread.
     *
     * @param listener The callback.
     */
    public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) {
        synchronized (mLock) {
            mOnAccessibilityEventListener = listener;
        }
    }

    /**
     * Destroy this UiAutomation. After calling this method, attempting to use the object will
     * result in errors.
     *
     * @hide
     */
    @TestApi
    public void destroy() {
        disconnect();
        mIsDestroyed = true;
    }

    /**
     * Adopt the permission identity of the shell UID. This allows you to call APIs protected
     * permissions which normal apps cannot hold but are granted to the shell UID. If you
     * already adopted the shell permission identity this method would be a no-op.
     * Note that your permission state becomes that of the shell UID and it is not a
     * combination of your and the shell UID permissions.
     *
     * @see #dropShellPermissionIdentity()
     */
    public void adoptShellPermissionIdentity() {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            // Calling out without a lock held.
            mUiAutomationConnection.adoptShellPermissionIdentity(Process.myUid());
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error executing adopting shell permission identity!", re);
        }
    }

    /**
     * Drop the shell permission identity adopted by a previous call to
     * {@link #adoptShellPermissionIdentity()}. If you did not adopt the shell permission
     * identity this method would be a no-op.
     *
     * @see #adoptShellPermissionIdentity()
     */
    public void dropShellPermissionIdentity() {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            // Calling out without a lock held.
            mUiAutomationConnection.dropShellPermissionIdentity();
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error executing dropping shell permission identity!", re);
        }
    }

    /**
     * Performs a global action. Such an action can be performed at any moment
     * regardless of the current application or user location in that application.
     * For example going back, going home, opening recents, etc.
     *
     * @param action The action to perform.
     * @return Whether the action was successfully performed.
     *
     * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_BACK
     * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_HOME
     * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS
     * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_RECENTS
     */
    public final boolean performGlobalAction(int action) {
        final IAccessibilityServiceConnection connection;
        synchronized (mLock) {
            throwIfNotConnectedLocked();
            connection = AccessibilityInteractionClient.getInstance()
                    .getConnection(mConnectionId);
        }
        // Calling out without a lock held.
        if (connection != null) {
            try {
                return connection.performGlobalAction(action);
            } catch (RemoteException re) {
                Log.w(LOG_TAG, "Error while calling performGlobalAction", re);
            }
        }
        return false;
    }

    /**
     * Find the view that has the specified focus type. The search is performed
     * across all windows.
     * <p>
     * <strong>Note:</strong> In order to access the windows you have to opt-in
     * to retrieve the interactive windows by setting the
     * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
     * Otherwise, the search will be performed only in the active window.
     * </p>
     *
     * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or
     *         {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}.
     * @return The node info of the focused view or null.
     *
     * @see AccessibilityNodeInfo#FOCUS_INPUT
     * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY
     */
    public AccessibilityNodeInfo findFocus(int focus) {
        return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId,
                AccessibilityWindowInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, focus);
    }

    /**
     * Gets the an {@link AccessibilityServiceInfo} describing this UiAutomation.
     * This method is useful if one wants to change some of the dynamically
     * configurable properties at runtime.
     *
     * @return The accessibility service info.
     *
     * @see AccessibilityServiceInfo
     */
    public final AccessibilityServiceInfo getServiceInfo() {
        final IAccessibilityServiceConnection connection;
        synchronized (mLock) {
            throwIfNotConnectedLocked();
            connection = AccessibilityInteractionClient.getInstance()
                    .getConnection(mConnectionId);
        }
        // Calling out without a lock held.
        if (connection != null) {
            try {
                return connection.getServiceInfo();
            } catch (RemoteException re) {
                Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re);
            }
        }
        return null;
    }

    /**
     * Sets the {@link AccessibilityServiceInfo} that describes how this
     * UiAutomation will be handled by the platform accessibility layer.
     *
     * @param info The info.
     *
     * @see AccessibilityServiceInfo
     */
    public final void setServiceInfo(AccessibilityServiceInfo info) {
        final IAccessibilityServiceConnection connection;
        synchronized (mLock) {
            throwIfNotConnectedLocked();
            AccessibilityInteractionClient.getInstance().clearCache();
            connection = AccessibilityInteractionClient.getInstance()
                    .getConnection(mConnectionId);
        }
        // Calling out without a lock held.
        if (connection != null) {
            try {
                connection.setServiceInfo(info);
            } catch (RemoteException re) {
                Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re);
            }
        }
    }

    /**
     * Gets the windows on the screen. This method returns only the windows
     * that a sighted user can interact with, as opposed to all windows.
     * For example, if there is a modal dialog shown and the user cannot touch
     * anything behind it, then only the modal window will be reported
     * (assuming it is the top one). For convenience the returned windows
     * are ordered in a descending layer order, which is the windows that
     * are higher in the Z-order are reported first.
     * <p>
     * <strong>Note:</strong> In order to access the windows you have to opt-in
     * to retrieve the interactive windows by setting the
     * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
     * </p>
     *
     * @return The windows if there are windows such, otherwise an empty list.
     */
    public List<AccessibilityWindowInfo> getWindows() {
        final int connectionId;
        synchronized (mLock) {
            throwIfNotConnectedLocked();
            connectionId = mConnectionId;
        }
        // Calling out without a lock held.
        return AccessibilityInteractionClient.getInstance()
                .getWindows(connectionId);
    }

    /**
     * Gets the root {@link AccessibilityNodeInfo} in the active window.
     *
     * @return The root info.
     */
    public AccessibilityNodeInfo getRootInActiveWindow() {
        final int connectionId;
        synchronized (mLock) {
            throwIfNotConnectedLocked();
            connectionId = mConnectionId;
        }
        // Calling out without a lock held.
        return AccessibilityInteractionClient.getInstance()
                .getRootInActiveWindow(connectionId);
    }

    /**
     * A method for injecting an arbitrary input event.
     * <p>
     * <strong>Note:</strong> It is caller's responsibility to recycle the event.
     * </p>
     * @param event The event to inject.
     * @param sync Whether to inject the event synchronously.
     * @return Whether event injection succeeded.
     */
    public boolean injectInputEvent(InputEvent event, boolean sync) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            if (DEBUG) {
                Log.i(LOG_TAG, "Injecting: " + event + " sync: " + sync);
            }
            // Calling out without a lock held.
            return mUiAutomationConnection.injectInputEvent(event, sync);
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error while injecting input event!", re);
        }
        return false;
    }

    /**
     * Sets the device rotation. A client can freeze the rotation in
     * desired state or freeze the rotation to its current state or
     * unfreeze the rotation (rotating the device changes its rotation
     * state).
     *
     * @param rotation The desired rotation.
     * @return Whether the rotation was set successfully.
     *
     * @see #ROTATION_FREEZE_0
     * @see #ROTATION_FREEZE_90
     * @see #ROTATION_FREEZE_180
     * @see #ROTATION_FREEZE_270
     * @see #ROTATION_FREEZE_CURRENT
     * @see #ROTATION_UNFREEZE
     */
    public boolean setRotation(int rotation) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        switch (rotation) {
            case ROTATION_FREEZE_0:
            case ROTATION_FREEZE_90:
            case ROTATION_FREEZE_180:
            case ROTATION_FREEZE_270:
            case ROTATION_UNFREEZE:
            case ROTATION_FREEZE_CURRENT: {
                try {
                    // Calling out without a lock held.
                    mUiAutomationConnection.setRotation(rotation);
                    return true;
                } catch (RemoteException re) {
                    Log.e(LOG_TAG, "Error while setting rotation!", re);
                }
            } return false;
            default: {
                throw new IllegalArgumentException("Invalid rotation.");
            }
        }
    }

    /**
     * Executes a command and waits for a specific accessibility event up to a
     * given wait timeout. To detect a sequence of events one can implement a
     * filter that keeps track of seen events of the expected sequence and
     * returns true after the last event of that sequence is received.
     * <p>
     * <strong>Note:</strong> It is caller's responsibility to recycle the returned event.
     * </p>
     * @param command The command to execute.
     * @param filter Filter that recognizes the expected event.
     * @param timeoutMillis The wait timeout in milliseconds.
     *
     * @throws TimeoutException If the expected event is not received within the timeout.
     */
    public AccessibilityEvent executeAndWaitForEvent(Runnable command,
            AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException {
        // Acquire the lock and prepare for receiving events.
        synchronized (mLock) {
            throwIfNotConnectedLocked();
            mEventQueue.clear();
            // Prepare to wait for an event.
            mWaitingForEventDelivery = true;
        }

        // Note: We have to release the lock since calling out with this lock held
        // can bite. We will correctly filter out events from other interactions,
        // so starting to collect events before running the action is just fine.

        // We will ignore events from previous interactions.
        final long executionStartTimeMillis = SystemClock.uptimeMillis();
        // Execute the command *without* the lock being held.
        command.run();

        List<AccessibilityEvent> receivedEvents = new ArrayList<>();

        // Acquire the lock and wait for the event.
        try {
            // Wait for the event.
            final long startTimeMillis = SystemClock.uptimeMillis();
            while (true) {
                List<AccessibilityEvent> localEvents = new ArrayList<>();
                synchronized (mLock) {
                    localEvents.addAll(mEventQueue);
                    mEventQueue.clear();
                }
                // Drain the event queue
                while (!localEvents.isEmpty()) {
                    AccessibilityEvent event = localEvents.remove(0);
                    // Ignore events from previous interactions.
                    if (event.getEventTime() < executionStartTimeMillis) {
                        continue;
                    }
                    if (filter.accept(event)) {
                        return event;
                    }
                    receivedEvents.add(event);
                }
                // Check if timed out and if not wait.
                final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
                final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
                if (remainingTimeMillis <= 0) {
                    throw new TimeoutException("Expected event not received within: "
                            + timeoutMillis + " ms among: " + receivedEvents);
                }
                synchronized (mLock) {
                    if (mEventQueue.isEmpty()) {
                        try {
                            mLock.wait(remainingTimeMillis);
                        } catch (InterruptedException ie) {
                            /* ignore */
                        }
                    }
                }
            }
        } finally {
            int size = receivedEvents.size();
            for (int i = 0; i < size; i++) {
                receivedEvents.get(i).recycle();
            }

            synchronized (mLock) {
                mWaitingForEventDelivery = false;
                mEventQueue.clear();
                mLock.notifyAll();
            }
        }
    }

    /**
     * Waits for the accessibility event stream to become idle, which is not to
     * have received an accessibility event within <code>idleTimeoutMillis</code>.
     * The total time spent to wait for an idle accessibility event stream is bounded
     * by the <code>globalTimeoutMillis</code>.
     *
     * @param idleTimeoutMillis The timeout in milliseconds between two events
     *            to consider the device idle.
     * @param globalTimeoutMillis The maximal global timeout in milliseconds in
     *            which to wait for an idle state.
     *
     * @throws TimeoutException If no idle state was detected within
     *            <code>globalTimeoutMillis.</code>
     */
    public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis)
            throws TimeoutException {
        synchronized (mLock) {
            throwIfNotConnectedLocked();

            final long startTimeMillis = SystemClock.uptimeMillis();
            if (mLastEventTimeMillis <= 0) {
                mLastEventTimeMillis = startTimeMillis;
            }

            while (true) {
                final long currentTimeMillis = SystemClock.uptimeMillis();
                // Did we get idle state within the global timeout?
                final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis;
                final long remainingGlobalTimeMillis =
                        globalTimeoutMillis - elapsedGlobalTimeMillis;
                if (remainingGlobalTimeMillis <= 0) {
                    throw new TimeoutException("No idle state with idle timeout: "
                            + idleTimeoutMillis + " within global timeout: "
                            + globalTimeoutMillis);
                }
                // Did we get an idle state within the idle timeout?
                final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis;
                final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis;
                if (remainingIdleTimeMillis <= 0) {
                    return;
                }
                try {
                     mLock.wait(remainingIdleTimeMillis);
                } catch (InterruptedException ie) {
                     /* ignore */
                }
            }
        }
    }

    /**
     * Takes a screenshot.
     *
     * @return The screenshot bitmap on success, null otherwise.
     */
    public Bitmap takeScreenshot() {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        Display display = DisplayManagerGlobal.getInstance()
                .getRealDisplay(Display.DEFAULT_DISPLAY);
        Point displaySize = new Point();
        display.getRealSize(displaySize);

        int rotation = display.getRotation();

        // Take the screenshot
        Bitmap screenShot = null;
        try {
            // Calling out without a lock held.
            screenShot = mUiAutomationConnection.takeScreenshot(
                    new Rect(0, 0, displaySize.x, displaySize.y), rotation);
            if (screenShot == null) {
                return null;
            }
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error while taking screnshot!", re);
            return null;
        }

        // Optimization
        screenShot.setHasAlpha(false);

        return screenShot;
    }

    /**
     * Sets whether this UiAutomation to run in a "monkey" mode. Applications can query whether
     * they are executed in a "monkey" mode, i.e. run by a test framework, and avoid doing
     * potentially undesirable actions such as calling 911 or posting on public forums etc.
     *
     * @param enable whether to run in a "monkey" mode or not. Default is not.
     * @see ActivityManager#isUserAMonkey()
     */
    public void setRunAsMonkey(boolean enable) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            ActivityManager.getService().setUserIsMonkey(enable);
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error while setting run as monkey!", re);
        }
    }

    /**
     * Clears the frame statistics for the content of a given window. These
     * statistics contain information about the most recently rendered content
     * frames.
     *
     * @param windowId The window id.
     * @return Whether the window is present and its frame statistics
     *         were cleared.
     *
     * @see android.view.WindowContentFrameStats
     * @see #getWindowContentFrameStats(int)
     * @see #getWindows()
     * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId()
     */
    public boolean clearWindowContentFrameStats(int windowId) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            if (DEBUG) {
                Log.i(LOG_TAG, "Clearing content frame stats for window: " + windowId);
            }
            // Calling out without a lock held.
            return mUiAutomationConnection.clearWindowContentFrameStats(windowId);
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error clearing window content frame stats!", re);
        }
        return false;
    }

    /**
     * Gets the frame statistics for a given window. These statistics contain
     * information about the most recently rendered content frames.
     * <p>
     * A typical usage requires clearing the window frame statistics via {@link
     * #clearWindowContentFrameStats(int)} followed by an interaction with the UI and
     * finally getting the window frame statistics via calling this method.
     * </p>
     * <pre>
     * // Assume we have at least one window.
     * final int windowId = getWindows().get(0).getId();
     *
     * // Start with a clean slate.
     * uiAutimation.clearWindowContentFrameStats(windowId);
     *
     * // Do stuff with the UI.
     *
     * // Get the frame statistics.
     * WindowContentFrameStats stats = uiAutomation.getWindowContentFrameStats(windowId);
     * </pre>
     *
     * @param windowId The window id.
     * @return The window frame statistics, or null if the window is not present.
     *
     * @see android.view.WindowContentFrameStats
     * @see #clearWindowContentFrameStats(int)
     * @see #getWindows()
     * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId()
     */
    public WindowContentFrameStats getWindowContentFrameStats(int windowId) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            if (DEBUG) {
                Log.i(LOG_TAG, "Getting content frame stats for window: " + windowId);
            }
            // Calling out without a lock held.
            return mUiAutomationConnection.getWindowContentFrameStats(windowId);
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error getting window content frame stats!", re);
        }
        return null;
    }

    /**
     * Clears the window animation rendering statistics. These statistics contain
     * information about the most recently rendered window animation frames, i.e.
     * for window transition animations.
     *
     * @see android.view.WindowAnimationFrameStats
     * @see #getWindowAnimationFrameStats()
     * @see android.R.styleable#WindowAnimation
     */
    public void clearWindowAnimationFrameStats() {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            if (DEBUG) {
                Log.i(LOG_TAG, "Clearing window animation frame stats");
            }
            // Calling out without a lock held.
            mUiAutomationConnection.clearWindowAnimationFrameStats();
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error clearing window animation frame stats!", re);
        }
    }

    /**
     * Gets the window animation frame statistics. These statistics contain
     * information about the most recently rendered window animation frames, i.e.
     * for window transition animations.
     *
     * <p>
     * A typical usage requires clearing the window animation frame statistics via
     * {@link #clearWindowAnimationFrameStats()} followed by an interaction that causes
     * a window transition which uses a window animation and finally getting the window
     * animation frame statistics by calling this method.
     * </p>
     * <pre>
     * // Start with a clean slate.
     * uiAutimation.clearWindowAnimationFrameStats();
     *
     * // Do stuff to trigger a window transition.
     *
     * // Get the frame statistics.
     * WindowAnimationFrameStats stats = uiAutomation.getWindowAnimationFrameStats();
     * </pre>
     *
     * @return The window animation frame statistics.
     *
     * @see android.view.WindowAnimationFrameStats
     * @see #clearWindowAnimationFrameStats()
     * @see android.R.styleable#WindowAnimation
     */
    public WindowAnimationFrameStats getWindowAnimationFrameStats() {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            if (DEBUG) {
                Log.i(LOG_TAG, "Getting window animation frame stats");
            }
            // Calling out without a lock held.
            return mUiAutomationConnection.getWindowAnimationFrameStats();
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error getting window animation frame stats!", re);
        }
        return null;
    }

    /**
     * Grants a runtime permission to a package.
     * @param packageName The package to which to grant.
     * @param permission The permission to grant.
     * @throws SecurityException if unable to grant the permission.
     */
    public void grantRuntimePermission(String packageName, String permission) {
        grantRuntimePermissionAsUser(packageName, permission, android.os.Process.myUserHandle());
    }

    /**
     * @deprecated replaced by
     *             {@link #grantRuntimePermissionAsUser(String, String, UserHandle)}.
     * @hide
     */
    @Deprecated
    @TestApi
    public boolean grantRuntimePermission(String packageName, String permission,
            UserHandle userHandle) {
        grantRuntimePermissionAsUser(packageName, permission, userHandle);
        return true;
    }

    /**
     * Grants a runtime permission to a package for a user.
     * @param packageName The package to which to grant.
     * @param permission The permission to grant.
     * @throws SecurityException if unable to grant the permission.
     */
    public void grantRuntimePermissionAsUser(String packageName, String permission,
            UserHandle userHandle) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            if (DEBUG) {
                Log.i(LOG_TAG, "Granting runtime permission");
            }
            // Calling out without a lock held.
            mUiAutomationConnection.grantRuntimePermission(packageName,
                    permission, userHandle.getIdentifier());
        } catch (Exception e) {
            throw new SecurityException("Error granting runtime permission", e);
        }
    }

    /**
     * Revokes a runtime permission from a package.
     * @param packageName The package to which to grant.
     * @param permission The permission to grant.
     * @throws SecurityException if unable to revoke the permission.
     */
    public void revokeRuntimePermission(String packageName, String permission) {
        revokeRuntimePermissionAsUser(packageName, permission, android.os.Process.myUserHandle());
    }

    /**
     * @deprecated replaced by
     *             {@link #revokeRuntimePermissionAsUser(String, String, UserHandle)}.
     * @hide
     */
    @Deprecated
    @TestApi
    public boolean revokeRuntimePermission(String packageName, String permission,
            UserHandle userHandle) {
        revokeRuntimePermissionAsUser(packageName, permission, userHandle);
        return true;
    }

    /**
     * Revokes a runtime permission from a package.
     * @param packageName The package to which to grant.
     * @param permission The permission to grant.
     * @throws SecurityException if unable to revoke the permission.
     */
    public void revokeRuntimePermissionAsUser(String packageName, String permission,
            UserHandle userHandle) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        try {
            if (DEBUG) {
                Log.i(LOG_TAG, "Revoking runtime permission");
            }
            // Calling out without a lock held.
            mUiAutomationConnection.revokeRuntimePermission(packageName,
                    permission, userHandle.getIdentifier());
        } catch (Exception e) {
            throw new SecurityException("Error granting runtime permission", e);
        }
    }

    /**
     * Executes a shell command. This method returns a file descriptor that points
     * to the standard output stream. The command execution is similar to running
     * "adb shell <command>" from a host connected to the device.
     * <p>
     * <strong>Note:</strong> It is your responsibility to close the returned file
     * descriptor once you are done reading.
     * </p>
     *
     * @param command The command to execute.
     * @return A file descriptor to the standard output stream.
     *
     * @see #adoptShellPermissionIdentity()
     */
    public ParcelFileDescriptor executeShellCommand(String command) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        warnIfBetterCommand(command);

        ParcelFileDescriptor source = null;
        ParcelFileDescriptor sink = null;

        try {
            ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
            source = pipe[0];
            sink = pipe[1];

            // Calling out without a lock held.
            mUiAutomationConnection.executeShellCommand(command, sink, null);
        } catch (IOException ioe) {
            Log.e(LOG_TAG, "Error executing shell command!", ioe);
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error executing shell command!", re);
        } finally {
            IoUtils.closeQuietly(sink);
        }

        return source;
    }

    /**
     * Executes a shell command. This method returns two file descriptors,
     * one that points to the standard output stream (element at index 0), and one that points
     * to the standard input stream (element at index 1). The command execution is similar
     * to running "adb shell <command>" from a host connected to the device.
     * <p>
     * <strong>Note:</strong> It is your responsibility to close the returned file
     * descriptors once you are done reading/writing.
     * </p>
     *
     * @param command The command to execute.
     * @return File descriptors (out, in) to the standard output/input streams.
     *
     * @hide
     */
    @TestApi
    public ParcelFileDescriptor[] executeShellCommandRw(String command) {
        synchronized (mLock) {
            throwIfNotConnectedLocked();
        }
        warnIfBetterCommand(command);

        ParcelFileDescriptor source_read = null;
        ParcelFileDescriptor sink_read = null;

        ParcelFileDescriptor source_write = null;
        ParcelFileDescriptor sink_write = null;

        try {
            ParcelFileDescriptor[] pipe_read = ParcelFileDescriptor.createPipe();
            source_read = pipe_read[0];
            sink_read = pipe_read[1];

            ParcelFileDescriptor[] pipe_write = ParcelFileDescriptor.createPipe();
            source_write = pipe_write[0];
            sink_write = pipe_write[1];

            // Calling out without a lock held.
            mUiAutomationConnection.executeShellCommand(command, sink_read, source_write);
        } catch (IOException ioe) {
            Log.e(LOG_TAG, "Error executing shell command!", ioe);
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error executing shell command!", re);
        } finally {
            IoUtils.closeQuietly(sink_read);
            IoUtils.closeQuietly(source_write);
        }

        ParcelFileDescriptor[] result = new ParcelFileDescriptor[2];
        result[0] = source_read;
        result[1] = sink_write;
        return result;
    }

    private boolean isConnectedLocked() {
        return mConnectionId != CONNECTION_ID_UNDEFINED;
    }

    private void throwIfConnectedLocked() {
        if (mConnectionId != CONNECTION_ID_UNDEFINED) {
            throw new IllegalStateException("UiAutomation not connected!");
        }
    }

    private void throwIfNotConnectedLocked() {
        if (!isConnectedLocked()) {
            throw new IllegalStateException("UiAutomation not connected!");
        }
    }

    private void warnIfBetterCommand(String cmd) {
        if (cmd.startsWith("pm grant ")) {
            Log.w(LOG_TAG, "UiAutomation.grantRuntimePermission() "
                    + "is more robust and should be used instead of 'pm grant'");
        } else if (cmd.startsWith("pm revoke ")) {
            Log.w(LOG_TAG, "UiAutomation.revokeRuntimePermission() "
                    + "is more robust and should be used instead of 'pm revoke'");
        }
    }

    private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper {

        public IAccessibilityServiceClientImpl(Looper looper) {
            super(null, looper, new Callbacks() {
                @Override
                public void init(int connectionId, IBinder windowToken) {
                    synchronized (mLock) {
                        mConnectionId = connectionId;
                        mLock.notifyAll();
                    }
                }

                @Override
                public void onServiceConnected() {
                    /* do nothing */
                }

                @Override
                public void onInterrupt() {
                    /* do nothing */
                }

                @Override
                public boolean onGesture(int gestureId) {
                    /* do nothing */
                    return false;
                }

                @Override
                public void onAccessibilityEvent(AccessibilityEvent event) {
                    final OnAccessibilityEventListener listener;
                    synchronized (mLock) {
                        mLastEventTimeMillis = event.getEventTime();
                        if (mWaitingForEventDelivery) {
                            mEventQueue.add(AccessibilityEvent.obtain(event));
                        }
                        mLock.notifyAll();
                        listener = mOnAccessibilityEventListener;
                    }
                    if (listener != null) {
                        // Calling out only without a lock held.
                        mLocalCallbackHandler.post(PooledLambda.obtainRunnable(
                                OnAccessibilityEventListener::onAccessibilityEvent,
                                listener, AccessibilityEvent.obtain(event))
                                .recycleOnUse());
                    }
                }

                @Override
                public boolean onKeyEvent(KeyEvent event) {
                    return false;
                }

                @Override
                public void onMagnificationChanged(@NonNull Region region,
                        float scale, float centerX, float centerY) {
                    /* do nothing */
                }

                @Override
                public void onSoftKeyboardShowModeChanged(int showMode) {
                    /* do nothing */
                }

                @Override
                public void onPerformGestureResult(int sequence, boolean completedSuccessfully) {
                    /* do nothing */
                }

                @Override
                public void onFingerprintCapturingGesturesChanged(boolean active) {
                    /* do nothing */
                }

                @Override
                public void onFingerprintGesture(int gesture) {
                    /* do nothing */
                }

                @Override
                public void onAccessibilityButtonClicked() {
                    /* do nothing */
                }

                @Override
                public void onAccessibilityButtonAvailabilityChanged(boolean available) {
                    /* do nothing */
                }
            });
        }
    }
}
package android.app;

public final class UiAutomation {
    /**
     * 构造方法,在
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public UiAutomation(Looper looper, IUiAutomationConnection connection) {
        if (looper == null) {
            throw new IllegalArgumentException("Looper cannot be null!");
        }
        if (connection == null) {
            throw new IllegalArgumentException("Connection cannot be null!");
        }
        mLocalCallbackHandler = new Handler(looper);
        mUiAutomationConnection = connection;
    }

    /**
     * 连接测试服务
     * @hide
     */
    public void connect() {connect(0);}

    /**
     * 连接测试服务
     * @hide
     */
    public void connect(int flags) {
        ...
    }

    /**
     * 断开测试连接
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public void disconnect() {
        ...
    }


    /**
     * 设置Accessibility辅助功能监听,可以在listener中获取屏幕变化事件
     * @param listener The callback.
     */
    public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) {
        ...
    }

    /**
     * 获取当前屏幕获取焦点的控件信息
     * @see AccessibilityNodeInfo#FOCUS_INPUT
     * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY
     */
    public AccessibilityNodeInfo findFocus(int focus) {
        ...
    }

    /**
     * 获取当前屏幕展示信息,仅获取可见控件信息
     */
    public AccessibilityNodeInfo getRootInActiveWindow() {
        ...
    }

    /**
     * A method for injecting an arbitrary input event.
     * 可以用来发送按键、屏幕事件(按下、滑动、抬起)
     */
    public boolean injectInputEvent(InputEvent event, boolean sync) {
        ...
    }

    /**
     * 设置屏幕方向
     * @param rotation The desired rotation.
     * @return Whether the rotation was set successfully.
     *
     * @see #ROTATION_FREEZE_0
     * @see #ROTATION_FREEZE_90
     * @see #ROTATION_FREEZE_180
     * @see #ROTATION_FREEZE_270
     * @see #ROTATION_FREEZE_CURRENT
     * @see #ROTATION_UNFREEZE
     */
    public boolean setRotation(int rotation) {
        ...
    }

    /**
     *
     */
    public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis)
            throws TimeoutException {
        ...
    }

    /**
     * Takes a screenshot.
     *  屏幕截图方法
     * @return The screenshot bitmap on success, null otherwise.
     */
    public Bitmap takeScreenshot() {
       ...
    }

    /**
     * Grants a runtime permission to a package.
     * 为应用赋予动态权限
     */
    public void grantRuntimePermission(String packageName, String permission) {
        ...
    }

    /**
     * Revokes a runtime permission from a package.
     * 取消应用的动态权限
     */
    public void revokeRuntimePermission(String packageName, String permission) {
        ...
    }
}

四、模拟Uiautomator实现测试服务

特点:
1、系统 已启动其他测试服务时,可以启动服务。
2、在系统内做服务时可以动态控制测试服务的注册、注销,实现多服务并存
3、API功能需自己实现,业务亲和度强。

创建测试类MyUiAutomation

//在包android.app 下创建类 
public class MyUiAutomation {
    private final HandlerThread mHandlerThread = new HandlerThread("MyUiAutomation");
    UiAutomation mUiAutomation;
    Method mDisconnectMethod;

    public void connect() {
        mHandlerThread.start();
        //如果不在android.app包下,则无法调用构造方法。
        mUiAutomation = new UiAutomation();
        final UiAutomationConnection uiAutomationConnection = new UiAutomationConnection();
        try {
        //系统已隐藏相关方法,因此使用反射方式调用
            Class[] parameterTypes = new Class[]{Looper.class, Class.forName("android.app.IUiAutomationConnection")};
            Object[] parameterValues = new Object[]{mHandlerThread.getLooper(), uiAutomationConnection};
            mUiAutomation = UiAutomation.class.getConstructor(parameterTypes).newInstance(parameterValues);
            Method connectMethod = UiAutomation.class.getMethod("connect");
            mDisconnectMethod = UiAutomation.class.getMethod("disconnect");
            connectMethod.setAccessible(true);
            connectMethod.invoke(mUiAutomation);
            AccessibilityNodeInfo windowNode = mUiAutomation.getRootInActiveWindow();
            if (windowNode != null) {
                System.out.println("get Widow:" + windowNode.getClassName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

创建Java接口类,并创建测试连接,调用测试服务方法

package com.android.demo;

import android.app.MyUiAutomation;
import android.app.UiAutomation;
/**
 * Created by walker on 2019-07-31.
 *
 * @description
 */
public class CmdMain {
    public static void main(String[] args) {
        MyUiAutomation myUiAutomation = new MyUiAutomation();
        myUiAutomation.connect();
        UiAutomation uiAutomation= myUiAutomation.getUiAutomation();

        AccessibilityNodeInfo windowNode = uiAutomation.getRootInActiveWindow();
        if (windowNode != null) {
            System.out.println("get Widow:" + windowNode.getClassName());
        }
    }
}

构造apk文件并执行

local_file=./test.apk
runClass=com.android.demo.CmdMain
app_file=/data/local/tmp/test.apk

//将执行文件推送到设备中
adb push ${local_file} ${app_file}
//通过shell命令调用程序,以CmdMain的main()方法开始执行
adb shell ANDROID_DATA=/data/local/tmp CLASSPATH=${app_file} exec app_process /system/bin ${runClass}
上一篇:洛谷 P2866 [USACO06NOV]糟糕的一天Bad Hair Day 牛客假日团队赛5 A (单调栈)


下一篇:基于 Babel 的 npm 包的最小化设置 [每日前端夜话0x2F]