Robotium Robotium 之跨应用操作

Heyniu · 2017年02月04日 · 最后由 Heyniu 回复于 2018年12月03日 · 7553 次阅读
本帖已被设为精华帖!

新年伊始,干货不断

新的一年里,heyniu 祝大家心想事成、身体健康、阖家欢乐,还有鸡年大吉吧[手动滑稽]

众所周知,Robotium 的 2 大痛处 >> 重签名、跨应用

重签名: 在我看来这个不是大问题,找研发要签名,然后签名我们的测试应用,就可以保持与被测应用签名的一致性。

跨应用:这个在之前是比较蛋疼的,网上的方案也是大多不适用。今天我就为这个给大家分享一下我的方案。

灵感来源

​ 抢红包应用层出不穷,到底它们是怎样抢到微信的红包呢?带着这个疑问,查阅了一下资料,发现它们是通过 Android 的辅助功能来实现的,于是我的方案是UiAutomation + Accessibility

重视 Robotium

​ 2 大痛处都解决了,那么优势就来啦,比Appium更快的速度,且与被测应用共享数据 ,这是我选择它的原因。

原理浅析

​ Robotium 基于 Instrumentation 的二次封装,然而 UiAutomation 也能通过instrumentation.getUiAutomation()拿到。顺带提一下 Uiautomator 也是基于 UiAutomation 的封装。

UiAutomation 跨应用操作三大利器:

setOnAccessibilityEventListener() 开启 Accessibility

executeShellCommand() 执行 shell 命令(权限比Runtime.getRuntime().exec()高,相当于 adb shell)

injectInputEvent() 注入事件,比如点击。

Accessibility 查找控件的 2 种方式:

findAccessibilityNodeInfosByViewId() 通过完整的资源查找

findAccessibilityNodeInfosByText() 通过文本查找

以上具体的方法查阅 API 描述即可。

举一反三

​ 目前已实现跨应用点击、输入文本、点击通知栏、拍照、授权、QQ 登录等等,其他跨应用场景也是类似。只要掌握原理浅析列出的几个核心方法,我相信跨应用处理已经不是问题了,这里只是抛砖引玉。

场景实例

点击

/**
     * Work across application boundaries for click.
     * @param x the x coordinate
     * @param y the y coordinate
     */
    public void acrossForClick(float x, float y){
        MotionEvent motionDown = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), KeyEvent.ACTION_DOWN,
                x,  y, 0);
        motionDown.setSource(InputDevice.SOURCE_TOUCHSCREEN);
        uiAutomation.injectInputEvent(motionDown, true);
        MotionEvent motionUp = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), KeyEvent.ACTION_UP,
                x, y, 0);
        motionUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
        uiAutomation.injectInputEvent(motionUp, true);
        motionUp.recycle();
        motionDown.recycle();
    }

拍照

/**
     * Work across application boundaries for camera.
     * @param viewId The fully qualified resource name of the view id to find. e.g: com.sec.android.app.camera:id/okay
     */
    public void acrossForCamera(String viewId){
        Log.d(LOG_TAG, "acrossForCamera()");
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return;
        UiAutomation uiAutomation = instrumentation.getUiAutomation();
        uiAutomation.setOnAccessibilityEventListener(new UiAutomation.OnAccessibilityEventListener() {
            @Override
            public void onAccessibilityEvent(AccessibilityEvent event) {
                if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
                    if (viewId.contains(event.getPackageName())) {
                        if (event.getSource() != null) {
                            List<AccessibilityNodeInfo> infoList = event.getSource().findAccessibilityNodeInfosByViewId(viewId);
                            if (infoList == null || infoList.isEmpty()) return;
                            performClick(infoList.get(0));
                        }
                    }
                }
            }
        });
        sleep(2000);
        uiAutomation.executeShellCommand("input keyevent 27");
    }

授权(核心代码)

/**
     * Requests permissions to be granted to this application.
     */
    public void requestPermissions(){
        if (Build.VERSION.SDK_INT >= 23) {
            String[] permissions = checkPermissions();
            if (permissions == null || permissions.length == 0) return;
            final String manufacturer = getManufacturer();
            ActivityCompat.requestPermissions((Activity) context, permissions, 10000);
            UiAutomation uiAutomation = instrumentation.getUiAutomation();
            uiAutomation.setOnAccessibilityEventListener(new UiAutomation.OnAccessibilityEventListener() {
                @Override
                public void onAccessibilityEvent(AccessibilityEvent event) {
                    android.util.Log.d(LOG_TAG, "UiAutomation: " + event.toString());
                    if (manufacturer.toLowerCase().contains("mi")) {
                        handlePermissions(event, PACKAGE_INSTALLER_XIAOMI, PERMISSION_ALLOW_ID_XIAOMI);
                    } else handlePermissions(event, PACKAGE_INSTALLER, PERMISSION_ALLOW_ID);
                }
            });
        }
    }

QQ 登录

