从零开始安卓无障碍服务Accessibility

从零开始无障碍服务



前言

以前安卓root权限很容易获取的时候,可以写一些日常工作批处理的助手工具,而现在的安卓手机权限管理越来越严,root权限越来越难获取,于是就开始使用安卓自带的无障碍服务来实现自己的操作了,虽然也有限制,但是总体的操作也是很符合预期。本文将从零编写一个无障碍服务的工具,不适合安卓初学者,请小白自行百度详细。


一、新建项目-选择Empty Activity

Minimun SDK我直接选择API 26,省去一些杂七杂八的问题,读者有更低版本兼容需求的话,请自行填坑。

在这里插入图片描述

二、新建BaseService类和AccessService类

1. BaseService类

BaseService继承AccessibilityService,里面封装了一些常用的查找、定位、手势方法。

代码如下:

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.GestureDescription;
import android.content.Context;
import android.content.Intent;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;


/*
 * 基类,封装了查找定位、点击、手势方法
 * */
public class BaseService extends AccessibilityService {

    private AccessibilityManager mAccessibilityManager;

    private Context mContext;

    private static BaseService mInstance;

    public void init(Context context) {
        mContext = context.getApplicationContext();
        mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);

    }

    public static BaseService getInstance() {
        if (mInstance == null) {
            mInstance = new BaseService();
        }
        return mInstance;
    }

    /**
     * Check当前辅助服务是否启用
     *
     * @param serviceName serviceName
     * @return 是否启用
     */
    public boolean checkAccessibilityEnabled(String serviceName) {
        List<AccessibilityServiceInfo> accessibilityServices =
                mAccessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
        for (AccessibilityServiceInfo info : accessibilityServices) {
            if (info.getId().equals(serviceName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 前往开启辅助服务界面
     */
    public void goAccess() {
        Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);
    }

    /**
     * 模拟点击事件,如果该node不能点击,则点击父node,将点击事件一直向父级传递,直至到根node或者找到一个可以点击的node
     *
     * @param nodeInfo nodeInfo
     */
    public void performViewClick(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo == null) {
            return;
        }
        while (nodeInfo != null) {
            if (nodeInfo.isClickable()) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                break;
            }
            nodeInfo = nodeInfo.getParent();
        }
    }

    /**
     * 模拟返回操作
     */
    public void performBackClick() {
        performGlobalAction(GLOBAL_ACTION_BACK);
    }

    /**
     * 模拟下滑操作
     */
    public void performScrollBackward() {
        performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
    }

    /**
     * 模拟上滑操作
     */
    public void performScrollForward() {
        performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);

    }

    /**
     * 查找对应文本的View,无论该node能不能点击
     *
     * @param text text
     * @return View
     */
    public AccessibilityNodeInfo findViewByText(String text) {
        AccessibilityNodeInfo viewByText = findViewByText(text, true);
        if (viewByText == null) {
            viewByText = findViewByText(text, false);
        }
        return viewByText;
    }

    /**
     * 查找对应文本的View
     *
     * @param text      text
     * @param clickable 该View是否可以点击
     * @return View
     */
    public AccessibilityNodeInfo findViewByText(String text, boolean clickable) {
        AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();
        if (accessibilityNodeInfo == null) {
            return null;
        }

        List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text);
        if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
            for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
                if (nodeInfo != null && (nodeInfo.isClickable() == clickable)) {
                    return nodeInfo;
                }
            }
        }
        return null;
    }

    /**
     * 查找对应ID的View
     *
     * @param id id
     * @return View
     */
    public AccessibilityNodeInfo findViewByID(String id) {
        AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();
        if (accessibilityNodeInfo == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id);
        if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
            Log.d("dd", "findViewByID: " + nodeInfoList.size());
            for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
                if (nodeInfo != null) {
                    Log.d("dd", "findViewByID: " + nodeInfo.toString());
                    return nodeInfo;
                }
            }
        }
        return null;
    }


    /**
     * 点击对应文本的一个view,前提是这个view能够点击,即 clickable == true,
     *
     * @param text 要查找的文本
     */
    public void clickViewByText(String text) {
        AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();
        if (accessibilityNodeInfo == null) {
            return;
        }
        List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text);
        if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
            for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
                if (nodeInfo != null) {
                    performViewClick(nodeInfo);
                    break;
                }
            }
        }
    }

    /**
     * 点击对应id的一个view,前提是这个view能够点击,即 clickable == true,
     *
     * @param id 要查找的id
     */
    public void clickViewByID(String id) {
        AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();
        if (accessibilityNodeInfo == null) {
            return;
        }
        List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id);
        if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
            for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
                if (nodeInfo != null) {
                    performViewClick(nodeInfo);
                    break;
                }
            }
        }
    }

    /**
     * 递归遍历node及其子node,点击文本相同的节点,全点击
     *
     * @param text
     * @param parentNode
     */
    public void clickNodesByText(String text, AccessibilityNodeInfo parentNode) {
        if (parentNode == null) {
            return;
        }

        int childCount = parentNode.getChildCount();
        if (childCount == 0) {  //叶节点
            if (parentNode.getText() == null) {
                return;
            }
            if (!text.equals(parentNode.getText().toString())) {
                return;
            }
            Rect rect = new Rect();
            parentNode.getBoundsInScreen(rect);

            int moveToX = (rect.left + rect.right) / 2;
            int moveToY = (rect.top + rect.bottom) / 2;
            int lineToX = (rect.left + rect.right) / 2;
            int lineToY = (rect.top + rect.bottom) / 2;

            gesture(moveToX, moveToY, lineToX, lineToY, 100L, 400L);
            return;
        }

        for (int i = 0; i < childCount; i++) {
            AccessibilityNodeInfo child = parentNode.getChild(i);
            clickNodesByText(text, child);
        }
    }

    /**
     * 根据文本查找节点
     *
     * @param text 要查找的文本
     * @return 与文本相同的节点列表,找不到则返回空
     */
    public List<AccessibilityNodeInfo> findNodesByText(String text) {
        List<AccessibilityNodeInfo> accessibilityNodeInfos = new ArrayList<>();
        Stack<AccessibilityNodeInfo> nodeStack = new Stack<>();
        AccessibilityNodeInfo node = getRootInActiveWindow();

        nodeStack.add(node);
        while (!nodeStack.isEmpty()) {
            node = nodeStack.pop();
            if (node != null && node.getText() != null && node.getText().toString().equals(text)) {
                accessibilityNodeInfos.add(node);
            }

            if (node == null || node.getChildCount() == 0) {
                continue;
            }

            //获得节点的子节点,对于二叉树就是获得节点的左子结点和右子节点
            int childCount = node.getChildCount();
            for (int i = 0; i < childCount; i++) {
                AccessibilityNodeInfo child = node.getChild(i);
                if (child != null) {
                    nodeStack.push(child);
                }
            }
        }
        if (accessibilityNodeInfos.size() > 0) {
            return accessibilityNodeInfos;
        } else {
            return null;
        }

    }

    /**
     * 模拟输入,低版本的输入有所不同,读者请自行百度
     *
     * @param nodeInfo nodeInfo
     * @param text     text
     */
    public void inputText(AccessibilityNodeInfo nodeInfo, String text) {
        Bundle arguments = new Bundle();
        arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
        nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
    }

    /**
     * 手势操作,因为path不能小于0,因此小于则直接返回,不操作,另外如果有需求,可以自行修改小于则设置为0或者屏幕的宽高
     *
     * @param moveToX
     * @param moveToY
     * @param lineToX
     * @param lineToY
     * @param startTime
     * @param duration
     */
    public void gesture(int moveToX, int moveToY, int lineToX, int lineToY, long startTime, long duration) {

        if (moveToX < 0 || moveToY < 0 || lineToX < 0 || lineToY < 0) {
            Log.e("path", "path nagative");
            return;
        }

        GestureDescription.Builder builder = new GestureDescription.Builder();
        Path path = new Path();
        path.moveTo(moveToX, moveToY);
        path.lineTo(lineToX, lineToY);
        GestureDescription gestureDescription = builder
                .addStroke(new GestureDescription.StrokeDescription(path, startTime, duration, false))
                .build();
        dispatchGesture(gestureDescription, new AccessibilityService.GestureResultCallback() {
            @Override
            public void onCompleted(GestureDescription gestureDescription) {
                super.onCompleted(gestureDescription);
            }

            @Override
            public void onCancelled(GestureDescription gestureDescription) {
            }
        }, new Handler(Looper.getMainLooper()));
    }

    protected void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) { }

    @Override
    public void onInterrupt() { }

    @Override
    protected void onServiceConnected() { super.onServiceConnected(); }
}

