发布时间:2025-12-09 21:49:00 浏览次数:4
提起AccessibilityService首先想到的肯定是抢红包插件。没错,目前基本上抢红包插件分为两类:root和免root,而免root的红包插件全是基于AccessibilityService。随着AccessibilityService的广泛应用,现今已经有比较多的方法可以防御基于AccessibilityService实现的自动化插件了。有兴趣的朋友可以参考这篇文章:红包外挂史及AccessibilityService分析与防御。
本文通过AccessibilityService加上OpenCV辅助识别一些关键的特征,以此在高版本微信中实现抢红包的效果。
编写自己的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 | 打开弹出窗口、菜单、对话框等的时候触发 |
首先,我们需要在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>我们必须注意:任何一个信息配置错误,都会使该服务无反应
这里我使用的是第一种方法:
在项目中增加accessible_service_config文件,配置如下:
获取了界面窗口变化后,这个时候就要获取控件的节点。整个窗口的节点本质是个树结构,通过以下操作节点信息
获取节点信息后可通过performAction方法或dispatchGesture方法产生点击屏幕的效果
这种方法在微信6.6.1或之前的版本可用次方法拆开红包
微信6.7.3版本由于无法再通过遍历AccessibilityNodeInfo子节点找到拆红包按钮,故只能通过次方法实现模拟点击
首先我们可以通过两种方式监控红包消息通知:监控微信悬浮框和监控屏幕状态变化
拆红包的流程可分为下列三种情况:
通过findAccessibilityNodeInfosByText方法查找包含领取红包、查看红包字样的节点,找到后通过performAction点击进入拆红包页面
微信6.7.3版本前可通过getRootInActiveWindow获取窗口根节点,再通过遍历的方法找到唯一的一个Button节点,通过performAction点击该节点拆红包。但在6.7.3版本不再有效。故改用dispatchGesture根据屏幕分辨率判断**[开]**按钮的位置实现拆红包的功能。以本人手机为例,按钮位置如下:
手机屏幕的尺寸是:1920*1080,则按钮点击的横坐标为x,纵坐标为y,则x、y的范围是:
386<x<694 1015<y<1323
计算按钮位置的代码如下:
上面提到由于新版微信重写了TextView所以通过AccessibilityService基本上是获取不了任何聊天内容的消息了,相应的findAccessibilityNodeInfosByText方法也没有用了。
这里我的解决方法是通过截屏剪裁含有未读消息信息的区域,然后通过OpenCv特征点匹配的方式来确认哪些未读消息是包含 [微信红包] 的。具体实现如下:
申请录屏权限
/*** 申请屏幕录取权限*/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他特征点匹配方法汇总。我使用的是ORB算法。这个算法的特点是速度快,但是在准确率和抗噪点能力上会有所欠缺。
Android接入OpenCv有三种方法:接入OpenCv的Java SDK包、封装JNI、编译OpenCv源码。其中最简单的是直接接入OpenCv的Java SDK包。下载地址
关键代码如下:
由于微信的字体是与系统字体有关。所以我在每次软件启动时在本地绘制一张 [微信红包] 的图片用于做特征点比对。当截图的特征点匹配的数目大于一定数量的时候就认为这个图片中的文字可能就是 [微信红包]
这里不采用ORC文字识别的原因是文字识别速度太慢了,采用这种方法的话会快好多。
而且随着微信版本的改动,想单纯通过AccessibilityService会原来越难,所以在以后的版本中可能需要用类似的方法去识别微信红包了
TYPE_NOTIFICATION_STATE_CHANGED为状态栏通知
获取当前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版本。如果哪位大神有更好的方法还望不吝赐教。
项目源码下载
安装包下载