安卓辅助功能(无障碍)AccessibilityService实战介绍

简要介绍

AccessibilityService是安卓平台上提供的无障碍服务,用于帮助残障人士使用手机,不过通过此功能可以完成很多事情.可以进行模拟界面操作,如点击界面上某个按钮等.

使用详细步骤

  1. 增加service定义
    在onAccessibilityEvent函数中进行相应操作(安卓界面有变化时,都会调用此函数)
class MyAccessibilityService : AccessibilityService() {
    private val TAG = MyAccessibilityService::class.java.simpleName
    private var mContext: Context? = null

    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "onCreate")
        mContext = applicationContext
        AccessibilityOperator.getInstance().init(this)
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        return Service.START_STICKY
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        AccessibilityOperator.getInstance().updateEvent(event)
        val packageName = AccessibilityOperator.getInstance().rootNodeInfo?.packageName?.toString()
//        pasteToEditTextContent(packageName)
        var accessibilityService = AccessibilityOperator.getInstance()
        //按下返回键
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_BACK)
        //向下拉出状态栏
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS)
        //向下拉出状态栏并显示出所有的快捷操作按钮
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS)
        //按下HOME键
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_HOME)
        //显示最近任务
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_RECENTS)
        //长按电源键
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_POWER_DIALOG)
        //分屏
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN)
        //锁屏(安卓9.0适用)
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN)
        //截屏(安卓9.0适用)
//        accessibilityService.performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT)
        //打开快速设置
        accessibilityService.performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS)
    }

    /**
     * 修改EditText输入框内容。
     * 下面样例修改了QQ搜索输入框内容。
     */
    private fun changeEditTextContent(packageName: String?) {
        getNodeToOperate(packageName)?.let {
            val arguments = Bundle()
            arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "被无障碍服务修改啦")
            it.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
        }
    }



    /**
     * 读取剪贴板内容,粘贴到EditText输入框。
     * 下面样例修改了QQ搜索输入框内容。
     */
    private fun pasteToEditTextContent(packageName: String?) {
        getNodeToOperate(packageName)?.let {
            it.performAction(AccessibilityNodeInfo.FOCUS_INPUT)
            it.performAction(AccessibilityNodeInfo.ACTION_PASTE)
            it.recycle()
        }
    }

    private fun getNodeToOperate(packageName: String?): AccessibilityNodeInfo? {
        if (packageName != null && packageName == "com.tencent.mobileqq") {
            val nodes = AccessibilityOperator.getInstance().findNodesById("com.tencent.mobileqq:id/et_search_keyword")
            if (nodes != null && nodes.isNotEmpty()) {
                return nodes[0]
            }
        }
        return null
    }

    override fun onInterrupt() {
    }

}
  1. 在Manifest文件中增加service定义
<application>
        ......
        <service
            android:name=".MyAccessibilityService"
            android:enabled="true"
            android:exported="true"
            android:label="@string/accessibility_service_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>
    </application>
  1. AccessibilityOperator工具类
class AccessibilityOperator private constructor() {
    private var mAccessibilityEvent: AccessibilityEvent? = null
    private var accessibilityService: AccessibilityService? = null

    val rootNodeInfo: AccessibilityNodeInfo?
        get() {
            var nodeInfo: AccessibilityNodeInfo? = null
            accessibilityService?.let {
                nodeInfo = accessibilityService!!.rootInActiveWindow
            }

            if (nodeInfo == null && mAccessibilityEvent != null) {
                nodeInfo = mAccessibilityEvent!!.source
            }

            return nodeInfo
        }

    fun init(service: AccessibilityService) {
        accessibilityService = service
    }

    fun updateEvent(event: AccessibilityEvent) {
        mAccessibilityEvent = event
    }

    /**
     * 根据Text搜索所有符合条件的节点, 模糊搜索方式
     */
    fun findNodesByText(text: String): List<AccessibilityNodeInfo>? {
        val nodeInfo = rootNodeInfo
        return nodeInfo?.findAccessibilityNodeInfosByText(text)
    }

    /**
     * 根据View的ID搜索符合条件的节点,精确搜索方式;
     * 这个只适用于自己写的界面,因为ID可能重复
     *
     * @param viewId
     */
    fun findNodesById(viewId: String): List<AccessibilityNodeInfo>? {
        val nodeInfo = rootNodeInfo
        return nodeInfo?.findAccessibilityNodeInfosByViewId(viewId)
    }

    fun clickByText(text: String): Boolean {
        return performClick(findNodesByText(text))
    }

    fun clickParentByText(text: String, depth: Int): Boolean {
        return this.performClick(this.findParentNodesByText(text, depth))
    }

    fun clickParentById(viewId: String, depth: Int): Boolean {
        return this.performClick(this.findParentNodesById(viewId, depth))
    }

