使用AccessibilityService(无障碍服务)自动完成手机设置功能

今天是19年1月30日,又要过年了,也是各个微信、qq群红包狂轰滥炸开启之际。在应用市场搜索微信红包助手,相关功能的app层出不穷。实际上红包助手实现原理就只是一个android官方自带的AccessibilityService(无障碍服务)。感谢安卓的开源精神,百度一下,会不费吹灰之力搜到实现自动抢红包功能的源码,当然,这篇文章不会又再次Crtl+C,crtl+V的介绍一遍。

项目的需求是用户打开手机应用时,必须处于USB调试开启状态。而这通过文字去引导用户如何开启显然是不现实的。用无障碍服务去实现这个需求是当时团队想到的方案之一,不过从结果而言,即使排除软件bug、手机适配的因素,实际的交互体验也是不好的,最后也没采用这个方案。写下这篇文章算是对那段时间工作的一个总结和代码的回顾,不过大半年时间过去了,当时走过的很多坑很多已经忘了。所以呀,今后但凡掌握新的知识,得第一时间记录下来。

1、AccessibilityService使用

1.1 无障碍服务声明和XML配置

一个辅助功能被声明为AndroidManifest.xml中的任何其他服务,但它必须做两件事情:1、指定它处理“android.accessibilitiy.AccessibilityService”意图。2、请求BIND_ACCESSIBILITY_SERVICE权限,以确保只有系统可以绑定到它。如果这些项目中的任一项丢失,系统将忽略辅助功能服务。以下是一个示例声明:


    
    

    
    

    
        
            
                
            
            
        
    

其中,引用的xml配置文件,在res/xml/autodebug.xml中创建的:代码示例如下:



     

1.2 启动服务

需要在无障碍服务列表找到你自己的app,手动打开该项功能。通过下面代码可以打开系统的无障碍功能列表

Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);

1.3 创建服务类

public class AutoDebugService extends AccessibilityService {
    private static String TAG = "AutoDebugService";
    private IAutoDebug mAutoDebug;

    public static final SpHelper SP_HELPER = new SpHelper(App.getContext(), CommonSpConsts.COMMON_SP, MODE_PRIVATE);


