輔助功能(AccessibilityService)是一個Android系統(tǒng)提供的一種服務(wù)怠硼,繼承自Service類鬼贱。AccessibilityService運行在后臺,能夠監(jiān)聽系統(tǒng)發(fā)出的一些事件(AccessibilityEvent)香璃,這些事件主要是UI界面一系列的狀態(tài)變化这难,比如按鈕點擊、輸入框內(nèi)容變化增显、焦點變化等等雁佳,查找當(dāng)前窗口的元素并能夠模擬點擊等事件脐帝。官方文檔
這個系統(tǒng)功能主要為一些殘障人士用戶設(shè)計,他們由于各種原因比如視力糖权、年齡堵腹、身體等因素導(dǎo)致使用Android設(shè)備困難。但是很多android開發(fā)者用這個功能來做一些不正常的操作星澳,當(dāng)然這種極客精神疚顷,只要不非法,我不認(rèn)為是錯誤的禁偎。
開始使用
AccessibilityService使用非常非常簡單腿堤。
1 首先新建一個類MyAccessibilityService并繼承AccessibilityService
代碼如下:
// 代碼片段1
class MyAccessibilityService : AccessibilityService() {
override fun onInterrupt() {
}
override fun onServiceConnected() {
super.onServiceConnected()
// val serviceInfo = AccessibilityServiceInfo()
// serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK//typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged
// serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC//feedbackGeneric
// serviceInfo.packageNames = arrayOf("com.tencent.mm")//com.tencent.mm
// serviceInfo.notificationTimeout = 100
// serviceInfo.flags = AccessibilityServiceInfo.DEFAULT
//// //android:canRetrieveWindowContent="true"
//// serviceInfo.canRetrieveWindowContent = true
// setServiceInfo(serviceInfo)
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
dispatchEvent(event, rootInActiveWindow)
}
}
onAccessibilityEvent(AccessibilityEvent event)
及onInterruput()
這兩個方法是抽象方法,必須重寫如暖。
常用API介紹:
-
onServiceConnected()
:做一些初始化的操作 -
onInterrupt ()
:AccessibilityService被中斷時會調(diào)用笆檀,在整個生命周期里會被調(diào)用多次。 -
onUnbind(intent: Intent)
:你可以做一些初始化的操作 -
onServiceConnected
:AccessibilityService將要關(guān)閉時會被調(diào)用盒至,這個方法做一些釋放資源的操作酗洒。 -
onAccessibilityEvent(event: AccessibilityEvent?)
:核心API,AccessibilityEvent事件的回調(diào)函數(shù)枷遂,系統(tǒng)通過sendAccessibiliyEvent()方法發(fā)送AccessibilityEvent事件到這里 -
getRootInActiveWindow()
:則會返回當(dāng)前活動窗口的根結(jié)點樱衷,查找View的時候用到它 -
findFoucs(int falg)
:查找擁有特定焦點類型的控件 -
disableSelf()
:禁用當(dāng)前服務(wù)
2 輔助類的聲明與配置
AccessibilityService繼承Service,因此也需要在AndroidManifest.xml中聲明:
// 代碼片段2
<service
android:name=".access.MyAccessibilityService"
android:enabled="true"
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/accessibility"/>
</service>
注意需要加上BIND_ACCESSIBILITY_SERVICE權(quán)限酒唉。代碼片段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"/>
設(shè)置配置信息還有第二種方法痪伦,就是在onServiceConnected()方法中使用代碼設(shè)置侄榴,如代碼片段1中的注釋部分所示。這里的配置有很多屬性流妻,我們只研究其中的6個:
-
android:packageNames
:指定輔助服務(wù)監(jiān)聽哪些應(yīng)用發(fā)出事件牲蜀,多個應(yīng)用包名之間用逗號分隔,如果不填绅这,則監(jiān)聽手機上所有應(yīng)用涣达。例如我們現(xiàn)在要利用輔助點擊做app的自動安裝功能,取值com.android.packageinstaller证薇。如果只關(guān)注微信發(fā)出的事件度苔,那么取值com.tencent.mm。 -
android:accessibilityEventTypes
:輔助服務(wù)監(jiān)聽的事件類型浑度,例如TYPE_VIEW_FOCUSED寇窑、TYPE_VIEW_CLICKED 、TYPE_WINDOW_STATE_CHANGED箩张、TYPE_NOTIFICATION_STATE_CHANGED等等甩骏,如果監(jiān)聽全部事件窗市,就取值typeAllMask -
android:accessibilityFlags
:輔助服務(wù)額外的flag信息,例如FLAG_REPORT_VIEW_IDS可以使回調(diào)的事件帶上view的ID饮笛。 -
android:accessibilityFeedbackType
:事件的反饋類型咨察,例如通用反饋FEEDBACK_GENERIC、聲音反饋FEEDBACK_AUDIBLE、語音反饋FEEDBACK_SPOKEN等。 -
android:notificationTimeout
:兩個同樣類型的監(jiān)聽事件發(fā)給輔助類的最小時間間隔 -
android:canRetrieveWindowContent
:是否可以獲取窗口內(nèi)容胖腾,一般設(shè)置為true
處理監(jiān)聽到的事件
前面就是使用輔助類的全部了,怎么樣媒役,是不是很簡單?但是處理監(jiān)聽到的事件就有點麻煩了宪迟。我在github上寫了一個微信搶紅包的的開源項目酣衷,代碼地址,我結(jié)合這個git庫的代碼解釋下如何處理監(jiān)聽事件次泽。
處理事件的入口是onAccessibilityEvent(event: AccessibilityEvent?)方法鸥诽,我寫了一個分發(fā)事件的類:DispatchEvent.kt
,里面的方法dispatchEvent(event: AccessibilityEvent?, rootInActiveWindow: AccessibilityNodeInfo?)
負(fù)責(zé)分發(fā)事件箕憾,代碼如下:
// 代碼片段4
fun dispatchEvent(event: AccessibilityEvent?, rootInActiveWindow: AccessibilityNodeInfo?) {
val pkgName = event?.packageName.toString()
val eventType = event?.getEventType()
Log.i(TAG, "pkgName:${pkgName} eventType:${eventType} className:${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()//避免重復(fù)創(chuàng)建實例通過recycle方法回收掉nodeInfo(我們自己手動去回收)
}
代碼片段4事件被分發(fā)成四個分流:handleNotification(event: AccessibilityEvent?)
、searchPacket(rootInActiveWindow: AccessibilityNodeInfo?)
拳昌、openPacket(rootInActiveWindow: AccessibilityNodeInfo?)
袭异、closePacket(rootInActiveWindow: AccessibilityNodeInfo?)
,這四個方法的處理邏輯在Packet.kt
類中炬藤。
handleNotification(event: AccessibilityEvent?)
御铃。當(dāng)eventType == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED == 64的時候執(zhí)行這個事件流,這個事件表示監(jiān)聽到了通知欄事件沈矿,微信處在后臺的時候來了聊天消息上真,就會出發(fā)這個事件,我們的方法檢測通知內(nèi)容是否包含為本"[微信紅包]"羹膳,如果包含就表示收到了紅包消息睡互,就執(zhí)行它附帶的PendingIntent,然后就會跳到相應(yīng)的聊天頁面陵像。searchPacket(rootInActiveWindow: AccessibilityNodeInfo?)
就珠。當(dāng)eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == 32或者eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED == 2048的時候執(zhí)行這個事件流。32表示窗口狀態(tài)發(fā)生了變化醒颖,比如微信的主頁"com.tencent.mm.ui.LauncherUI"從后臺調(diào)到前臺就會觸發(fā)這個事件妻怎,并且它附帶的className就是"com.tencent.mm.ui.LauncherUI";2048表示窗口的內(nèi)容發(fā)生了變化泞歉,比如你在微信的第一個tab頁面逼侦,這時候來了個聊天消息匿辩,就會觸發(fā)這個事件,附帶的className是android.widget.ListView榛丢,嗯铲球,沒錯,微信竟然還是在用ListView這個過時的組件而不是RecyclerView涕滋。我們捕捉到這個事件后調(diào)用searchPacket()方法睬辐,顧名思義,這個方法要搜索紅包并點擊宾肺。我們傳給它的參數(shù)通過APIAccessibilityService.getRootInActiveWindow()
獲取的溯饵,我有點搞不懂這個API和AccessibilityEvent.getSource()
有什么區(qū)別,前者是輔助服務(wù)調(diào)用的锨用,應(yīng)該是窗口的根節(jié)點丰刊,后者是監(jiān)聽到的某個事件獲取的,應(yīng)該是這個事件的源節(jié)點增拥,我用Log顯示大部分時候兩者是一致的啄巧。searchPacket方法通過遞歸查找紅包,當(dāng)找到某個節(jié)點內(nèi)容包含“領(lǐng)取紅包”就終止遞歸掌栅,然后循環(huán)查找這個節(jié)點和它的父節(jié)點的第一個能夠點擊的節(jié)點秩仆,執(zhí)行點擊事件rootInActiveWindow.performAction(AccessibilityNodeInfo.ACTION_CLICK)
就能自動點擊紅包。openPacket(rootInActiveWindow: AccessibilityNodeInfo?)
猾封。條件同上澄耍,當(dāng)eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == 32或者eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED == 2048的時候執(zhí)行這個事件流。通過上面的searchPacket我們搜索到了紅包并點擊了晌缘,這時會出現(xiàn)紅包領(lǐng)取頁面齐莲,我們這里openPacket方法是要找到領(lǐng)取紅包的節(jié)點并執(zhí)行這個節(jié)點的點擊事件進行領(lǐng)取。關(guān)鍵是如何找到這個節(jié)點磷箕,一種方法是通過ViewId选酗,APIAccessibilityNodeInfo.getViewIdResourceName()
可以獲取這個節(jié)點的id,但是你需要事先知道這個節(jié)點的id岳枷,而且輔助的配置標(biāo)記必須是android:accessibilityFlags="flagReportViewIds"
才能獲取節(jié)點的id芒填,可以使用Android Device Monitor或者Layout Inspector查看id,也可以直接把節(jié)點的id打印出來進行查看對比空繁,但是微信的程序員經(jīng)常改變id氢烘,我不認(rèn)為這個方法是可靠的,我的方法是如果滿足條件(rootInActiveWindow?.isClickable == true && rootInActiveWindow?.className?.contains("android.widget.Button") == true)
就認(rèn)為這個節(jié)點是領(lǐng)取紅包的按鈕家厌,然后執(zhí)行點擊事件:rootInActiveWindow?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
播玖。closePacket(rootInActiveWindow: AccessibilityNodeInfo?)
。條件同上饭于,當(dāng)eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == 32或者eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED == 2048的時候執(zhí)行這個事件流蜀踏。這個方法是為了找到左上角的返回按鈕维蒙,進行點擊返回聊天頁面。這個也不是通過id的方式果覆,而是如果滿足(rootInActiveWindow?.className == "android.widget.LinearLayout" && rootInActiveWindow?.isClickable && TextUtils.isEmpty(rootInActiveWindow?.text))
就認(rèn)為是左上角的返回節(jié)點颅痊。
下面貼出代碼:
// 代碼片段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() == "領(lǐng)取紅包") {
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里面局待,還有判斷服務(wù)是否開啟的邏輯斑响,如果沒有開啟,則可以點擊跳轉(zhuǎn)帶開啟頁面:
// 代碼片段6 MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
hello.also {
val isOn = isAccessibilityServiceOn()
it.text = if (isOn) "服務(wù)已經(jīng)開啟" else "點擊開啟服務(wù)"
it.isEnabled = !isOn
it.setOnClickListener {
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
}
}
}
}
// 代碼片段7 Tools.kt
val TAG = AccessibilityService::class.java.simpleName
fun listToString(list : List<Any>?): String {
var result = StringBuilder("")
list?.forEach {
result.append("${it.toString()}\t")
}
return result.toString()
}
fun isPrePagePacket(prePageName: String): Boolean {
return prePageName == "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI" || prePageName == "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI"
}
fun Context.isAccessibilityServiceOn(): Boolean {
var service = "${packageName}/${MyAccessibilityService::class.java.canonicalName}"
var enabled = Settings.Secure.getInt(applicationContext.contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED)
var splitter = TextUtils.SimpleStringSplitter(':')
if (enabled == 1) {
var settingValue = Settings.Secure.getString(applicationContext.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
if (settingValue != null) {
splitter.setString(settingValue)
while (splitter.hasNext()) {
var accessibilityService = splitter.next()
if (accessibilityService.equals(service, ignoreCase = true)) {
return true
}
}
}
}
return false
}
自動領(lǐng)取紅包的代碼寫完了钳榨,運行安裝到手機上舰罚,還差最后一步了,就是在手機的“設(shè)置”里面把剛剛裝上的應(yīng)用的服務(wù)開啟薛耻,我的小米5手機開啟方法如圖所示:好了营罢,本文是對AccessibilityService簡單的應(yīng)用,有更好的想法和項目請留言饼齿,我去star饲漾。
參考:
http://www.reibang.com/p/4cd8c109cdfb
http://www.reibang.com/p/959217070c87
https://www.cnblogs.com/happyhacking/p/6368888.html