【无USB连接、无ROOT】Android AccessibleService自动化测试实战(一)

0.127字数 1689阅读 796

背景:

说起Android自动化测试,相信有不少童鞋都会心头“隐隐作痛”,做这个选择也多,麻烦也多,貌似暂时就没有一个非常趁手的工具或框架可以一步到位,无论是基于Instrumentation的实现的Robotium还是跨平台的Appium,抑或android推荐的UiAutomator2.0和Espresso,或多或少都有一些局限,或许以后自动化测试领域真要靠TensorFlow来大放异彩……
那么今天为什么要提到AccessibleService呢?查看源码你会发现,其实UiAutomator2.0就是基于AccessibleService来实现的,可是上面的种种测试框架要么不支持Web要么就是不可拔插USB或需要ROOT(可能是android基于安全性考虑,就没开放出来“全方位”的接口),这总是限制了某些自动化需求的实现,所以有时候不得已还得用起了android的AccessibleService辅助服务。

需求是这样的:我想用一个客户端应用来测试(操作或控制)另一个应用,不插线,不root,真正实现test in anytime,test in anywhere……上面提到的或市面上已存在的诸多框架貌似都不能实现吧?(如果你知道还存在其他变态框架谢请留言告诉我)。

了解AccessibleService:

辅助功能(AccessibilityService)其实是一个Android系统提供给的一种服务,本身是继承Service类的。这个服务提供了增强的用户界面,旨在帮助残障人士或者可能暂时无法与设备充分交互的人们。
简单的说AccessibilityService就是一个运行在你手机上的后台监控服务,当你监控的内容(比如点击、滑动、页面变化等)发生改变时,就会调用后台服务的回调方法。AccessibilityService服务运行在后台中,通过AccessibilityEvent接收指定事件的回调,这样的事件表示用户在界面中的一些状态转换,例如:焦点改变,点击,界面滑动,界面改变等。因此,我们可以使用AccessibilityService的监控-回调功能将一些重复或简单的人工操作进行自动化处理,从而将双手从无聊繁琐的重复操作中解放出来。

AccessibleService的应用场景:

AccessibleService应用的场景可以很广,理论上手机上的绝大部分基于View的操作事件都可以监控到,目前最出名的应用莫过于自动抢微信或QQ红包的红包助手了。那么AccessibleService在自动化测试方面为什么没有推广开来呢?对于深度和复杂的自动化测试,其他它并不是很擅长,因为基于监控事件回调这种干扰比较大,不太容易控制,此外AccessibleService提供的查找对象方法有限,API中只提供了两种根据id和text来匹配,对于自动化测试没有精准好用的对象定位是一个很麻烦的事情,所以这也限制了它被直接应用在自动化测试上。但是简单明确的自动化测试需求还是可以胜任的,所以在实施自动化测试之前,前期的工具和框架预研也是不可或缺的步骤。

AccessibleService实战:

1、原理:

从开发者的角度看,AccessibleService就是提供了一些查找界面元素和模拟用户操作的功能,比如模拟点击、输入、滑动、HOME/BACK键等。
实现一个辅助功能服务要求继承AccessibilityService类并实现它的抽象方法。自定义一个服务类MyAccessibilityService(这个命名可以随意),继承系统的AccessibilityService并覆写onAccessibilityEvent和onInterrupt方法。编写好服务类之后,在系统配置文件(AndroidManifest.xml)中注册服务。完成前面两个步骤就完成了基本发辅助功能服务注册与配置,具体的功能实现需要在onAccessibilityEvent中完成,根据onAccessibilityEvent回调方法传递过来的AccessibilityEvent对象可以对事件进行过滤,结合AccessibilitySampleService本身提供的查找节点与模拟点击相关的接口即可实现权限节点的查找与点击。

2、实战应用:

下面简单介绍下AccessibleService的配置和应用搭建:
1> 创建自定义辅助功能服务类,覆写onAccessibilityEvent和onInterrupt方法

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

