android 辅助功能(无障碍) AccessibilityService 实战入门详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weimingjue/article/details/82744146

本君初入辅助功能也是一头雾水,各种百度结果还是一知半解,得到的大多都是对辅助功能类的翻译,仅仅是理论层面上,到实战上确是千差万别,在此记下以帮助后人。

一.原理:大致简述一下,谷歌已经在view,viewgroup,textview等控件的文字改变、滑动、ui变化埋下了接口,当状态变化时这些控件会回调系统api,系统api然后对这些对象的数据进行组装,为了数据的安全性,系统会重新创建一些对象(AccessibilityEvent、AccessibilityNodeInfo)来间接保存这些数据,然后通过跨进程将这些数据返回给对应的service中。

二.使用范围:首先,辅助功能不可能直接操作外部对象。辅助功能只能在本进程调用指定系统方法,由系统再分发给指定外部对象。辅助功能做的事基本和用户能做的差不多(目前多的就是可以修改text文本了)

三.特别注意事项:AccessibilityEvent、AccessibilityNodeInfo里面的所有set方法均无用(这些方法是系统调用把数据塞进去的),我们能做到只有:get、is、find等获取数据的方法,以及极少的设置操作performAction、dispatchGesture等

前面描述是先做个认知

实战第一步:配置辅助功能的service:


        <service
            android:name="你的service名字(例如博主的HongBaoService)"
            android:enabled="true"
            android:exported="true"
            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="res/xml目录新增一个xml(例如博主@xml/qianghongbao)"/>
        </service>

qianghongbao.xml文件:对应对的象及介绍AccessibilityServiceInfo

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
    android:canPerformGestures="true"
    android:canRetrieveWindowContent="true"
    android:description="@string/hongbao"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm,com.android.systemui"
    />

accessibilityEventTypes:响应的事件类型(单击,长按,滑动,通知等),这里当然是全部事件啦

accessibilityFeedbackType:回显给用户的方式(例如:配置tts引擎,实现发音),辅助嘛...

accessibilityFlags:很关键,你的app需要获取哪些信息:1.flagDefault默认;    2.flagIncludeNotImportantViews显示所有view节点(主要是效率,才会有这个属性,比如LinearLayout嵌套TextView,如果LL均没有任何可交互的属性(比如没点击事件),则这个LL会当做不重要的view来处理.没有flagIncludeNotImportantViews属性时AccessibilityNodeInfo.getChild(0)会直接得到TextView,而不是LL);   3.flagReportViewIds获得view的id;    4.其他flag:flagRequestTouchExplorationMode、flagRequestEnhancedWebAccessibility、flagRequestFilterKeyEvents、flagRetrieveInteractiveWindows具体介绍见AccessibilityServiceInfo类以FLAG_开头的静态常量

canPerformGestures:允许app发送手势(api24以上可以使用手势),肯定是true了

description:描述(会在开启辅助功能页面看到这段文字)

notificationTimeout:响应时间间隔100就好了

packageNames:需要辅助的 app包名(比如博主的只针对 wx和系统桌面),不写表示所有app

这些注释都可以在AccessibilityServiceInfo里面搜索到,建议去看看

第二步:写service类


public class HongBaoService extends AccessibilityService {
    private final String TAG = getClass().getName();
    /**
     * 辅助功能是否启动
     */
    public static boolean mIsStart = false;

    public static HongBaoService mService;

    //初始化
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        Utils.toast("O(∩_∩)O~~\r\n红包锁定中...");//吐司
        mIsStart = true;
        mService = this;
    }

    //实现辅助功能
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
        Utils.toast("(;′⌒`)\r\n红包功能被迫中断");//吐司
        mIsStart = false;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Utils.toast("%>_<%\r\n红包功能已关闭");//吐司
        mIsStart = false;
    }
}

 第三步:在MainActivity中检查并跳转到辅助功能

        if (!HongBaoService.mIsStart) {
                try {
                    mActivity.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
                } catch (Exception e) {
                    mActivity.startActivity(new Intent(Settings.ACTION_SETTINGS));
                    e.printStackTrace();
                }
        }

第四步:选择你的app,打开辅助功能

应该立即就可以看到"红包锁定中..."的提示语

第一个辅助功能项目:

首先贴个自己封装好的Service,都有注释,自己可以先晾着


public class HongBaoService extends AccessibilityService {
    private final String TAG = getClass().getName();
    /**
     * 辅助功能是否启动
     */
    public static boolean mIsStart = false;

    public static HongBaoService mService;

