基于uiautomator 相关功能

Uiautomator相关

一 uiautomatorviewer工具和Automator UI测试框架

UiAutomator是Google提供的用来做安卓自动化测试的一个Java库,基于Accessibility服务。功能很强,可以对第三方App进行测试,获取屏幕上任意一个APP的任意一个控件属性,并对其进行任意操作,但有两个缺点:1. 测试脚本只能使用Java语言 2. 测试脚本要打包成jar或者apk包上传到设备上才能运行。
uiautomatorviewer工具

  1. 工具目录:
Android\Sdk\tools\bin\uiautomatorviewer.bat
  1. 操作效果:
    在这里插入图片描述
    点击箭头按钮可以刷新界面

Automator框架使用

1>添加依赖:
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

//    testImplementation 'junit:junit:4.12'
//    androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
//    androidTestImplementation 'com.android.support.test:runner:1.0.2'
//    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'


    implementation 'junit:junit:4.12'
    implementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
    implementation 'com.android.support.test:runner:1.0.2'
    implementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

注释中引用只能在debug环境使用

2>uiautomator中常用的API
  • 设备对象:UiDevice
  1. 获取设备对象UiDevice:
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
UiDevice uiDevice = UiDevice.getInstance(instrumentation);
  1. UiDevice支持的操作:
    以press开头的按压相关功能
public boolean pressMenu()//Simulates a short press on the MENU button.
public boolean pressBack()//Simulates a short press on the BACK button.
public boolean pressHome()//Simulates a short press on the HOME button.
public boolean pressSearch()//Simulates a short press on the SEARCH button.
  1. 点击某个点
public boolean click(int x, int y)
  1. 滑动和拖拽
public boolean swipe(int startX, int startY, int endX, int endY, int steps)
public boolean drag(int startX, int startY, int endX, int endY, int steps)
  1. 等待空闲
public void waitForIdle(long timeout)
  1. 唤起和休眠相关
public void wakeUp()
public void sleep()
public boolean isScreenOn()
  1. 查找UIObject
public UiObject findObject(UiSelector selector)
public boolean hasObject(BySelector selector)
public UiObject2 findObject(BySelector selector) 
public List<UiObject2> findObjects(BySelector selector)
  • 查找条件对象 UiSelector,BySelector
  1. 属性-文本匹配
返回值API说明
UiSelector/BySelectortext(String text)/By.text(String substring)文本
UiSelector/BySelectortextContains(String text)/By.textContains(String substring)文本包含
UiSelector/BySelectortextMatches(String regex)/By.text(Pattern textValue)文本正则
UiSelector/BySelectortextStartsWith(String text)/By.textStartsWith(String substring)文本起始匹配
UiSelector/BySelector其他其他文本起匹配
  1. 属性-资源ID匹配
返回值API说明
UiSelector/BySelectorresourceId(String id)/res(String resourceName)资源ID
UiSelector/BySelectorresourceIdMatches(String regex)/res(Pattern resourceName)资源ID正则
  1. 属性-类名
返回值API说明
UiSelector/BySelectorclassName(String className)/clazz(String className)类名
UiSelector/BySelectorclassNameMatches(String regex)/clazz(Pattern className)正则类名
  1. 其他匹配规则
  • UI控件对象 UiObject,UiObject2
  1. 判断控件是否存在
boolean waitForExists(long timeout)
boolean exists()
  1. 获取属性
String getText()
Rect getVisibleBounds()
  1. 操作事件
boolean click()
boolean clickAndWaitForNewWindow()
boolean dragTo(int destX, int destY, int steps)
boolean longClick()
3>代码示例

