Accessibility辅助自动化测试

前言:什么是Accessibility(辅助功能)

考虑到部分用户不能很好地使用Android设备,比如由于视力、身体、年龄方面的限制,造成阅读内容、触控操作、声音信息等方面的获取困难,Android提供了Accessibility特性和服务帮助用户更好地使用Android设备。依据Android官方的详细介绍,开发者在增加视图属性如contentDescription等内容后,可以在不修改原有代码逻辑的情况下使用户体验得到优化,如预装在Android 设备上的屏幕阅读器TalkBack,在没有修改系统源码的情况下,满足了视力不足的用户使用Android设备的需求,TalkBack会使用语音反馈描述用户所执行的操作,以及告知用户收到的提醒和通知,可以帮助视力水平较低的用户顺利进行手机的触控、阅读内容的进行。

一. 如何实现Accessibility

为了能够实现Accessibility只需要简单的三个步骤:

  1. 继承AccessibilityService
  2. AndroidManifest.xml注册服务
  3. accessibility.xml配置
    下面就此简单的介绍一下:

1.1 继承AccessibilityService

首先继承AccessibilityService。这里,必须实现"onAccessibilityEvent"和“onInterrupt”两个接口

public class MyAccessibility extends AccessibilityService{
 
    @Override 
    public void onAccessibilityEvent(AccessibilityEvent event){
        // TODO Auto-generated method stub
    } 
   
    @Override 
    public void onInterrupt(){
        // TODO Auto-generated method stub 
    }
}

实现Accessibility功能最主要还是实现的"onAccessibilityEvent"接口,其中的AccessibilityEvent对象提供系统捕捉到的监听事件。
除了"onAccessibilityEvent"接口外,还有其他的方法,如下表:

代码比喻
disableSelf()禁用当前服务,也就是在服务可以通过该方法停止运行
findFoucs(int falg)查找拥有特定焦点类型的控件
getRootInActiveWindow()如果配置能够获取窗口内容,则会返回当前活动窗口的根结点
onAccessibilityEvent(AccessibilityEvent event)有关AccessibilityEvent事件的回调函数.系统通过sendAccessibiliyEvent()不断的发送AccessibilityEvent到此处
performGlobalAction(int action)执行全局操作,比如返回,回到主页,打开最近等操作
setServiceInfo(AccessibilityServiceInfo info)设置当前服务的配置信息
getSystemService(String name)获取系统服务
onKeyEvent(KeyEvent event)如果允许服务监听按键操作,该方法是按键事件的回调,需要注意,这个过程发生了系统处理按键事件之前
onServiceConnected()系统成功绑定该服务时被触发,也就是当你在设置中开启相应的服务,系统成功的绑定了该服务时会触发,通常我们可以在这里做一些初始化操作
值得注意的是"performGlobalAction" 该API可以模拟用户点击返回键和home键。

1.2 AndroidManifest.xml注册服务

<!--固定配置,不可更改-->
<service android:name=".services.AnalogClickService"
    android:exported="true"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
    android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService"/>
    </intent-filter>
 
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessibility"/>
</service>

以上的在AndroidManifest.xml注册的服务是固定配置,不可更改。

1.3 accessibility.xml配置

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:notificationTimeout="100"/>

在安卓项目中创建xml文件夹,在改文件夹中创建accessibility.xml。
accessibilityEventTypes:表示该服务对界面中的哪些变化感兴趣,即哪些事件通知,比如窗口打开,滑动,焦点变化,长按等.具体的值可以在AccessibilityEvent类中查到,如typeAllMask表示接受所有的事件通知。
accessibilityFeedbackType:表示反馈方式,比如是语音播放,还是震动。
canRetrieveWindowContent:表示该服务能否访问活动窗口中的内容.也就是如果你希望在服务中获取窗体内容的化,则需要设置其值为true。
notificationTimeout:接受事件的时间间隔,通常将其设置为100即可。
packageNames:表示对该服务是用来监听哪个包的产生的事件。如果不写代表监听所有的应用。中间可以用";"来分割。
除了上面罗列的一部分。还有更加详细的API文档。
AccessibilityService
同时,上述的配置在"onServiceConnected"中同样可以配置。

@Override
protected void onServiceConnected() {
    super.onServiceConnected();
    AccessibilityServiceInfo info = new AccessibilityServiceInfo();
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; //反馈方式
    info.notificationTimeout = 100; //事件时间间隔
    info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; //服务对界面中哪些变化感兴趣
    setServiceInfo(info);
}

通过以上三个配置,直接安装。当然首先要在"无障碍"的界面中打开Accessibility功能。
这里给出跳到系统的辅助功能界面的代码。在程序开始时,提醒用户打开Accessibility功能。

//跳转到安卓的辅助功能界面
private void openAccessibilitySettings (){
    Intent intent =new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
    startActivity(intent);
}

