简单封装AccessibilityService写个库,助力Android自动化

看过杰哥专栏的童鞋,应该都知道 无障碍服务AccessibilityService 的作用:经过APP控制Android设备自动化,不了解的童鞋能够先翻阅下《AccessibilityService根底》。

简单封装AccessibilityService写个库,助力Android自动化

之前百无聊赖的时分顺手写了这个库,本节首要是记载库的完成思路,会涉及到开发无障碍服务常见的一些问题,相信会对需求的读者有协助~


0x1、为什么要写这个库?

  • 时常有APP自动化的需求 → 自己爬数据、做各种APP日常、接单子等;
  • 每次都得CV许多代码 → 没有啥复用性可言,开发功率极低;
  • 急需一个趁手武器 → 利用Kotlin简洁的语法特性,封装常用代码逻辑露出简略API,达到快速开发的目的;

当然,也不是非得自己写个库,也能够用别人封装得比较好的方案,比方 autojs,省时省力好多。自己写库最大的优点便是:可定制,想加什么功用就加什么功用,比方加个截图OCR啥的~


0x2、库设计关键

运用AccessibilityService完结APP自动化三个首要的中心过程如下:

  • 获取页面结点
  • 解析定位结点
  • 触发交互

所以,这个库要做的事情便是围绕着这三步打开,然后封装,接着细化下开发关键~


1、判别无障碍服务是否敞开

常规的判别办法:判别无障碍功用是否可用获取启用的无障碍服务字符串拆分红多个子串迭代判别是否包含咱们服务的包名。代码示例如下:

fun Context.isAccessibilitySettingsOn(clazz: Class<out AccessibilityService?>): Boolean {
    // 判别设备的无障碍功用是否可用
    var accessibilityEnabled = false
    try {
        accessibilityEnabled = Settings.Secure.getInt(
            applicationContext.contentResolver,
            Settings.Secure.ACCESSIBILITY_ENABLED
        ) == 1
    } catch (e: Settings.SettingNotFoundException) {
        e.printStackTrace()
    }
    // 创建一个字符串拆分东西实例
    val mStringColonSplitter = TextUtils.SimpleStringSplitter(':')
    if (accessibilityEnabled) {
        // 获取启用的无障碍服务
        val settingValue: String? = Settings.Secure.getString(
            applicationContext.contentResolver,
            Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
        )
        if (settingValue != null) {
            // 迭代判别是否包含咱们的服务
            mStringColonSplitter.setString(settingValue)
            while (mStringColonSplitter.hasNext()) {
                val accessibilityService = mStringColonSplitter.next()
                if (accessibilityService.equals("${packageName}/${clazz.canonicalName}", ignoreCase = true))
                    return true
            }
        }
    }
    return false
}

但这儿,咱们不选用这种办法,而是经过:界说一个 笼统的AccessibilityService父类 + 单例 来完成:

简单封装AccessibilityService写个库,助力Android自动化

在子类无障碍服务绑定回调 onServiceConnected() 时实例化,在服务销毁回调 onDestroy() 时清空。即假如 instance 有值,阐明现已无障碍功用可用,简略粗暴的判别。

简单封装AccessibilityService写个库,助力Android自动化

别的,这儿的 specificServiceClass 是详细子类无障碍服务的类类型,界说了一个 init() 办法用于初始化,在这儿传入,有些当地要用到。

简单封装AccessibilityService写个库,助力Android自动化

该办法需求在服务启用前调用,比方放到App类中:

简单封装AccessibilityService写个库,助力Android自动化


2、跳转无障碍服务设置页

判别服务未启用,然后跳转设置页授权,没啥好讲的,直接封装个东西办法:

简单封装AccessibilityService写个库,助力Android自动化

然后在父类里界说一个请求无障碍权限的静态办法:

简单封装AccessibilityService写个库,助力Android自动化

简略调用下,在onResume()的时分判别服务状况,别离显示敞开与未敞开的UI:

简单封装AccessibilityService写个库,助力Android自动化

点击敞开的时分,同样判别服务是否敞开,是提示,不然跳转设置页:

简单封装AccessibilityService写个库,助力Android自动化

运转看下作用(部分手机或许不会自动定位咱们的APP,还要再操作下~):

简单封装AccessibilityService写个库,助力Android自动化

作用仍是挺骚气的,继续往下走~


3、获取页面节点信息

有三个可选办法,依次讲解:

AccessibilityEvent.getSource()

获取触发当时 AccessibilityEvent 事情的 源View,比方监听到按钮点击事情,这个办法拿到的便是触发点击的View。

AccessibilityService.getRootInActiveWindow()