在androidTest中ExampleInstrumentedTest类中实现

 /**
     * 步骤:输入 0.01金额, 人脸支付,等待刷脸失败,关闭按钮
     * 目的:测试人脸支付中录屏功能是否稳定,占内存没有释放,卡顿等
     */
    @Test
    public void facePayAuto() throws Exception {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        LogUtil.startLogger(LogUtil.VERBOSE, true, InstrumentationRegistry.getContext());
        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
        UiDevice uiDevice = UiDevice.getInstance(instrumentation);
        while (true) {
            long start = System.currentTimeMillis();
            LogUtil.d(TAG, "facePayAuto:start-->" + simpleDateFormat.format(start));

            //寻找收款界面的0
            UiObject input_0 = uiDevice.findObject(new UiSelector().text("0"));
            UiObject input_dot = uiDevice.findObject(new UiSelector().text("•"));
            UiObject input_1 = uiDevice.findObject(new UiSelector().text("1"));
            if (input_0.waitForExists(2 * 1000)) {
                input_0.click();
                LogUtil.d(TAG, "input_0.click()");
            }
            if (input_dot.waitForExists(2 * 1000)) {
                input_dot.click();
                LogUtil.d(TAG, "input_dot.click()");
            }
            if (input_0.waitForExists(2 * 1000)) {
                input_0.click();
                LogUtil.d(TAG, "input_0.click()");
            }
            if (input_1.waitForExists(2 * 1000)) {
                input_1.click();
                LogUtil.d(TAG, "input_1.click()");
            }

            UiObject facePayBtn = uiDevice.findObject(new UiSelector().text("刷脸收款"));
            if (facePayBtn.waitForExists(2 * 1000)) {
                facePayBtn.click();
                LogUtil.d(TAG, "facePayBtn.click()");
            }

            UiObject close = uiDevice.findObject(new UiSelector().text("关闭"));
            if (close.waitForExists(50 * 1000)) {
                close.click();
                LogUtil.d(TAG, "close.click()");
            }
            long end = System.currentTimeMillis();
            LogUtil.d(TAG, "facePayAuto:end-->" + simpleDateFormat.format(end));
            LogUtil.d(TAG, "use:-->" + getGapTime(end - start));
        }
    }

二 AccessibilityService 服务

AccessibilityService设计初衷在于帮助残障用户使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,应用静默安装

  1. 首先先写一个类去继承AccessibilityService
public class MyAccessibilityService extends AccessibilityService {

    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
    //服务断开连接
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        //获取触发事件的控件节点
        AccessibilityNodeInfo sourceNodeInfo=event.getSource();
        //获取事件类型
        int eventType=event.getEventType();
    }

    private ActivityInfo tryGetActivity(ComponentName componentName) {
        try {
            return getPackageManager().getActivityInfo(componentName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Override
    public void onInterrupt() {
    }
}
  1. 服务配置
        <service
            android:name=".MyAccessibilityService"
            android:label="@string/accessibility_service_name"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>

            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/config" />
        </service>
  1. AccessibilityService相关配置
//config.xml

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagIncludeNotImportantViews"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibility_service_description"
    tools:ignore="UnusedAttribute" />

  1. AccessibilityEvent事件分发流程

参考:Android 9.0源码学习-AccessibilityManager

三uiautomator dump 命令和底层实现

  • 命令执行效果:
C:\Users\**>adb shell
T2:/ $ uiautomator dump
UI hierchary dumped to: /sdcard/window_dump.xml
T2:/ $

得到整个ui节点树打印

window_dump.xml

<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
    <node bounds="[0,0][1920,1011]" checkable="false" checked="false" class="android.widget.FrameLayout"
        clickable="false" content-desc="" enabled="true" focusable="false"
        focused="false" index="0" long-clickable="false" package="com.woyou.launcher" password="false"
        resource-id="" scrollable="false" selected="false" text="">
        <node bounds="[0,0][1920,1011]" checkable="false" checked="false" class="android.widget.FrameLayout"
            clickable="false" content-desc="" enabled="true" focusable="false"
            focused="false" index="0" long-clickable="false" package="com.woyou.launcher" password="false"
            resource-id="" scrollable="false" selected="false" text="">
            <node bounds="[0,0][1920,1011]" checkable="false" checked="false"
                class="android.widget.FrameLayout" clickable="false" content-desc=""
                enabled="true" focusable="false" focused="false" index="0" long-clickable="false"
                package="com.woyou.launcher" password="false" resource-id="android:id/content" scrollable="false"
                selected="false" text="">
                ……
            </node>
        </node>
    </node>
</hierarchy>

window_dump.xml中顶层节点为hierarchy,控件对应一个个node.组成一个树

node中属性:

  bounds="[0,0][540,888]"  控件边界
  checkable="false" 
  checked="false"
  class="android.widget.FrameLayout"  控件对应的view的完整类名
  clickable="false" 
  content-desc="" 
  enabled="true"
  focusable="false"
  focused="false" 
  index="0" 
  long-clickable="false"
  package="com.sunmi.payment.mobile"  控件所在的包名
  password="false" 
  resource-id="com.sunmi.payment.mobile:id/input_money_view"  控件id
  scrollable="false"
  selected="false" 
  text="" 控件的显示文本
  • 底层代码实现
  1. /frameworks/base/cmds/uiautomator/cmds/uiautomator/uiautomator
exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}
  1. /frameworks/base/cmds/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/Launcher.java
129    private static Command[] COMMANDS = new Command[] {
130        HELP_COMMAND,
131        new RunTestCommand(),
132        new DumpCommand(),
133        new EventsCommand(),
134    };
  1. /frameworks/base/cmds/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
/**
34 * Implementation of the dump subcommand
35 *
36 * This creates an XML dump of current UI hierarchy
37 */
38public class DumpCommand extends Command {
39
40    private static final File DEFAULT_DUMP_FILE = new File(
41            Environment.getLegacyExternalStorageDirectory(), "window_dump.xml");
42
43    public DumpCommand() {
44        super("dump");
45    }
46
47    @Override
48    public String shortHelp() {
49        return "creates an XML dump of current UI hierarchy";
50    }
51
52    @Override
53    public String detailedOptions() {
54        return "    dump [--verbose][file]\n"
55            + "      [--compressed]: dumps compressed layout information.\n"
56            + "      [file]: the location where the dumped XML should be stored, default is\n      "
57            + DEFAULT_DUMP_FILE.getAbsolutePath() + "\n";
58    }
59
60    @Override
61    public void run(String[] args) {
62        File dumpFile = DEFAULT_DUMP_FILE;
63        boolean verboseMode = true;
64
65        for (String arg : args) {
66            if (arg.equals("--compressed"))
67                verboseMode = false;
68            else if (!arg.startsWith("-")) {
69                dumpFile = new File(arg);
70            }
71        }
72
73        UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
74        automationWrapper.connect();
75        if (verboseMode) {
76            // default
77            automationWrapper.setCompressedLayoutHierarchy(false);
78        } else {
79            automationWrapper.setCompressedLayoutHierarchy(true);
80        }
81
82        // It appears that the bridge needs time to be ready. Making calls to the
83        // bridge immediately after connecting seems to cause exceptions. So let's also
84        // do a wait for idle in case the app is busy.
85        try {
86            UiAutomation uiAutomation = automationWrapper.getUiAutomation();
87            uiAutomation.waitForIdle(1000, 1000 * 10);
88            AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();
89            if (info == null) {
90                System.err.println("ERROR: null root node returned by UiTestAutomationBridge.");
91                return;
92            }
93
94            Display display =
95                    DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
96            int rotation = display.getRotation();
97            Point size = new Point();
98            display.getSize(size);
99            AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x, size.y);
100        } catch (TimeoutException re) {
101            System.err.println("ERROR: could not get idle state.");
102            return;
103        } finally {
104            automationWrapper.disconnect();
105        }
106        System.out.println(
107                String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath()));
108    }
109}
  1. /frameworks/base/cmds/uiautomator/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java