    //初始化
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        Utils.toast("O(∩_∩)O~~\r\n红包锁定中...");
        mIsStart = true;
        mService = this;
    }

    //实现辅助功能
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        //此处抽离了Service,单一实现可以直接调用自己的方法
        AbstractAccessibility.getAbstarct(this).onAccessibilityEvent(event);
    }

    @Override
    public void onInterrupt() {
        Utils.toast("(;′⌒`)\r\n红包功能被迫中断");
        mIsStart = false;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Utils.toast("%>_<%\r\n红包功能已关闭");
        mIsStart = false;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // 公共方法
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * 点击该控件
     *
     * @return true表示点击成功
     */
    public static boolean clickView(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            if (nodeInfo.isClickable()) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                return true;
            } else {
                AccessibilityNodeInfo parent = nodeInfo.getParent();
                if (parent != null) {
                    boolean b = clickView(parent);
                    parent.recycle();
                    if (b) return true;
                }
            }
        }
        return false;
    }

    /**
     * 根据getRootInActiveWindow查找包含当前text的控件
     *
     * @param containsText 只要内容包含就会找到(应该是根据drawText找的)
     */
    @Nullable
    public List<AccessibilityNodeInfo> findViewByContainsText(@NonNull String containsText) {
        AccessibilityNodeInfo info = getRootInActiveWindow();
        if (info == null) return null;
        List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByText(containsText);
        info.recycle();
        return list;
    }

    /**
     * 根据getRootInActiveWindow查找和当前text相等的控件
     *
     * @param equalsText 需要找的text
     */
    @Nullable
    public List<AccessibilityNodeInfo> findViewByEqualsText(@NonNull String equalsText) {
        List<AccessibilityNodeInfo> listOld = findViewByContainsText(equalsText);
        if (Utils.isEmptyArray(listOld)) {
            return null;
        }
        ArrayList<AccessibilityNodeInfo> listNew = new ArrayList<>();
        for (AccessibilityNodeInfo ani : listOld) {
            if (ani.getText() != null && equalsText.equals(ani.getText().toString())) {
                listNew.add(ani);
            } else {
                ani.recycle();
            }
        }
        return listNew;
    }

    /**
     * 根据getRootInActiveWindow查找当前id的控件
     *
     * @param pageName 被查找项目的包名:com.android.xxx
     * @param idName   id值:tv_main
     */
    @Nullable
    public AccessibilityNodeInfo findViewById(String pageName, String idName) {
        return findViewById(pageName + ":id/" + idName);
    }

    /**
     * 根据getRootInActiveWindow查找当前id的控件
     *
     * @param idfullName id全称:com.android.xxx:id/tv_main
     */
    @Nullable
    public AccessibilityNodeInfo findViewById(String idfullName) {
        List<AccessibilityNodeInfo> list = findViewByIdList(idfullName);
        return Utils.isEmptyArray(list) ? null : list.get(0);
    }

    /**
     * 根据getRootInActiveWindow查找当前id的控件集合(类似listview这种一个页面重复的id很多)
     *
     * @param idfullName id全称:com.android.xxx:id/tv_main
     */
    @Nullable
    public List<AccessibilityNodeInfo> findViewByIdList(String idfullName) {
        try {
            AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
            if (rootInfo == null) return null;
            List<AccessibilityNodeInfo> list = rootInfo.findAccessibilityNodeInfosByViewId(idfullName);
            rootInfo.recycle();
            return list;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 只找第一个ClassName
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public AccessibilityNodeInfo findViewByFirstClassName(String className) {
        AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
        if (rootInfo == null) return null;
        AccessibilityNodeInfo info = findViewByFirstClassName(rootInfo, className);
        rootInfo.recycle();
        return info;
    }

    /**
     * 只找第一个ClassName
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public static AccessibilityNodeInfo findViewByFirstClassName(AccessibilityNodeInfo parent, String className) {
        if (parent == null) return null;
        for (int i = 0; i < parent.getChildCount(); i++) {
            AccessibilityNodeInfo child = parent.getChild(i);
            if (child == null) continue;
            if (className.equals(child.getClassName().toString())) {
                return child;
            }
            AccessibilityNodeInfo childChild = findViewByFirstClassName(child, className);
            child.recycle();
            if (childChild != null) {
                return childChild;
            }
        }
        return null;
    }

    /**
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public List<AccessibilityNodeInfo> findViewByClassName(String className) {
        ArrayList<AccessibilityNodeInfo> list = new ArrayList<>();
        AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
        if (rootInfo == null) return list;
        findViewByClassName(list, rootInfo, className);
        rootInfo.recycle();
        return list;
    }

    /**
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public static void findViewByClassName(List<AccessibilityNodeInfo> list, AccessibilityNodeInfo parent, String className) {
        if (parent == null) return;
        for (int i = 0; i < parent.getChildCount(); i++) {
            AccessibilityNodeInfo child = parent.getChild(i);
            if (child == null) continue;
            if (className.equals(child.getClassName().toString())) {
                list.add(child);
            } else {
                findViewByClassName(list, child, className);
                child.recycle();
            }
        }
    }

    /**
     * 只找第一个相等的ContentDescription
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public AccessibilityNodeInfo findViewByFirstEqualsContentDescription(String contentDescription) {
        AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
        if (rootInfo == null) return null;
        AccessibilityNodeInfo info = findViewByFirstEqualsContentDescription(rootInfo, contentDescription);
        rootInfo.recycle();
        return info;
    }

    /**
     * 只找第一个相等的ContentDescription
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public static AccessibilityNodeInfo findViewByFirstEqualsContentDescription(AccessibilityNodeInfo parent, String contentDescription) {
        if (parent == null) return null;
        for (int i = 0; i < parent.getChildCount(); i++) {
            AccessibilityNodeInfo child = parent.getChild(i);
            if (child == null) continue;
            CharSequence cd = child.getContentDescription();
            if (cd != null && contentDescription.equals(cd.toString())) {
                return child;
            }
            AccessibilityNodeInfo childChild = findViewByFirstEqualsContentDescription(child, contentDescription);
            child.recycle();
            if (childChild != null) {
                return childChild;
            }
        }
        return null;
    }

    /**
     * 只找第一个包含的ContentDescription
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public AccessibilityNodeInfo findViewByFirstContainsContentDescription(String contentDescription) {
        AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
        if (rootInfo == null) return null;
        AccessibilityNodeInfo info = findViewByFirstContainsContentDescription(rootInfo, contentDescription);
        rootInfo.recycle();
        return info;
    }

    /**
     * 只找第一个包含的ContentDescription
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public static AccessibilityNodeInfo findViewByFirstContainsContentDescription(AccessibilityNodeInfo parent, String contentDescription) {
        if (parent == null) return null;
        for (int i = 0; i < parent.getChildCount(); i++) {
            AccessibilityNodeInfo child = parent.getChild(i);
            if (child == null) continue;
            CharSequence cd = child.getContentDescription();
            if (cd != null && cd.toString().contains(contentDescription)) {
                return child;
            }
            AccessibilityNodeInfo childChild = findViewByFirstContainsContentDescription(child, contentDescription);
            child.recycle();
            if (childChild != null) {
                return childChild;
            }
        }
        return null;
    }

    /**
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public List<AccessibilityNodeInfo> findViewByContentDescription(String contentDescription) {
        ArrayList<AccessibilityNodeInfo> list = new ArrayList<>();
        AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
        if (rootInfo == null) return list;
        findViewByContentDescription(list, rootInfo, contentDescription);
        rootInfo.recycle();
        return list;
    }

    /**
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public static void findViewByContentDescription(List<AccessibilityNodeInfo> list, AccessibilityNodeInfo parent, String contentDescription) {
        if (parent == null) return;
        for (int i = 0; i < parent.getChildCount(); i++) {
            AccessibilityNodeInfo child = parent.getChild(i);
            if (child == null) continue;
            CharSequence cd = child.getContentDescription();
            if (cd != null && contentDescription.equals(cd.toString())) {
                list.add(child);
            } else {
                findViewByContentDescription(list, child, contentDescription);
                child.recycle();
            }
        }
    }

    /**
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public List<AccessibilityNodeInfo> findViewByRect(Rect rect) {
        ArrayList<AccessibilityNodeInfo> list = new ArrayList<>();
        AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
        if (rootInfo == null) return list;
        findViewByRect(list, rootInfo, rect);
        rootInfo.recycle();
        return list;
    }

    public static Rect mRecycleRect = new Rect();

    /**
     * 此方法效率相对较低,建议使用之后保存id然后根据id进行查找
     */
    public static void findViewByRect(List<AccessibilityNodeInfo> list, AccessibilityNodeInfo parent, Rect rect) {
        if (parent == null) return;
        for (int i = 0; i < parent.getChildCount(); i++) {
            AccessibilityNodeInfo child = parent.getChild(i);
            if (child == null) continue;
            child.getBoundsInScreen(mRecycleRect);
            if (mRecycleRect.contains(rect)) {
                list.add(child);
            } else {
                findViewByRect(list, child, rect);
                child.recycle();
            }
        }
    }

    /**
     * 由于太多,最好回收这些AccessibilityNodeInfo
     */
    public static void recycleAccessibilityNodeInfo(List<AccessibilityNodeInfo> listInfo) {
        if (Utils.isEmptyArray(listInfo)) return;

        for (AccessibilityNodeInfo info : listInfo) {
            info.recycle();
        }
    }
}

 顺便说一下findAccessibilityNodeInfosByViewId方法API>=18才有

常用的操作(核心逻辑就是performAction和dispatchGesture,想了解更多自己看源码,注释很详细)

ddms工具:快速查看手机上的id、text、ContentDescription、坐标等必不可少,studio3.0的打方式:你的Android SDK的目录>tools>monitor.bat(这是mac的,windows自己瞅瞅,sdk在mac中的默认位置:当前user/lib(资源库)/Android/sdk)

performAction:发送点击,发送文本改变,发送获取焦点,发送home、back等具体见AccessibilityNodeInfo类以ACTION_开头静态常量以及注释会告诉你如何使用

dispatchGesture:分发手势,手势可以模拟用户全部操作,如手指点击、滑动。进行中的手势只允许有一个,多次发送手势,之前正在进行的手势会被取消

findAccessibilityNodeInfosByText:只要包含就能招到,并且只要显示在手机上就能找到(比如wx的聊天记录,得不到文本内容,但如果知道文本内容就可以模拟点击),除了密码

findAccessibilityNodeInfosByViewId:参数是com.android.xxx:id/tv_main,必须要带上包名

ContentDescription:被称为描述,webview页或者imageview中经常见到,作用很大见HongBaoService.findViewByFirstEqualsContentDescription及相关方法

通知栏点击操作


    public void onAccessibilityEvent(AccessibilityEvent event) {
        //通知栏,打开红包
        switch (event.getEventType()) {//先判断是否是通知栏红包和转圈圈界面,这两个任何状态都会去点击
            //第一步:监听通知栏消息,拦截通知的红包
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                for (CharSequence text : event.getText()) {
                    String content = text.toString();
                    //收到红包提醒
                    if (content.contains("[微信红包]") || content.contains("[QQ红包]")) {
                        //模拟打开通知栏消息,打开后会有新的广播进入微信或者qq
                        if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
                            HongBaoService.pingUnLock();//开屏,打开屏幕
                            final PendingIntent contentIntent = ((Notification) event.getParcelableData()).contentIntent;
                            //延时的handler(因为开屏有动画)
                            TimeUtil.mHandler.postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    try {
                                        contentIntent.send();
                                    } catch (PendingIntent.CanceledException e) {
                                        e.printStackTrace();
                                    }
                                }
                            }, 500);
                        }
                        break;
                    }
                }
                break;
        }
    }

 设置text文本操作

                    AccessibilityNodeInfo textInfo = service.findViewByFirstClassName(ST_EDITTEXT);//假设只有一个edittext
                    if (textInfo != null) {
                        Bundle arguments = new Bundle();
                        arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "粘贴内容");
                        textInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
                    }

