AccessibilityService

/ 0评 / 1

前言

AccessibilityService是一种辅助服务,大谷歌开发出来的初衷原本是为了方便身体不便的用户更好的使用Android系统,比如为视力不好的用户朗读屏幕上面的文字等,在2013年之前,使用这个的开发者还是很少的,到了2013年被国人发现以后,一下子就打开了新世界的大门,什么免root自动安装,免root清理进程,其中最为人所知的就是自己抢红包了。还有一大堆的黑产总之AccessibilityService在国内的开发者手上慢慢的就走偏了,当然啦,这里就不再讨论技术之外的东西了,下面就来介绍下AccessibilityService的使用。

AccessibilityService的使用

第一步

首先新建一个类继承自AccessibilityService,然后有以下几个方法会被系统自动调用,当服务被启动的时候系统会调用onServiceConnected(),当服务在运行的过程中,系统会根据监听的消息回调onAccessibilityEvent() onInterrupt()这两个方法。当服务停止的时候,系统会回调onUnbind() 方法,其中onServiceConnected()方法和onUnbind()方法分别对应的是Service中生命周期中的两个回调,关于更多可以查看:Android四大组件之Service,这篇博客。

主要函数介绍:

onAccessibilityEvent():(必须覆写)这个方法会被系统调用当匹配你设置的过滤条件的时候,关于过滤条件在下面再说

onInterrupt():(必须覆写)这个方法表示系统想要打断刚才发送过来的反馈,通常是用户移动焦点到不同的控件上了,这个方法在AccessibilityService的生命周期中可能会被调用多次。

第二步

既然AccessibilityService是一种服务,那么就需要在AndroidManifest.xml文件中配置。固定格式如下:

<service
    android:name=".MyService"
    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="@xml/accessibilityservice" />
</service>

和普通Service的不同是增加了权限android:permission=”android.permission.BIND_ACCESSIBILITY_SERVICE”方便系统绑定,二是meta-data中指定此服务为AccessibilityService,两者缺一不可。

第三步:配置AccessibilityService,在第二步中我们可以看到meta-data中有一个android:resource属性指向了一个xml文件,这个xml中也就是我们的配置文件了。首先在res/下新建一个xml文件夹,然后新建一个accessible_service_config.xml文件(文件名不是必须这样的)。然后我们就可以在里面开始配置了。下面给出一个模板,然后再来介绍字段含义。

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:accessibilityFlags="flagReportViewIds"
    android:canPerformGestures="true"
    android:canRetrieveWindowContent="true"
    android:description="@string/html_desc"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.wework"
    android:settingsActivity="xyz.monkeytong.hongbao.activities.SettingsActivity"
    android:summary="简短说明" />

关于以上属性的取值大家可以查看sdk安装目录下sdk/docs/reference/android/accessibilityservice/AccessibilityServiceInfo.html中的介绍。

这里我简单介绍下几个属性的使用

android:packageNames:主要是指定你想监听的应用的包名,比如微信的就是com.tencent.mm,如果你想监听多个应用,那么用逗号分隔开不同包名即可。如果不指定,那么就是全部的消息

android:settingsActivity:主要指定设置此服务的activity,当设置了这个属性以后,在AccessibilityService服务开启界面下面就一个设置按钮,点击此按钮就可以打开指定的activity了。

android:canPerformGestures:表示可以执行滚动等操作

android:canRetrieveWindowContent:可以获取活动界面的内容

android:accessibilityFlags="flagReportViewIds":可以获取界面上控件的id

android:canTakeScreenshot:是否可以手动截图

AccessibilityService的生命周期

AccessibilityService的生命周期是由系统控制的,当用户选择开启的时候,系统会启动AccessibilityService,并调用它的onServiceConnected()方法,当用户关闭或者AccessibilityService自己调用了disableSelf(),那么AccessibilityService就会关闭,并且系统会回调onUnbind() 方法.

获取当前系统中AccessibilityService信息

获取AccessibilityService信息需要使用到AccessibilityManager,下面这段代码就是打印出系统中已经安装了的AccessibilityService信息。输出信息为应用包名和Service在包名下的位置。

AccessibilityManager manager = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
List<AccessibilityServiceInfo> installedAccessibilityServiceList = manager.getInstalledAccessibilityServiceList();
for (AccessibilityServiceInfo info : installedAccessibilityServiceList) {
    String id = info.getId();
    Log.i(TAG, "onAccessibilityEvent: id = " + id);
}
output:
onAccessibilityEvent: id = com.google.android.marvin.talkback/.TalkBackService

AccessibilityManager一共提供了两个方法用来检测系统的所有的快捷服务和启用的快捷服务。

List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackTypeFlags)

List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList()

常用方法

按键

//执行全局动作,api16以上可用
performGlobalAction(GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN)//切换到分屏
performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS)//打开快速设置,暂时不知道这个有什么用
performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS)//打开通知栏
performGlobalAction(GLOBAL_ACTION_BACK)//模拟back键
performGlobalAction(GLOBAL_ACTION_HOME)//模拟home键
performGlobalAction(GLOBAL_ACTION_RECENTS)//模拟最近任务键
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG)//打开电源键长按对话框
performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN)//锁屏
performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT)//截图

滑动点击

