微信自动抢红包助手

背景

每逢佳节红包多,而我却在灯火阑珊处,搓着麻将无暇顾及,错失几个亿 o(╥﹏╥)o。 为了让各位都能马上抢到红包,发大财,写下这边文章,教大家如何写一个自动抢红包的app,手机放在边上便能让钱叮叮叮进入你的钱包。

实现原理

app需要获取监听通知和使用无障碍服务的权限,然后监听红包消息来实现自动点击红包

文章结尾有github源码地址

实现效果

实现思路

首先要获取红包来了的消息,那红包消息来了微信会有多少种提醒形式呢,现有的两种形式有:

  1. 不在聊天列表,红包消息会以通知的形式通知用户
  2. 在聊天列表时会有'[微信红包]'的提示字样

那么如何获取微信红包通知呢?

官方推荐的方式是继承NotificationListenerService来实现监听通知消息,当然你也可以通过无障碍服务来监听通知消息,但是在我的小米14手机上发现如果用无障碍服务监听通知消息有时会监听不到,因此最终还是采用了NotificationListenerService,通过过滤微信的消息然后监听内容是否包含'微信红包'来判断是否是红包消息。

又如何在聊天列表监听红包消息呢?

那就是无障碍服务,通过该服务监听微信的布局变化事件,然后判断消息列表的内容是否有'[微信红包]'

监听到了消息就要自动点击,那么我们如何能找到相关的按钮view呢?

这时候就要用到android sdk自带的一个布局分析工具uiautomatorviewer.bat,该文件在安装目录的Android\Sdk\tools\bin下面,该工具需要再jdk8环境下运行,如果点击闪退,记事本打开该脚本,找到set java_exe= ,在后面添加上我们上一步安装的jdk8中的java.exe路径,并且注释掉下面call lib\find_java.bat的命令(在前面加一个rem),保存即可

uiautomatorviewer.bat

通过该工具可分析当前界面相关view的id和是否可点击等信息,找到对的view就可以执行自动点击抢红包的操作了(tip:微信相关view的id会不定期变化,就是为了防止我们这种坏人,如果运行受阻既可以用该工具自己分析一下是否是id变化了)

代码实现细节

1.创建工程

2.AndroidMnifest.xml文件添加无障碍权限

ini 复制代码
<uses-permission
    android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
    tools:ignore="ProtectedPermissions" />

3.创建通知监听service

kotlin 复制代码
class WechatNotificationListenerService : NotificationListenerService() {

    private val tag = WechatNotificationListenerService::class.java.simpleName

    override fun onListenerConnected() {
        super.onListenerConnected()
    }

    override fun onListenerDisconnected() {
        super.onListenerDisconnected()
        requestRebind(ComponentName(this,NotificationListenerService::class.java))
    }


    override fun onNotificationPosted(sbn: StatusBarNotification?) {
        // 如果该通知的包名不是微信,那么 pass 掉
        if ( PACKAGE_WX  != sbn!!.packageName) {
            return
        }
        val notification = sbn.notification ?: return
        var pendingIntent: PendingIntent? = null
        val extras = notification.extras
        if (extras != null) {
            // 获取通知内容
            val content = extras.getString(Notification.EXTRA_TEXT, "")
            if (!TextUtils.isEmpty(content) && content.contains("[微信红包]")) {
                Log.d(tag, "收到微信红包通知")
                pendingIntent = notification.contentIntent
            }
        }
        try {
            pendingIntent?.send()
            Log.d(tag, "成功:打开红包")
        } catch (e: PendingIntent.CanceledException) {
            Log.d(tag, "失败:打开失败")
            e.printStackTrace()
        }
    }

    companion object{
        private const val PACKAGE_WX="com.tencent.mm"
    }
}

4.创建无障碍服务service

kotlin 复制代码
class AutoOpenLuckyMoneyService : AccessibilityService() {

    private val tag = AutoOpenLuckyMoneyService::class.java.simpleName

    /** 是正在开红包 */
    private var isOpening = false

    /** 是否在查看红包 */
    private var isLooking = false