    fun findParentNodesByText(text: String, depth: Int): List<AccessibilityNodeInfo> {
        val rootNodeInfo = this.rootNodeInfo
        val resultNodeList = mutableListOf<AccessibilityNodeInfo>()
        if (rootNodeInfo != null) {
            val nodeList = findAccessibilityNodeInfosByText(rootNodeInfo, text)
            val iterator = nodeList.iterator()

            while (iterator.hasNext()) {
                val accessibilityNodeInfo = iterator.next() as AccessibilityNodeInfo
                resultNodeList.add(getParentNode(accessibilityNodeInfo, depth))
            }
        }

        return resultNodeList
    }

    fun findParentNodesById(viewId: String, depth: Int): List<AccessibilityNodeInfo> {
        val rootNodeInfo = this.rootNodeInfo
        val resultNodeList = mutableListOf<AccessibilityNodeInfo>()
        if (rootNodeInfo != null) {
            val nodeList = rootNodeInfo.findAccessibilityNodeInfosByViewId(viewId)
            val iterator = nodeList.iterator()

            while (iterator.hasNext()) {
                val accessibilityNodeInfo = iterator.next() as AccessibilityNodeInfo
                resultNodeList.add(this.getParentNode(accessibilityNodeInfo, depth))
            }
        }

        return resultNodeList
    }

    private fun findAccessibilityNodeInfosByText(node: AccessibilityNodeInfo?, text: String?): List<AccessibilityNodeInfo> {
        val resultNodeList = mutableListOf<AccessibilityNodeInfo>()
        if (node != null && text != null) {
            val nodeList = node.findAccessibilityNodeInfosByText(text)
            if (nodeList != null && !nodeList.isEmpty()) {
                val iterator = nodeList.iterator()
                while (iterator.hasNext()) {
                    val nodeInList = iterator.next() as AccessibilityNodeInfo
                    if (TextUtils.equals(nodeInList.text, text)) {
                        resultNodeList.add(nodeInList)
                    }
                }
            }

            return resultNodeList
        } else {
            return resultNodeList
        }
    }

    private fun getParentNode(nodeInfo: AccessibilityNodeInfo, depth: Int): AccessibilityNodeInfo {
        var resultNodeInfo = nodeInfo

        for (i in 0 until depth) {
            val parentNode = resultNodeInfo.parent
            resultNodeInfo = parentNode
        }

        return resultNodeInfo
    }

    /**
     * 根据View的ID搜索符合条件的节点,精确搜索方式;
     * 这个只适用于自己写的界面,因为ID可能重复
     *
     * @param viewId
     * @return 是否点击成功
     */
    fun clickById(viewId: String): Boolean {
        return performClick(findNodesById(viewId))
    }

    private fun performClick(nodeInfoList: List<AccessibilityNodeInfo>?): Boolean {
        if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
            var node: AccessibilityNodeInfo
            for (i in nodeInfoList.indices) {
                node = nodeInfoList[i]
                // 进行模拟点击
                if (node.isEnabled) {
                    return node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                }
            }
        }
        return false
    }

    fun clickBackKey(): Boolean {
        return performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
    }

    fun performGlobalAction(action: Int): Boolean {
        return accessibilityService!!.performGlobalAction(action)
    }

    private fun getNodeInfo(nodeInfo: AccessibilityNodeInfo?): String {
        var result = ""
        if (nodeInfo != null) {
            result = nodeInfo.className.toString() + ";text:" + nodeInfo.text + ";id:" + nodeInfo.viewIdResourceName + ";"
        }
        return result
    }

    fun clickTextParent(text: String): Boolean {
        val nodeInfo = rootNodeInfo
        return nodeInfo?.let { clickTextParent(it, text) } ?: false
    }

    private fun clickTextParent(rootInfo: AccessibilityNodeInfo?, text: String): Boolean {
        if (rootInfo != null && !TextUtils.isEmpty(rootInfo.className)) {
            if ("android.widget.TextView" == rootInfo.className.toString()) {
                if (!TextUtils.isEmpty(rootInfo.text) && rootInfo.text.toString().startsWith(text)) {
                    val result = performClick(rootInfo.parent)
                    Log.v(TAG, rootInfo.parent.className.toString() + ":result=" + result)

                    return result
                }
            }
            for (i in 0 until rootInfo.childCount) {
                val result = clickTextParent(rootInfo.getChild(i), text)
                if (result) {
                    return result
                }
            }
            return false
        }
        return false
    }

    private fun performClick(targetInfo: AccessibilityNodeInfo): Boolean {
        return targetInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK)
    }

    companion object {
        private val TAG = "AccessibilityOperator"
        val instance = AccessibilityOperator()
    }
}

Demo源代码

https://gitee.com/cxyzy1/accessibilityDemo

安卓开发技术分享: https://www.jianshu.com/p/442339952f26
更多技术总结好文,请关注:「程序园中猿」

《安卓辅助功能(无障碍)AccessibilityService实战介绍》

    原文作者:程序园中猿
    原文地址: https://www.jianshu.com/p/e8b569cd7de9
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