分发手势操作(需要API>=24) 


            //发送一个点击事件
            Path mPath=new Path();//线性的path代表手势路径,点代表按下,封闭的没用
            mPath.moveTo(500, 500);
            service.dispatchGesture(new GestureDescription.Builder().addStroke(new GestureDescription.StrokeDescription(mPath, 手势开始时间比如立即开始0, 手势总时长比如100)).build(), 回调函数(可以为null), 回调的线程(null表示主线程));

 调用手机的返回键,home,最近任务,拉下通知栏

        //AccessibilityService.GLOBAL_ACTION_BACK
        //GLOBAL_ACTION_HOME
        //GLOBAL_ACTION_NOTIFICATIONS
        //GLOBAL_ACTION_RECENTS
        service.performGlobalAction(mAction);

粘贴操作


        AccessibilityNodeInfo idInfo = service.findViewById(mId);
        if (idInfo == null) return;
        ((ClipboardManager) service.getSystemService(Context.CLIPBOARD_SERVICE))
                .setPrimaryClip(ClipData.newPlainText("复制", "复制内容"));
        idInfo.performAction(AccessibilityNodeInfo.FOCUS_INPUT);
        idInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE);//粘贴
        idInfo.recycle();//尽量在最后都回收掉

辅助功能的具体属性及方法我就不多说了,bd上到处都是,随便贴个链接吧:

其他博客翻译:https://www.jianshu.com/p/ef01ce654302

中国信息无障碍产品联盟的翻译:http://www.siaa.org.cn/home/content/share

注意:再重申一遍AccessibilityEvent、AccessibilityNodeInfo里面的所有set方法均无用

转载请注明出处:王能的博客https://blog.csdn.net/weimingjue/article/details/82744146

只能帮你们到这了

猜你喜欢

转载自blog.csdn.net/weimingjue/article/details/82744146