AccessibilityService+OpenCV实现微信6.7.3抢红包插件

作者 : 开心源码 本文共14576个字,预计阅读时间需要37分钟 发布时间: 2022-05-12 共212人阅读

引言

提起AccessibilityService首先想到的一定是抢红包插件。没错,目前基本上抢红包插件分为两类:root和免root,而免root的红包插件全是基于AccessibilityService。随着AccessibilityService的广泛应用,现今已经有比较多的方法可以防御基于AccessibilityService实现的自动化插件了。有兴趣的朋友可以参考这篇文章:红包外挂史及AccessibilityService分析与防御。
本文通过AccessibilityService加上OpenCV辅助识别少量关键的特征,以此在高版本微信中实现抢红包的效果。

AccessibilityService基本用法

1、继承AccessibilityService

编写自己的Service类,必需重写onAccessibilityEvent()方法和onInterrupt()方法

public class HongbaoService extends AccessibilityService {    /**     * 当启动服务的时候就会被调用(非必需重写)     */    @Override    protected void onServiceConnected() {        super.onServiceConnected();    }    /**     * 监听窗口变化的回调     */    @Override    public void onAccessibilityEvent(AccessibilityEvent event) {        int eventType = event.getEventType();        //根据事件回调类型进行解决    }    /**     * 中断服务的回调     */    @Override    public void onInterrupt() {    }}

下面简要地详情用到的几个AccessibilityEvent的事件类型

事件类型形容
TYPE_VIEW_CLICKEDView被点击
TYPE_VIEW_LONG_CLICKEDView被长按
TYPE_VIEW_SELECTEDView被选中
TYPE_NOTIFICATION_STATE_CHANGED状态栏发生变化
TYPE_WINDOW_CONTENT_CHANGED窗口内容发生变化
TYPE_WINDOW_STATE_CHANGED打开弹出窗口、菜单、对话框等的时候触发

2、公告服务

首先,我们需要在manifests中配置该服务信息

