微信自动抢红包助手

背景

每逢佳节红包多,而我却在灯火阑珊处,搓着麻将无暇顾及,错失几个亿 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下载体验

相关推荐
踏雪羽翼2 小时前
android TextView实现文字字符不同方向显示
android·自定义view·textview方向·文字方向·textview文字显示方向·文字旋转·textview文字旋转
lxysbly2 小时前
安卓玩MRP冒泡游戏:模拟器下载与使用方法
android·游戏
夏沫琅琊5 小时前
Android 各类日志全面解析(含特点、分析方法、实战案例)
android
程序员JerrySUN5 小时前
OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录
android·java·开发语言·redis·yolo·架构
TeleostNaCl6 小时前
Android | 启用 TextView 跑马灯效果的方法
android·经验分享·android runtime
TheNextByte17 小时前
Android USB文件传输无法使用?5种解决方法
android
quanyechacsdn8 小时前
Android Studio创建库文件用jitpack构建后使用implementation方式引用
android·ide·kotlin·android studio·implementation·android 库文件·使用jitpack
程序员陆业聪9 小时前
聊聊2026年Android开发会是什么样
android
编程大师哥9 小时前
Android分层
android
极客小云11 小时前
【深入理解 Android 中的 build.gradle 文件】
android·安卓·安全架构·安全性测试