二. Accessibility的API获得UI节点

编写此次的自动化测试辅助程序,主要用到了以下几个API
1.AccessibilityEvent的对象的getText(),getEventType()
2.getRootInActiveWindow
3.findAccessibilityNodeInfosByText
4.findAccessibilityNodeInfosByViewId

2.1 AccessibilityEvent对象

AccessibilityEvent中的主要的方法:

方法说明
getEventType()事件类型
getSource()获取事件源对应的结点信息
getClassName()获取事件源对应类的类型,比如点击事件是有某个Button产生的,那么此时获取的就是Button的完整类名
getText()获取事件源的文本信息,比如事件是有TextView发出的,此时获取的就是TextView的text属性.如果该事件源是树结构,那么此时获取的是这个树上所有具有text属性的值的集合
isEnabled()事件源(对应的界面控件)是否处在可用状态
getItemCount()如果事件源是树结构,将返回该树根节点下子节点的数量

2.2 getRootInActiveWindow方法

该API可以获取当前的窗口的根节点。返回值是AccessibilityNodeInfo对象。通过该对象可以遍历该窗口的所有节点。
注意在使用完毕之后,需要调用"recycle"的方法,释放资源。

2.3 findAccessibilityNodeInfosByText和findAccessibilityNodeInfosByViewId

顾名思义,findAccessibilityNodeInfosByViewId,是通过布局的ID来获取。
findAccessibilityNodeInfosByText,是通过布局上的text来获取。
同时,为了能够获取指定的AccessibilityNodeInfo对象,自己封装一个方法:

private AccessibilityNodeInfo findAccessibilityNodeInListByText(List<AccessibilityNodeInfo> listData,
                                                                String text){
    if (listData == null){
        return null;
    }
    for (AccessibilityNodeInfo info: listData){
        CharSequence charSequence = info.getText();
        if (charSequence != null){
            String msg = charSequence.toString();
            if (msg.equals(text)){
                return info;
            }
        }
    }
    return null;
}

通过遍历获得List列表,再通过text获取需要的AccessibilityNodeInfo对象。

2.4 获取当前界面的ID

怎么样知道该控件的id呢?其实很简单,android中已经为我们提供了相关的工具:
在Android Studio中开启Android Device Monitor,选择设备后点击Dump View Hierarchy for UI Automator,如下:
找到Android Device Monitor,点击:
这里写图片描述
点击之后,可能会弹出error对话框。这里不需要管,直接点击"OK"按钮。
弹出Android Device Monitor后,选择设备后,点击Dump View Hierarchy for UI Automator
这里写图片描述
稍等片刻之后,便会出现当前设备的窗口。在该窗口中点击相关控件,便会显示该控件的属性。
借助该工具,可以帮我们快速的分析界面结构,帮助我们从其他app布局策略中学习。
这里写图片描述

三. Accessibility在自动化测试中的应用

在Android测试中,有时候需要将应用的数据清空。但是,清除数据后,应用的权限都会被关闭。遇到这种情况没办法通过代码中去打开。对于这种情况,采用Accessibility辅助功能,便可以很好的被解决。

3.1 允许开启AccessibilityService服务

打开Activity,直接跳转到AccessibilityService服务开启界面。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    if (!isAccessibilitySettingsOn(MainActivity.this))
        openAccessibilitySettings();
    //跳转到辅助功能界面后,关闭该Activity
    finish();
}
 
 
// 跳转到安卓的辅助功能界面
private void openAccessibilitySettings (){
    //跳转系统自带界面 辅助功能界面
    Toast.makeText(this,"请打开辅助功能!", Toast.LENGTH_SHORT).show();
    Intent intent =new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
    startActivity(intent);
}

3.2 清空数据和开启权限

首先给出开启该应用的办法

/**
 * 跳转到"应用详细信息"
 * @param context context
 * @param packageName 包名
 */
public void openApplicationAetailsSettings(Context context, String packageName){
    // "com.antiy.securityprovider"
    final String SETTINGS_ACTION = Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
    Intent intent = new Intent()
            .setAction(SETTINGS_ACTION)
            .setData(Uri.fromParts("package", packageName, null));
    context.startActivity(intent);
}

3.3 基本代码封装

private boolean pressDownHome(){
    return performGlobalAction(GLOBAL_ACTION_HOME);
}
 
private boolean pressDownBack(){
    return performGlobalAction(GLOBAL_ACTION_BACK);
}
 
private void sleep(){
    sleep(500);
}
 
