AccessibilityService+OpenCV实现微信7.0.0抢红包插件
引言
提起AccessibilityService首先想到的肯定是搶紅包插件。沒錯,目前基本上搶紅包插件分為兩類:root和免root,而免root的紅包插件全是基于AccessibilityService。隨著AccessibilityService的廣泛應用,現今已經有比較多的方法可以防御基于AccessibilityService實現的自動化插件了。有興趣的朋友可以參考這篇文章:紅包外掛史及AccessibilityService分析與防御。
本文通過AccessibilityService加上OpenCV輔助識別一些關鍵的特征,以此在高版本微信中實現搶紅包的效果。
AccessibilityService基本用法
1、繼承AccessibilityService
編寫自己的Service類,必須重寫onAccessibilityEvent()方法和onInterrupt()方法
public class HongbaoService extends AccessibilityService {/*** 當啟動服務的時候就會被調用(非必須重寫)*/@Overrideprotected void onServiceConnected() {super.onServiceConnected();}/*** 監聽窗口變化的回調*/@Overridepublic void onAccessibilityEvent(AccessibilityEvent event) {int eventType = event.getEventType();//根據事件回調類型進行處理}/*** 中斷服務的回調*/@Overridepublic void onInterrupt() {} }下面簡要地介紹用到的幾個AccessibilityEvent的事件類型
| TYPE_VIEW_CLICKED | View被點擊 |
| TYPE_VIEW_LONG_CLICKED | View被長按 |
| TYPE_VIEW_SELECTED | View被選中 |
| TYPE_NOTIFICATION_STATE_CHANGED | 狀態欄發生變化 |
| TYPE_WINDOW_CONTENT_CHANGED | 窗口內容發生變化 |
| TYPE_WINDOW_STATE_CHANGED | 打開彈出窗口、菜單、對話框等的時候觸發 |
2、聲明服務
首先,我們需要在manifests中配置該服務信息
<serviceandroid:name=".service.HongbaoService"android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"><intent-filter><action android:name="android.accessibilityservice.AccessibilityService" /></intent-filter><meta-dataandroid:name="android.accessibilityservice"android:resource="@xml/accessible_service_config" /></service>我們必須注意:任何一個信息配置錯誤,都會使該服務無反應
- android:label:在無障礙列表中顯示該服務的名字(默認與APP名字相同)
- android:permission:需要指定BIND_ACCESSIBILITY_SERVICE權限,這是4.0以上的系統要求的
- intent-filter:這個name是固定不變的
##3、配置服務參數
配置服務參數是指:配置用來接受指定類型的事件,監聽指定package,檢索窗口內容,獲取事件類型的時間等等。其配置服務參數有兩種方法: - 方法一:安卓4.0之后可以通過meta-data標簽指定xml文件進行配置
- 方法二:通過代碼動態配置參數
這里我使用的是第一種方法:
在項目中增加accessible_service_config文件,配置如下:
- accessibilityEventTypes:表示該服務對界面中的哪些變化感興趣,即哪些事件通知,比如窗口打開,滑動,焦點變化,長按等。具體的值可以在AccessibilityEvent類中查到,如typeAllMask表示接受所有的事件通知
- accessibilityFeedbackType:表示反饋方式,比如是語音播放,還是震動
- canRetrieveWindowContent:表示該服務能否訪問活動窗口中的內容。也就是如果你希望在服務中獲取窗體內容,則需要設置其值為true
- description:對該無障礙功能的描述
- notificationTimeout:接受事件的時間間隔,這里我設置的是300
- packageNames:表示對該服務是用來監聽哪個包的產生的事件,如"com.tencent.mm"為微信的包名
- canPerformGestures: 安卓7.0后可通過dispatchGesture實現點擊屏幕的操作,如需用此方法需將canPerformGestures設置為true
4、預備知識
4.1、獲取節點信息
獲取了界面窗口變化后,這個時候就要獲取控件的節點。整個窗口的節點本質是個樹結構,通過以下操作節點信息
1、獲取窗口節點(根節點)
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();2、獲取指定子節點(控件節點)
//通過文本找到對應的節點集合 List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(text); //通過控件ID找到對應的節點集合,如com.tencent.mm:id/gd List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(clickId);4.2、模擬點擊的方法
獲取節點信息后可通過performAction方法或dispatchGesture方法產生點擊屏幕的效果
1、performAction
//模擬點擊 accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK); //模擬長按 accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);這種方法在微信6.6.1或之前的版本可用次方法拆開紅包
2、dispatchGesture
GestureDescription gestureDescription = builder.addStroke(new GestureDescription.StrokeDescription(path, 450, 50)).build(); dispatchGesture(gestureDescription, new GestureResultCallback() {@Overridepublic void onCompleted(GestureDescription gestureDescription) {Log.e(TAG, "onCompleted");mPockeyOpenMutex = false;super.onCompleted(gestureDescription);}@Overridepublic void onCancelled(GestureDescription gestureDescription) {Log.e(TAG, "onCancelled");mPockeyOpenMutex = false;super.onCancelled(gestureDescription);}}, null);微信6.7.3版本由于無法再通過遍歷AccessibilityNodeInfo子節點找到拆紅包按鈕,故只能通過次方法實現模擬點擊
微信搶紅包原理分析及實現
1、原理分析
首先我們可以通過兩種方式監控紅包消息通知:監控微信懸浮框和監控屏幕狀態變化
拆紅包的流程可分為下列三種情況:
懸浮框通知
- 通過TYPE_NOTIFICATION_STATE_CHANGED監控狀態欄變化,當出現 [微信紅包] 時進入微信,此時會直接進入到聊天頁面
屏幕狀態變化
- 此時若在聊天頁面則進入拆紅包流程
- 若在聊天列表頁面則查找未讀消息中是否包含 [微信紅包] 字眼的信息,有則點擊進入聊天頁面
[聊天頁面]拆紅包流程
-
通過findAccessibilityNodeInfosByText方法查找包含領取紅包、查看紅包字樣的節點,找到后通過performAction點擊進入拆紅包頁面
-
微信6.7.3版本前可通過getRootInActiveWindow獲取窗口根節點,再通過遍歷的方法找到唯一的一個Button節點,通過performAction點擊該節點拆紅包。但在6.7.3版本不再有效。故改用dispatchGesture根據屏幕分辨率判斷**[開]**按鈕的位置實現拆紅包的功能。以本人手機為例,按鈕位置如下:
手機屏幕的尺寸是:1920*1080,則按鈕點擊的橫坐標為x,縱坐標為y,則x、y的范圍是:
386<x<694 1015<y<1323
計算按鈕位置的代碼如下:
- 拆開紅包后通過performGlobalAction(GLOBAL_ACTION_BACK)方法從 [紅包詳情] 頁面返回
- 一次拆紅包流程結束
流程圖如下:
2、注意事項
- 在聊天列表中新版微信重寫了TextView控件,這意味著不能通過findAccessibilityNodeInfosByText方法查找 [微信紅包] 了
- 由于新版微信中對已領取的紅包會顯示紅包已被領取且不可再次點擊,對于自己發放的紅包若紅包已領完會顯示紅包已被領完,若未領完則一直顯示查看紅包。所以只有在拆開自己發的紅包時才需要做防止重復打開的處理。
- 在國內第三方定制系統中出于對電量的優化,有可能限制AccessibilityService后臺長時間運行。所以需要提醒用戶設置后臺運行權限
3、代碼實現
聊天列表查找[微信紅包]字樣的新消息的實現
上面提到由于新版微信重寫了TextView所以通過AccessibilityService基本上是獲取不了任何聊天內容的消息了,相應的findAccessibilityNodeInfosByText方法也沒有用了。
這里我的解決方法是通過截屏剪裁含有未讀消息信息的區域,然后通過OpenCv特征點匹配的方式來確認哪些未讀消息是包含 [微信紅包] 的。具體實現如下:
-
MediaProjection實現屏幕截圖
申請錄屏權限
/*** 申請屏幕錄取權限*/private void requestScreenShot() {startActivityForResult(((MediaProjectionManager) this.getActivity().getSystemService("media_projection")).createScreenCaptureIntent(),REQUEST_MEDIA_PROJECTION);}截取屏幕內容生成BMP
public Bitmap getScreenShotSync() {if (!isShotterUseful()) {return null;}if (mImageReader == null) {mImageReader = ImageReader.newInstance(getScreenWidth(),getScreenHeight(),PixelFormat.RGBA_8888,//此處必須和下面 buffer處理一致的格式 ,RGB_565在一些機器上出現兼容問題。1);}VirtualDisplay tmpDisplay = virtualDisplay();try{Thread.sleep(50); //需要稍微停一下,否則截圖為空}catch (InterruptedException e){e.printStackTrace();}Image img = mImageReader.acquireLatestImage();if (img == null) {return null;}int width = img.getWidth();int height = img.getHeight();final Image.Plane[] planes = img.getPlanes();final ByteBuffer buffer = planes[0].getBuffer();//每個像素的間距int pixelStride = planes[0].getPixelStride();//總的間距int rowStride = planes[0].getRowStride();int rowPadding = rowStride - pixelStride * width;Bitmap bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height,Bitmap.Config.ARGB_8888);//雖然這個色彩比較費內存但是 兼容性更好bitmap.copyPixelsFromBuffer(buffer);bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);img.close();//mImageReader.close();tmpDisplay.release();return bitmap;}@TargetApi(Build.VERSION_CODES.LOLLIPOP)private VirtualDisplay virtualDisplay() {return mMediaProjection.createVirtualDisplay("screen-mirror",getScreenWidth(),getScreenHeight(),Resources.getSystem().getDisplayMetrics().densityDpi,DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,mImageReader.getSurface(), null, null);}出于對效率的考慮Bitmap一直都是在內存中操作的,并不需要輸出成文件
MediaProjection的詳細介紹可參考:Android 5.0及以上實現屏幕截圖
-
微信聊天列表新消息的定位
如圖:
從上圖我們可以看到,新消息是包含一個現實未讀消息數的TextView,我們可以以此來判斷未讀消息。消息內容則是View所以我們在AccessibilityService中無法獲取其內容,但是我們可以定位到未讀消息內容在屏幕中的位置。
-
OpenCv特征點匹配
關于OpenCv特征點匹配的方法有很多,有興趣的讀者可以參考這篇文章OpenCv他特征點匹配方法匯總。我使用的是ORB算法。這個算法的特點是速度快,但是在準確率和抗噪點能力上會有所欠缺。
Android接入OpenCv有三種方法:接入OpenCv的Java SDK包、封裝JNI、編譯OpenCv源碼。其中最簡單的是直接接入OpenCv的Java SDK包。下載地址
關鍵代碼如下:
由于微信的字體是與系統字體有關。所以我在每次軟件啟動時在本地繪制一張 [微信紅包] 的圖片用于做特征點比對。當截圖的特征點匹配的數目大于一定數量的時候就認為這個圖片中的文字可能就是 [微信紅包]
這里不采用ORC文字識別的原因是文字識別速度太慢了,采用這種方法的話會快好多。
而且隨著微信版本的改動,想單純通過AccessibilityService會原來越難,所以在以后的版本中可能需要用類似的方法去識別微信紅包了
搶紅包服務核心實現代碼
-
區分AccessibilityEvent消息類型
TYPE_NOTIFICATION_STATE_CHANGED為狀態欄通知
-
根據ActivityName區分當前狀態
獲取當前ActivityName
private void setCurrentActivityName(AccessibilityEvent event) {String activitiesName = event.getClassName().toString();currentNodeInfoName = activitiesName;if (activitiesName.startsWith("com.tencent.mm")) {//prevActivityName = currentActivityName;currentActivityName = activitiesName;Log.e(TAG, "current_name:" + event.getClassName().toString());}} private static final String CHATTING_LAUNCHER_UI = "com.tencent.mm.ui.LauncherUI";private static final String LUCKY_MONEY_RECV_UI = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI";private static final String LUCKY_MONEY_DETAIL_UI = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI";CHATTING_LAUNCHER_UI 聊天列表頁面或聊天頁面
LUCKY_MONEY_RECV_UI 拆紅包頁面或紅包過期頁面
LUCKY_MONEY_DETAIL_UI 紅包詳情頁面
-
根據當前狀態決策處理的方法
聊天列表頁面或聊天頁面
區分當前實在聊天列表還是在聊天頁面,如果在聊天列表則查找是否有未讀消息如果在聊天頁面則通過findAccessibilityNodeInfosByText查找紅包。
判斷方法:
在聊天列表:判斷是否有未讀消息-截屏-截取未讀消息區域-查找是否有紅包來了
/*** 查找未讀消息區域,如果沒有則返回空列表* @return*/private List<Rect> findNewsRectInScreen() {AccessibilityNodeInfo nodeInfo = findNodeInfoByClass(getRootInActiveWindow(), LIST_VIEW_NAME);List<Rect> resultList = new ArrayList<>();if (nodeInfo != null) {int chartCount = nodeInfo.getChildCount();for (int i = 0; i < chartCount; i++) {AccessibilityNodeInfo subChartInfo = nodeInfo.getChild(i);if (subChartInfo != null) {if (subChartInfo.getChildCount() > 0) { //表示是未讀消息,有可能有紅包Rect outputRect = new Rect();subChartInfo.getBoundsInScreen(outputRect);if (!isNormalScreen) {outputRect.top -= 30;outputRect.bottom -= 30;}if (outputRect.height() == 0 || outputRect.width() == 0) {continue;}outputRect.left += (int) (outputRect.width() * 0.2); //去除頭像區域outputRect.top += (int) (outputRect.height() * 0.3);resultList.add(outputRect);}}}}return resultList;}在聊天頁面:查找包含[領取紅包]的子節點-點擊該節點-進入拆紅包階段
由于[領取紅包]關鍵字是個TextView,所以可以通過findAccessibilityNodeInfosByText查找到該子節點,具體代碼如下:
這里還做了關鍵字過濾的處理,如果包含某些關鍵字則不打開該紅包。做到專屬紅包不搶
拆紅包頁面或紅包過期頁面
拆紅包代碼
private void openPacket() {DisplayMetrics metrics = getResources().getDisplayMetrics();float dpi = metrics.densityDpi;Log.e(TAG, "openPacket!" + dpi);if (android.os.Build.VERSION.SDK_INT <= 23) {//nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);Toast.makeText(MyApplication.getContext(), getString(R.string.not_support_low_level), Toast.LENGTH_SHORT).show();} else {if (android.os.Build.VERSION.SDK_INT > 23) {if (!mPockeyOpenMutex) {mPockeyOpenMutex = true;Path path = new Path();if (640 == dpi) { //1440path.moveTo(720, 1575);} else if (320 == dpi) {//720ppath.moveTo(355, 780);} else if (480 == dpi) {//1080ppath.moveTo(533, 1115);} else if (440 == dpi) {//1080*2160path.moveTo(450, 1250);}GestureDescription.Builder builder = new GestureDescription.Builder();try {GestureDescription gestureDescription = builder.addStroke(new GestureDescription.StrokeDescription(path, 450, 50)).build();dispatchGesture(gestureDescription, new GestureResultCallback() {@Overridepublic void onCompleted(GestureDescription gestureDescription) {Log.e(TAG, "onCompleted");mPockeyOpenMutex = false;super.onCompleted(gestureDescription);}@Overridepublic void onCancelled(GestureDescription gestureDescription) {Log.e(TAG, "onCancelled");mPockeyOpenMutex = false;super.onCancelled(gestureDescription);}}, null);} catch (Exception e) {e.printStackTrace();}}}}}這里做了一個延時自動關閉的操作,因為如果紅包過期了是不會進入紅包詳情頁面的。所以這里如果過了一定時間還沒有進入紅包詳情頁面則認為這是過期紅包。自動關閉
在微信6.6.1及之前的版本是可以遍歷窗口節點,查找類名為android.vew.button的方法來查找到按鈕的。再通過performAction點擊該節點打開紅包,這里就不貼出代碼了。
紅包詳情頁面
調用performGlobalAction(GLOBAL_ACTION_BACK)返回
寫在最后
這是一個根據前人的版本結合自己的一些想法創造的版本。可能不一定是最優解,但是目前可以支持到微信7.0.0版本。如果哪位大神有更好的方法還望不吝賜教。
項目源碼下載
安裝包下載
總結
以上是生活随笔為你收集整理的AccessibilityService+OpenCV实现微信7.0.0抢红包插件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springboot怎么杀进程_线上服务
- 下一篇: 链表(Linked List)之单链表