        <service            android:name=".service.HongbaoService"            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/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文件,配置如下:

<accessibility-service    xmlns:android="http://schemas.android.com/apk/res/android"    android:description="@string/app_description"    android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeNotificationStateChanged"    android:accessibilityFeedbackType="feedbackAllMask"    android:packageNames="com.tencent.mm"    android:notificationTimeout="300"    android:settingsActivity="com.shareder.ln_jan.wechatluckymoneygetter.activities.MainActivity"    android:accessibilityFlags="flagDefault"    android:canRetrieveWindowContent="true"    android:canPerformGestures="true"/>
  • 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/gdList<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() {                            @Override                            public void onCompleted(GestureDescription gestureDescription) {                                Log.e(TAG, "onCompleted");                                mPockeyOpenMutex = false;                                super.onCompleted(gestureDescription);                            }                            @Override                            public 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

    拆红包按钮.png
    计算按钮位置的代码如下:

                    Path path = new Path();                    if (640 == dpi) { //1440                        path.moveTo(720, 1575);                    } else if (320 == dpi) {//720p                        path.moveTo(355, 780);                    } else if (480 == dpi) {//1080p                        path.moveTo(533, 1115);                    } else if (440 == dpi) {//1080*2160                        path.moveTo(450, 1250);                    }
  • 拆开红包后通过performGlobalAction(GLOBAL_ACTION_BACK)方法从[红包介绍]页面返回
  • 一次拆红包流程结束
    流程图如下:
    抢红包流程图.png

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及以上实现屏幕截图

  • 微信聊天列表新消息的定位

如图:聊天列表截图.png

从上图我们可以看到,新消息是包含一个现实未读消息数的TextView,我们可以以此来判断未读消息。消息内容则是View所以我们在AccessibilityService中无法获取其内容,但是我们可以定位到未读消息内容在屏幕中的位置。

  • OpenCv特征点匹配

关于OpenCv特征点匹配的方法有很多,有兴趣的读者可以参考这篇文章OpenCv他特征点匹配方法汇总。我使用的是ORB算法。这个算法的特点是速度快,但是在精确率和抗噪点能力上会有所欠缺。
Android接入OpenCv有三种方法:接入OpenCv的Java SDK包、封装JNI、编译OpenCv源码。其中最简单的是直接接入OpenCv的Java SDK包。下载地址
关键代码如下:

/**     * 检测输入的图像能否匹配[微信红包]的本地图像     *     * @param bmInput        输入的图像     * @param isNormalScreen 对全面屏手机截取的区域不同,特征点也会不同     * @return     */    public boolean isPictureMatchLuckyMoney(Bitmap bmInput, boolean isNormalScreen) throws CvException {        if (!isCachePictureExist()) {            return false;        }        if (bmLocal == null) {            bmLocal = BitmapFactory.decodeFile(strMoneyPicPath);        }        Mat inputGrayMat = getGrayMat(bmInput);        Mat localGrayMat = getGrayMat(bmLocal);        //特征点提取        ORB orb = ORB.create(1000);                           //精度越小越精确        MatOfKeyPoint kptsInput = new MatOfKeyPoint();        MatOfKeyPoint kptsLocal = new MatOfKeyPoint();        orb.detect(inputGrayMat, kptsInput);        orb.detect(localGrayMat, kptsLocal);        //特征点形容,采用ORB默认的形容算法        Mat descInput = new Mat();        Mat descLocal = new Mat();        orb.compute(inputGrayMat, kptsInput, descInput);        orb.compute(localGrayMat, kptsLocal, descLocal);        //BFMatcher matcher = new BFMatcher(BFMatcher.BRUTEFORCE_HAMMING, false);        DescriptorMatcher matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE_HAMMING);        MatOfDMatch matchPoints = new MatOfDMatch();        //Log.e("matchoutput", "--start---");        //matcher.knnMatch(descInput,descLocal,matchPointsList,2);        try {            matcher.match(descInput, descLocal, matchPoints);        } catch (CvException ex) {            Log.e("matchoutput", ex.toString());            return false;        }        //Log.e("matchoutput", "--end---");        float min_dist = 0;        DMatch[] arrays = matchPoints.toArray();        for (int i = 0; i < descInput.rows(); ++i) {            float dist = arrays[i].distance;            if (dist < min_dist) min_dist = dist;        }        int goodMatchPointNum = 0;        //挑选特征点        float compareNum = Math.max(min_dist * 2, 30.0f);        for (int j = 0; j < descInput.rows(); j++) {            if (arrays[j].distance <= compareNum) {                goodMatchPointNum++;            }        }        Log.e("matchoutput", goodMatchPointNum + "");        if (isNormalScreen) {            return goodMatchPointNum > 10;        } else {            return goodMatchPointNum >= 7;        }    }

因为微信的字体是与系统字体有关。所以我在每次软件启动时在本地绘制一张[微信红包]的图片用于做特征点比对。当截图的特征点匹配的数目大于肯定数量的时候就认为这个图片中的文字可能就是[微信红包]
这里不采用ORC文字识别的起因是文字识别速度太慢了,采用这种方法的话会快好多。
而且随着微信版本的改动,想单纯通过AccessibilityService会原来越难,所以在以后的版本中可能需要用相似的方法去识别微信红包了

抢红包服务核心实现代码

  • 区分AccessibilityEvent消息类型

    @Override    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {        //Log.e(TAG, "event recv");        Log.e(TAG, "class:" + accessibilityEvent.getClassName().toString());        if (!mGlobalMutex) {            mGlobalMutex = true;            setCurrentActivityName(accessibilityEvent);            switch (accessibilityEvent.getEventType()) {                case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:                    handleNotificationMessage(accessibilityEvent);                    break;                case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:                case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:                    handleScreenMessage(accessibilityEvent);                    break;                default:                    break;            }            mGlobalMutex = false;        }    }

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 boolean     */    private boolean isInChartList() {        boolean b = false;        AccessibilityNodeInfo root = getRootInActiveWindow();        if (root != null) {            if (root.getChildCount() > 0) {                b = root.getChild(0).getChildCount() > 1;            } else {                b = false;            }        }        return b;    }

在聊天列表:判断能否有未读消息-截屏-截取未读消息区域-查找能否有红包来了

    /**     * 查找未读消息区域,假如没有则返回空列表     * @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;    }

在聊天页面:查找包含[领取红包]的子节点-点击该节点-进入拆红包阶段

聊天内容页面.png

因为[领取红包]关键字是个TextView,所以可以通过findAccessibilityNodeInfosByText查找到该子节点,具体代码如下:

    private void findRedpockeyAndClick(AccessibilityEvent ev) {        AccessibilityNodeInfo rootInfo = getRootInActiveWindow();        //检查领取红包和查看红包        List<AccessibilityNodeInfo> redPackeyInfoLst = null;        if (mSharedPreferences.getBoolean("pref_watch_self", false)) {            redPackeyInfoLst = getPacketNode(rootInfo, WECHAT_VIEW_OTHERS_CH, WECHAT_VIEW_SELF_CH);        } else {            redPackeyInfoLst = getPacketNode(rootInfo, WECHAT_VIEW_OTHERS_CH);        }        if (redPackeyInfoLst != null && !redPackeyInfoLst.isEmpty()) {            //redPackeyInfo.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);            String str_filter = mSharedPreferences.getString("pref_watch_exclude_words", "");            AccessibilityNodeInfo openPackeyInfo = null;            if (str_filter.equals("")) {                openPackeyInfo = redPackeyInfoLst.get(redPackeyInfoLst.size() - 1);            } else {                String[] str_fiilter_array = str_filter.split(" +");                for (int k = redPackeyInfoLst.size() - 1; k >= 0; k--) {                    AccessibilityNodeInfo tmpInfo = redPackeyInfoLst.get(k);                    if (tmpInfo.getParent().getChildCount() <= 0) {                        continue;                    }                    String strPackeyMsg = tmpInfo.getParent().getChild(0).getText().toString();                    boolean b = false;                    for (String filter_text : str_fiilter_array) {                        if ((filter_text.length() > 0) &&                                strPackeyMsg.contains(filter_text)) {                            b = true;                            break;                        }                    }                    if (!b) {                        openPackeyInfo = tmpInfo;                        break;                    }                }            }            if (openPackeyInfo != null) {                AccessibilityNodeInfo parentInfo = openPackeyInfo.getParent();                if (parentInfo != null) {                    parentInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);                }            }            Log.e(TAG, "pockey find!");        }    }

这里还做了关键字过滤的解决,假如包含某些关键字则不打开该红包。做到专属红包不抢

拆红包页面或者红包过期页面

拆红包代码

    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) { //1440                        path.moveTo(720, 1575);                    } else if (320 == dpi) {//720p                        path.moveTo(355, 780);                    } else if (480 == dpi) {//1080p                        path.moveTo(533, 1115);                    } else if (440 == dpi) {//1080*2160                        path.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() {                            @Override                            public void onCompleted(GestureDescription gestureDescription) {                                Log.e(TAG, "onCompleted");                                mPockeyOpenMutex = false;                                super.onCompleted(gestureDescription);                            }                            @Override                            public 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)返回

写在最后

这是一个根据前人的版本结合自己的少量想法创造的版本。可能不肯定是最优解,但是目前可以支持到微信6.7.3版本,新出的7.0.0版本还没研究。假如哪位大神有更好的方法还望不吝赐教。
项目源码下载
安装包下载

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » AccessibilityService+OpenCV实现微信6.7.3抢红包插件

发表回复