private void sleep(long millis){
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
private AccessibilityNodeInfo findAccessibilityNodeInListByText(List<AccessibilityNodeInfo> listData,
                                                                String text){
    if (listData == null){
        return null;
    }
    for (AccessibilityNodeInfo info: listData){
        CharSequence charSequence = info.getText();
        if (charSequence != null){
            String msg = charSequence.toString();
            if (msg.equals(text)){
                return info;
            }
        }
    }
    return null;
}
 
private boolean clickNodeInfo(AccessibilityNodeInfo nodeInfo){
    if (nodeInfo != null){
        if (nodeInfo.isClickable()){
            return nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
        }
    }
    return false;
}
 
private boolean isTextInWindow(String text, AccessibilityNodeInfo node){
    List<AccessibilityNodeInfo> list = node.findAccessibilityNodeInfosByText(text);
    AccessibilityNodeInfo info = findAccessibilityNodeInListByText(list, text);
    if (info != null){
        return true;
    }
    return false;
}

3.4 逻辑处理流程

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
 
    AccessibilityNodeInfo nodeInfo;
    AccessibilityNodeInfo parentNodeInfo;
    List<AccessibilityNodeInfo> listData;
    AccessibilityNodeInfo rootNode;
 
    if (mSharedPreferences == null){
        mSharedPreferences = getSharedPreferences(TABLE_NAME, Context.MODE_PRIVATE);
        mEditor = mSharedPreferences.edit();
    }
 
    int eventType = event.getEventType();
 
    switch (eventType){
        case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
        case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
            rootNode = getRootInActiveWindow();
            if (rootNode == null){
                return;
            }
            List<CharSequence> textList = event.getText();
            if (textList != null){
                // 识别'系统管家安全服务'页面信息
                if ((textList.contains("应用信息")) &&
                        isTextInWindow("系统管家安全服务", rootNode)){
 
                    // 通过使用SharedPreferences约束点击权限,该系列操作只执行一次。
                    if (!mSharedPreferences.getBoolean(KEY_CLICK_SIGN, false)){
                        mEditor.putBoolean(KEY_CLICK_SIGN, true).apply();
 
                        // 点击'存储'按钮,打开'存储详情页面'
                        listData = rootNode.findAccessibilityNodeInfosByText("存储");
                        nodeInfo = findAccessibilityNodeInListByText(listData, "存储");
                        parentNodeInfo = nodeInfo.getParent();
                        if (parentNodeInfo != null){
                            clickNodeInfo(parentNodeInfo);
                        }
                        sleep();
 
                        // 点击'清除数据'按钮,弹出对话框
                        rootNode = getRootInActiveWindow();
                        listData = rootNode.findAccessibilityNodeInfosByText("清除数据");
                        nodeInfo = findAccessibilityNodeInListByText(listData, "清除数据");
                        boolean flag = clickNodeInfo(nodeInfo);
                        AntiyLog.v("点击'清除数据'按钮::"+flag);
                        sleep();
 
                        // 点击'确定'按钮,清除'系统管家安全服务'数据
                        rootNode = getRootInActiveWindow();
                        listData = rootNode.findAccessibilityNodeInfosByText("确定");
                        nodeInfo = findAccessibilityNodeInListByText(listData, "确定");
                        flag = clickNodeInfo(nodeInfo);
                        AntiyLog.v("点击确定::"+flag);
                        sleep();
                        flag = pressDownBack();
                        AntiyLog.v("点击返回键::"+flag);
                        sleep();
 
                        //点击'权限'按钮,打开'权限详情页面'
                        rootNode = getRootInActiveWindow();
                        listData = rootNode.findAccessibilityNodeInfosByText("权限");
                        AntiyLog.v("============listData==============::"+listData);
                        nodeInfo = findAccessibilityNodeInListByText(listData, "权限");
                        AntiyLog.v("============nodeInfo==============::"+nodeInfo);
                        parentNodeInfo = nodeInfo.getParent();
                        if (parentNodeInfo != null){
                            clickNodeInfo(parentNodeInfo);
                        }
                        sleep();
 
                        // 开启'存储空间'权限
                        rootNode = getRootInActiveWindow();
                        listData = rootNode.findAccessibilityNodeInfosByViewId("android:id/list");
                        nodeInfo = listData.get(0);
                        int size = nodeInfo.getChildCount();
                        for (int i = 0; i < size; i++){
                            AccessibilityNodeInfo childInfo = nodeInfo.getChild(i);
                            if (childInfo != null){
                                listData = childInfo.findAccessibilityNodeInfosByText("存储空间");
                                nodeInfo = findAccessibilityNodeInListByText(listData, "存储空间");
                                if (nodeInfo != null) {
                                    clickNodeInfo(childInfo);
                                }
                            }
                        }
 
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
 
                        // 点击home按钮,同时清除'SharedPreferences'缓存
                        if (pressDownHome()){
                            mEditor.clear().apply();
                        }
                        rootNode.recycle();
                    }
                }
            }
            break;
    }
}

至此,Accessibility辅助自动化测试的初步完成了,各位读者可以根据自己需求写出一整套的测试程序。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rockyou666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值