考虑到部分用户不能很好地使用Android设备,比如由于视力、身体、年龄方面的限制,造成阅读内容、触控操作、声音信息等方面的获取困难,Android提供了Accessibility特性和服务帮助用户更好地使用Android设备。依据Android官方的详细介绍,开发者在增加视图属性如contentDescription等内容后,可以在不修改原有代码逻辑的情况下使用户体验得到优化,如预装在Android 设备上的屏幕阅读器TalkBack,在没有修改系统源码的情况下,满足了视力不足的用户使用Android设备的需求,TalkBack会使用语音反馈描述用户所执行的操作,以及告知用户收到的提醒和通知,可以帮助视力水平较低的用户顺利进行手机的触控、阅读内容的进行。
为了能够实现Accessibility只需要三个步骤:
首先继承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键。
<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注册的服务是固定配置,不可更改。
<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文档。
https://developer.android.com/reference/android/accessibilityservice/AccessibilityService.html
同时,上述的配置在"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);
}
编写此次的自动化测试辅助程序,主要用到了以下几个API
1.AccessibilityEvent的对象的getText(),getEventType()
2.getRootInActiveWindow
3.findAccessibilityNodeInfosByText
4.findAccessibilityNodeInfosByViewId
AccessibilityEvent中的主要的方法:
方法 | 说明 |
---|---|
getEventType() | 事件类型 |
getSource() | 获取事件源对应的结点信息 |
getClassName() | 获取事件源对应类的类型,比如点击事件是有某个Button产生的,那么此时获取的就是Button的完整类名 |
getText() | 获取事件源的文本信息,比如事件是有TextView发出的,此时获取的就是TextView的text属性.如果该事件源是树结构,那么此时获取的是这个树上所有具有text属性的值的集合 |
isEnabled() | 事件源(对应的界面控件)是否处在可用状态 |
getItemCount() | 如果事件源是树结构,将返回该树根节点下子节点的数量 |
该API可以获取当前的窗口的根节点。返回值是AccessibilityNodeInfo对象。通过该对象可以遍历该窗口的所有节点。
注意在使用完毕之后,需要调用"recycle"的方法,释放资源。
顾名思义,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对象。
怎么样知道该控件的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布局策略中学习。
在Android测试中,有时候需要将应用的数据清空。但是,清除数据后,应用的权限都会被关闭。遇到这种情况没办法通过代码中去打开。对于这种情况,采用Accessibility辅助功能,便可以很好的被解决。
打开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);
}
首先给出开启该应用的办法
/**
* 跳转到"应用详细信息"
* @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);
}
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;
}
@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辅助自动化测试的初步完成了,各位读者可以根据自己需求写出一整套的测试程序。