新鮮出爐的搶紅包神器
前提:實(shí)現(xiàn)一個微信自動搶紅包并不是很難胯甩,原理就是利用android的輔助功能,監(jiān)聽一下窗口變化堪嫂,找到對應(yīng)控件ID偎箫,模擬點(diǎn)擊。寫好一個service服務(wù)類即可完成皆串。但上手會發(fā)現(xiàn)這其中還是有很多問題的淹办,所以我們主要是適配版本解決問題。
一恶复、問題
關(guān)于搶紅包神器的文章也很多怜森,但使用起來效果卻不佳速挑,總結(jié)一下會有以下幾個問題 (看看是不是就是你們遇到的問題):
-
1. 紅包來源
大多數(shù)搶紅包神器只適配了從通知欄進(jìn)入的紅包 也就是微信在后臺 此時來了紅包,監(jiān)聽通知欄副硅,然后點(diǎn)進(jìn)去【搶】姥宝。但是事實(shí)上我認(rèn)為還需要適配兩個場景,一個是在聊天列表頁來紅包恐疲,一個是在好友會話頁面來了紅包腊满。
-
2. 微信版本更新,搶紅包神器紅能就不能用了
這個就是需要各個版本進(jìn)行適配 因?yàn)槲⑿琶扛掳姹九嗉海總€控件的id會發(fā)生變化碳蛋,類名有時候甚至都會混淆,改變省咨。所以當(dāng)類名肃弟、id名稱發(fā)生改變時,程序找id為null零蓉,也就沒有辦法繼續(xù)操作點(diǎn)擊笤受,從而導(dǎo)致?lián)尲t包神器功能失效。
-
3. 卡在【拆】的頁面不繼續(xù)
關(guān)于這個問題有以下發(fā)生的可能壁公,第一種就是【拆】這個button的id發(fā)生了變化感论,第二種是手機(jī)版本大于21,在搶紅包這個頁面可能存在window嵌套紊册,不能單單在getRootInActiveWindow 查找比肄,需要對21前和21后的版本進(jìn)行適配。親測有效囊陡。
二芳绩、解決方案
-
1. 紅包來源問題
若想實(shí)現(xiàn)在聊天列表頁和好友會話頁面的紅包監(jiān)聽,需要我們監(jiān)聽窗口內(nèi)容變化撞反,也就是AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED妥色,當(dāng)監(jiān)聽到窗口內(nèi)容變化時做相應(yīng)的處理。
-
2. 適配微信版本
本地適配遏片,存放在一個靜態(tài) map中 key存放微信版本 value存放一個需要適配的id合集對象嘹害。然后在服務(wù)開始前判斷當(dāng)前手機(jī)安裝微信版本,然后去本地map中取出相應(yīng)的id合集吮便。這樣使用map存儲版本號以及需要適配的id值來做兼容笔呀。或是直接適配最新的版本髓需,用戶版本過低许师,提示升級。
-
3.拆紅包
手機(jī)版本21前后做適配
三、了解android輔助功能Accessibility
AccessibilityServiceInfo
AccessibilityService
AccessibilityManager
AccessibilityEvent
Build accessibility services
四微渠、需要適配的對象(id名稱搭幻、class名稱等)
我們把需要適配的對象起名為WxChatTag 云控更新(構(gòu)建json)或本地靜態(tài)map存儲。
public static class WxChatTag implements Serializable {
//4個Activity名字
private String ChatListClassName;//聊天列表頁
private String ConversationClassName;//會話頁面
private String PopOpenRedPacketClassName;//彈出開紅包的頁面
private String MoneyDetailClassName;//紅包領(lǐng)取詳情頁面
//id
private String OpenRedPacketButtonId;//拆紅包的id
private String MoneyDetailTextId;//紅包詳情頁的錢數(shù)id
//會話詳情頁
private String ConversationPageListViewId;//會話頁面的listview的id
private String ConversationPageRedPacketItemId;//會話頁面紅包View的id
private String ConversationPageRecentMsgId;//會話頁面最新消息的id
//聊天記錄列表頁面
private String ChatListPageItemId;//聊天列表頁面Item的id
private String ChatListPageRedHintId;//聊天列表頁紅色圓點(diǎn)提示view的id
private String ChatListPageKeyTextId;//聊天列表頁新消息信息text的id
private String KeySearchName;//會話列表頁面查找紅包的關(guān)鍵詞 領(lǐng)取紅包
//getter setter
}
關(guān)于class name名字獲取的技巧 【adb命令】
adb shell dumpsys activity | grep "mFocusedActivity" 打印棧頂當(dāng)前activity
關(guān)于id對應(yīng)情況如下圖示
五逞盆、簡易流程圖
以下為我的搶紅包神器的主要流程圖 大家作為參考檀蹋。
為了適配微信版本,首先要判斷當(dāng)前用戶的微信版本和你適配的微信版本是否相同纳击。然后進(jìn)行自定義操作续扔。你可以本地map存儲取值,也可以動態(tài)適配焕数,網(wǎng)絡(luò)請求纱昧。
監(jiān)聽通知欄
監(jiān)聽到窗口變化
首先判斷當(dāng)前activity 一共有四個activity窗口需要我們做處理,其他不做處理堡赔。
監(jiān)聽內(nèi)容變化
這一部的主要目的就是實(shí)現(xiàn)在聊天列表頁和好友會話頁面的紅包監(jiān)聽识脆。
六、主要代碼
public class MyWxQhbService extends AccessibilityService {
/**
* 微信的包名
*/
public static final String PACKAGE_NAME = "com.tencent.mm";
/**
* 紅包消息的關(guān)鍵字
*/
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; // 沉默, 防止沒搶到紅包而反復(fù)點(diǎn)
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"; // 紅包領(lǐng)取詳情頁面
private static String OpenRedPacketButtonId = "com.tencent.mm:id/cnu"; // 拆紅包頁面 '開'的id
private static String MoneyDetailTextId = "com.tencent.mm:id/cnu";//紅包詳情頁 錢數(shù)id
private static String ChatListPageItemId = "com.tencent.mm:id/azj"; // 聊天列表頁面Item的id
private static String ChatListPageRedHintId = "com.tencent.mm:id/lu"; // 聊天列表頁紅色圓點(diǎn)提示view的id
private static String ChatListPageKeyTextId = "com.tencent.mm:id/azn"; // 聊天記錄列表中Item關(guān)鍵詞的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 = "領(lǐng)取紅包";//會話頁面關(guān)鍵字搜索
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");
//網(wǎng)絡(luò)請求
ConfigHelper.getConfig(getApplicationContext(), new ConfigHelper.RequestConfigCallBack() {
@Override
public void onRequestFailed() {
Toast.makeText(getApplicationContext(), "網(wǎng)絡(luò)錯誤,啟動工具失敗", 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", "發(fā)送廣播");
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: // 通知欄狀態(tài)變化
notificationEvent(event);
break;
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: // 窗口狀態(tài)變化
windowStateEvent(event);
break;
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: // 窗口內(nèi)容變化
windowContentEvent(event);
break;
default:
break;
}
}
/**
* 微信是否運(yùn)行在前臺
*/
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())) { // 是否為鎖屏或黑屏狀態(tài)
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());
}
}
}
/**
* 窗口狀態(tài)變化
*/
private void windowStateEvent(AccessibilityEvent event) {
// Log.d("qhb", "窗口狀態(tài)變化");
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(); // 在聊天界面, 去點(diǎn)中紅包
break;
case WINDOW_LUCKY_MONEY_OPEN:
getLuckyMoney();
break;
case WINDOW_LUCKY_MONEY_DETAILUI:
detailsRedPacket(); //看詳細(xì)的紀(jì)錄界面
break;
}
}
/**
* 窗口內(nèi)容變化
*/
private void windowContentEvent(AccessibilityEvent event) {
Log.d("qhb", "窗口內(nèi)容變化");
if (mCurrentWindow != WINDOW_LAUNCHER_UI) { // //不在聊天界面或聊天列表灼捂,不處理
return;
}
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo == null) {
return;
}
// 直接去獲取當(dāng)前會話的最后一條Item, 不為null, 則是當(dāng)前會話列表
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) { // 有小圓點(diǎn), 說明有未讀消息
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;
// 有紅包, 點(diǎn)開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" + "為空");
}
// 聊天會話窗口,遍歷節(jié)點(diǎn)匹配
AccessibilityNodeInfo node = AccessibilityUtils.findNodeInfosByText(nodeInfo, KeySearchName);
if (node != null) {
Log.d("qhb", "會話頁領(lǐng)取紅包的node" + "不為空");
AccessibilityUtils.performClick(node);
} else {
Log.d("qhb", "會話頁領(lǐng)取紅包的node" + "為空");
return;
}
}
/**
* 點(diǎn)最新消息
*/
private void clickLastMsg(AccessibilityNodeInfo nodeInfo) {
AccessibilityNodeInfo listView = AccessibilityUtils.findNodeInfosById(nodeInfo, ConversationPageListViewId); //找到聊天會話頁面的會話內(nèi)容的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) { // 每一條新消息都試著點(diǎn)紅包
AccessibilityNodeInfo real = AccessibilityUtils.findNodeInfosById(item, ConversationPageRedPacketItemId);
if (real != null) { // 真紅包
// 新版本后, 1100(包括)以上, 能判斷紅包是否已經(jīng)領(lǐng)取
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换团,則將界面上所有子節(jié)點(diǎn)都點(diǎn)擊一次
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();
}
}
/**
* 領(lǐng)取詳情
*/
private void detailsRedPacket() {
// 到這, 領(lǐng)取流程算是完了
Log.d("qhb", "領(lǐng)取流程完事");
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 搶紅包服務(wù)中斷");
}
}