UiAutomator 由 uiautomator 二次开发得到的启发以及完善

saii · 2015年08月03日 · 最后由 群主 回复于 2017年03月18日 · 2208 次阅读
本帖已被设为精华帖!

最近看到 cpfeng0124 的关于二次开发之自动生成控件定位符,心里感概真是相当棒的思路,但是可能由于是公司的项目所以就点到为止并没有深入去解释其他一些功能。
这两天抽空研究了下吧,将部分的功能也一一的进行了研究完善。
uiautomatorviewer 的原理我就不在这里说明里,首先说一下右键单击弹出菜单。我们要动手脚的地方是

UiAutomatorView 是用来显示出截图后的界面的,

mScreenshotCanvas.addMouseListener(new MouseAdapter() {
            @SuppressWarnings("unused")
            @Override
            public void mouseUp(MouseEvent e) {
                if (mModel != null) {
                    mModel.toggleExploreMode();
                    redrawScreenshot();
                }
            }
}

我们找到这个鼠标监听事件,可以看到 readrawScreenshot() 这函数的从字面上就能明白它的作用就是将你鼠标点击的区域绘制的红色,那么我就从这里入手了,修改代码如下:

mScreenshotCanvas.addMouseListener(new MouseAdapter() {
            @SuppressWarnings("unused")
            @Override
            public void mouseUp(MouseEvent e) {
                if (mModel != null) {
                    mModel.toggleExploreMode();
                    redrawScreenshot();
                }
                //如果为鼠标右键点击那么就弹出右键菜单   
                if (e.button==3){
                    Menu menu = new Menu(mScreenshotCanvas);  
                    mScreenshotCanvas.setMenu(menu);  
                    MenuItem item = new MenuItem(menu, SWT.PUSH);  
                    item.setText("控件点击");  
                    item.addSelectionListener(new SelectionAdapter() {
                        @Override
                        public void widgetSelected(SelectionEvent e) {
                                        //获取选择的矩形区域
                            Rectangle rect = mModel.getCurrentDrawingRect();
                            objectClick(rect);  
                        };
                    });
                }
            }
        }


以上的代码就是实现右键弹出菜单,按钮叫控件点击,接着实现该按钮的点击事件,获取到所选择控件的矩形区域,进行点击操作。
那下来我们来看看到底要如何实现点击的操作呢,实际也很点击,通过 adb shell input 就可以了。

public void objectClick(Rectangle rectangle){
    String adbStr = "adb shell input tap "+(rectangle.x+rectangle.width/2)+" " +(rectangle.y+rectangle.height/2);
    execCmd(adbStr);
}

置于 execCmd 做什么操作就不用多说了,直接 java 调用 cmd 运行就可以了。

再就是一个脚本录制的功能,实际上这东西也不麻烦。只要你模范 com.android.uiautomator.actions 中的类写一个

package com.android.uiautomator.actions;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.resource.ImageDescriptor;

import com.android.uiautomator.UiAutomatorViewer;

public class RecordAction extends Action {


    private static boolean isRun = false;
    public RecordAction(UiAutomatorViewer viewer){
         super("&录制/停止");
    }

    @Override
    public ImageDescriptor getImageDescriptor() {

        if (isRun){
            return ImageHelper.loadImageDescriptorFromResource("images/stop.png");

        }else{
            return ImageHelper.loadImageDescriptorFromResource("images/play.png");
        }
    }

    @Override
    public void run() {


        if (isRun){
            StringBuffer sb = new StringBuffer();
            sb.append("if __name__ == \"__main__\":\n");
            sb.append("    unittest.main()\n");
            writeFile(sb);
            isRun = false;
            this.setImageDescriptor(ImageHelper.loadImageDescriptorFromResource("images/play.png"));
        }else{
            isRun = true;
            this.setImageDescriptor(ImageHelper.loadImageDescriptorFromResource("images/stop.png"));
            try{
                //生成部分初始化话的代码
                File file = new File("C:\\a.py");
                try{
                    file.delete();
                }catch(Exception e){
                    e.printStackTrace();
                }
                file.createNewFile();
                FileOutputStream out=new FileOutputStream(file,true);
                StringBuffer sb = new StringBuffer();
                String deviceName = getDeviceList();
                sb.append("# -*- coding: utf-8 -*-\n");
                sb.append("from appium import webdriver\n");
                sb.append("from appium.webdriver.common import touch_action\n");
                sb.append("import unittest\n");
                sb.append("class testDemo(unittest.TestCase):\n");
                sb.append("    def setUp(self):\n");
                sb.append("        desired_caps = {}\n");
                sb.append("        desired_caps['deviceName'] = '"+deviceName+"'\n");
                sb.append("        desired_caps['udid'] = '"+deviceName+"'\n");
                sb.append("        desired_caps['appPackage'] = 'com.seewo.teachercare'\n");
                sb.append("        desired_caps['platformName'] = 'Android'\n");
                sb.append("        desired_caps['platformVersion'] = '4.4.2'\n");
                sb.append("        desired_caps['appActivity'] = '.ui.TeacherMainActivity'\n");
                sb.append("        self.driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps)\n");
                sb.append("    def tearDown(self):\n");
                sb.append("        self.driver.quit()\n");
                sb.append("    def testa(self):\n");
                out.write(sb.toString().getBytes("utf-8"));
                out.close();
            }catch(IOException ex){
                System.out.println(ex.getStackTrace());
            }

        }
    }


    /**
     * 获取设备列表
     * @throws IOException 
     */
    @SuppressWarnings("resource")
    private String  getDeviceList() throws IOException{
        Process p = Runtime.getRuntime().exec("cmd.exe /c adb devices");
        InputStream inStream = p.getInputStream();
        InputStreamReader inReader = new InputStreamReader(inStream,"UTF-8");
        BufferedReader inBuffer = new BufferedReader(inReader);
        String s = null;
        while((s = inBuffer.readLine()) != null){

            if(s.endsWith("device")){
                s =s.substring(0, s.lastIndexOf("device")).trim(); 
                System.out.println(s);
                return s;
            }

        }

        return null;
    }
    /**
     * 写文件操作
     * @param sb
     */
    public static void writeFile(StringBuffer sb){

        if(isRun){
            try{
                //生成部分初始化话的代码
                File file = new File("C:\\a.py");
                if(!file.exists()){
                    file.createNewFile();
                }
                FileOutputStream out=new FileOutputStream(file,true);
                out.write(sb.toString().getBytes("utf-8"));
                out.close();

            }catch(IOException ex){
                System.out.println(ex.getStackTrace());
            }
        }   
    }
}

上面有些代码我都写死了。因为这也是仓促完成,另外本人真是新手,所有代码写的烂求轻点拍。
上面主要的功能就是点击录制按钮时,初始化就写入内容,点击结束时,将 main 方法补上。其他写文件就是在右键时进行控件点击等时同时进行写文件操作就可以了。
最后在 Uiautomatorviewer 类中实例化该类

//加上按钮
toolBarManager.add(new RecordAction(this));

第一次发帖,写的可能很乱,代码也写的很渣,不知道各位能不能看的懂。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 32 条回复 时间 点赞

Good Job!UIAutomatorViewer 本身的架构确实很方便扩展。

录制那块有个疑问:看你写的是 Appium 的 python 脚本,但没看到具体是哪些代码把每个操作步骤 write 到文件中的。是不是把 writeFile 分别放在了各个点击事件中了?

saii #36 · 2015年08月04日 Author

#1 楼 @chenhengjie123 是的 目前我做的就是每一个点击的操作做 writeFile,我觉得我写的还是比较麻烦。

学习了,厉害。

#2 楼 @zsx10110

每个点击都写一次文件,是不是有点耗时,占用资源了。

可以将点击后生成的脚本直接放在 StringBuilder 中,这样最后再直接生成。或则将每行的脚本放在一个数据结构中,最后保存脚本的时候,再写入文件。

有新意

果然是粗糙的,还有好多地方可以优化。可以用它本身抓到的 idevice 来执行指令嘛。录制的方法在实际中会遇到细节上的问题,我是指拖拽等较复杂的动作的时候。

有值得借鉴的地方,哈哈哈哈,感谢分享

saii #7 · 2015年08月04日 Author

#6 楼 @carl 没办法 花一天的时间匆匆忙忙写的,而且对 swt/jface 实际上我并不是很了解。
另外你说的一点通过拿到的 idevice 可以做什么操作呢?

saii #29 · 2015年08月04日 Author

#4 楼 @kilmer 嗯 是的。

#8 楼 @zsx10110
adb 指令都在里面的,还有一些其他的,我也是最近在做这一工具,不过先做了基本的东西。比如添加一块区域不编辑脚本。你才做一天就这样了,牛人呀!

saii #27 · 2015年08月04日 Author

#10 楼 @carl 有点明白了,类似于 uiautomator 的截图就是通过 device 实现的吧。我当时一开始的想法也是通过 IDevice 进行操作的, 但是我去看了 IDevice 这个类了,没找到我需要的操作啊,难道说是需要用 executeShellCommand(String, IShellOutputReceiver) 这个方法吗?不太会,能够举个例子吗

#9 楼 @zsx10110
源代码中有例子的

一个优化点:页面元素的获取可以不用 dump 来获取,那样会有 1-2s 的时间耗时,可以通过将信息获取存储到内存中再进行解析,这样可以提升执行效率

是个不错的创新!

我也在做相关的二次开发,但看了您的过后很多东西更明白了

@kasi 没有很懂你的意思,怎么通过信息获取?

我反编译了这个包,在重新打包的时候有几个保报错,修改之后打包正常,但是有如下几个问题,请赐教!1.多个手机连接不管选哪个序列号都是默认第一个 2.图片显示界面没有自动缩放,鼠标点在上面也没有方框显示。

—— 来自 TesterHome 官方 安卓客户端

楼主,你这个方法可以在 PC 上录制测试脚本,但是如果从手机上操作 app,就无法录制了。

saii #17 · 2015年12月16日 Author

#18 楼 @heavennash 是的。而且其实这个框架也是不太适合这么去改这个的,只是说提供一个思路而已

#19 楼 @zsx10110 那如果是在手机上录制,请问有什么思路吗?

saii #17 · 2016年01月05日 Author

#17 楼 @everflier 你为什么要反编译那个包,这个是开源的。

uiautomator 获取页面元素中文乱码,机型安卓 5.0.2,请问能解决吗

@alisawu 中文乱码的原因是 Android_5.0.* 的 AccessibilityNodeInfoDumper.java#stripInvalidXMLChars(CharSequence cs) 中文字符处理的不好,Android6.0 已经改回 4.4 原来的方法了,所以你把 android4.4 或者 6.0 版本的 uiautomator 重编就 OK,简单的方法就是找一个非 5.0 的手机把/system/framework/uiautomator.jar 替换掉你 5.0 的手机的 uiautomator.jar

你好,能给个 QQ 吗,我也想做 uiautomatorview 的修改,但是工程翻遍后导进来还需要很多 jar 包,我已经导入了一些,但感觉还差很多,想具体请教下还需要导入哪些 jar 包,谢谢啦,哪位兄弟伙能帮个忙

#23 楼 @plasma 怎么替换 uiautomator.jar 这个文件,root 手机了都不行,提示 Read-only file system

#23 楼 @plasma 我的三星 Note4 5.0 系统的时候中文也是乱码升级官方 5.1 之后就不乱码了呢

saii #11 · 2016年04月15日 Author

#26 楼 @niuniudd 这个是 android 5.0 都有的问题。5.1 已经解决了。

感谢分享,看完几个帖子后受到启发不少,于是最近也开始动手折腾功能扩展
楼主,想问下你的输入框输入文本是怎么处理的?adb shell input text 不支持中文,而且还有手机输入法的干扰。。

saii #29 · 2016年04月18日 Author

#28 楼 @snowmaster

确实解决不了,不过如果你真想要做好的话,就拿 appium 中的输入法将这个输入法安装上并且设置成默认输入法 试试看。不过我没试过 ,所以不敢说可以。

@utopia root 手机后要 adb remount 啊,然后再 push

@niuniudd 是的,Android 5.0.* 版本都是乱码

JoeJoe [该话题已被删除] 中提及了此贴 07月04日 14:13
雪怪 [该话题已被删除] 中提及了此贴 08月19日 16:31
JoeJoe 打造专属 uiautomatorviewer 中提及了此贴 11月25日 16:46
雪怪 uiautomatorviewer 功能扩展实践 中提及了此贴 12月06日 22:35
伊釨言 java 初学者-uiautomatorviewer 的改造 中提及了此贴 01月17日 17:02


我想实现二级菜单的功能。
先创建一个一级菜单
Menu menu = new Menu(mScreenshotCanvas);

mScreenshotCanvas.setMenu(menu);

添加了一个菜单选项
MenuItem newItem = new MenuItem(menu , SWT.CASCADE);
newItem .setText("物理按键");

Menu submenu = new Menu(mScreenshotCanvas, SWT.DROP_DOWN);
这里写的时候报错了,说是 mScreenshotCanvas 的类型不对

newItem .setMenu(submenu );

请问下你的二级菜单怎么实现的啊

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册