    override fun onServiceConnected() {
        serviceInfo.packageNames = arrayOf(Config.WechatPackageName)
        initView()
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        when (event.eventType) {
            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
                if (!isOpening && event.className == Config.RedPackageReceiveClassName) {
                    Log.d(tag, "显示红包弹窗")
                    isOpening = if (openRedPackage(rootInActiveWindow ?: return)) {
                        //点击开按钮
                        Log.d(tag, "成功:打开红包")
                        true
                    } else {
                        //不能开 返回聊天界面
                        Log.e(tag, "失败:打开失败")
                        performGlobalAction(GLOBAL_ACTION_BACK)
                        false
                    }
                } else if (isOpening && (event.className == Config.RedPackageDetailClassName || event.className == Config.LuckyMoneyBeforeDetailUI)) {
                    Log.d(tag, "进入红包详情页")
                    performGlobalAction(GLOBAL_ACTION_BACK)
                    isOpening = false
                }
            }
            AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> {
                if (!isLooking) {
                    if (isChatListPage(rootInActiveWindow)) {
                        // 聊天列表
                        findRedPackageInChatList(rootInActiveWindow)

                    } else if (isChatDetailPage(rootInActiveWindow)) {
                        // 聊天详情
                        if (!findAndClickRedPackage(rootInActiveWindow) && Runtime.backHome) {
                            performGlobalAction(GLOBAL_ACTION_BACK)
                        }
                    }
                }
            }
        }
    }

    private fun openRedPackage(nodeInfo: AccessibilityNodeInfo): Boolean {
        val nodes = nodeInfo.findAccessibilityNodeInfosByViewId(Config.OpenButtonResId)
        if (nodes.size == 0) return false
        val openBtn = nodes[0]
        //过滤不能点击
        if (!openBtn.isClickable) return false
        openBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
        return true
    }

    private fun findAndClickRedPackage(nodeInfo: AccessibilityNodeInfo): Boolean {
        isLooking = true
        val list = nodeInfo.findAccessibilityNodeInfosByViewId(Config.RedPackageLayoutResId)
        if (list.isNullOrEmpty()) {
            Log.e(tag, "聊天页面未找到红包")
            isLooking = false
            return false
        } else {
            Log.d(tag, "聊天页面找到红包")
        }
        val rootRect = Rect()
        nodeInfo.getBoundsInScreen(rootRect)
        for (i in list.size - 1 downTo 0) {
            val node = list[i]
            //根据左下角"微信红包"资源id过滤红包消息
            if (node.findAccessibilityNodeInfosByViewId(Config.RedPackageTextResId).size == 0) continue
            //过滤已领取|已过期
            if (node.findAccessibilityNodeInfosByViewId(Config.RedPackageExpiredResId).size > 0) continue

            if (!node.isClickable) continue
            Log.d(tag, "点击红包")
            node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
            isLooking = false
            return true
        }
        isLooking = false
        return false
    }
    
    private fun findRedPackageInChatList(nodeInfo: AccessibilityNodeInfo): Boolean {
        isLooking = true
        Log.d(tag, "聊天列表开始找红包...")
        val list = nodeInfo.findAccessibilityNodeInfosByViewId(Config.HomeRedPackageLayoutResId)
        if (list.isNullOrEmpty()) {
            Log.e(tag, "聊天列表暂无消息")
            isLooking = false
            return false
        }
        val rootRect = Rect()
        nodeInfo.getBoundsInScreen(rootRect)
        for (i in list.size - 1 downTo 0) {
            val node = list[i]
            val contentView = node.findAccessibilityNodeInfosByViewId(Config.HomeRedPackageResId)
            if (contentView.size == 0) continue
            contentView.forEach {
                if (it.text.contains("[微信红包]")) {
                    if (Runtime.NeedFilterSelf) {
                        //红包矩形位置离右边更近
                        if (it.text.indexOf("[微信红包]") == 0) return@forEach
                    }

                    if (!node.isClickable) return@forEach
                    node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                    isLooking = false
                    return true
                }
            }
        }
        isLooking = false
        return false
    }

    private fun initView() {
        try {
            val wm = this.getSystemService(WINDOW_SERVICE) as? WindowManager
            val lp = WindowManager.LayoutParams().apply {
                type = TYPE_ACCESSIBILITY_OVERLAY // 因为此权限才能展现处理
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
                }
                format = PixelFormat.TRANSLUCENT
                flags = flags or
                        FLAG_LAYOUT_NO_LIMITS or
                        FLAG_NOT_TOUCHABLE or  // 透传接触事情
                        FLAG_NOT_FOCUSABLE or  // 透传输入事情
                        FLAG_LAYOUT_IN_SCREEN
                width = DisplayUtil.dpToPx(BaseApp.getApp(), 60f)
                height = DisplayUtil.dpToPx(BaseApp.getApp(), 60f)
                gravity = Gravity.END or Gravity.CENTER_VERTICAL
            }
            val view = LayoutInflater.from(this).inflate(R.layout.red_float, null)
            wm?.addView(view, lp)
        } catch (e: Exception) {
            Log.e(tag, e.toString())
        }

    }

    private fun isChatListPage(rootNode: AccessibilityNodeInfo): Boolean {
        val nodeList = rootNode.findAccessibilityNodeInfosByViewId(Config.HomeRedPackageTitleResId)
        return nodeList.any { it.text.contains("微信") }
    }

    private fun isChatDetailPage(rootNode: AccessibilityNodeInfo): Boolean {
        val nodeList = rootNode.findAccessibilityNodeInfosByViewId(Config.ChatDetailPageLayoutResId)
        return !nodeList.isNullOrEmpty()
    }
}