    /**
     * 当窗口发生的事件是我们配置监听的事件时,会回调此方法.会被调用多次
     * 

* 其中参数accessibilityEvent封装来自界面相关事件的信息,比如我们可以获得该事件的事件类型,进而根据起类型选择不同的处理方式: * * @param accessibilityEvent */ @Override public void onAccessibilityEvent (final AccessibilityEvent accessibilityEvent) { if (mAutoDebug==null){ return; } mAutoDebug.autoDebug(accessibilityEvent, AutoDebugService.this); } /** * 系统成功绑定该服务时被触发,也就是当你在设置中开启相应的服务, * 系统成功的绑定了该服务时会触发,通常我们可以在这里做一些初始化操作 */ @Override public void onServiceConnected () { ToastUtil.toast("无障碍服务开启"); SP_HELPER.initEditor().putBoolean(CommonSpConsts.ACCESSIBILITY_SERVICE_STATE, true).commit();//无障碍服务已打开 mAutoDebug = AutoDebugFactory.createDebug(); SP_HELPER.initEditor().putBoolean(CommonSpConsts.AUTO_USB_COMPLETE, false).commit(); PackageManager packageManager = getPackageManager(); Intent intent = packageManager.getLaunchIntentForPackage("com.android.settings"); if (intent != null) { startActivity(intent); //打开设置 return; } } /** * 当服务要被中断时调用.会被调用多次 */ @Override public void onInterrupt () { // SP_HELPER.initEditor().putBoolean(CommonSpConsts.ACCESSIBILITY_SERVICE_STATE, false).commit();//无障碍服务已关闭 } @Override public boolean onUnbind (Intent intent) { ToastUtil.toast("无障碍服务关闭"); SP_HELPER.initEditor().putBoolean(CommonSpConsts.ACCESSIBILITY_SERVICE_STATE, false).commit();//无障碍服务已关闭 return super.onUnbind(intent); } /** * 判断无障碍服务是否开启 * * @param mContext * @return */ public static boolean isAccessibilitySettingsOn (String packageName,Context mContext) { int accessibilityEnabled = 0; final String service = packageName + "/" + AutoDebugService.class.getCanonicalName(); try { accessibilityEnabled = Settings.Secure.getInt( mContext.getApplicationContext().getContentResolver(), android.provider.Settings.Secure.ACCESSIBILITY_ENABLED); } catch (Settings.SettingNotFoundException e) { } TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':'); if (accessibilityEnabled == 1) { 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(); if (accessibilityService.equalsIgnoreCase(service)) { return true; } } } } else { } return false; } }

打开usb调试模式,涉及到很多界面的跳转,为了要适配不同品牌手机的不同android版本,所以采取简单工厂模式来设计适配工作的这部分代码。1、首先,在assets目录创建一个json文件放置不同品牌手机的不同android版本手机跳转界面的步骤对应的关键信息;2、再用一个抽象类,加载配置文件,根据具体机型来生成操作步骤;3、产品具体实现类继承这个抽象类并实现产品接口里的方法,在这个方法里完成具体的操作。4、工厂类里,创建不同品牌不同android版本的产品实例。

手动打开无障碍服务开关后,进入onServiceConnected回调,完成具体产品实例创建。mAutoDebug = AutoDebugFactory.createDebug();

工厂类 AutoDebugFactory

public class AutoDebugFactory {

    public static IAutoDebug createDebug() {
        String name = Build.MANUFACTURER;//手机品牌
        String release = android.os.Build.VERSION.RELEASE;//android 版本
        switch (name) {
            case "samsung":
                switch (release){
                    case "6.0.1":
                        return new SamSung();
                }
            case "HUAWEI":
                switch (release){
                    case "7.0":
                        return new HuaWei_P10();
                    case "8.0.0":
                        return new HuaWei_8_0_0();
                }
            case "OPPO":
                switch (release){
                    case "7.1.1":
                        return new OPPO_7_1_1();
                }
            case "vivo":
                switch (release){
                    case "7.0":
                        return new Vivo_7_0();
                }
            case "Xiaomi":
                switch (release){
                    case "7.1.2":
                        return new XiaoMi_7_1_2();
                }
            case "Meizu":
                switch (release){
                    case "7.0":
                        return new MeiZu_7_0();
                }
            default:
                return null;
        }
    }

下面以一款具体手机为例,华为8.0.0android版本。代码如下:

public class HuaWei_8_0_0 extends AbstractAutoDebug implements IAutoDebug {
    private static final String TAG = "HuaWei";
    public static final SpHelper SP_HELPER = new SpHelper(App.getContext(), CommonSpConsts.COMMON_SP, MODE_PRIVATE);
    public HuaWei_8_0_0() {
        super("huawei_8.0.0");
    }

    @Override
    public void autoDebug(AccessibilityEvent accessibilityEvent,AccessibilityService service) {
        boolean autoUSBComplete=SP_HELPER.getBoolean(CommonSpConsts.AUTO_USB_COMPLETE,false);
        if (autoUSBComplete){
            return;
        }
        int eventType = accessibilityEvent.getEventType();
        Log.d(TAG, "onAccessibilityEvent " + eventType);

        if (!Common.StepFlags.stepOneDone) { //第一步,打开 原生设置 界面
            Log.d(TAG, "第一步,打开 原生设置 界面");
            String keyName = mConfig.getSteps().get(0).getKeyName();
            String containerResId = mConfig.getSteps().get(0).getContainerResId();
            int clickNums = mConfig.getSteps().get(0).getClickNums();
            Common.scroll2PositionClick(service,keyName, containerResId, clickNums, Common.StepFlags.STEP_ONE);
        }

        //第二步,打开 关于手机 界面
        if (Common.StepFlags.stepOneDone && !Common.StepFlags.stepTwoDone) {
            Log.d(TAG, "第二步,打开 关于手机 界面");
            String keyName = mConfig.getSteps().get(1).getKeyName();
            String containerResId = mConfig.getSteps().get(1).getContainerResId();
            int clickNums = mConfig.getSteps().get(1).getClickNums();
            if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED || eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED)
            {
                Common.scroll2PositionClick(service,keyName, containerResId, clickNums, Common.StepFlags.STEP_TWO);

            }
        }

        //第三步,点击7次版本号
        if (Common.StepFlags.stepTwoDone && !Common.StepFlags.stepThreeDone) {
            Log.d(TAG, "第三步,点击7次版本号 Common.StepFlags.stepThreeDone: " + Common.StepFlags.stepThreeDone);
            String keyName = mConfig.getSteps().get(2).getKeyName();
            String containerResId = mConfig.getSteps().get(2).getContainerResId();
            int clickNums = mConfig.getSteps().get(2).getClickNums();
            if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED || eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
                Common.scroll2PositionClick(service,keyName, containerResId, clickNums, Common.StepFlags.STEP_THREE);
            }

        }

        //第四步:先判断已经返回到关于手机界面 android 8.0 会弹出输入密码界面
        if (Common.StepFlags.stepThreeDone && !Common.StepFlags.stepFourDone && Common.isShowToast("处于开发者模式", accessibilityEvent)) {
            //第四步,返回到 原生设置 界面
            Log.d(TAG, "第四步,返回到 原生设置 界面");

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
                Common.setStepsFlag(Common.StepFlags.STEP_FOUR);
            }
        }

        //第五步,打开 开发人员选项界面
        if (Common.StepFlags.stepFourDone && !Common.StepFlags.stepFiveDone) {
            Log.d(TAG, "第五步,打开 开发人员选项界面");
            String keyName = mConfig.getSteps().get(4).getKeyName();
            String containerResId = mConfig.getSteps().get(4).getContainerResId();
            int clickNums = mConfig.getSteps().get(4).getClickNums();
            if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
                Common.scroll2PositionClick(service,keyName, containerResId, clickNums, Common.StepFlags.STEP_FIVE);
            }
        }