获取当时 活动窗口或前台界面的根视图,对应DecorView,经过该视图能够获得界面一切View树来进行遍历操作。

AccessibilityService.getWindows()

获取显示在屏幕上的 一切窗口列表,包含Activity、Dialog、Toast 等窗口,能够 依据窗口ID 或称号获取指定的窗口

一般来说,办法②用得比较多,但在有 悬浮框 的场景(即方针APP不是处于前台),就需求用到办法③了。如依据title获取微信的窗口,并调用getRoot()办法获得它的AccessibilityNodeInfo:

accessibilityService.windows.first { it.title == "微信" }.root

接着是 获取页面节点信息 的时机,一般都是重写 AccessibilityService.onAccessibilityEvent() 办法,按需处理 体系发送过来的Event,这是 被迫 的状况,而有时咱们需求 自动 获取当时页面的结点信息。那该怎么完成呢?一种办法便是 自己结构Event,代码示例:

简单封装AccessibilityService写个库,助力Android自动化

运转后上述代码后,发现Event宣布去了,但却没有回调 onAccessibilityEvent() 办法。由于这个Event是你的App宣布的,而非监听的方针APP(比方微信),还需求进行两项修改:

无障碍服务的装备xml文件 → 添加自己APP的包名

简单封装AccessibilityService写个库,助力Android自动化

event指定packageName名

简单封装AccessibilityService写个库,助力Android自动化

然后就能够收到自己发送的Event啦。别的,由于咱们界说的 FastAccessibilityService 是一个单例,完全能够直接露出一个静态办法,供外部直接调用,直接省去发送Event这一步。


4、解析节点

本质便是 递归遍历View树,这儿界说一个结点包装类,提取节点的常用属性,方便处理:

简单封装AccessibilityService写个库,助力Android自动化

写下遍历节点的办法:

简单封装AccessibilityService写个库,助力Android自动化

解析结点是一种 耗时操作,假如直接在主线程履行,频繁调用或许会引起 界面卡顿,这儿把解析操作丢到线程池里:

简单封装AccessibilityService写个库,助力Android自动化

把结点都保存到list中,方便后续定位结点处理:

简单封装AccessibilityService写个库,助力Android自动化


5、定位节点

这一步的话,便是按需遍历上面解析出来的结点列表,封装了下述可供调用的办法:

/**
 * 依据文本查找结点
 *
 * @param text 匹配的文本
 * @param textAllMatch 文本全匹配
 * @param includeDesc 一起匹配desc
 * @param descAllMatch desc全匹配
 * @param enableRegular 是否启用正则
 * */
findNodeByText(
    text: String,
    textAllMatch: Boolean = false,
    includeDesc: Boolean = false,
    descAllMatch: Boolean = false,
    enableRegular: Boolean = false,
): NodeWrapper? { /*...*/ }
/**
 * 依据文本查找结点列表
 *
 * @param text 匹配的文本
 * @param textAllMatch 文本全匹配
 * @param includeDesc 一起匹配desc
 * @param descAllMatch desc全匹配
 * @param enableRegular 是否启用正则
 * */
fun AnalyzeSourceResult.findNodesByText(
    text: String,
    textAllMatch: Boolean = false,
    includeDesc: Boolean = false,
    descAllMatch: Boolean = false,
    enableRegular: Boolean = false,
): AnalyzeSourceResult { /*...*/ }
/**
 * 依据id查找结点 (含糊匹配)
 * @param ids 结点id,可传入多个
 * */
fun AnalyzeSourceResult.findNodeById(vararg ids: String): NodeWrapper? { /*...*/ }
/**
 * 依据id查找结点列表 (含糊匹配)
 * @param ids 结点id, 可传入多个
 * */
fun AnalyzeSourceResult.findNodesById(vararg ids: String): AnalyzeSourceResult { /*...*/ }
/**
 * 依据传入的表达式成果查找结点
 * @param expression 匹配条件表达式
 * */
fun AnalyzeSourceResult.findNodeByExpression(expression: (NodeWrapper) -> Boolean): NodeWrapper? { /*...*/ }
/**
 * 依据传入的表达式成果查找结点列表
 * @param expression 匹配条件表达式
 * */
fun AnalyzeSourceResult.findNodesByExpression(expression: (NodeWrapper) -> Boolean): AnalyzeSourceResult { /*...*/ }
/**
 * 查找一切文本不为空的结点
 * */
fun AnalyzeSourceResult.findAllTextNode(includeDesc: Boolean = false): AnalyzeSourceResult { /*...*/ }

6、触发交互

节点找到了,接着便是 触发交互 的封装了,先是:performGlobalAction() 全局操作 的相关API:

fun performAction(action: Int) = FastAccessibilityService.require.performGlobalAction(action)
// 回来
fun back() = performAction(AccessibilityService.GLOBAL_ACTION_BACK)
// Home键
fun home() = performAction(AccessibilityService.GLOBAL_ACTION_HOME)
// 最近使命
fun recent() = performAction(AccessibilityService.GLOBAL_ACTION_RECENTS)
// 电源菜单
fun powerDialog() = performAction(AccessibilityService.GLOBAL_ACTION_POWER_DIALOG)
// 告诉栏
fun notificationBar() = performAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS)
// 告诉栏 → 快捷设置
fun quickSettings() = performAction(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS)
// 锁屏
@RequiresApi(Build.VERSION_CODES.P)
fun lockScreen() = performAction(AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN)
// 运用分屏
fun splitScreen() = performAction(AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN)
// 休眠
fun sleep(millis: Long) = Thread.sleep(millis)

然后是 结点的交互操作,现在许多APP都做了防护,调用 节点的performAction() 办法履行点击、滑动等操作,基本是不管用的,这儿运用 手势 来进行模拟。一起加入 随机偏移随机延时 来防止风控检测。

/**
 * 运用手势模拟点击,长按的话,传入的Duration大一些就好,比方1000(1s)
 *
 * @param x 点击坐标点的x坐标
 * @param y 点击坐标点的y坐标
 * @param delayTime 推迟多久进行本次点击,单位毫秒
 * @param duration 模拟接触屏幕的时长(按下到抬起),太短会导致部分运用下点击无效,单位毫秒
 * @param repeatCount 本次点击重复多少次,必须大于0
 * @param randomPosition 点击方位随机偏移间隔,用于反检测
 * @param randomTime 在随机参数上加减延时时长,有助于防止点击器检测,单位毫秒
 *
 * */
fun click(
    x: Int,
    y: Int,
    delayTime: Long = 0,
    duration: Long = 200,
    repeatCount: Int = 1,
    randomPosition: Int = 0,
    randomTime: Long = 0
) { /*...*/ }
/**
 * 运用手势模拟滑动
 *
 * @param startX 滑动起始坐标点的x坐标
 * @param startY 滑动起始坐标点的y坐标
 * @param endX 滑动结尾坐标点的x坐标
 * @param endY 滑动结尾坐标点的y坐标
 * @param duration 滑动持续时间,一般在300~500ms作用会好一些,太快会导致滑动不可用
 * @param repeatCount 滑动重复次数
 * @param randomPosition 点击方位随机偏移间隔,用于反检测
 * @param randomTime 在随机参数上加减延时时长,有助于防止点击器检测,单位毫秒
 * */
fun swipe(
    startX: Int,
    startY: Int,
    endX: Int,
    endY: Int,
    duration: Long = 1000L,
    repeatCount: Int = 1,
    randomPosition: Int = 0,
    randomTime: Long = 0
) { /*...*/ }
/* 向前、向后滑动一段间隔 */
fun NodeWrapper?.forward(isForward: Boolean = true) { /*...*/ }
fun NodeWrapper?.backward() = forward(false)
/* 文本填充 */
fun NodeWrapper?.input(content: String) { /*...*/ }

7、前台服务

保活,2333,懂的都懂,便是在Notification显示一个前台服务,直接封装显示和关闭前台服务的API

简单封装AccessibilityService写个库,助力Android自动化

简单封装AccessibilityService写个库,助力Android自动化

简略调用下:

简单封装AccessibilityService写个库,助力Android自动化

运转后看看作用:

简单封装AccessibilityService写个库,助力Android自动化


8、写一个解析页面节点的悬浮框

定位结点,需求知道方针节点的要素:id、文本等,能够用 《AccessibilityService根底-节点查找》 里提到的会集办法来分析。而这儿能够直接写一个悬浮框,点击就解析当时页面节点,直接在Logcat输出。

简单封装AccessibilityService写个库,助力Android自动化

运转后会呈现一个小的悬浮框:

简单封装AccessibilityService写个库,助力Android自动化

点击悬浮框后,logcat会输出一切TextView的节点信息:

简单封装AccessibilityService写个库,助力Android自动化

舒畅~


0x3、简略运用示例:朋友圈自动点赞

逻辑很明晰:找到并点击定位的图标 → 点击赞 → 往上滑动,直接写出代码~

简单封装AccessibilityService写个库,助力Android自动化

库全体运用起来仍是十分简略的,当然完成得比较 粗糙,后续有时间再渐渐优化,感兴趣的能够先Star → CpFastAccessibility

简单封装AccessibilityService写个库,助力Android自动化