4/**
15 * @hide
16 */
17public class UiAutomationShellWrapper {
18
19    private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread";
20
21    private final HandlerThread mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
22
23    private UiAutomation mUiAutomation;
24
25    public void connect() {
26        if (mHandlerThread.isAlive()) {
27            throw new IllegalStateException("Already connected!");
28        }
29        mHandlerThread.start();
30        mUiAutomation = new UiAutomation(mHandlerThread.getLooper(),
31                new UiAutomationConnection());
32        mUiAutomation.connect();
33    }
34
35    /**
36     * Enable or disable monkey test mode.
37     *
38     * Setting test as "monkey" indicates to some applications that a test framework is
39     * running as a "monkey" type. Such applications may choose not to perform actions that
40     * do submits so to avoid allowing monkey tests from doing harm or performing annoying
41     * actions such as dialing 911 or posting messages to public forums, etc.
42     *
43     * @param isSet True to set as monkey test. False to set as regular functional test (default).
44     * @see ActivityManager#isUserAMonkey()
45     */
46    public void setRunAsMonkey(boolean isSet) {
47        IActivityManager am = ActivityManagerNative.getDefault();
48        if (am == null) {
49            throw new RuntimeException("Can't manage monkey status; is the system running?");
50        }
51        try {
52            if (isSet) {
53                am.setActivityController(new DummyActivityController(), true);
54            } else {
55                am.setActivityController(null, true);
56            }
57        } catch (RemoteException e) {
58            throw new RuntimeException(e);
59        }
60    }
61
62    public void disconnect() {
63        if (!mHandlerThread.isAlive()) {
64            throw new IllegalStateException("Already disconnected!");
65        }
66        mUiAutomation.disconnect();
67        mHandlerThread.quit();
68    }
69
70    public UiAutomation getUiAutomation() {
71        return mUiAutomation;
72    }
73
74    public void setCompressedLayoutHierarchy(boolean compressed) {
75        AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
76        if (compressed)
77            info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
78        else
79            info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
80        mUiAutomation.setServiceInfo(info);
81    }
82
83    /**
84     * Dummy, no interference, activity controller.
85     */
86    private class DummyActivityController extends IActivityController.Stub {
87        @Override
88        public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
89            /* do nothing and let activity proceed normally */
90            return true;
91        }
92
93        @Override
94        public boolean activityResuming(String pkg) throws RemoteException {
95            /* do nothing and let activity proceed normally */
96            return true;
97        }
98
99        @Override
100        public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
101                long timeMillis, String stackTrace) throws RemoteException {
102            /* do nothing and let activity proceed normally */
103            return true;
104        }
105
106        @Override
107        public int appEarlyNotResponding(String processName, int pid, String annotation)
108                throws RemoteException {
109            /* do nothing and let activity proceed normally */
110            return 0;
111        }
112
113        @Override
114        public int appNotResponding(String processName, int pid, String processStats)
115                throws RemoteException {
116            /* do nothing and let activity proceed normally */
117            return 0;
118        }
119
120        @Override
121        public int systemNotResponding(String message)
122                throws RemoteException {
123            /* do nothing and let system proceed normally */
124            return 0;
125        }
126    }
127}

