微信紅包自打出世以來就極其受歡迎,搶紅包插件可謂紅極一時.今天,我們重新談談搶紅包插件的哪些事兒.本質上,搶紅包插件的原理不難理解,其過程就是在收到紅包時,自動模擬點擊.做過自動化UI測試的童鞋應該非常熟悉了.那么問題來了,我們怎么知道有沒有紅包,又怎么模擬點擊操作呢?在PC端我們有按鍵精靈,那么在Android設備上呢?話說也偶然,Google為了讓Android系統更實用,為用戶提供了無障礙輔助服務(accessibilityService
).
AccessibilityService
運行在后臺,并且能夠收到由系統發出的一些事件(AccessibilityEvent
,這些事件表示用戶界面一系列的狀態變化),比如焦點改變,輸入內容變化,按鈕被點擊了等等,該種服務能夠請求獲取當前活動窗口并查找其中的內容.換言之,界面中產生的任何變化都會產生一個時間,并由系統通知給AccessibilityService
.這就像監視器監視著界面的一舉一動,一旦界面發生變化,立刻發出警報.
是不是感覺很棒?接下來,讓我們來看看如何AccessibilityService的基本使用,在不同的階段,對其中的一些點做深入的說明,之后我們從實際應用出發,探討其中的一些使用場景.
編寫自己的服務類,需要繼承AccessibilityService類.其中要實現onAccessibilityEvent(AccessibilityEvent event)
及onInterruput()
兩個重要的方法:
這里我們簡單的介紹一下該類常用的方法:
方法 | 作用 |
---|---|
disableSelf() | 禁用當前服務,也就是在服務可以通過該方法停止運行 |
findFoucs(int falg) | 查找擁有特定焦點類型的控件 |
getRootInActiveWindow() | 如果配置能夠獲取窗口內容,則會返回當前活動窗口的根結點 |
getSeviceInfo() | 獲取當前服務的配置信息 |
onAccessibilityEvent(AccessibilityEvent event ) | 有關AccessibilityEvent事件的回調函數.系統通過sendAccessibiliyEvent()不斷的發送AccessibilityEvent到此處 |
performGlobalAction(int action) | 執行全局操作,比如返回,回到主頁,打開最近等操作 |
setServiceInfo(AccessibilityServiceInfo info) | 設置當前服務的配置信息 |
getSystemService(String name) | 獲取系統服務 |
onKeyEvent(KeyEvent event) | 如果允許服務監聽按鍵操作,該方法是按鍵事件的回調,需要注意,這個過程發生了系統處理按鍵事件之前 |
onServiceConnected() | 系統成功綁定該服務時被觸發,也就是當你在設置中開啟相應的服務,系統成功的綁定了該服務時會觸發,通常我們可以在這里做一些初始化操作 |
更詳細的內容參見官方文檔
像其他Service服務一樣,需要在AndroidManifest.xml中聲明.除此之外,該服務還必須配置以下兩項:
配置<intent-filter>
,其name為固定的android.accessibilityservice.AccessibilityService聲明BIND_ACCESSIBILITY_SERVICE權限,以便系統能夠綁定該服務(4.1版本后要求)注意:任何一點配置錯誤,系統都檢測不到該服務,因此其固定配置如下:
<service android:name=".RobService" android:enabled="true" android:exported="true" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService"/> </intent-filter></service>1234567891012345678910在AndroidManifest.xml聲明了該服務之后,接下來就是需要對該服務進行一些參數設置了.該服務能夠被配置用來接受指定類型的事件,監聽指定package,檢索窗口內容,獲取事件類型的時間等等.目前有兩種配置方法:
4.0之后提供了可以通過<meta-data>
標簽進行配置通過setServiceInfo()
進行配置<meta-data>
進行配置在manifest生命的servce中提供一個meta-data標簽,然后通過android:resource指定相應的配置文件(在res目錄下創建xml文件,并在其中創建配置文件accessibility.xml):
<service android:name=".RobService" 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>12345678910111213141234567891011121314接下來我們來看accessibility.xml的相關配置:
<?xml version="1.0" encoding="utf-8"?><accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:notificationTimeout="100" android:packageNames="com.tencent.mm" />123456789123456789后面,我們在只需要仿照該配置文件根據自己的需求進行修改即可.下面我們會對這些屬性進行介紹.
setServiceInfo(AccessibilityServiceInfo info)
也可以通過setServiceInfo(AccessibilityServiceInfo)
為其配置信息,除此之外,通過該方法可以在運行期間動態修改服務配置.需要注意,該方法只能用來配置動態屬性:eventTypes,feedbackType,flags,notificaionTimeout及packageNames.
通常是在onServiceConnected()
進行配置,如下代碼:
在這里涉及到了AccessibilityServiceInfo類,做個說明: AccessibilityServiceInfo該類被用于配置AccessibilityService信息,該類中包含了大量用于配置的常量字段及用來xml 屬性,比如常見的android:accessibilityEventTypes,android:canRequestFilterKeyEvents,android:packageNames等等,更多信息參見官方文檔
這里,簡單的對重要屬性進行說明:
accessibilityEventTypes
:表示該服務對界面中的哪些變化感興趣,即哪些事件通知,比如窗口打開,滑動,焦點變化,長按等.具體的值可以在AccessibilityEvent類中查到,如typeAllMask表示接受所有的事件通知.accessibilityFeedbackType
:表示反饋方式,比如是語音播放,還是震動canRetrieveWindowContent
:表示該服務能否訪問活動窗口中的內容.也就是如果你希望在服務中獲取窗體內容的化,則需要設置其值為true.notificationTimeout
:接受事件的時間間隔,通常將其設置為100即可.packageNames
:表示對該服務是用來監聽哪個包的產生的事件當我們做完以上操作,便可將app安裝到手機.安裝成功后,在設置->輔助功能中便可以找到我們的服務.該服務默認處在關閉狀態,需要手動開啟.
上面我們說道,onAccessibilityEvent(AccessibilityEvent event)
是該服務的核心方法,其中參數event封裝來自界面相關事件的信息,比如我們可以獲得該事件的事件類型,進而根據起類型選擇不同的處理方式:
這里我們對AccessibilityEvent進行簡單的說明: 當用戶發生發生變化時,系統會發送一系列的AccessibilityEvent事件,比如按鈕被電擊時會發送TYPE_VIEW_CLICKED類型的事件.
方法 | 說明 |
---|---|
getEventType() | 事件類型 |
getSource() | 獲取事件源對應的結點信息 |
getClassName() | 獲取事件源對應類的類型,比如點擊事件是有某個Button產生的,那么此時獲取的就是Button的完整類名 |
getText() | 獲取事件源的文本信息,比如事件是有TextView發出的,此時獲取的就是TextView的text屬性.如果該事件源是樹結構,那么此時獲取的是這個樹上所有具有text屬性的值的集合 |
isEnabled() | 事件源(對應的界面控件)是否處在可用狀態 |
getItemCount() | 如果事件源是樹結構,將返回該樹根節點下子節點的數量 |
系統不斷的產生各種事件,有些是界面控件產生的,有些是系統產生的.對于由界面控件的產生的事件,通常我們將該控件稱之為事件源.并不是所有的事件都能通過getSource()方法獲取到事件源,比如像通知消息類型的事件(TYPE_NOTIFICATION_STATE_CHANGED
).
僅僅知道事件的信息是不夠的,我們還希望通過事件來獲取發出該事件(事件源)的信息,比如Button按鈕被點擊時它的text.一個服務可以配置為可以檢索窗口內容,即獲取窗口內容.整個窗口內容本質上是關于AccessibilityWindowInfo和AccessibilityNodeInfo的樹結構,我稱之為內容樹.(類似View Tree,但由不完全相同)
需要注意,該服務可能配置了只檢測了部分事件,而不是全部事件,這就意味著,當內容樹發生變化后,該服務可能并不知道,即該服務無法及時的了解當前的內容樹是否發生了變化.比如說,你的服務只檢測了點擊事件,但是此時界面的輸入焦點已經變化,這樣整個結點樹也發生了變化,但是你的服務卻不知道,此時你在結點中拿到的窗口內容可能已經不是最新的了.因此,如果你想及時的獲知當前窗口的內容,那么就在配置的時候,設置監聽全部事件.
正如上面所提到的,要想獲取窗口內容,,在配置AccessibilityService時需設置canRetrieveWindowContent為true.之后,便可以通過AccessibilityEvent.getSource()
,findFocus(int)
,getWindow()
或者getRootInActiveWindow()
獲取窗口內容.
要理解該中服務的生命周期只需要記住以下三點即可:
該種服務完全由系統管理,并遵循已有的服務周期.開啟一個服務只能由用戶在設置中打開,而關閉則只能由用戶在設置中關閉或者服務本身通過diableSelf()方法關閉(當然,現在有些第三放軟件也可以強制關閉該類型服務)系統綁定該服務之后,會調用onServiceConnected()方法,這個方法可以被重寫,在其中,你可以做一些初始化的操作.介紹了一些AccessibilityService的基礎知識之后,再補充一點關于檢測某個服務是否開啟的知識.通常來說大體有一下兩種方法:
方法一:借助服務管理器AccessibilityManager來判斷,但是該方法不能檢測app本身開啟的服務.
private boolean enabled(String name) { AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE); List<AccessibilityServiceInfo> serviceInfos = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC); List<AccessibilityServiceInfo> installedAccessibilityServiceList = am.getInstalledAccessibilityServiceList(); for (AccessibilityServiceInfo info : installedAccessibilityServiceList) { Log.d("MainActivity", "all -->" + info.getId()); if (name.equals(info.getId())) { return true; } } return false; }12345678910111213141234567891011121314既然談到了AccessibilityManager,那么在這里我們就做個簡單的介紹: AccessibilityManager是系統級別的服務,用來管理AccessibilityService服務,比如分發事件,查詢系統中服務的狀態等等,更多信息參考官方文檔
方法 | 說明 |
---|---|
getAccessibilityServiceList() | 獲取服務列表(api 14之后廢棄,用下面的方法代替) |
getInstalledAccessibilityServiceList() | 獲取已安裝到系統的服務列表 |
getEnabledAccessibilityServiceList(int feedbackTypeFlags) | 獲取已啟用的服務列表 |
isEnabled() | 判斷服務是否啟用 |
sendAccessibilityEvent(AccessibilityEvent event) | 發送事件 |
方法二:我們知道大部分的系統屬性都在settings中進行設置,比如wifi,藍牙狀態等,而這些信息主要是存儲在settings對應的的數據庫中(system表和serure表),同樣我們也可以通過直接讀取setting設置來判斷相關服務是否開啟:
private boolean checkStealFeature1(String service) { int ok = 0; try { ok = Settings.Secure.getInt(getapplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED); } catch (Settings.SettingNotFoundException e) { } TextUtils.SimpleStringSplitter ms = new TextUtils.SimpleStringSplitter(':'); if (ok == 1) { String settingValue = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); if (settingValue != null) { ms.setString(settingValue); while (ms.hasNext()) { String accessibilityService = ms.next(); if (accessibilityService.equalsIgnoreCase(service)) { return true; } } }1234567891011121314151617181920212212345678910111213141516171819202122到現在有關AccessibilityService的一些知識,我們已經講完,下面我們就看它的具體使用,其中典型的應用就是搶紅包插件.
先回顧一下搶紅包的的流程: 1. 狀態欄出現”[微信紅包]”的消息提示,點擊進入聊天界面 2. 點擊相應的紅包信息,彈出搶紅包界面 3. 在搶紅包界面點擊”開”,打開紅包 4. 在紅包詳情頁面,查看詳情,點擊返回按鈕返回微信聊天界面.
以上是不在微信聊天界面時的流程.如果你所在的微信聊天窗口出現紅包,則不會執行步驟1,而是直接執行2,3,4.如果是在微信好友列表時,收到紅包,則會在列表項中顯示[微信紅包],需要點即該列表項,進入聊天界面,隨后執行2,3,4.為了方便演示,這里我們暫時不考慮好友列表時出現紅包的情況.
明白了搶紅包流程,之后我們通過AccessibilityService獲取通知欄信息及微信聊天窗口界面,繼而通過模擬點擊實現打開紅包,搶紅包等操作. AccessibilityService配置如下:
<?xml version="1.0" encoding="utf-8"?><accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:notificationTimeout="100" android:packageNames="com.tencent.mm" />1234567812345678具體實現代碼如下:
public class RobService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { int eventType = event.getEventType(); switch (eventType) { case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: handleNotification(event); break; case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: String className = event.getClassName().toString(); if (className.equals("com.tencent.mm.ui.LauncherUI")) { getPacket(); } else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI")) { openPacket(); } else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI")) { close(); } break; } } /** * 處理通知欄信息 * * 如果是微信紅包的提示信息,則模擬點擊 * * @param event */ private void handleNotification(AccessibilityEvent event) { List<CharSequence> texts = event.getText(); if (!texts.isEmpty()) { for (CharSequence text : texts) { String content = text.toString(); //如果微信紅包的提示信息,則模擬點擊進入相應的聊天窗口 if (content.contains("[微信紅包]")) { if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) { Notification notification = (Notification) event.getParcelableData(); PendingIntent pendingIntent = notification.contentIntent; try { pendingIntent.send(); } catch (PendingIntent.CanceledException e) { e.printStackTrace(); } } } } } } /** * 關閉紅包詳情界面,實現自動返回聊天窗口 */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private void close() { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null) { //為了演示,直接查看了關閉按鈕的id List<AccessibilityNodeInfo> infos = nodeInfo.findAccessibilityNodeInfosByViewId("@id/ez"); nodeInfo.recycle(); for (AccessibilityNodeInfo item : infos) { item.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } } /** * 模擬點擊,拆開紅包 */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private void openPacket() { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null) { //為了演示,直接查看了紅包控件的id List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId("@id/b9m"); nodeInfo.recycle(); for (AccessibilityNodeInfo item : list) { item.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } } /** * 模擬點擊,打開搶紅包界面 */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void getPacket() { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); AccessibilityNodeInfo node = recycle(rootNode); node.performAction(AccessibilityNodeInfo.ACTION_CLICK); AccessibilityNodeInfo parent = node.getParent(); while (parent != null) { if (parent.isClickable()) { parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); break; } parent = parent.getParent(); } } /** * 遞歸查找當前聊天窗口中的紅包信息 * * 聊天窗口中的紅包都存在"領取紅包"一詞,因此可根據該詞查找紅包 * * @param node */ public AccessibilityNodeInfo recycle(AccessibilityNodeInfo node) { if (node.getChildCount() == 0) { if (node.getText() != null) { if ("領取紅包".equals(node.getText().toString())) { return node; } } } else { for (int i = 0; i < node.getChildCount(); i++) { if (node.getChild(i) != null) { recycle(node.getChild(i)); } } } return node; } @Override public void onInterrupt() { } @Override protected void onServiceConnected() { super.onServiceConnected(); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143上面的代碼簡單演示了搶紅包的原理,為了方便起見,我直接通過findAccessibilityNodeInfosByViewId()
獲取制定id控件.在實際中,這種方法不太可靠,到目前為止,微信已經改過幾次相關控件的id了.
有童鞋問,怎么樣知道該控件的id呢.其實很簡單,android中已經為我們提供了相關的工具:在Android Studio中開啟Android Device Monitor,選擇設備后點擊Dump View Hierarchy for UI Automator,如下:
稍等片刻之后,便會出現當前設備的窗口,在該窗口中點擊相關控件,便會顯示該控件的屬性.借助該工具,可以幫我們快速的分析界面結構,幫助我們從其他app布局策略中學習
我們用Dump View Hierarchy for UI Automator分析聊天界面微信紅包信息:
搶紅包界面:
講完了微信紅包插件的實現原理,不難發現其本質是根據相關的界面狀態,模擬后續的操作(比如點擊等). 既然這樣,那么我們完全可以利用該服務實現更多的功能,比如apk自動安裝,傳統的安裝過程大概是如下流程:
點擊apk文件,彈出安裝信息界面,在該界面點擊”下一步”,然后在點擊”安裝”,最后在安裝完成界面點擊”完成”.
不難發現,該流程完全可以通過模擬點擊操作完成.現在我們簡單的講一下AccessibilityService在這方面的具體應用.我們知道系統的安裝程序由PackageInstaller負責,其包名是com.android.packageinstaller
,那么我們只需要監聽該package下的安裝信息界面和安裝完成界面,并模擬點擊”下一步”,”安裝”,完成”“操作即可實現自動安裝.
AccessibilityService配置如下:
<?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="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/auto_service_des" android:notificationTimeout="100" android:packageNames="com.android.packageinstaller"/>1234567891012345678910具體實現代碼如下:
public class InstallService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { Log.d("InstallService", event.toString()); checkInstall(event); } private void checkInstall(AccessibilityEvent event) { AccessibilityNodeInfo source = event.getSource(); if (source != null) { boolean installPage = event.getPackageName().equals("com.android.packageinstaller"); if (installPage) { installAPK(event); } } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void installAPK(AccessibilityEvent event) { AccessibilityNodeInfo source = getRootInActiveWindow(); List<AccessibilityNodeInfo> nextInfos = source.findAccessibilityNodeInfosByText("下一步"); nextClick(nextInfos); List<AccessibilityNodeInfo> installInfos = source.findAccessibilityNodeInfosByText("安裝"); nextClick(installInfos); List<AccessibilityNodeInfo> openInfos = source.findAccessibilityNodeInfosByText("打開"); nextClick(openInfos); runInBack(event); } private void runInBack(AccessibilityEvent event) { event.getSource().performAction(AccessibilityService.GLOBAL_ACTION_BACK); } private void nextClick(List<AccessibilityNodeInfo> infos) { if (infos != null) for (AccessibilityNodeInfo info : infos) { if (info.isEnabled() && info.isClickable()) info.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private boolean checkTilte(AccessibilityNodeInfo source) { List<AccessibilityNodeInfo> infos = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("@id/app_name"); for (AccessibilityNodeInfo nodeInfo : infos) { if (nodeInfo.getClassName().equals("android.widget.TextView")) { return true; } } return false; } @Override public void onInterrupt() { } @Override protected void onServiceConnected() { Log.d("InstallService", "auto install apk"); }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667681234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768在很多情況下,我們需要檢測自己的app是不是處在前臺,借助該服務同樣也能夠完成該檢測操作.下面,我們就演示一下如何實現:
AccessibilityService配置如下:
<?xml version="1.0" encoding="utf-8"?><accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/auto_detection" android:notificationTimeout="100" />1234567891012345678910具體實現代碼如下:
public class DetectionService extends AccessibilityService { private static volatile String foregroundPackageName = "error"; /** * 檢測是否是前臺服務 * * @param packagenName * @return */ public static boolean isForeground(String packagenName) { return foregroundPackageName.equals(packagenName); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { foregroundPackageName = event.getPackageName().toString(); } } @Override public void onInterrupt() { }}123456789101112131415161718192021222324252627123456789101112131415161718192021222324252627在使用時,需要引導用戶開啟該服務,之后通過調用DetectionService.isForeground()即可.
上面的所有示例演示的都是AccessibilityService在具體應用中發揮的良好作用.但是該服務也存在一定的風險,很多人利用該服務做一些”壞事”,比如竊取短信驗證碼,竊取短信內容,想要看看自己女朋友最近在和誰聊QQ,偷偷安裝流氓軟件等.
上面我們了解微信搶紅包插件的原理,那么利用該AccessibilityService編寫相應的反搶紅包插件:通過模擬微信紅包的通知信息,發送虛假的事件通知.不出意外,我們編寫的反搶紅包插件會讓失眠絕大多數的搶紅包插件.這里我就不做深入的解釋,有興趣的同學可以自行研究.
你現在是不是想能否借助該服務直接獲取一些app的密碼呢?凡是EditText中設置inputType為passWord類型的,都無法獲取其輸入值.除此之外,大多數軟件都針對該中風險做了提前的防范.因此,你想要借助該服務來實現竊取密碼還是比較有難度的,所以,少年覺悟吧.
暫時先到這里,后面再補充其他的吧.其實該服務能做的事情遠不止這些,比如也可以通過該服務獲取微信公眾號的key,進而爬去文章閱讀數,進行UI自動化測試等.
新聞熱點
疑難解答