Android--抢红包神器 辅助功能Accessibility
新鲜出炉的抢红包神器
前提:实现一个微信自动抢红包并不是很难,原理就是利用android的辅助功能,监听一下窗口变化,找到对应控件ID,模拟点击。写好一个service服务类即可完成。但上手会发现这其中还是有很多问题的,所以我们主要是适配版本解决问题。
一、问题
关于抢红包神器的文章也很多,但使用起来效果却不佳,总结一下会有以下几个问题 (看看是不是就是你们遇到的问题):
-
1. 红包来源
大多数抢红包神器只适配了从通知栏进入的红包 也就是微信在后台 此时来了红包,监听通知栏,然后点进去【抢】。但是事实上我认为还需要适配两个场景,一个是在聊天列表页来红包,一个是在好友会话页面来了红包。
-
2. 微信版本更新,抢红包神器红能就不能用了
这个就是需要各个版本进行适配 因为微信每更新版本,每个控件的id会发生变化,类名有时候甚至都会混淆,改变。所以当类名、id名称发生改变时,程序找id为null,也就没有办法继续操作点击,从而导致抢红包神器功能失效。
-
3. 卡在【拆】的页面不继续
关于这个问题有以下发生的可能,第一种就是【拆】这个button的id发生了变化,第二种是手机版本大于21,在抢红包这个页面可能存在window嵌套,不能单单在getRootInActiveWindow 查找,需要对21前和21后的版本进行适配。亲测有效。
二、解决方案
-
1. 红包来源问题
若想实现在聊天列表页和好友会话页面的红包监听,需要我们监听窗口内容变化,也就是AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,当监听到窗口内容变化时做相应的处理。
-
2. 适配微信版本
本地适配,存放在一个静态 map中 key存放微信版本 value存放一个需要适配的id合集对象。然后在服务开始前判断当前手机安装微信版本,然后去本地map中取出相应的id合集。这样使用map存储版本号以及需要适配的id值来做兼容。或是直接适配最新的版本,用户版本过低,提示升级。
-
3.拆红包
手机版本21前后做适配
三、了解android辅助功能Accessibility
AccessibilityServiceInfo
AccessibilityService
AccessibilityManager
AccessibilityEvent
Build accessibility services
AccessibilityServiceInfo
AccessibiliyEvent
AccessibilityManager
四、需要适配的对象(id名称、class名称等)
我们把需要适配的对象起名为WxChatTag 云控更新(构建json)或本地静态map存储。
public static class WxChatTag implements Serializable {
//4个Activity名字
private String ChatListClassName;//聊天列表页
private String ConversationClassName;//会话页面
private String PopOpenRedPacketClassName;//弹出开红包的页面
private String MoneyDetailClassName;//红包领取详情页面
//id
private String OpenRedPacketButtonId;//拆红包的id
private String MoneyDetailTextId;//红包详情页的钱数id
//会话详情页
private String ConversationPageListViewId;//会话页面的listview的id
private String ConversationPageRedPacketItemId;//会话页面红包View的id
private String ConversationPageRecentMsgId;//会话页面最新消息的id
//聊天记录列表页面
private String ChatListPageItemId;//聊天列表页面Item的id
private String ChatListPageRedHintId;//聊天列表页红色圆点提示view的id
private String ChatListPageKeyTextId;//聊天列表页新消息信息text的id
private String KeySearchName;//会话列表页面查找红包的关键词 领取红包
//getter setter
}
关于class name名字获取的技巧 【adb命令】
adb shell dumpsys activity | grep "mFocusedActivity" 打印栈顶当前activity
关于id对应情况如下图示
MoneyDetailTextIdConversationPageRedPacketItemId
ConversationPageListViewId
ConversationPageRecentMsgId
OpenRedPacketButtonId
ChatListPageItemId;
ChatListPageKeyTextId
ChatListPageRedHintId
五、简易流程图
以下为我的抢红包神器的主要流程图 大家作为参考。
监听onAccessibilityEvent为了适配微信版本,首先要判断当前用户的微信版本和你适配的微信版本是否相同。然后进行自定义操作。你可以本地map存储取值,也可以动态适配,网络请求。
监听到通知栏的后续操作监听通知栏
监听窗口变化监听到窗口变化
首先判断当前activity 一共有四个activity窗口需要我们做处理,其他不做处理。
监听内容变化监听内容变化
这一部的主要目的就是实现在聊天列表页和好友会话页面的红包监听。
六、主要代码
public class MyWxQhbService extends AccessibilityService {
/**
* 微信的包名
*/
public static final String PACKAGE_NAME = "com.tencent.mm";
/**
* 红包消息的关键字
*/
private static final String KEY = "[微信红包]";
private static final int APP_STATE_BACKGROUND = -1;
private static final int APP_STATE_FOREGROUND = 1;
private static final int WINDOW_LAUNCHER_UI = 1; // ChatListClassName
private static final int WINDOW_LUCKY_MONEY_OPEN = 2; // PopOpenRedPacketClassName
private static final int WINDOW_LUCKY_MONEY_DETAILUI = 3; // MoneyDetailClassName
private static final int WINDOW_OTHER = -1;
private static final int SILENCE_TIME = 1300;
private int mAppState = APP_STATE_FOREGROUND;
private int mCurrentWindow = WINDOW_OTHER;
private boolean isSilence; // 沉默, 防止没抢到红包而反复点
private static String ChatListClassName = "com.tencent.mm.ui.LauncherUI"; // 聊天列表页
private static String ConversationClassName = "com.tencent.mm.ui.LauncherUI"; //会话页面
private static String PopOpenRedPacketClassName = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI"; //弹出开红包的页面
private static String MoneyDetailClassName = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI"; // 红包领取详情页面
private static String OpenRedPacketButtonId = "com.tencent.mm:id/cnu"; // 拆红包页面 '开'的id
private static String MoneyDetailTextId = "com.tencent.mm:id/cnu";//红包详情页 钱数id
private static String ChatListPageItemId = "com.tencent.mm:id/azj"; // 聊天列表页面Item的id
private static String ChatListPageRedHintId = "com.tencent.mm:id/lu"; // 聊天列表页红色圆点提示view的id
private static String ChatListPageKeyTextId = "com.tencent.mm:id/azn"; // 聊天记录列表中Item关键词的id
private static String ConversationPageListViewId = "com.tencent.mm:id/ahf"; // 会话页面的listview的id
private static String ConversationPageRedPacketItemId = "com.tencent.mm:id/aku"; // 会话页面红包View的id
private static String ConversationPageRecentMsgId = "com.tencent.mm:id/a5"; // 会话页面最新一条信息的id
private static String KeySearchName = "领取红包";//会话页面关键字搜索
private PackageInfo mWeChatPackageInfo;
private Intent intent = new Intent("com.wx.qhb.receiver");
@Override
protected void onServiceConnected() {
super.onServiceConnected();
Log.d("qhb", "onServiceConnected");
updateConfig();
}
/**
* 更新可变id、class的Config
*/
private void updateConfig() {
Log.d("qhb", "updateConfig");
//获取微信包信息
mWeChatPackageInfo = PackageUtils.getPackageInfo(getApplicationContext(), "com.tencent.mm");
final int versionCode = mWeChatPackageInfo.versionCode;
Log.d("qhb", "微信:versionCode:" + versionCode);
if (versionCode == 1360) {
return;
} else {
Log.d("qhb", "versionCode>1360");
//网络请求
ConfigHelper.getConfig(getApplicationContext(), new ConfigHelper.RequestConfigCallBack() {
@Override
public void onRequestFailed() {
Toast.makeText(getApplicationContext(), "网络错误,启动工具失败", Toast.LENGTH_SHORT).show();
}
@Override
public void onSuccess(ConfigBean.Config config) {
if (config != null) {
boolean isInit = false;
List<ConfigBean.WeChatVersionConfig> weChatVersionConfigs = config.getWeChatVersionConfigs();
for (ConfigBean.WeChatVersionConfig weChatVersionConfig : weChatVersionConfigs) {
if (weChatVersionConfig.getWeChatVersionCode() == versionCode) {
isInit = true;
initField(weChatVersionConfig.getWxChatTag());
}
}
if (!isInit) {
//说明没有适配该微信版本
Log.d("qhb", "发送广播");
sendBroadcast(intent);
}
}
}
});
}
}
/**
* 赋值config
*
* @param mWxChatTag
*/
private void initField(ConfigBean.WxChatTag mWxChatTag) {
Log.d("qhb", "initField");
if (mWxChatTag == null) {
return;
}
ChatListClassName = mWxChatTag.getChatListClassName();
ConversationClassName = mWxChatTag.getConversationClassName();
PopOpenRedPacketClassName = mWxChatTag.getPopOpenRedPacketClassName();
MoneyDetailClassName = mWxChatTag.getMoneyDetailClassName();
OpenRedPacketButtonId = mWxChatTag.getOpenRedPacketButtonId();
MoneyDetailTextId = mWxChatTag.getMoneyDetailTextId();
ChatListPageItemId = mWxChatTag.getChatListPageItemId();
ChatListPageRedHintId = mWxChatTag.getChatListPageRedHintId();
ChatListPageKeyTextId = mWxChatTag.getChatListPageKeyTextId();
ConversationPageListViewId = mWxChatTag.getConversationPageListViewId();
ConversationPageRecentMsgId = mWxChatTag.getConversationPageRecentMsgId();
ConversationPageRedPacketItemId = mWxChatTag.getConversationPageRedPacketItemId();
KeySearchName = mWxChatTag.getKeySearchName();
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
switch (event.getEventType()) {
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: // 通知栏状态变化
notificationEvent(event);
break;
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: // 窗口状态变化
windowStateEvent(event);
break;
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: // 窗口内容变化
windowContentEvent(event);
break;
default:
break;
}
}
/**
* 微信是否运行在前台
*/
private boolean isRunningForeground(Context context) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
if (!tasks.isEmpty()) {
String packageName = tasks.get(0).topActivity.getPackageName();
if (PACKAGE_NAME.equals(packageName)) {
return true;
}
}
return false;
}
/**
* 处理通知栏事件
*/
private void notificationEvent(AccessibilityEvent event) {
Log.d("qhb", "通知栏事件");
if (!isRunningForeground(getApplicationContext())) {
mAppState = APP_STATE_BACKGROUND;
} else {
mAppState = APP_STATE_FOREGROUND;
}
Parcelable data = event.getParcelableData();
if (data == null || !(data instanceof Notification)) {
return;
}
List<CharSequence> texts = event.getText();
if (!texts.isEmpty()) {
String text = String.valueOf(texts.get(0));
int index = text.lastIndexOf(":");
if (index != -1) {
text = text.substring(index + 1);
}
if (text.contains(KEY)) {
// isHasRedPacket = true;
Notification nf = (Notification) data;
PendingIntent pendingIntent = nf.contentIntent;
if (NotifyUtils.isLockScreen(getApplicationContext())) { // 是否为锁屏或黑屏状态
if (SpUtils.isLockScreenAutomaticGrab()) {
NotifyUtils.wakeAndUnlock(getApplicationContext());
NotifyUtils.send(pendingIntent); // 打开微信
} else {
NotifyUtils.showNotify(getApplicationContext(), String.valueOf(nf.tickerText), pendingIntent); // 显示有红包通知
}
} else {
NotifyUtils.send(pendingIntent); // 打开微信
}
// 播放声音和震动
NotifyUtils.playEffect(getApplicationContext());
}
}
}
/**
* 窗口状态变化
*/
private void windowStateEvent(AccessibilityEvent event) {
// Log.d("qhb", "窗口状态变化");
CharSequence className = event.getClassName();
if (className == null) {
return;
}
String name = className.toString();
if (className.equals(ChatListClassName)) {
mCurrentWindow = WINDOW_LAUNCHER_UI;
} else if (className.equals(ConversationClassName)) {
mCurrentWindow = WINDOW_LAUNCHER_UI;
} else if (className.equals(PopOpenRedPacketClassName)) {
mCurrentWindow = WINDOW_LUCKY_MONEY_OPEN;
} else if (className.equals(MoneyDetailClassName)) {
mCurrentWindow = WINDOW_LUCKY_MONEY_DETAILUI;
} else {
mCurrentWindow = WINDOW_OTHER;
}
switch (mCurrentWindow) {
case WINDOW_LAUNCHER_UI:
clickRedPackets(); // 在聊天界面, 去点中红包
break;
case WINDOW_LUCKY_MONEY_OPEN:
getLuckyMoney();
break;
case WINDOW_LUCKY_MONEY_DETAILUI:
detailsRedPacket(); //看详细的纪录界面
break;
}
}
/**
* 窗口内容变化
*/
private void windowContentEvent(AccessibilityEvent event) {
Log.d("qhb", "窗口内容变化");
if (mCurrentWindow != WINDOW_LAUNCHER_UI) { // //不在聊天界面或聊天列表,不处理
return;
}
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo == null) {
return;
}
// 直接去获取当前会话的最后一条Item, 不为null, 则是当前会话列表
AccessibilityNodeInfo item = AccessibilityUtils.findNodeInfosByIdLast(nodeInfo, ConversationPageRecentMsgId);
if (item != null) {
if (isSilence) { // 沉默中, return
return;
}
clickLastMsg(nodeInfo);
return;
}
// 直接去获取聊天记录的第一条Item, 不为null, 则是聊天记录列表
item = AccessibilityUtils.findNodeInfosById(nodeInfo, ChatListPageItemId); //第一条消息
if (item != null) {
AccessibilityNodeInfo red = AccessibilityUtils.findNodeInfosById(item, ChatListPageRedHintId);
if (red != null) { // 有小圆点, 说明有未读消息
AccessibilityNodeInfo label = AccessibilityUtils.findNodeInfosById(item, ChatListPageKeyTextId);
if (label != null) {
String text = String.valueOf(label.getText());
Log.d("qhb", "列表页" + label.getText());
int index = text.lastIndexOf(":");
if (index != -1) {
text = text.substring(index + 1);
}
if (text.contains(KEY)) {
// isHasRedPacket = true;
// 有红包, 点开item
AccessibilityUtils.performClick(label);
}
}
}
return;
}
}
private void clickRedPackets() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo == null) {
return;
}
//分两种情况1.在聊天列表页
//2.在聊天会话页
//这两个页面的名称都是com.tencent.mm.ui.LauncherUI这个名字
AccessibilityNodeInfo listNode = AccessibilityUtils.findNodeInfosByTexts(nodeInfo, KEY);
if (listNode != null) {
Log.d("qhb", "聊天列表的微信红包的node" + "不为空");
AccessibilityUtils.performClick(listNode.getParent().getParent().getParent().getParent());
return;
} else {
Log.d("qhb", "聊天列表的微信红包的node" + "为空");
}
// 聊天会话窗口,遍历节点匹配
AccessibilityNodeInfo node = AccessibilityUtils.findNodeInfosByText(nodeInfo, KeySearchName);
if (node != null) {
Log.d("qhb", "会话页领取红包的node" + "不为空");
AccessibilityUtils.performClick(node);
} else {
Log.d("qhb", "会话页领取红包的node" + "为空");
return;
}
}
/**
* 点最新消息
*/
private void clickLastMsg(AccessibilityNodeInfo nodeInfo) {
AccessibilityNodeInfo listView = AccessibilityUtils.findNodeInfosById(nodeInfo, ConversationPageListViewId); //找到聊天会话页面的会话内容的listview
if (listView == null) {
return;
}
if (mWeChatPackageInfo.versionCode >= 1360 && listView.getChildCount() > 0) {
listView = listView.getChild(0);
}
int childCount = listView.getChildCount();
if (childCount <= 0) {
return;
}
AccessibilityNodeInfo item = listView.getChild(childCount - 1);
if (item != null) { // 每一条新消息都试着点红包
AccessibilityNodeInfo real = AccessibilityUtils.findNodeInfosById(item, ConversationPageRedPacketItemId);
if (real != null) { // 真红包
// 新版本后, 1100(包括)以上, 能判断红包是否已经领取
if (mWeChatPackageInfo.versionCode >= 1100) {
AccessibilityNodeInfo realToo = AccessibilityUtils.findNodeInfosByText(real, KeySearchName);
if (realToo == null) {
return;
}
}
AccessibilityUtils.performClick(real);
}
}
return;
}
/**
* 抢红包
*/
private void getLuckyMoney() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
List<AccessibilityWindowInfo> nodeInfos = getWindows();
for (AccessibilityWindowInfo window : nodeInfos) {
AccessibilityNodeInfo nodeInfo = window.getRoot();
if (nodeInfo == null) {
break;
}
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(OpenRedPacketButtonId);
if (list != null && list.size() > 0) {
AccessibilityUtils.performOpenRedPacketWithDelay(list.get(0));
return;
}
}
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); //获得整个窗口对象
if (nodeInfo == null) {
return;
}
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(OpenRedPacketButtonId);
if (list != null && list.size() > 0) {
AccessibilityUtils.performOpenRedPacketWithDelay(list.get(0));
return;
}
//如果没找到拆红包的button,则将界面上所有子节点都点击一次
for (int i = nodeInfo.getChildCount() - 1; i >= 0; i--) {
if (("android.widget.Button").equals(nodeInfo.getChild(i).getClassName())) {
AccessibilityUtils.performOpenRedPacketWithDelay(nodeInfo.getChild(i));
return;
}
}
// Toast.makeText(this, "未找到开红包按钮", Toast.LENGTH_SHORT).show();
}
}
/**
* 领取详情
*/
private void detailsRedPacket() {
// 到这, 领取流程算是完了
Log.d("qhb", "领取流程完事");
List<AccessibilityNodeInfo> moneyNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(MoneyDetailTextId);
if (moneyNode != null && moneyNode.size() > 0) {
String moneyStr = moneyNode.get(0).getText().toString();
float money = Float.parseFloat(moneyStr);
SpUtils.put("totalNum", SpUtils.get("totalNum", 0) + 1);
SpUtils.put("totalMoney", SpUtils.get("totalMoney", 0f) + money);
}
back();
}
/**
* 返回
*/
private void back() {
back(-1);
}
/**
* 返回
*/
private void back(int count) {
Log.d("qhb", "back:" + count);
silence(); // 沉默
int backCount;
if (mAppState == APP_STATE_BACKGROUND) {
mAppState = APP_STATE_FOREGROUND;
backCount = 3;
} else {
backCount = 1;
}
if (count != -1) {
backCount = count;
}
for (int i = 0; i < backCount; i++) {
AccessibilityUtils.performBack(this);
if (i < backCount - 1) {
SystemClock.sleep(666);// 需要个时间差
}
}
}
/**
* 沉默
*/
private void silence() {
isSilence = true; // 开启沉默
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
isSilence = false;
}
}, SILENCE_TIME);
}
@Override
public void onInterrupt() {
Log.d("qhb", "onInterrupt 抢红包服务中断");
}
}
七、效果图
开启服务通知栏红包
聊天列表页来了红包
会话页的红包