网上搜了一圈抢红包的插件,发现有很多抢红包的实现教程,基本上都是借助AccessibilityService来实现的。
而且大多说文章说的都是原理,没有结合实际需求来讲怎么过滤已抢红包的,只能实现盲目的抢红包。
因此,只好自己想办法解决如何过滤已抢红包问题了,以下结合代码与注释给大家分享几处重点代码段:
1. 让我们的 AccessibilityService 响应微信的所有类型的事件:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
notificationTimeout="100"
android:description="@string/accessibility_service_description"
android:packageNames="com.tencent.mm" />
2. 只处理有用的三个类型事件:
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
switch (eventType) {
/* 通知栏有来消息时,会触发*/
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
mNeededToGetPacket = true;
getPacketNotification(event);
break;
/* 微信界面有跳转时,会触发 */
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
String className = event.getClassName().toString();
/* 判断是不是聊天界面 */
if (className.equals("com.tencent.mm.ui.LauncherUI")) {
/* 开始抢红包 */
if (mNeededToGetPacket) {
mNeededToGetPacket = false;
getPacket(eventType);
}
}
/* 判断是不是发红包界面 */
else if (className.contains("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyPrepareUI")) {
Log.i(TAG, "TYPE_WINDOW_STATE_CHANGED: 发红包:");
}
/* 判断是不是红包打开的界面 */
else if (className.contains("com.tencent.mm.plugin.luckymoney")) {
/* 开始拆红包 */
openPacket();
}
break;
/* 聊天界面内容的变化,会触发 */
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
String changedClassName = event.getClassName().toString();
/*
* 判断是不是聊天界面有疑似红包的信息,
* 目前最新的微信版本6.5.8,新红包到来时,
* 会有RelativeLayout更新
*/
if (changedClassName.equals("android.widget.RelativeLayout")) {
/* 开始抢红包 */
getPacket(eventType);
}
break;
}
}
3.通知栏有来消息时 getPacketNotification 要做的就是找到"[微信红包]",然后点进去:private void getPacketNotification(AccessibilityEvent event) { List<CharSequence> texts = event.getText(); if (!texts.isEmpty()) { for (CharSequence text : texts) { String content = text.toString(); Log.i(TAG, "message:" + content); /*判断是不是包含[微信红包]*/ if (content.contains(RED_PACKET_NOTIFICATION)) { /* 模拟打开通知栏消息 */ if (parcelable != null && parcelable instanceof Notification) { Notification notification = (Notification) parcelable; PendingIntent pendingIntent = notification.contentIntent; try { pendingIntent.send(); } catch (CanceledException e) { e.printStackTrace(); } } } } } }
4. 以上 getPacketNotification 中用到的 mNeededToGetPacket 变量说明:
/* * 说明: * 一般不在聊天界面时,有微信红包来了,状态栏又没有通知的话。 * 当用户发现红包,再手动进入聊天界面,基本上已经错过抢红包时机了,所以这种情况没必要抢。 * * 所以,当通知栏有[微信红包]时,在进入有来红包的聊天界面前,将该变量会设置成true, * 让界面跳转到来红包聊天界面时,进去就抢 * 如果不是自动从通知栏进入的,该变量为false,跳转到红包聊天界面时,不会自动抢, * 这样可以有效避免用户手动点击聊天界面时打开已抢红包。 */ private boolean mNeededToGetPacket = false;
5. 抢红包方法说明:
/** * 抢红包接口,过滤已抢红包的主要思路,就是抢最后收到的一个红包, * 结合执行该函数前的 mNeededToGetPacket 条件,可以起到过滤已抢红包的功能。 */ private void getPacket(int eventType) { /* 红包的底部位置 */ mPacketRectBottom = 0; /* 已领取红包标识的底部位置 */ mHasGetNoteRectBottom = 0; /* 当前界面红包个数 */ mPacketCount = 0; /* 是否有找到红包已领取的标识 */ mHasFindGetNote = false; /* 是否有找到红包 */ mHasFindPacket = false; /* 用于存放红包与红包底部位置 */ mPacketMap.clear(); /* 获取根节点 */ AccessibilityNodeInfo rootNode = getRootInActiveWindow(); /* 试图找到一个可以领取红包的点击入口 */ tryToGetPacketrecycle(rootNode, eventType); /* * 过滤方案一: mPacketRectBottom: 为列表最后面一个红包的底部坐标 * mHasGetNoteRectBottom:为列表最后面一个“领取了”提示的底部坐标 当mPacketRectBottom > * mHasGetNoteRectBottom时,说明这个红包自己没有抢过,可以抢 * * 过滤方案二: mHasPicked:为是否已经打开地的标志; 当来新通知时, * 如果检测到是红包通知时,把mHasPicked设为false; * 抢完了设为true;当聊天内容中有文字内容变化时, * 也把mHasPicked设为false(这里很容易发生误点,待优化); * 抢完了设为true; * * 过滤方案三: mPacketCount:为查找到的红包总数; mLastPacketCount:为上一次查找到的红包总数; * 当mPacketCount != mLastPacketCount,说明红包数量有变,可以抢抢看。 * 有一种情况会出现bug:当整页都是红包时,如果再有新红包消息, * 红包的数量是不会发生变化的 HD720的分辨率满屏红包是6个。 * * 该方案已验证失败,太容易出现一个屏上,来红包前后红包数量相等的情况了, * 而且扫描多个红包很耗时,待优化! */ if (mPacketRectBottom > mHasGetNoteRectBottom /* && mPacketCount != mLastPacketCount && !mHasPicked */) { if (mPacketRectBottom != 0) { AccessibilityNodeInfo info = mPacketMap.get(mPacketRectBottom); mLastPacketCount = mPacketCount; if (info != null) { mPacketOpenByAutoClick = true; info.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } } } /** * 试图找到一个可以领取红包的点击入口,这个函数是递归调用的 */ public void tryToGetPacketrecycle(AccessibilityNodeInfo info, int eventType) { /* 这里一般不可能出现,但是辅助类功能绝不能出现不友好的异常提示 */ if (info == null) { return; } /* 如果找到了红包,就可以退出递归调用了 */ if (mHasFindPacket) { return; } int childcount = info.getChildCount(); if (childcount > 0) { /* 从最后一个View往前找,找红包的效率会相对高一点,因为最新的红包总是在列表的最底部 */ for (int i = childcount - 1; i >= 0; i--) { if (info.getChild(i) != null) { tryToGetPacketrecycle(info.getChild(i), eventType); } } } else { if (info.getText() != null) { /* 先通过一些关键字定位到红包消息的View */ if (GET_RED_PACKET1.equals(info.getText().toString())) { AccessibilityNodeInfo parent = info.getParent(); while (parent != null) { /* * 点击事件一般会注册在一些大一点的父布局上, * 这样增加了可点击区域,用户体验较好,微信也是如此, * 此处也难免微信版本升级后会进行一些修改,需使用后持续更新 */ if (parent.isClickable()) { /* 确认红包区域(红包位置) */ parent.getBoundsInScreen(mPacketRect); /* * 将红包部底位置与红包节点保存到HashMap里,这里先不点,以免打开了已经抢过的红包, * 而影响用户去抢真正需要抢的红包。 * * 备注: * 此处之前是为了在扫描多个红包后抢最底下的红包而设计的, * 但是由于通过红包个数来判断是否有新红包的方案已经放弃, * 此处代码也没必要这么写了,可以直接把bottom值最大的 parent 保存到全局变量中即可。 */ mPacketMap.put(mPacketRect.bottom, parent); /* * 将界面中最靠下的红包的底部位置保存下来,我们目标就是在最快的时间里, * 抢到最有把握的红包,这个值保存下来,一会用它去HashMap在获取对应的红包节点 */ if (mPacketRect.bottom > mPacketRectBottom) { mPacketRectBottom = mPacketRect.bottom; } /* 是否已经找到红包消息了的标志 */ mHasFindPacket = true; /* 找到红包的个数 */ // mPacketCount++; break; } parent = parent.getParent(); } } /* 通过一些关键字定位到红包已领取的标志View */ else if ((info.getText().toString() .contains(RED_PACKET_PICKED_FLAG))) { /* 确认红包已领取的标志区域 */ info.getBoundsInScreen(mHasGetNoteRect); /* * 将界面中最靠下红包已领取的标志的底部位置保存下来, * 跟红包的位置进行对比,如果它大,说明是抢过的,如果它小, * 说明它提示的是前面的红包. */ if (mHasGetNoteRect.bottom > mHasGetNoteRectBottom) { mHasGetNoteRectBottom = mHasGetNoteRect.bottom; } /* 是否已经找到红包已领取的标志 */ mHasFindGetNote = true; } } } }
6.折红包方法说明(比较简单,打开红包后,发现有没折过的红包就点击一下就好了):/** * 拆红包接口 */ private void openPacket() { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); tryToOpenPacketrecycle(rootNode); } /** * 试图拆开领取到的红包 */ public void tryToOpenPacketrecycle(AccessibilityNodeInfo info) { /* 这里一般不会出现,但是辅助类功能绝不能出现不友好的异常提示 */ if (info == null) { return; } if (info.getChildCount() == 0) { /* 微信6.5.7版本,拆红包是一个button,此处版本升级后可能会发生改变,需使用后持续更新 */ if (info.getClassName().equals("android.widget.Button")) { if (info.isClickable()) { info.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } /* 拆开的红包已经是抢过了,或者被别人抢了,按返回键退出 */ else if (info.getText() != null) { String text = info.getText().toString(); if (text.contains(RED_PACKET_PICKED1) || text.contains(RED_PACKET_PICKED2) || text.contains(RED_PACKET_PICKED3)) { if (mPacketOpenByAutoClick) { mPacketOpenByAutoClick = false; /* 抢完后要自动回到聊天界面继续抢的话可以把这行代码放出来 */ // performGlobalAction(GLOBAL_ACTION_BACK); } } } } else { for (int i = 0; i < info.getChildCount(); i++) { if (info.getChild(i) != null) { tryToOpenPacketrecycle(info.getChild(i)); } } } }
目前这个方案还有已知缺陷:
在当前聊天窗口,如果满屏全都是红包的话,再有新红包发入时,不会自动抢红包。原因是:这种情况下触发 TYPE_WINDOW_CONTENT_CHANGED 时,
android.widget.RelativeLayout 没有发生变化,而把 android.widget.RelativeLayout 换成 TextView 或者ListView 的话,又会导致没有新红包收到时也被触发。暂时没有想到解决办法。当然这种情况也比较少见,一般不太可能出现这种满屏全是红包,没有一句聊天信息的情景。发这篇文章的希望能对跟我一样找不到如何过滤已抢红包方案的同学们有一点点小帮忙,
也希望大神们能给些更好的思路,来让我们一起优化下这个插件。