//执行手势,api24以上可用
val path = Path()
//手指的坐标
path.moveTo(100f, 1000f)
//连缀1个addStroke()表示单点触控
//连缀2个addStroke()表示2点触控
//单击的话clickTime就设为1,长按就设为500,单位是毫秒
var clickTime=500
val build = GestureDescription.Builder()
    .addStroke(GestureDescription.StrokeDescription(path, 0, clickTime))
    .build()
dispatchGesture(build, object : GestureResultCallback() {
    override fun onCompleted(gestureDescription: GestureDescription?) {
        super.onCompleted(gestureDescription)
        Log.i(TAG, "onCompleted: ")
    }

    override fun onCancelled(gestureDescription: GestureDescription?) {
        super.onCancelled(gestureDescription)
        Log.i(TAG, "onCancelled: ")
    }
}, null)

如果需要滑动的话,就直接给Path对象添加路径记录

val path = Path()
//起点
path.moveTo(100f, 1000f)
//终点,可以添加多个
path.lineTo(200f,2000f)

需要设置权限android:canPerformGestures="true"

获取界面上的控件

//根据Text获取控件
fun checkIsEmpty(event: AccessibilityEvent?): Boolean {
    val byText = event?.source?.findAccessibilityNodeInfosByText("手慢了,红包派完了")
    if (!byText.isNullOrEmpty()) {
        performGlobalAction(GLOBAL_ACTION_BACK)
        return true
    }
    return false
}

//根据id获取控件
fun findAndOpen(event: AccessibilityEvent?): Boolean {
    val chatRootNode =
        event?.source?.findAccessibilityNodeInfosByViewId("com.tencent.wework:id/gsv")
    //.....
    return false
}

我们可以通过id或者text获取指定的控件,不过对于界面上的,其实我们可以直接读取所有的控件以及id,便于编写

override fun onAccessibilityEvent(event: AccessibilityEvent?) {
    if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
        //通过getRootInActiveWindow获取整个界面的根布局
        recycle(rootInActiveWindow)
    }
}

//打印所有的控件
fun recycle(info: AccessibilityNodeInfo, depth: Int = 0) {
    val bounds = Rect()
    info.getBoundsInScreen(bounds)
    Log.i(TAG, "${getSpace(depth)}widget:" + info.className)
    Log.i(TAG, "${getSpace(depth)}bounds:" + bounds)
    Log.i(TAG, "${getSpace(depth)}isClickable:" + info.isClickable)
    Log.i(TAG, "${getSpace(depth)}isLongClickable:" + info.isLongClickable)
    Log.i(TAG, "${getSpace(depth)}Text:" + info.text)
    Log.i(TAG, "${getSpace(depth)}windowId:" + info.windowId)
    Log.i(TAG, "${getSpace(depth)}id:" + info.viewIdResourceName)
    Log.i(TAG, "------------------------------------")
    for (i in 0 until info.childCount) {
        if (info.getChild(i) != null) {
            recycle(info.getChild(i), depth + 1)
        }
    }
}

private fun getSpace(depth: Int): String {
    val sb = StringBuffer()
    for (i in 0 until depth) {
        sb.append("    ")
    }
    return sb.toString()
}

小技巧:当我们使用id或者Text获取到的控件无法点击的时候,可以使用info.parent.isClickable获取父控件,然后判断是否可以点击,当然,我们也可以直接获取到控件在屏幕的位置,然后直接使用手势去点击

示例:XX抢红包

private const val TAG = "MyService"

private const val MAIN_UI = "com.tencent.wework.launch.WwMainActivity"
private const val CHAT_LIST = "com.tencent.wework.msg.controller.MessageListActivity"
private const val RED_UI =
    "com.tencent.wework.enterprise.redenvelopes.controller.RedEnvelopeCollectorWithCoverActivity"
private const val RED_RESULT_UI =
    "com.tencent.wework.enterprise.redenvelopes.controller.RedEnvelopeDetailWithCoverActivity"

class MyService : AccessibilityService() {

    @RequiresApi(Build.VERSION_CODES.N)
    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
            if (findAndOpen(event)) {
                return
            }
        }
        if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            if (event.className == RED_UI) {
                if (!checkIsEmpty(event)) {
                    val byText =
                        event.source?.findAccessibilityNodeInfosByViewId("com.tencent.wework:id/i78")
                    if (!byText.isNullOrEmpty()) {
                        byText[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
                    }
                }
            }
            if (event.className == RED_RESULT_UI) {
                performGlobalAction(GLOBAL_ACTION_BACK)
            }
        }
    }

    fun checkIsEmpty(event: AccessibilityEvent?): Boolean {
        val byText = event?.source?.findAccessibilityNodeInfosByText("手慢了,红包派完了")
        if (!byText.isNullOrEmpty()) {
            performGlobalAction(GLOBAL_ACTION_BACK)
            return true
        }
        return false
    }

    fun findAndOpen(event: AccessibilityEvent?): Boolean {
        val chatRootNode =
            event?.source?.findAccessibilityNodeInfosByViewId("com.tencent.wework:id/gsv")

        if (!chatRootNode.isNullOrEmpty()) {
            chatRootNode.forEach {
                val redTip = it.findAccessibilityNodeInfosByViewId("com.tencent.wework:id/ir5")
                if (redTip.isNotEmpty()) {
                    val notClickRed =
                        it.findAccessibilityNodeInfosByViewId("com.tencent.wework:id/i7_")
                    if (notClickRed.isEmpty()) {
                        it.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                        return true
                    }
                }
            }
        }
        return false
    }

    override fun onInterrupt() {
    }
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注