import com.accessibility.utils.AccessibilityLog;
public class MyAccessibilityService extends AccessibilityService {

    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        //除了xml配置文件,还可以在这里初始化配置
        AccessibilityServiceInfo serviceInfo = getServiceInfo();
        serviceInfo.flags |= AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
        // 响应事件的类型,这里是全部的响应事件(长按,单击,滑动等)
        serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
        // 反馈给用户的类型,这里是语音提示
        //serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
        // 过滤的包名
        List<String> packages = MyApplication.appPackageInfos;
        //可以设置多个监听应用,不设置默认监听全部应用
        String[] array = new String[]{"abc.com","alany.com"};
        serviceInfo.packageNames = array;
        setServiceInfo(serviceInfo);
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // 此方法是在主线程中回调过来的,所以消息是阻塞执行的
        // 获取包名
        String pkgName = event.getPackageName().toString();
        // AccessibilityOperator封装了辅助功能的界面查找与模拟点击事件等操作
        AccessibilityOperator.getInstance().updateEvent(this, event);
        AccessibilityLog.printLog("eventType: " + eventType + " pkgName: " + pkgName);
        switch (event.getEventType()) {
                case AccessibilityEvent.TYPE_VIEW_CLICKED:
                    break;
                case AccessibilityEvent.TYPE_VIEW_SCROLLED:
                    break;
                case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
                    break;
                case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                    break;
                case AccessibilityEvent.TYPE_WINDOWS_CHANGED:                
                    //事件触发时对应的自动化测试操作在这里实现
                    break;
                case AccessibilityEvent.TYPE_VIEW_FOCUSED:

                    break;
                case AccessibilityEvent.TYPE_VIEW_LONG_CLICKED:

                    break;
                case AccessibilityEvent.TYPE_VIEW_SELECTED:

                    break;
                case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:

                    break;
                case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:

                    break;
                case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:

                    break;
                case AccessibilityEvent.TYPE_ANNOUNCEMENT:

                    break;
                case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:

                    break;
                case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:

                    break;
                case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:

                    break;
                case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED:

                    break;
            }
    }

    @Override
    public void onInterrupt() {

    }
}

2> 在清单文件注册辅助功能服务

<service
    android:name=".service.MyAccessibilityService"
    android:label="@string/app_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/accessibility_service_config" />
</service>

3> 设置相关配置,比如上面的@xml/accessibility_service_config文件
各个配置字段的含义可以去查API的解释说明,这里就不一一列举了。

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:description="@string/accessibility_service_description"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:notificationTimeout="100"
    android:accessibilityFlags="flagIncludeNotImportantViews"
    android:canRetrieveWindowContent="true"
    android:canRequestTouchExplorationMode="true"
    android:canRequestEnhancedWebAccessibility="true"
    tools:ignore="UnusedAttribute"/>

4> 跳转到系统辅助功能页面,开启辅助功能服务
完成上面步骤之后,运行起来辅助功能服务就注册成功了,在系统辅助功能页面就能找到这个服务,但是默认是关闭的,也就是说,这个服务要开始为我们服务,还需要去系统界面开启那个开关。下面是跳转到辅助功能页面的代码,跳转过去之后,手动点击开关按钮。开关打开之后,这个辅助功能服务就开始工作了,系统开始回调onAccessibilityEvent方法。我们可以在onAccessibilityEvent方法中处理查找节点与点击操作。
这里我就模拟通过一个按钮去触发和判断,如果已经开启辅助服务,就直接打开目标应用,没有打开就跳转到辅助服务设置界面,下面我给出onClick()时的跳转逻辑:

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.btnStart:
            if (isAccessibilitySettingsOn(mContext)) {
                startApp();
            } else {
                Toast.makeText(mContext, "辅助服务未打开,请在设置中打开:辅助功能->无障碍->Reply Assistant", Toast.LENGTH_SHORT).show();
            }
            break;
        default:
            break;
    }
}

private boolean isAccessibilitySettingsOn(Context mContext) {
    int accessibilityEnabled = 0;
    final String service = getPackageName() + "/" + ReplyAccessibilityService.class.getCanonicalName();
    try {
        accessibilityEnabled = Settings.Secure.getInt(
                mContext.getApplicationContext().getContentResolver(),
                Settings.Secure.ACCESSIBILITY_ENABLED);
        Log.v(TAG, "accessibilityEnabled = " + accessibilityEnabled);
    } catch (Settings.SettingNotFoundException e) {
        Log.e(TAG, "Error finding setting, default accessibility to not found: "
                + e.getMessage());
    }
    TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');

    if (accessibilityEnabled == 1) {
        Log.v(TAG, "***ACCESSIBILITY IS ENABLED*** -----------------");
        String settingValue = Settings.Secure.getString(
                mContext.getApplicationContext().getContentResolver(),
                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
        if (settingValue != null) {
            mStringColonSplitter.setString(settingValue);
            while (mStringColonSplitter.hasNext()) {
                String accessibilityService = mStringColonSplitter.next();

                Log.v(TAG, "-------------- > accessibilityService :: " + accessibilityService + " " + service);
                if (accessibilityService.equalsIgnoreCase(service)) {
                    Log.v(TAG, "We've found the correct setting - accessibility is switched on!");
                    return true;
                }
            }
        }
    } else {
        Log.v(TAG, "***ACCESSIBILITY IS DISABLED***");
    }

    return false;
}

就这样应用的框架就搭好了,但这只是一个开始,具体的测试实现和测试代码组织还有不少工作需要做,我在AccessibleService的基础上做了进一步的封装和实现,方便更好的调用和处理,这些内容就在下一篇再来讲解。敬请期待!同时欢迎大家转发和分享~

原文来自下方公众号,转载请联系作者,并务必保留出处。
想第一时间看到更多原创技术好文和资料,请关注公众号:测试开发栈

推荐阅读更多精彩内容