2. AccessService类

AccessService则继承BaseService,具体的无障碍处理逻辑都在这个类里面实现。

代码如下:

import android.view.accessibility.AccessibilityEvent;

/**
 * 操作类,在这里实现具体逻辑
 */
public class AccessService extends BaseService {

    private String appPackageName = "xxx.xxx.xxx";

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        String packageName = event.getPackageName() == null ? "" : event.getPackageName().toString();

        if (!packageName.equals(appPackageName)) {// 如果活动APP不是目标APP则不响应
            return;
        }
        int eventType = event.getEventType();

        switch (eventType) {
            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:// 捕获窗口内容改变事件
                break;
            default:
                break;
        }
    }
}

三、修改AndroidManifest.xml

1. 添加AccessibilityService配置

在application节点下添加service节点,为程序配置AccessibilityService的属性

内容如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.anheimoxin.access">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!--  添加AccessibilityService配置    -->
        <service
            android:name="com.anheimoxin.access.service.AccessService"
            android:label="暗黑魔心的无障碍服务"
            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/allocation" />
        </service>

    </application>

</manifest>

2. 添加allocation.xml配置文件

在res目录下新建xml文件夹,并在新建的xml目录下新建一个名为allocation.xml的文件

文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowContentChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagRequestEnhancedWebAccessibility|flagReportViewIds"
    android:canPerformGestures="true"
    android:canRetrieveWindowContent="true"
    android:canRequestFilterKeyEvents="true"
    android:canRequestEnhancedWebAccessibility="true"
    android:notificationTimeout="300" />