/**
     * Work across application boundaries for QQ login.
     */
    public void acrossForQQLogin(String account, String password){
        boolean installed = isInstalled(QQ);
        if (!installed) {
            Log.w(LOG_TAG, "QQ is not installed.");
            return;
        }
        uiAutomation.setOnAccessibilityEventListener(new UiAutomation.OnAccessibilityEventListener() {
            @Override
            public void onAccessibilityEvent(AccessibilityEvent event) {
                Log.d(LOG_TAG, "Event: " + event.toString());
                if (event.getEventType() == TYPE_WINDOW_STATE_CHANGED && QQ.contains(event.getPackageName())){
                    handleQQLogin(event, account, password);
                    handleAuthorization(event);
                }
            }
        });
    }

    private void handleAuthorization(AccessibilityEvent event) {
        if (event.getClassName().toString().contains("com.tencent.open.agent.AuthorityActivity")) {
            Log.i(LOG_TAG, "QQ Login: " + event.toString());
            sleep(2000);
            AccessibilityNodeInfo nodeInfo = uiAutomation.getRootInActiveWindow();
            IterationNode(nodeInfo, "android.widget.Button");
        }
    }

    /**
     * Loop through the view and click.
     * @param nodeInfo node
     * @param name class name, e.g: android.widget.Button
     */
    private void IterationNode(AccessibilityNodeInfo nodeInfo, String name) {
        for (int i = 0; i < nodeInfo.getChildCount(); i ++){
            AccessibilityNodeInfo node = nodeInfo.getChild(i);
            Log.i(LOG_TAG, "QQ Login nodeInfo.getChild(i): " + node.getClassName());
            if(name.contains(node.getClassName())) {
                // Click the authorization button.
                performClick(node);
                break;
            } else IterationNode(node, name);
        }
    }

    private void handleQQLogin(AccessibilityEvent event, String account, String password) {
        if (event.getClassName().toString().contains("com.tencent.qqconnect.wtlogin.Login")) {
            AccessibilityNodeInfo nodeInfo = uiAutomation.getRootInActiveWindow();
            List<AccessibilityNodeInfo> infoList = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mobileqq:id/account");
            if (infoList == null || infoList.isEmpty()) return;
            // Click the account edit text and enter text.
            performClick(infoList.get(0));
            acrossForEnterText(account);
            infoList = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mobileqq:id/password");
            if (infoList == null || infoList.isEmpty()) return;
            // Click the password edit text and enter text.
            performClick(infoList.get(0));
            acrossForEnterText(password);
            sleep(4000);
            IterationNode(nodeInfo, "android.widget.Button");
        }
    }

部分演示及完整代码

百度网盘:链接: http://pan.baidu.com/s/1mh6bdJ6 密码: ffnp

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

感谢分享,先学习学习

新年来了, 这么勤奋啊.

思寒_seveniruby 将本帖设为了精华贴 02月05日 07:56

加精理由:对跨应用的解析很全面

robotium 跟 uiautomator 2.0 结合使用就好了,跨应用很方便。

Heyniu #26 · 2017年02月05日 Author

#6 楼 @xuxu uiautomator 就麻烦了,还要搭环境,而且 Accessibility 比 uiautomator 更快,绝无虚发 [捂脸]

#8 楼 @heyniu uiautomator 也不用怎么搭环境吧,导入个包就可以,不过速度没有 Accessibility 快倒是真的!

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

Heyniu #24 · 2017年02月05日 Author

#9 楼 @erickyang 恩,学习了

@heyniu 你好,我看到代码是 API >= 23 才能使用,请问 23 以下能兼容吗?

Heyniu #22 · 2017年02月06日 Author

#11 楼 @Mrxiaoxie 23 以下是不用授权的

#12 楼 @heyniu 在 vivo 上,android 是 5.1.1 的,然后打开应用后系统自带的 i 管家会提示权限授权的。卡在这里,不知道怎样结合 uiautomator 去授权

Heyniu #20 · 2017年02月06日 Author

#13 楼 @Mrxiaoxie 那可以这样,判断机型和版本,再拿到弹框的那个授权的资源 id 就能点了

#14 楼 @heyniu 嗯,但我不知道代码放在什么位置好?我尝试把代码放在 setup 那里,但权限授权提示过后才会运行 setup 的代码。不知道怎样攻破。

Heyniu #18 · 2017年02月06日 Author

#15 楼 @Mrxiaoxie 我放在 solo 初始化的地方了

Heyniu #17 · 2017年02月06日 Author

#15 楼 @Mrxiaoxie 每次启动都会检查权限,如果有哪个未授权就会拉起授权框并授权

Heyniu 基于 Robotium 的自动遍历方案——开源 中提及了此贴 02月13日 11:40
Heyniu 基于 Robotium 的自动遍历方案——开源 中提及了此贴 02月14日 14:28

楼主,我想请教一下,injectInputEvent() 和 accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK) 的差别?
我看了源码发现 injectInputEvent 是注入点击事件,performAction 是执行 clickAction 也算是点击事件。啊,我明白了。

楼主,请问下,这种跨应用的实现是否需要手机 root 呢?谢谢!

Heyniu #22 · 2017年02月23日 Author
紫露凝香 回复

不需要

楼主,uiAutomation = instrumentation.getUiAutomation();总是报 getUiAutomation() 方法没有定义的问题,是怎么回事呢?

Heyniu #27 · 2017年02月23日 Author
紫露凝香 回复

要连接 adb 才行

请问手机预装应用,比如设置应用,用 Robotium 可以吗?目前我用的都是 uiautormator2.0
感谢!

bauul 回复

能拿到系统签名应该就可以的,现在也可以遍历设置应用,只是不能启动特定页面,相当于基于深度遍历的概念

请问怎么调用你的点击方法呢?我新建了一个 robotium 的项目 ,就是最基本的代码 ,包含 teardown,setup,test.然后执 run as ,你的那个方法我该怎么用呢?

冯XX 回复

你可以直接拷贝过去,然后解决报错

你的示范代码,AcrossApplication.java,导入到我的 android studio 工程里,提示 private Solo.Config config;错误,可以分享下你的 Solo.Config 吗,这个是什么作用哈,非常感谢!

王薇 回复

你要用我项目里的 robotium jar 我改过

Heyniu 回复

我刚加了你的 QQ 群,通过我一下哈,谢谢啦

Heyniu #33 · 2018年12月03日 Author
王薇 回复

已经解散了

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