5.反射封装

public class UiAutomationShellWrapper {

    private static final String TAG = "UiAutomationWrapper";

    private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread";

    private final HandlerThread mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);

    private UiAutomation mUiAutomation;


    private UiAutomationShellWrapper() {
    }

    static private UiAutomationShellWrapper INSTANCE = new UiAutomationShellWrapper();

    static public UiAutomationShellWrapper getInstance() {
        return INSTANCE;
    }

    public void init() throws Exception {
        INSTANCE.connect();
//        INSTANCE.setCompressedLayoutHierarchy(true);
        mUiAutomation.waitForIdle(1000, 1000 * 10);
    }

    public void connect() throws Exception {
        if (mHandlerThread.isAlive()) {
            throw new IllegalStateException("Already connected!");
        }
        mHandlerThread.start();

//        try {

        Class<?> IUiAutomationConnection = Class.forName("android.app.IUiAutomationConnection");
        Class<?> uiAutomationConnection = Class.forName("android.app.UiAutomationConnection");
        Object uiAutomationShellWrapperObject = uiAutomationConnection.newInstance();


        Constructor uiAutomationConstructor = UiAutomation.class.getConstructor(Looper.class, IUiAutomationConnection);
        uiAutomationConstructor.setAccessible(true);
        mUiAutomation = (UiAutomation) uiAutomationConstructor.newInstance(mHandlerThread.getLooper(), uiAutomationShellWrapperObject);

        Method connectMethod = UiAutomation.class.getDeclaredMethod("connect");
        connectMethod.setAccessible(true);
        connectMethod.invoke(mUiAutomation);
//        } catch (IllegalAccessException e) {
//            e.printStackTrace();
//        } catch (InstantiationException e) {
//            e.printStackTrace();
//        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
//        } catch (InvocationTargetException e) {
//            e.printStackTrace();
//        } catch (NoSuchMethodException e) {
//            e.printStackTrace();
//        }


    }


    public void disconnect() {
        if (!mHandlerThread.isAlive()) {
            throw new IllegalStateException("Already disconnected!");
        }

        try {
            Method disconnectMethod = UiAutomation.class.getDeclaredMethod("disconnect");
            disconnectMethod.setAccessible(true);
            disconnectMethod.invoke(mUiAutomation);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        mHandlerThread.quit();
    }

    public UiAutomation getUiAutomation() {
        return mUiAutomation;
    }

    public void setCompressedLayoutHierarchy(boolean compressed) {
        AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
        if (compressed)
            info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
        else
            info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
        mUiAutomation.setServiceInfo(info);
    }

}
  • 使用须知
添加权限
<uses-permission android:name="android.permission.RETRIEVE_WINDOW_CONTENT" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />

应用需要系统签名(我是使用公司的统一签名)

此文要是对你有帮助,如果方便麻烦点个赞,谢谢!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值