文件结构如下图:
在这里插入图片描述


四、修改MainActivity类

修改MainActivity类,让程序打开后就检查检查是否开启无障碍服务并跳转到设置页面。

代码如下:

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

import com.anheimoxin.access.service.BaseService;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        BaseService instance = BaseService.getInstance();
        instance.init(this);
        if (!instance.checkAccessibilityEnabled("暗黑魔心的无障碍服务")) {
            instance.goAccess();
        }
    }
}

项目当前的运行效果如下:
请添加图片描述

五、实现1+2+3=6

万变不离其宗,这里就举个简单的例子,实现打开计算器后自动点击1+2+3=6的操作。

1. 获取APP包名

首先要获取到计算器的包名,可以将手机设置调试模式连接到电脑,使用ADB指令获取。

查看当前活动的APP的包名的指令如下:

adb logcat | findstr Displayed

在这里插入图片描述

2. 查找并点击1、2、3、+、=

获取到了包名就可以直接操作了,很简单的实现,先查找定位,然后就可以点击了。如果要详细分析APP的布局,可以使用android studio自带的工具UI Automator Viewer,路径在SDK文件夹下面的tools/bin/中

在这里插入图片描述
在这里插入图片描述

实现代码如下:

import android.graphics.Rect;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import java.util.List;

/**
 * 操作类,在这里实现具体逻辑
 */
public class AccessService extends BaseService {

    private String appPackageName = "com.huawei.calculator";
    private boolean refresh = true; // 控制在未处理完逻辑前不要进入逻辑空间

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        String packageName = event.getPackageName() == null ? "" : event.getPackageName().toString();

        if (!packageName.equals(appPackageName)) {// 如果活动APP不是目标APP则不响应
            return;
        }
        int eventType = event.getEventType();

        switch (eventType) {
            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:// 捕获窗口内容改变事件
                if (packageName.equals(appPackageName)) {
                    if (refresh) {
                        refresh = false;

                        AccessibilityNodeInfo nodeOne = findViewByText("1");
                        if (nodeOne != null) {
                            performViewClick(nodeOne);
                            sleep(500);
                        }

                        // 有些view是没有text的,就可以通过ID、类名等属性来获取
                        AccessibilityNodeInfo nodeAdd = findViewByID("com.huawei.calculator:id/op_add");
                        if (nodeAdd != null) {
                            performViewClick(nodeAdd);
                            sleep(500);
                        }

                        // 查找所有的2,并点击
                        List<AccessibilityNodeInfo> nodeOneList = findNodesByText("2");
                        if (nodeOneList != null && nodeOneList.size() != 0) {
                            for (int i = 0; i < nodeOneList.size(); i++) {
                                AccessibilityNodeInfo node = nodeOneList.get(i);
                                if (node != null) {
                                    Rect rect = new Rect();
                                    node.getBoundsInScreen(rect);

                                    int moveToX = (rect.left + rect.right) / 2;
                                    int moveToY = (rect.top + rect.bottom) / 2;
                                    int lineToX = (rect.left + rect.right) / 2;
                                    int lineToY = (rect.top + rect.bottom) / 2;

                                    // 有些View是不能点击,这时候可以用手势来处理
                                    gesture(moveToX, moveToY, lineToX, lineToY, 100L, 400L);
                                    sleep(500);
                                }
                            }
                        }

                        nodeAdd = findViewByID("com.huawei.calculator:id/op_add");
                        if (nodeOne != null) {
                            performViewClick(nodeAdd);
                            sleep(500);
                        }

                        // getRootInActiveWindow返回整个view的root节点,深度优先遍历查找所有的3,并点击
                        clickNodesByText("3", getRootInActiveWindow());
                        sleep(500);

                        AccessibilityNodeInfo nodeEq = findViewByID("com.huawei.calculator:id/eq");
                        if (nodeOne != null) {
                            performViewClick(nodeEq);
                            sleep(500);
                        }

                        // 更多的操作请看BaseService,或者自行百度

                        refresh = true;
                    }
                }
                break;
            default:
                break;
        }
    }
}

3. 运行效果

至此整个教程的内容就完了,如果有疑问也可以扫码关注我的微信公众号与我联系,下面就是本次项目的效果动图、微信公众号二维码和源码下载链接

请添加图片描述
需要项目源码的话,可关注公众号【编程杂绪】回复【安卓无障碍服务源码】获取百度云盘链接,或者点击此处下载

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

编程杂绪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值