背景
每逢佳节红包多,而我却在灯火阑珊处,搓着麻将无暇顾及,错失几个亿 o(╥﹏╥)o。 为了让各位都能马上抢到红包,发大财,写下这边文章,教大家如何写一个自动抢红包的app,手机放在边上便能让钱叮叮叮进入你的钱包。
实现原理
app需要获取监听通知和使用无障碍服务的权限,然后监听红包消息来实现自动点击红包
文章结尾有github源码地址
实现效果

实现思路
首先要获取红包来了的消息,那红包消息来了微信会有多少种提醒形式呢,现有的两种形式有:
- 不在聊天列表,红包消息会以通知的形式通知用户
- 在聊天列表时会有'[微信红包]'的提示字样
那么如何获取微信红包通知呢?
官方推荐的方式是继承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>
```