        //第六步,打开 debug模式
        if (Common.StepFlags.stepFiveDone && !Common.StepFlags.stepSixDone) {
            Log.d(TAG, "第六步,打开 debug模式");
            String keyName = mConfig.getSteps().get(5).getKeyName();
            String containerResId = mConfig.getSteps().get(5).getContainerResId();
            int clickNums = mConfig.getSteps().get(5).getClickNums();
            if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED || eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
                Common.scroll2PositionClick(service,keyName, containerResId, clickNums, Common.StepFlags.STEP_SIX);
            }
        }

        //第七步,点击确定
        if (Common.StepFlags.stepSixDone && !Common.StepFlags.stepSevenDone) {
            Log.d(TAG, "第七步,点击确定");
            String keyName = mConfig.getSteps().get(6).getKeyName();
            String containerResId = mConfig.getSteps().get(6).getContainerResId();
            int clickNums = mConfig.getSteps().get(6).getClickNums();
            if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
                Common.scroll2PositionClick(service,keyName, containerResId, clickNums, Common.StepFlags.STEP_SEVEN);
            }

            if (Common.StepFlags.stepSevenDone) {
                Common.StepFlags.stepOneDone = false;
                Common.StepFlags.stepTwoDone = false;
                Common.StepFlags.stepThreeDone = false;
                Common.StepFlags.stepFourDone = false;
                Common.StepFlags.stepFiveDone = false;
                Common.StepFlags.stepSixDone = false;
                Common.StepFlags.stepSevenDone = false;
                SP_HELPER.initEditor().putBoolean(CommonSpConsts.AUTO_USB_COMPLETE, true).commit();
            }

        }
    }
}

加载配置文件的抽象类 AbstractAutoDebug

