微信抢红包助手实践

前言

来啦老铁!

咱们在前一阵子的文章 老铁,原来做个Android App这么简单~ 中学习了如何动手做 Android App,当时只是很简单的涉及了一下,今天咱们来稍微应用一下:

  • 微信抢红包助手实践;

我个人挺好奇抢红包助手是怎么实现的,几年前有听过一个朋友说这个东西挺简单的,我倒想知道,到底有多简单,顺便为自己做个抢红包助手,他不香嘛~

本文章代码已上传 LuckyDog GitHub 仓库,欢迎取阅。

整体步骤

  1. 下载安装Android Studio;
  2. 新建Android项目;
  3. 创建 service 包;
  4. 创建 LuckyService 类;
  5. 在 LuckyService 类中完成抢红包逻辑;
  6. 编写无障碍配置;
  7. AndroidManifest.xml 中注册 LuckyService 类和无障碍配置;
  8. 创建配置信息文件;
  9. 安装抢红包助手;
  10. 设置抢红包助手;
  11. 自动抢红包;

1. 下载安装Android Studio;

参考前一阵子的文章 老铁,原来做个Android App这么简单~

2. 新建Android项目;

新建Android项目

3. 创建 service 包;

项目结构

4. 创建 LuckyService 类;

package com.dylanz.luckydog.service;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class LuckyService extends AccessibilityService {
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

    }

    @Override
    public void onInterrupt() {

    }
}

5. 在 LuckyService 类中完成抢红包逻辑;

package com.dylanz.luckydog.service;

import android.accessibilityservice.AccessibilityService;
import android.app.Notification;
import android.app.PendingIntent;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.annotation.TargetApi;
import android.os.Build;

import androidx.annotation.RequiresApi;

import java.util.List;

public class LuckyService extends AccessibilityService {
    /**
     * 日志的 tag,随意
     */
    public static final String TAG = "LuckyService";

    /**
     * 红包是否打开的状态记录变量
     */
    private boolean isRedPacketOpened = false;

    /**
     * 红包消息辨别关键字
     */
    private static final String HONG_BAO_TXT = "[微信红包]";

    /**
     * 有"开"的那个小弹窗的 className
     */
    private static final String ACTIVITY_DIALOG_LUCKY_MONEY = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI";

    /**
     * 红包领取后的详情页面的 className
     */
    private static final String LUCKY_MONEY_DETAIL = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI";

    /**
     * 收到的红包,整个控件的 id
     */
    private static final String RED_PACKET_ID = "com.tencent.mm:id/tv";

    /**
     * 已领过的红包有个"已领取"字眼,这个字眼对应的控件 id
     */
    private static final String OPENED_ID = "com.tencent.mm:id/tt";

    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        //获取当前界面包名
        CharSequence packageNameChar = event.getPackageName();
        Log.e(TAG, "packageNameChar:" + packageNameChar);
        if (packageNameChar == null || !packageNameChar.toString().equals("com.tencent.mm")) {
            return;
        }
        isRedPacketOpened = false;
        //获取当前类名
        String className = event.getClassName().toString();

        //红包领取后的详情页面,自动返回
        if (className.equals(LUCKY_MONEY_DETAIL)) {
            Log.e(TAG, "红包已领取,返回聊天页面:");
            performGlobalAction(GLOBAL_ACTION_BACK);
            return;
        }

