onAccessibilityEvent(AccessibilityEvent event)
及onInterruput()
这两个方法是抽象方法,必须重写。
常用API介绍:
- onServiceConnected():做一些初始化的操作
- onInterrupt ():AccessibilityService被中断时会调用,在整个生命周期里会被调用多次。
- onUnbind(intent: Intent):你可以做一些初始化的操作
- onServiceConnected:AccessibilityService将要关闭时会被调用,这个方法做一些释放资源的操作。
- onAccessibilityEvent(event: AccessibilityEvent?):核心API,AccessibilityEvent事件的回调函数,系统通过sendAccessibiliyEvent()方法发送AccessibilityEvent事件到这里
- getRootInActiveWindow():则会返回当前活动窗口的根结点,查找View的时候用到它
- findFoucs(int falg):查找拥有特定焦点类型的控件
- disableSelf():禁用当前服务
2 辅助类的声明与配置
AccessibilityService继承Service,因此也需要在AndroidManifest.xml中声明:
// 代码片段2
注意需要加上BIND_ACCESSIBILITY_SERVICE权限。代码片段2中的meta部分是AccessibilityService的配置信息,这是android 4.0后才支持的,代码如下:
// 代码片段3
<?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”
android:canRetrieveWindowContent=“true”
android:notificationTimeout=“100”
android:packageNames=“com.tencent.mm”/>
设置配置信息还有第二种方法,就是在onServiceConnected()方法中使用代码设置,如代码片段1中的注释部分所示。这里的配置有很多属性,我们只研究其中的6个:
- android:packageNames:指定辅助服务监听哪些应用发出事件,多个应用包名之间用逗号分隔,如果不填,则监听手机上所有应用。例如我们现在要利用辅助点击做app的自动安装功能,取值com.android.packageinstaller。如果只关注微信发出的事件,那么取值com.tencent.mm。
- android:accessibilityEventTypes:辅助服务监听的事件类型,例如TYPE_VIEW_FOCUSED、TYPE_VIEW_CLICKED 、TYPE_WINDOW_STATE_CHANGED、TYPE_NOTIFICATION_STATE_CHANGED等等,如果监听全部事件,就取值typeAllMask
- android:accessibilityFlags:辅助服务额外的flag信息,例如FLAG_REPORT_VIEW_IDS可以使回调的事件带上view的ID。
- android:accessibilityFeedbackType:事件的反馈类型,例如通用反馈FEEDBACK_GENERIC、声音反馈FEEDBACK_AUDIBLE、语音反馈FEEDBACK_SPOKEN等。
- android:notificationTimeout:两个同样类型的监听事件发给辅助类的最小时间间隔
- android:canRetrieveWindowContent:是否可以获取窗口内容,一般设置为true
处理监听到的事件
前面就是使用辅助类的全部了,怎么样,是不是很简单?但是处理监听到的事件就有点麻烦了。我在github上写了一个微信抢红包的的开源项目,代码地址,我结合这个git库的代码解释下如何处理监听事件。
处理事件的入口是onAccessibilityEvent(event: AccessibilityEvent?)方法,我写了一个分发事件的类:DispatchEvent.kt
,里面的方法dispatchEvent(event: AccessibilityEvent?, rootInActiveWindow: AccessibilityNodeInfo?)
负责分发事件,代码如下:
// 代码片段4
fun dispatchEvent(event: AccessibilityEvent?, rootInActiveWindow: AccessibilityNodeInfo?) {
val pkgName = event?.packageName.toString()
val eventType = event?.getEventType()
Log.i(TAG, “pkgName:
p
k
g
N
a
m
e
e
v
e
n
t
T
y
p
e
:
{pkgName} eventType:
pkgNameeventType:{eventType} className:
e
v
e
n
t
?
.
g
e
t
C
l
a
s
s
N
a
m
e
(
)
.
t
o
S
t
r
i
n
g
(
)
"
+
"
e
v
e
n
t
.
t
e
x
t
:
{event?.getClassName().toString()} " + "event.text:
event?.getClassName().toString()"+"event.text:{listToString(event?.text)} event?.getContentChangeTypes()😒{event?.getContentChangeTypes()}\n”)
when (eventType) {
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> com.example.zhouzhihui.accessibilitydemo.access.packet.handleNotification(event)//64 1–>click
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> {//32 2048
val className = event.getClassName().toString()
if (className == “com.tencent.mm.ui.LauncherUI” || className == “com.tencent.mm.ui.mogic.WxViewPager” || className == “android.widget.EditText”/* || className == “android.widget.ListView”*/) {
com.example.zhouzhihui.accessibilitydemo.access.packet.searchPacket(rootInActiveWindow)
} else if (className == “com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI”) {//com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyPrepareUI
com.example.zhouzhihui.accessibilitydemo.access.packet.openPacket(rootInActiveWindow)
} else if (className == “com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI”) {
com.example.zhouzhihui.accessibilitydemo.access.packet.closePacket(rootInActiveWindow)
}
}
}
if (event?.getContentChangeTypes() == AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT) {
Withdraw().withDraw(event, rootInActiveWindow)//防消息撤回
}
rootInActiveWindow?.recycle()//避免重复创建实例通过recycle方法回收掉nodeInfo(我们自己手动去回收)
}
代码片段4事件被分发成四个分流:handleNotification(event: AccessibilityEvent?)、searchPacket(rootInActiveWindow: AccessibilityNodeInfo?)、openPacket(rootInActiveWindow: AccessibilityNodeInfo?)、closePacket(rootInActiveWindow: AccessibilityNodeInfo?),这四个方法的处理逻辑在Packet.kt
类中。
-
handleNotification(event: AccessibilityEvent?)。当eventType == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED == 64的时候执行这个事件流,这个事件表示监听到了通知栏事件,微信处在后台的时候来了聊天消息,就会出发这个事件,我们的方法检测通知内容是否包含为本"[微信红包]",如果包含就表示收到了红包消息,就执行它附带的PendingIntent,然后就会跳到相应的聊天页面。
-
searchPacket(rootInActiveWindow: AccessibilityNodeInfo?)。当eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == 32或者eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED == 2048的时候执行这个事件流。32表示窗口状态发生了变化,比如微信的主页"com.tencent.mm.ui.LauncherUI"从后台调到前台就会触发这个事件,并且它附带的className就是"com.tencent.mm.ui.LauncherUI";2048表示窗口的内容发生了变化,比如你在微信的第一个tab页面,这时候来了个聊天消息,就会触发这个事件,附带的className是android.widget.ListView,嗯,没错,微信竟然还是在用ListView这个过时的组件而不是RecyclerView。我们捕捉到这个事件后调用searchPacket()方法,顾名思义,这个方法要搜索红包并点击。我们传给它的参数通过API
AccessibilityService.getRootInActiveWindow()
获取的,我有点搞不懂这个API和AccessibilityEvent.getSource()
有什么区别,前者是辅助服务调用的,应该是窗口的根节点,后者是监听到的某个事件获取的,应该是这个事件的源节点,我用Log显示大部分时候两者是一致的。searchPacket方法通过递归查找红包,当找到某个节点内容包含“领取红包”就终止递归,然后循环查找这个节点和它的父节点的第一个能够点击的节点,执行点击事件rootInActiveWindow.performAction(AccessibilityNodeInfo.ACTION_CLICK)
就能自动点击红包。 -
openPacket(rootInActiveWindow: AccessibilityNodeInfo?)。条件同上,当eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == 32或者eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED == 2048的时候执行这个事件流。通过上面的searchPacket我们搜索到了红包并点击了,这时会出现红包领取页面,我们这里openPacket方法是要找到领取红包的节点并执行这个节点的点击事件进行领取。关键是如何找到这个节点,一种方法是通过ViewId,API
AccessibilityNodeInfo.getViewIdResourceName()
可以获取这个节点的id,但是你需要事先知道这个节点的id,而且辅助的配置标记必须是android:accessibilityFlags="flagReportViewIds"
才能获取节点的id,可以使用Android Device Monitor或者Layout Inspector查看id,也可以直接把节点的id打印出来进行查看对比,但是微信的程序员经常改变id,我不认为这个方法是可靠的,我的方法是如果满足条件(rootInActiveWindow?.isClickable == true && rootInActiveWindow?.className?.contains("android.widget.Button") == true)
就认为这个节点是领取红包的按钮,然后执行点击事件:rootInActiveWindow?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
。 -
closePacket(rootInActiveWindow: AccessibilityNodeInfo?)。条件同上,当eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == 32或者eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED == 2048的时候执行这个事件流。这个方法是为了找到左上角的返回按钮,进行点击返回聊天页面。这个也不是通过id的方式,而是如果满足
(rootInActiveWindow?.className == "android.widget.LinearLayout" && rootInActiveWindow?.isClickable && TextUtils.isEmpty(rootInActiveWindow?.text))
就认为是左上角的返回节点。
下面贴出代码:
// 代码片段5
fun handleNotification(event: AccessibilityEvent?) {
if (event == null) {
return
}
val texts = event.text
if (!texts.isEmpty()) {
for (text in texts) {
val content = text.toString()
if (content.contains(“[微信红包]”)) {
if (event.parcelableData != null && event.parcelableData is Notification) {
val notification = event.parcelableData as Notification
val pendingIntent = notification.contentIntent
try {
pendingIntent.send()
} catch (e: PendingIntent.CanceledException) {
e.printStackTrace()
}
}
}
}
}
}
fun searchPacket(rootInActiveWindow: AccessibilityNodeInfo?) {
Log.i(TAG, “searchPacket node: ${rootInActiveWindow} childCount: ${rootInActiveWindow?.childCount} idName: ${rootInActiveWindow?.getViewIdResourceName()}”)
if (rootInActiveWindow?.text.toString() == “领取红包”) {
if (rootInActiveWindow?.isClickable == true) {
rootInActiveWindow.performAction(AccessibilityNodeInfo.ACTION_CLICK)
} else {
var parent: AccessibilityNodeInfo? = rootInActiveWindow?.getParent()
while (parent != null) {
if (parent.isClickable) {
parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break
}
parent = parent.parent
}
}
} else {
for (i in 0 until (rootInActiveWindow?.childCount ?: -1)) {
searchPacket(rootInActiveWindow?.getChild(i))
}
}
}
fun openPacket(rootInActiveWindow: AccessibilityNodeInfo?) {
Log.i(TAG, “openPacket node: ${rootInActiveWindow} childCount: ${rootInActiveWindow?.childCount} idName: ${rootInActiveWindow?.getViewIdResourceName()}”)
if (rootInActiveWindow?.isClickable == true && rootInActiveWindow?.className?.contains(“android.widget.Button”) == true) {
rootInActiveWindow?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
// node?.traversalAfter
for (i in 0 until (rootInActiveWindow?.childCount ?: -1)) {
openPacket(rootInActiveWindow?.getChild(i))
}
}
fun closePacket(rootInActiveWindow: AccessibilityNodeInfo?) {
Log.i(TAG, “closePacket node: ${rootInActiveWindow} childCount: ${rootInActiveWindow?.childCount} idName: ${rootInActiveWindow?.getViewIdResourceName()}”)
if (rootInActiveWindow?.className == “android.widget.LinearLayout” && rootInActiveWindow?.isClickable && TextUtils.isEmpty(rootInActiveWindow?.text)) {
//className: android.widget.LinearLayout; text: null; error: null; maxTextLength: -1; contentDescription: null; viewIdResName: com.tencent.mm:id/ho;
rootInActiveWindow?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
return
}
for (i in 0 until (rootInActiveWindow?.childCount ?: -1)) {
closePacket(rootInActiveWindow?.getChild(i))
}
}
此外,在MainActivity里面,还有判断服务是否开启的逻辑,如果没有开启,则可以点击跳转带开启页面:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://img-blog.csdnimg.cn/img_convert/d46be645237999b5a873ab4722d0960b.jpeg)
最后
针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!
往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
oid进阶实践技术,群内还有技术大牛一起讨论交流解决问题。**
[外链图片转存中…(img-rYtCa4KI-1711810206608)]
[外链图片转存中…(img-alm0ZNfQ-1711810206608)]