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包文件构成
apk文件构成
构建apk
执行下图Gradle任务,获取打包apk文件
任务执行完毕可以得到apk打包文件
执行脚本
//将构建的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}