public abstract class AbstractAutoDebug {
    AutoDebugConfig.Config mConfig;
    public AbstractAutoDebug(String string) {
        Gson gson = new Gson();
        //将json数据变成字符串
        StringBuilder stringBuilder = new StringBuilder();
        try {
            //获取assets资源管理器
            AssetManager assetManager = App.getContext().getAssets();
            //通过管理器打开文件并读取
            BufferedReader bf = new BufferedReader(new InputStreamReader(
                    assetManager.open("auto_debug_config.json")));
            String line;
            while ((line = bf.readLine()) != null) {
                stringBuilder.append(line);
            }
            AutoDebugConfig config = gson.fromJson(stringBuilder.toString(), AutoDebugConfig.class);
            HashMap map = config.getMap();
            mConfig = map.get(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

assets目录下的auto_debug_config.json文件

{
  "map": {
    "huawei_8.0.0": {
      "openServiceClassName": "",
      "openSettingsClassName": "",
      "stepNums": 0,
      "steps": [
        {
          "keyName": "系统导航、系统更新",
          "containerResId": "com.android.settings:id/dashboard_container",
          "clickNums": 1
        },
        {
          "keyName": "关于手机",
          "containerResId": "com.android.settings:id/list",
          "clickNums": 1
        },
        {
          "keyName": "版本号",
          "containerResId": "com.android.settings:id/list",
          "clickNums": 7
        },
        {
          "keyName": "",
          "containerResId": "",
          "clickNums": 0
        },
        {
          "keyName": "开发人员选项",
          "containerResId": "com.android.settings:id/list",
          "clickNums": 1
        },
        {
          "keyName": "USB调试",
          "containerResId": "com.android.settings:id/list",
          "clickNums": 1
        },
        {
          "keyName": "确定",
          "containerResId": "",
          "clickNums": 1
        }
      ]
    }
    }
}

由于在xml我们设置了监听界面的变化: android:accessibilityEventTypes= “typeAllMask|typeWindowStateChanged|typeNotificationStateChanged”,在打开设置界面后,由于手机界面的变化,所以会进入onAccessibilityEven的回调函数,在回调函数里,我们调用IAutoDebug接口的方法autoDebug()。

1.4 获取节点信息

获取了界面窗口变化后,这个时候就要获取控件的节点。整个窗口的节点本质是个树结构,通过以下操作节点信息

1.4.1 获取窗口节点(根节点)

AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();

1.4.2 获取指定子节点(控件节点)

List item = rootInActiveWindow.findAccessibilityNodeInfosByText(text); //根据关键字查找某元素  
List list = rootInActiveWindow.findAccessibilityNodeInfosByViewId(listId); //根据resource id 查找容器元素;根据关键字查找出的元素在该容器元素中;

1.5 模拟节点点击

获取了节点信息之后,对空间节点进行模拟点击、长按等操作,AccessibilityNodeInfo类提供了performAction()方法让我们执行模拟操作,具体操作可看官方文档介绍,这里列举常用的操作:

  accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);//模拟点击事件
  accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);//触发容器元素的滚动事件

2、打开usb调试功能具体实现

大致过程就是,1、首先通过Android sudio里Android Device Monitor中的选择设备并开启Dump View Hierarchy for UI Automator工具获取当前ui布局树状结构,能获取到每一个控件的id,是否可点击等信息。2、在获取到当前展示窗口后,根据关键字查找需要点击跳转到下一个界面的控件元素,通常是一个textviw。然后根据resource Id 查找滚动容器控件,通常是一个recycleview,再判断需要点击的textview控件是否在当前窗口的recycleview里。若存在,判断此textview控件是否可点击,若可以,触发模拟点击事件;若不可以,获取他的父元素,触发模拟点击事件。如果需要点击的textview控件是不在当前窗口的recycleview里,测触发recycleview的滚动事件,此时再重复步骤2
比如,我们在打开设置界面后,需要点击“系统导航、系统更细、关于手机、语言和输入法”这一textview控件进行下一个界面的跳转,此时我们需要判断这个item是否在手机屏幕当前窗口里,此时是不在的,所以我们要模拟滚动事件,并找到它。再模拟点击事件操作。此时就可以完成一个界面的跳转。
使用AccessibilityService(无障碍服务)自动完成手机设置功能_第1张图片
使用AccessibilityService(无障碍服务)自动完成手机设置功能_第2张图片

完成模拟点击、滚动逻辑的相关代码如下:

public class Common {


    private static final String TAG ="Conmon" ;

    public static class StepFlags {
        public static boolean stepOneDone;
        public static final int STEP_ONE = 0;

        public static boolean stepTwoDone;
        public static final int STEP_TWO = 1;

        public static boolean stepThreeDone;
        public static final int STEP_THREE = 2;

        public static boolean stepFourDone;
        public static final int STEP_FOUR = 3;

        public static boolean stepFiveDone;
        public static final int STEP_FIVE = 4;

        public static boolean stepSixDone;
        public static final int STEP_SIX = 5;

        public static boolean stepSevenDone;
        public static final int STEP_SEVEN = 6;

        public static boolean stepEightDone;
        public static final int STEP_EIGHT = 7;

        public static boolean stepNineDone;
        public static final int STEP_NINE = 8;

    }
    /**
     * 滑动直到控件显示后,触发点击事件
     *
     * @param text   查找的控件显示的内容
     * @param listId 滚动的容器id
     * @param num    触发控件的点击次数
     */
    public   static void scroll2PositionClick(AccessibilityService service,String text, String listId, int num, int stepFlagIndex) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { //必须android4.3以上的版本
            AccessibilityNodeInfo rootInActiveWindow = service.getRootInActiveWindow(); //获取当前展示的窗口
            if (rootInActiveWindow != null) {
                List item = rootInActiveWindow.findAccessibilityNodeInfosByText(text); //根据关键字查找某控件元素
                List list = rootInActiveWindow.findAccessibilityNodeInfosByViewId(listId); //根据resource id 查找容器元素;判断关键字查找出的元素是否在该容器元素中;

                if (item == null || item.size() == 0) { // 关键字元素不存在,则滚动容器元素
                    Log.d(TAG, "不存在 " + text);
                    if (list != null && list.size() > 0) {
                        try {
                            Thread.sleep(200); //隔200 ms 滚动一次
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        list.get(0).performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); //触发容器元素的滚动事件
                        Log.d(TAG, "---- [ " + text + " ] 滚动查找中 ----");
                    }
                } else {
                    Log.d(TAG, "存在 " + text);
                    AccessibilityNodeInfo clickableItem = item.get(0);
                    if (clickableItem.isEnabled() && clickableItem.isClickable()) { //关键字元素存在,则判断它是否可用,是否可点击
                        for (int i = 0; i < num; i++) {
                            clickableItem.performAction(AccessibilityNodeInfo.ACTION_CLICK); //触发点击 num 次
                            Log.d(TAG, "点击: " + text);
                            setStepsFlag(stepFlagIndex);    //更新状态
                        }
                    } else {
                        AccessibilityNodeInfo parent = clickableItem.getParent(); //关键字元素不可用或者不可点击,则直接获取它的父元素
                        if (parent.isEnabled() && parent.isClickable()) {
                            for (int i = 0; i < num; i++) {
                                parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);  //触发点击 num 次
                                Log.d(TAG, "点击parent: " + text);
                                setStepsFlag(stepFlagIndex);
                            }
                        }
                    }
                }
            }
        }
    }
    /**
     * 更新每一步执行状态
     * @param stepsFlagIndex
     */
    public static void setStepsFlag(int stepsFlagIndex) {
        switch (stepsFlagIndex) {
            case StepFlags.STEP_ONE:
                StepFlags.stepOneDone = true;
                break;
            case StepFlags.STEP_TWO:
                StepFlags.stepTwoDone = true;
                break;
            case StepFlags.STEP_THREE:
                StepFlags.stepThreeDone = true;
                break;
            case StepFlags.STEP_FOUR:
                StepFlags.stepFourDone = true;
                break;
            case StepFlags.STEP_FIVE:
                StepFlags.stepFiveDone = true;
                break;
            case StepFlags.STEP_SIX:
                StepFlags.stepSixDone = true;
                break;
            case StepFlags.STEP_SEVEN:
                StepFlags.stepSevenDone = true;
                break;
                case StepFlags.STEP_EIGHT:
                    StepFlags.stepEightDone=true;
            default:
                break;
        }
    }
    /**
     * 判断当前界面是否弹出toast
     * @param msg
     * @param event
     * @return
     */
    public static boolean isShowToast(String msg, AccessibilityEvent event) {
        boolean rel = false;
        //判断是否是通知事件
        if (event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
            rel = false;
        }
        //获取事件具体信息
        Parcelable parcelable = event.getParcelableData();
        //如果是下拉通知栏消息
        if (parcelable instanceof Notification) {
            rel = false;
        } else {
            //其它通知信息,包括Toast
            List texts = event.getText();
            if (texts != null && texts.size() > 0) {
                String toastMsg = (String) texts.get(0);
                if (!TextUtils.isEmpty(toastMsg)) { // toast内容不为空
                    Log.d(TAG, "弹出toast: " + toastMsg);
                    if (toastMsg.contains(msg)) {
                        rel = true;
                    }
                }
            }
        }
        return rel;
    }

}

后面界面的跳转逻辑大致相同,后面的步骤分别是第二步:点击“关于手机”控件;第三步,点击7次版本号;第四步:先判断已经返回到关于手机界面 (android 8.0 会弹出输入密码界面);第五步,打开 开发人员选项界面 ;第六步,打开 debug模式;第七步,点击确定 。 根据具体点击操作方法去一一实现就可以了。

相关的完整demo git地址和gif图演示后续会后续上传。

你可能感兴趣的:(使用AccessibilityService(无障碍服务)自动完成手机设置功能)