最近有一段时间没写博客了,一方面是工作比较忙,一方面也着实本人水平有限,没有太多能与大家分享的东西,也就是在最近公司要做一个抢红包的功能,老板发话了咋们就开干呗,本人就开始在网上收集资料,经过整理和实践,总算完美实现了功能,这里拿出本人一点微薄的成就与大家分享。
首先界面是这样的
开启自动抢红包只需点击相应的选项即可,下面我们进入正题,实现自动抢红包的原理,其实是借助android下的一个辅助服务AccessibilityService,这个服务是google公司为许多Android使用者因为各种情况导致他们要以不同的方式与手机交互。这包括了有些用户由于视力上,身体上,年龄上的问题致使他们不能看完整的屏幕或者使用触屏,也包括了无法很好接收到语音信息和提示的听力能力比较弱的用户。Android提供了Accessibility功能和服务帮助这些用户更加简单地操作设备,包括文字转语音(这个不支持中文),触觉反馈,手势操作,轨迹球和手柄操作。
而开发者可以利用这些服务使得程序更好用,我们来看代码:
<service android:name="com.hxwl.qhb.service.QiangHongBaoService" android:enabled="true" android:exported="true" android:label="XXXX" 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/qianghongbao_service_config" /> </service>
以上内容配置在AndroidManifest中,qianghongbao_service_config内容如下
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_description" android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged|typeWindowsChanged" android:packageNames="com.tencent.mm,com.tencent.mobileqq" android:accessibilityFeedbackType="feedbackGeneric" android:notificationTimeout="100" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true"/>
android:description 这个是设置服务的描述,在用户授权的界面可以看到。
android:accessibilityEventTypes 这个是配置要监听的辅助事件,我们只需要用到typeNotificationStateChanged(通知变化事件)、typeWindowStateChanged(界面变化事件)
android:packageNames 这个是要监听应用的包名,如果要监听多个应用,则用","去分隔
android:accessibilityFeedbackType 这个是设置反馈方式,方式有如下几种
android:accessibilityFeedbackType="feedbackGeneric"通用的反馈类型
android:notificationTimeout="100"为事件回调的延迟时间
android:accessibilityFlags="flagDefault"默认标记
android:canRetrieveWindowContent="true"如果不设为true,AccessibilityEvent.getSource()获取的对象即为空
AccessibilityService类主要有以下四个方法:
@Override public void onAccessibilityEvent(AccessibilityEvent event) { //接收事件,如触发了通知栏变化、界面变化等 } @Override protected boolean onKeyEvent(KeyEvent event) { //接收按键事件 return super.onKeyEvent(event); } @Override public void onInterrupt() { //服务中断,如授权关闭或者将服务杀死 } @Override protected void onServiceConnected() { super.onServiceConnected(); //连接服务后,一般是在授权成功后会接收到 }
当我们监听的包名发生通知或界面改变时,就会被onAccessibilityEvent方法捕抓到,我们先看看微信的实现:
@Override public void onReceiveJob(AccessibilityEvent event) { final int eventType = event.getEventType(); if (eventType == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {// 通知栏事件 Parcelable data = event.getParcelableData(); if (data == null || !(data instanceof Notification)) { return; } // 开启快速模式,不处理 if (QiangHongBaoService.isNotificationServiceRunning() && getConfig().isEnableNotificationService()) { return; } List<CharSequence> texts = event.getText(); if (!texts.isEmpty()) { String text = String.valueOf(texts.get(0)); notificationEvent(text, (Notification) data); } } else if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {// 界面改变事件 openHongBao(event); } else if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {// 界面内容改变事件 if (mCurrentWindow != WINDOW_LAUNCHER) { // 不在聊天界面或聊天列表,不处理 return; } if (isReceivingHongbao) { handleChatListHongBao(); } } }
这里我们做了简单的封装,onAccessibilityEvent方法触发时,便会进入AccessibilityEvent传入onReceiveJob方法中,在该方法中分别处理了通知栏事件,界面改变事件,界面内容改变事件,我们逐个看。
通知栏事件中,我们进行了判空,并拦截快速模式,重点是event.getText()获取通知中的文本,进行判断:
/** 所有通知栏事件,都在该方法处理 */ private void notificationEvent(String ticker, Notification nf) { String text = ticker; int index = text.indexOf(":"); if (index != -1) { text = text.substring(index + 1); } text = text.trim(); if (text.contains("[微信红包]")) { // 红包消息 newHongBaoNotification(nf); } }
如果有[微信红包]字眼,便进入下一步处理
/** 打开通知栏消息 */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void newHongBaoNotification(Notification notification) { isReceivingHongbao = true; // 以下是精华,将微信的通知栏消息打开 PendingIntent pendingIntent = notification.contentIntent; boolean lock = NotifyHelper.isLockScreen(getContext()); if (!lock) {// 未锁,自动点开通知 NotifyHelper.send(pendingIntent); } else {// 锁屏,显示通知,微信自带,不作任何处理 NotifyHelper.showNotify(getContext(), String.valueOf(notification.tickerText), pendingIntent); } // 锁屏 || 模式非自动抢 if (lock || getConfig().getWechatMode() != Config.WX_MODE_0) { // 开启声音,震动提示 NotifyHelper.playEffect(getContext(), getConfig()); } }
获取到PendingIntent,并判断当前是否锁屏,这里主要用到了
/** 执行PendingIntent事件 */ public static void send(PendingIntent pendingIntent) { try { pendingIntent.send(); } catch (PendingIntent.CanceledException e) { e.printStackTrace(); } }
该方法可以执行模拟点击,如同点击了红包通知一般,这样就会触发下一个内容改变事件
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void openHongBao(AccessibilityEvent event) { if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI".equals(event.getClassName())) { mCurrentWindow = WINDOW_LUCKYMONEY_RECEIVEUI; // 点中了红包,下一步就是去拆红包 handleLuckyMoneyReceive(); } else if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI".equals(event.getClassName())) { mCurrentWindow = WINDOW_LUCKYMONEY_DETAIL; // 拆完红包后看详细的纪录界面 if (getConfig().getWechatAfterGetHongBaoEvent() == Config.WX_AFTER_GET_GOHOME) { // 返回主界面,以便收到下一次的红包通知 AccessibilityHelper.performHome(getService()); } } else if ("com.tencent.mm.ui.LauncherUI".equals(event.getClassName())) { mCurrentWindow = WINDOW_LAUNCHER; // 在聊天界面,去点中红包 handleChatListHongBao(); } else { mCurrentWindow = WINDOW_OTHER; } }
我们根据当前的activity判断下一步处理
/** * 收到聊天里的红包 * */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private void handleChatListHongBao() { int mode = getConfig().getWechatMode(); if (mode == Config.WX_MODE_3) { // 只通知模式 return; } AccessibilityNodeInfo nodeInfo = getService().getRootInActiveWindow(); if (nodeInfo == null) { return; } if (mode != Config.WX_MODE_0) {// 非自动抢 boolean isMember = isMemberChatUi(nodeInfo); if (mode == Config.WX_MODE_1 && isMember) {// 过滤群聊 return; } else if (mode == Config.WX_MODE_2 && !isMember) { // 过滤单聊 return; } } // 下面就是,激动人心的抢红包代码 List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("领取红包"); if (list != null && list.isEmpty()) {// "领取红包"关键字节点获取不到 // 从消息列表查找红包 AccessibilityNodeInfo node = AccessibilityHelper.findNodeInfosByText(nodeInfo, HONGBAO_TEXT_KEY); if (node != null) { isReceivingHongbao = true; AccessibilityHelper.performClick(node); } } else if (list != null) { if (isReceivingHongbao) { // 最新的红包领起 AccessibilityNodeInfo node = list.get(list.size() - 1); AccessibilityHelper.performClick(node); isReceivingHongbao = false; } } }
同样根据判断关键文本进入模拟点击,然后再次触发界面改变事件
/** * 点击聊天里的红包后,显示的界面 * */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void handleLuckyMoneyReceive() { AccessibilityNodeInfo nodeInfo = getService().getRootInActiveWindow(); AccessibilityNodeInfo targetNode = null; int event = getConfig().getWechatAfterOpenHongBaoEvent(); int wechatVersion = getWechatVersion(); if (event == Config.WX_AFTER_OPEN_HONGBAO) { // 拆红包 if (wechatVersion < USE_ID_MIN_VERSION) { targetNode = AccessibilityHelper.findNodeInfosByText(nodeInfo, "拆红包"); } else { String buttonId = "com.tencent.mm:id/b43"; if (wechatVersion == 700) { buttonId = "com.tencent.mm:id/b2c"; } if (buttonId != null) { targetNode = AccessibilityHelper.findNodeInfosById(nodeInfo, buttonId); } if (targetNode == null) { // 分别对应固定金额的红包 拼手气红包 AccessibilityNodeInfo textNode = AccessibilityHelper.findNodeInfosByTexts(nodeInfo, "发了一个红包", "给你发了一个红包", "发了一个红包,金额随机"); if (textNode != null) { for (int i = 0; i < textNode.getChildCount(); i++) { AccessibilityNodeInfo node = textNode.getChild(i); if ("android.widget.Button".equals(node.getClassName())) { targetNode = node; break; } } } } if (targetNode == null) { // 通过组件查找 targetNode = AccessibilityHelper.findNodeInfosByClassName(nodeInfo, "android.widget.Button"); } } } else if (event == Config.WX_AFTER_OPEN_SEE) { // 看一看 if (getWechatVersion() < USE_ID_MIN_VERSION) { // 低版本才有 看大家手气的功能 targetNode = AccessibilityHelper.findNodeInfosByText(nodeInfo, "看看大家的手气"); } } else if (event == Config.WX_AFTER_OPEN_NONE) {// 静静地看着 return; } if (targetNode != null) { final AccessibilityNodeInfo n = targetNode; long sDelayTime = getConfig().getWechatOpenDelayTime(); if (sDelayTime != 0) { getHandler().postDelayed(new Runnable() { @Override public void run() { AccessibilityHelper.performClick(n); } }, sDelayTime); } else { AccessibilityHelper.performClick(n); } } }
微信中有两种红包,一种是固定金额的红包,还有一种是拼手气红包,两个的关键字不同,我们分开判断。
QQ的实现也基本相同,我们直接上代码
@Override public void onReceiveJob(AccessibilityEvent event) { openRed(event); }
private void openRed(AccessibilityEvent event) { this.rootNodeInfo = event.getSource(); if (rootNodeInfo == null) { return; } mReceiveNode = null; checkNodeInfo(); /* 如果已经接收到红包并且还没有戳开 */ if (mLuckyMoneyReceived && (mReceiveNode != null)) { int size = mReceiveNode.size(); if (size > 0) { String id = getHongbaoText(mReceiveNode.get(size - 1)); long now = System.currentTimeMillis(); if (this.shouldReturn(id, now - lastFetchedTime)) return; lastFetchedHongbaoId = id; lastFetchedTime = now; AccessibilityNodeInfo cellNode = mReceiveNode.get(size - 1); if (cellNode.getText().toString().equals("口令红包已拆开")) { return; } cellNode.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); if (cellNode.getText().toString().equals(QQ_HONG_BAO_PASSWORD)) { AccessibilityNodeInfo rowNode = getService().getRootInActiveWindow(); if (rowNode == null) { return; } else { recycle(rowNode); } } mLuckyMoneyReceived = false; } } }
@TargetApi(Build.VERSION_CODES.KITKAT) public void recycle(AccessibilityNodeInfo info) { if (info.getChildCount() == 0) { /* 这个if代码的作用是:匹配“点击输入口令的节点,并点击这个节点” */ if (info.getText() != null && info.getText().toString().equals(QQ_CLICK_TO_PASTE_PASSWORD)) { info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); } /* 这个if代码的作用是:匹配文本编辑框后面的发送按钮,并点击发送口令 */ if (info.getClassName().toString().equals("android.widget.Button") && info.getText().toString().equals("发送")) { info.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } else { for (int i = 0; i < info.getChildCount(); i++) { if (info.getChild(i) != null) { recycle(info.getChild(i)); } } } }
可见这是一个递归方法,反复获取节点并判断节点文本,模拟点击红包按钮。
因为是公司的项目,不方便给Demo,这里附上一个大神链接,我便是在大神的基础上作得更改,点这里