相关view信息

perl 复制代码
object Config {
    /** 微信包名 */
    const val WechatPackageName = "com.tencent.mm"
    /////////////////////////////////////////////////////////////////////////
    // 微信红包
    /////////////////////////////////////////////////////////////////////////
    /** 点开红包弹窗类名 */
    const val RedPackageReceiveClassName =
        "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI"
    /** 红包详情类名 */
    const val RedPackageDetailClassName = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI"
    const val LuckyMoneyBeforeDetailUI =
        "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyBeforeDetailUI"
    /** "开"图片按钮资源id */
    const val OpenButtonResId = "com.tencent.mm:id/j6g"

    ///////////////////////////
    // 聊天详情页
    //////////////////////////
    /** 聊天界面父布局id */
    const val ChatDetailPageLayoutResId = "com.tencent.mm:id/bks"
    /** 红包父布局资源id */
    const val RedPackageLayoutResId = "com.tencent.mm:id/bkg"
    /** 左下角"微信红包"资源id */
    const val RedPackageTextResId = "com.tencent.mm:id/a3y"
    /** 中间的"已过期|以领取"资源id */
    const val RedPackageExpiredResId = "com.tencent.mm:id/a3m"
    /////////////////////////
    // 首页聊天列表
    //////////////////////////
    /** 标题id */
    const val HomeRedPackageTitleResId = "android:id/text1"

    /**  每个抽屉的父布局id */
    const val HomeRedPackageLayoutResId = "com.tencent.mm:id/cj1"

    /**  包含消息内容的控件id */
    const val HomeRedPackageResId = "com.tencent.mm:id/ht5"
}

在res-xml文件夹下创建文件accessibility_service.xml, 只需要监听typeWindowContentChanged和typeWindowStateChanged事件即可

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/wx_service_desc"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm" /> 

在AndroidManifest.xml中添加service

ini 复制代码
<application>
    <activity
        android:name=".ui.MainActivity"
        android:exported="true" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <service
        android:name=".service.AutoOpenLuckyMoneyService"
        android:enabled="true"
        android:exported="true"
        android:label="@string/wx_service_desc"
        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" />
    </service>
    <service android:name=".service.WechatNotificationListenerService"
        android:label="微信通知监听服务"
        android:exported="true"
        android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
        <intent-filter>
            <action android:name="android.service.notification.NotificationListenerService" />
        </intent-filter>
    </service>
</application>
```

Github地址
APP下载体验

相关推荐
服装学院的IT男1 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽2 小时前
Android 源码集成可卸载 APP
android
码农明明2 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风3 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教4 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python
编程乐学5 小时前
基于Android Studio 蜜雪冰城(奶茶饮品点餐)—原创
android·gitee·android studio·大作业·安卓课设·奶茶点餐
problc6 小时前
Android中的引用类型:Weak Reference, Soft Reference, Phantom Reference 和 WeakHashMap
android
IH_LZH6 小时前
Broadcast:Android中实现组件及进程间通信
android·java·android studio·broadcast
去看全世界的云6 小时前
【Android】Handler用法及原理解析
android·java
机器之心7 小时前
o1 带火的 CoT 到底行不行?新论文引发了论战
android·人工智能