        //当前为红包弹出窗(有"开"的那个小弹窗)
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && className.equals(ACTIVITY_DIALOG_LUCKY_MONEY)) {
            //有可能会由于网络原因,"开"的那个小弹框会需要加载后才显示,我们此处最多等 5 秒钟
            for (int i = 0; i < 1000; i++) {
                if (isRedPacketOpened) {
                    break;
                }
                AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                openRedPacket(rootNode);
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return;
        }

        //遍历消息列表的每个消息,点击红包控件
        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        jumpIntoRedPacket(nodeInfo);

        //是否微信聊天页面的类
        if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
            Log.e(TAG, "接收到通知栏消息");
            Notification notification = (Notification) event.getParcelableData();
            //获取通知消息详情
            if (notification.tickerText == null) {
                return;
            }
            String content = notification.tickerText.toString();
            //解析消息
            String[] msg = content.split(":");
            if (msg.length == 0) {
                return;
            }
            String text = msg[1].trim();
            if (text.contains(HONG_BAO_TXT)) {
                Log.e(TAG, "接收到通知栏红包消息,点击消息,进入聊天界面");
                //打开通知栏的intent,即打开对应的聊天界面
                PendingIntent pendingIntent = notification.contentIntent;
                try {
                    pendingIntent.send();
                } catch (PendingIntent.CanceledException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 点击"开"按钮
     *
     * @param rootNode rootNode
     */
    private void openRedPacket(AccessibilityNodeInfo rootNode) {
        if (rootNode == null) {
            return;
        }
        for (int i = 0; i < rootNode.getChildCount(); i++) {
            AccessibilityNodeInfo node = rootNode.getChild(i);
            if ("android.widget.Button".equals(node.getClassName().toString())) {
                node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                isRedPacketOpened = true;
                break;
            }
            openRedPacket(node);
        }
    }

    @Override
    public void onInterrupt() {

    }

    /**
     * 点击"开"按钮
     *
     * @param rootNode rootNode
     */
    private void jumpIntoRedPacket(AccessibilityNodeInfo rootNode) {
        if (rootNode == null) {
            return;
        }
        for (int i = 0; i < rootNode.getChildCount(); i++) {
            //获取到子控件
            AccessibilityNodeInfo node = rootNode.getChild(i);
            //获取红包控件
            AccessibilityNodeInfo target = findViewByID(node, RED_PACKET_ID);
            if (target != null) {
                //已领取这个控件为空,红包还没有被领取
                if (findViewByID(node, OPENED_ID) == null) {
                    Log.e(TAG, "找到未领取的红包,点击红包");
                    performViewClick(target);
                    break;
                }
            }
            jumpIntoRedPacket(node);
        }
    }

    /**
     * 模拟点击事件
     *
     * @param nodeInfo nodeInfo
     */
    public void performViewClick(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo == null) {
            return;
        }
        while (nodeInfo != null) {
            Log.e(TAG, "打开红包1");
            if (nodeInfo.isClickable()) {
                Log.e(TAG, "打开红包2");
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                break;
            }
            nodeInfo = nodeInfo.getParent();
        }
    }

    /**
     * 查找对应ID的View
     *
     * @param accessibilityNodeInfo AccessibilityNodeInfo
     * @param id                    id
     * @return View
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    public AccessibilityNodeInfo findViewByID(AccessibilityNodeInfo accessibilityNodeInfo, String id) {
        if (accessibilityNodeInfo == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id);
        if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
            for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
                if (nodeInfo != null) {
                    return nodeInfo;
                }
            }
        }
        return null;
    }
}

6. 编写无障碍配置;

res 下新建 xml 文件夹(名字任意),创建一个 xml 文件,如:accessible_service_wx_config.xml,写入:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/app_description"
    android:notificationTimeout="10"/>
参数解读:后续有机会补上;

7. AndroidManifest.xml 中注册 LuckyService 类和无障碍配置;

修改前:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dylanz.luckydog">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.LuckyDog">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

修改后:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dylanz.luckydog">

    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.LuckyDog">
        <activity
            android:name=".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.LuckyService"
            android:label="@string/app_description"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
            android:exported="true">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessible_service_wx_config" />
        </service>
    </application>

</manifest>
注:android:exported 代表是否支持其它应用调用当前组件,默认值:若包含有intent-filter 默认值为true,若没有intent-filter 则默认值为false,我们此处应设置为true;

8. 创建配置信息文件;

res/values 下创建 strings.xml 文件,内容如下:

<resources>
    <string name="app_name">LuckyDog微信红包助手 - 基础版</string>
    <string name="app_description">LuckyDog抢红包助手</string>
</resources>
注:这里头的配置信息,就是 accessible_service_wx_config.xml 文件和 AndroidManifest.xml 文件中会用到的 @string/app_name 和 @string/app_description,此步骤根据 Android Studio 提示完成;

9. 安装抢红包助手;

  • 在 Android Studio 中编译后,手机弹出安装提示后,安装助手;
安装调试
咱们也可以找到编译出来的 apk 文件,后续就不用再在 Android Studio 里头编译安装了,直接用 apk 文件安装就好啦!
apk 文件

apk 文件安装法也是将红包助手与其他人分享的手段。

  • 安装过程:
安装过程1
安装过程2

这个页面不要怕,咱们自己开发的软件,没有发布,都会这样,当然咱们没有做什么见不得人的功能;

安装完成

10. 设置抢红包助手;

点击抢红包助手页面上的“红包助手 - 无障碍设置”,打开设置页面后,选择“LuckyDog抢红包助手”,打开“使用LuckyDog抢红包助手”即可;

11. 自动抢红包;

以上设置好了之后,返回检查助手是否是启动状态,是的话就开始了自动抢红包辅助了!不难吧~

助手启动状态

目前支持的自动抢红包场景:

  • 在聊天页面自动抢红包;
  • 未开启消息免打扰时,微信后台运行;
  • 未开启消息免打扰时,退出微信;
  • 开启消息免打扰,微信界面后台运行,人工进入聊天页面;
  • 聊天界面有多个红包,自动全部抢;

咱们出发点就想做个简单的抢红包助手,没有考虑特别多的其他交互,如发感谢信、红包统计等功能,如果您有兴趣,可以自行研究,原理与本文章介绍的类似。

感谢其他简书作者的参考文献,包括并不限于:Android微信抢红包辅助

好了,熬了好几个晚上的助手终于可以告一段落了,敬默默努力的每一位朋友~

如果本文对您有帮助,麻烦动动手指点点赞?

非常感谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容