大家好,又见面了,我是你们的朋友全栈君。
妙用AccessibilityService黑科技实现微信自动加好友拉人进群聊
标签: 2018
引言:
在上上周的周六和周日,我发了两篇利用itchat实现微信机器人的文章(Python):
- 小猪的Python学习之旅 —— 18.Python微信转发小宇宙早报
- 小猪的Python学习之旅 —— 19.Python微信自动好友验证,自动回复,发送群聊链接
通过把脚本挂到服务器上,自此告别手动挡,不用去手动转发小宇宙, 不用手动加好友,然后把别人一个个拉到我的Py交易群里。正当我 暗自窃喜的时候,微信并没有放过我这只小猫咪。
我还记得那天早上,我兴高采烈早早来到公司,更新了一波代码准备为 我的机器人添砖加瓦的时候,当我关闭了阿里云上的脚本,这时候意外来了, 我的机器人小号,再也无法通过微信网页端的接口登录了!!! 扫描完二维码,永远提示的都是下面这样一句话:
代码语言:javascript复制<error><ret>1203</ret><message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。
你可以通过Windows微信、Mac微信或者手机客户端微信登录。</message></error>
是的,就是这样一句话,找不到申诉渠道,也不知道何时才可能会解封。(客户端任可正常使用) 而现在另外新申请的微信小号是无法登录微信网页端的,其实这是微信在慢慢关停网页版登录, 最主要的原因就是机器人泛滥!
没有了网页版微信,日子还是要过的,难道只能回归手动档么?几种解决方案:
- 1.研究客户端协议(这个成本巨高,而且官方稍微改点东西,够你哭的)
- 2.APP逆向,利用Xposed框架,hook相关的方法,也是有些研究成本的;
- 3.利用类似与按键精灵的东西,编写脚本让他自动点点点,自动化测试 工具或者本节讲的这个无障碍服务——AccessibilityService
AccessibilityService其实不是一个新的东西了,老久之前就有了, 官方原意:优化残障人士的使用体验的,而在我大天朝:
抢红包,自动安装,一键XXX等等,可谓欣欣向荣。
使用AccessibilityService也非常Easy,核心要点就是:
通过UI Automator找到节点,通过resource-id,text,content-desc等 唯一特征定位到具体的节点,接着执行各种模拟操作,点,滚动,填充, 用法比较简单的,大部分时间会花在试错和逻辑调整上!
来一发通过AccessibilityService实现的自动加好友以及拉人进群聊的Gif体验下:
Gif加速了一点,不过完成加好友以及拉人总共也就耗时15s,是相当客观的啦。 下面就来介绍下AccessibilityService这个玩意怎么用吧~
AccessibilityService用法简介
1.自定义Service继承AccessibilityService
如题,自定义一个AccessibilityService类,重写两个主要方法:
onInterrupt
( ):辅助功能中断的回调,基本不用理,核心还是
onAccessibilityEvent
(AccessibilityEvent event) 上。
当界面发生了什么事情,比如顶部Notification,界面更新,内容变化等, 会触发这个方法,你可以根据不同的事件响应不同的操作,比如小猪这个 就是当顶部出现加好友的Notification的event时,跳转到加好友页。 点开AccessibilityEvent类可以看到一堆的事件类型~
事件类型 | 描述 |
---|---|
TYPE_VIEW_CLICKED | View被点击 |
TYPE_VIEW_LONG_CLICKED | View被长按 |
TYPE_VIEW_SELECTED | View被选中 |
TYPE_VIEW_FOCUSED | View获得焦点 |
TYPE_VIEW_TEXT_CHANGED | View文本变化 |
TYPE_WINDOW_STATE_CHANGED | 打开了一个PopupWindow,Menu或Dialog |
TYPE_NOTIFICATION_STATE_CHANGED | Notification变化 |
TYPE_VIEW_HOVER_ENTER | 一个View进入悬停 |
TYPE_VIEW_HOVER_EXIT | 一个View退出悬停 |
TYPE_TOUCH_EXPLORATION_GESTURE_START | 触摸浏览事件开始 |
TYPE_TOUCH_EXPLORATION_GESTURE_END | 触摸浏览事件完成 |
TYPE_WINDOW_CONTENT_CHANGED | 窗口的内容发生变化,或子树根布局发生变化 |
TYPE_VIEW_SCROLLED | View滚动 |
TYPE_VIEW_TEXT_SELECTION_CHANGED | Edittext文字选中发生改变事件 |
TYPE_ANNOUNCEMENT | 应用产生一个通知事件 |
TYPE_VIEW_ACCESSIBILITY_FOCUSED | 获得无障碍焦点事件 |
TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED | 无障碍焦点事件清除 |
TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY | 在给定的移动粒度下遍历视图文本的事件 |
TYPE_GESTURE_DETECTION_START | 开始手势监测 |
TYPE_GESTURE_DETECTION_END | 结束手势监测 |
TYPE_TOUCH_INTERACTION_START | 触摸屏幕事件开始 |
TYPE_TOUCH_INTERACTION_END | 触摸屏幕事件结束 |
TYPE_WINDOWS_CHANGED | 屏幕上的窗口变化事件,需要API 21 |
TYPE_VIEW_CONTEXT_CLICKED | View中的上下文点击事件 |
TYPE_ASSIST_READING_CONTEXT | 辅助用户读取当前屏幕事件 |
好吧,上面的表其实并没什么大用,我还是习惯直接把event.toString()给打印出来, 然后自行去判断~
如图就可以拿到event类型,以及产生对应事件的类名,核心是这两个, 除此之外还有Text和ContentDescription等。
比如我那个监听Notification跳转到添加好友页的:
这里就是对事件类型做了下判断,然后获取contentIntent,跳转而已。 简单点讲就是:
你在这个方法里,去判断一波事件类型和className, 然后再获取控件,做一些点击,滚动,填充文本等。
2.服务的配置
自定义完这个服务要想让他启用你还得执行下面的操作:
Step 1:在res文件夹下创建xml文件夹,新建一个配置的xml文件(名字自己定)
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:notificationTimeout="100" android:packageNames="com.tencent.mm" android:settingsActivity="com.coderpig.wechathelper.MainActivity" />
属性简介如下:
- accessibilityEventTypes:设置监听的事件种类,用|隔开,监听所有可以用typeAllMask;
- accessibilityFeedbackType:服务提供的反馈类型,feedbackGeneric通用反馈;
- accessibilityFlags:辅助功能附加的标志,flagDefault默认的配置
- canRetrieveWindowContent:辅助功能服务是否能够取回活动窗口内容的属性
- notificationTimeout:响应时间
- packageNames:监听的应用包名,不填,默认监听所有应用的事件
- settingsActivity:允许用户修改辅助功能的activity类名
Step 2:接着AndroidManifest.xml文件中对该Service进行配置
先是添加一个权限:
代码语言:javascript复制android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
接着是Service的配置:
这里是你那个配置文件xml文件的文件名,其他照抄。
Step 3:安装到手机后,需要在手机设置的无障碍处开启服务
一般在设置的辅助功能处能找到:
如果Logcat那里能看到打印的LOG,说明服务正常运行,接下来要找控件节点
3.找控件
这里可以用到神器UI Automator来查看布局层次,打开Android Studio, Ctrl alt A,输入 monitor
依次点击:选中设备 -> Dump View Hierarchy for UI Automator
稍等一会,右侧就会出现当前页面的布局层次图,如图随手选中一个邀请的节点:
右侧可以拿到对应的信息,一般比较常用的是这几个,有一点要注意!!! resource-id不一定是唯一的
获得控件基本都会通过下述这个方法:
getRootInActiveWindow
( ):获取当前整个活动窗口的根节点
返回的是一个AccessibilityNodeInfo
类,代表View的状态信息,
提供了下述几个非常实用的方法:
- getParent:获取父节点。
- getChild:获取子节点。
- performAction:在节点上执行一个动作。
- findAccessibilityNodeInfosByText:通过字符串查找节点元素。
- findAccessibilityNodeInfosByViewId:通过视图id查找节点元素。
后面的这两个方法会返回一个AccessibilityNodeInfo列表,一般操作是 遍历,然后筛选特定节点,比如我程序里的,获得底部Tab节点为”通讯录”, 然后点击,跳转后遍历,筛选”群聊”的节点,点击。
另外,UI Automator有时并不可靠(实时问题),我建议写多一个遍历节点 的方法,可以更清楚里面的控件情况:
拿到控件,接着就到触发事件了。
4.触发事件
通过调用performAction
()传入一个时间类型即可触发相应时间,比如点击,长按等
事件就多了,自己点开AccessibilityNodeInfo类查看吧,这里介绍下最常用的几个事件:
//点击
performAction(AccessibilityNodeInfo.ACTION_CLICK);
//长按
performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
//滚动
performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); //向下滚一下
performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); //向上滚一下
//填充EditText(API版本需要>18可用方法1,API>21两种方法都可以使用)
//方法1:
ClipboardManager clipboard = (ClipboardManager)this.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text", "填充内容");
clipboard.setPrimaryClip(clip);
//获得焦点
info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
粘贴进入内容
info.performAction(AccessibilityNodeInfo.ACTION_PASTE);
//方法2:
Bundle arguments = new Bundle();
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "填充内容");
info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
除了控件触发事件外,AccessibilityService提供了一个performGlobalAction
(),用于执行
一些通用的事件:
GLOBAL_ACTION_BACK 点击返回按钮
GLOBAL_ACTION_HOME 点击home
GLOBAL_ACTION_NOTIFICATIONS 打开通知
GLOBAL_ACTION_RECENTS 打开最近应用
GLOBAL_ACTION_QUICK_SETTINGS 打开快速设置
GLOBAL_ACTION_POWER_DIALOG 打开长按电源键的弹框
另外在实际开发中,直接调用这些全局方法又是并没有生效, 我在调GLOBAL_ACTION_BACK的时候就发现有时不会回退, 个人的解决方案是使用handler.postDelay()延时执行:
除了这样玩以外,我还利用时间差,串行去执行几个任务,比如:
上面的步骤是:
进入群聊聊天信息页后,列表滚动两次,接着依次:
- 1.延时1s后,找到添加成员按钮并点击;
- 2.延时2.3s后,把名字填充到EditText里
- 3.延时3s后,点击确定按钮
就不用过于依赖onAccessibilityEvent方法,除了用handler.postDelay外, 还可以用Thread.sleep(休眠时长),用到的点大概就这么多,其余的自行探究吧。
小结
本节讲解一波如何通过AccessibilityService来实现自动加好友以及拉人进群, 之前是打算用xposed来写的,后面发现没我想像中简单,而且很多用安卓机的都 不会搞机(基),root也不会,后来还是选择了AccessibilityService,简单易用, 当然后面还是会研究一波xposed实现的,敬请期待~ 对了,还有,之前那个网页端的机器人被封原因估计是信息秒回,如果有还用 itchat那个做机器人的,建议回复的时间可以稍微延长些;
关于AccessibilityService更多内容可见:
- Android辅助功能:https://blog.csdn.net/qq_24800377/article/details/78283662
- Building Accessibility Services:https://developer.android.com/guide/topics/ui/accessibility/services.html
- Developing an Accessibility Service:https://developer.android.com/training/accessibility/service.html
附:关键代码(都可以在:https://github.com/coder-pig/WechatHelper 找到): 代码有Bug的话正常,后续会优化下逻辑,感觉写得有点杂~
代码语言:javascript复制package com.coderpig.wechathelper;
import android.accessibilityservice.AccessibilityService;
import android.app.Notification;
import android.app.PendingIntent;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import java.util.List;
/** * 描述:微信监控服务类 * * @author CoderPig on 2018/04/04 13:46. */
public class HelperService extends AccessibilityService {
private static final String TAG = "HelperService";
private Handler handler = new Handler();
private String userName = "123";
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
CharSequence classNameChr = event.getClassName();
String className = classNameChr.toString();
Log.d(TAG, event.toString());
switch (eventType) {
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) {
Notification notification = (Notification) event.getParcelableData();
String content = notification.tickerText.toString();
if (content.contains("请求添加你为朋友")) {
PendingIntent pendingIntent = notification.contentIntent;
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
}
break;
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
switch (className) {
case "com.tencent.mm.plugin.subapp.ui.friend.FMessageConversationUI":
addFriend();
break;
case "com.tencent.mm.plugin.profile.ui.SayHiWithSnsPermissionUI":
verifyFriend();
break;
case "com.tencent.mm.plugin.profile.ui.ContactInfoUI":
performBackClick();
break;
case "com.tencent.mm.ui.LauncherUI":
if (!userName.equals("123")) {
openGroup();
}
break;
case "com.tencent.mm.ui.contact.ChatroomContactUI":
if (!userName.equals("123")) {
inviteGroup();
}
break;
case "com.tencent.mm.ui.chatting.ChattingUI":
if (!userName.equals("123")) {
openGroupSetting();
}
break;
case "com.tencent.mm.plugin.chatroom.ui.ChatroomInfoUI":
if (userName.equals("123")) {
performBackClick();
} else {
addToGroup();
}
break;
case "com.tencent.mm.ui.base.i":
dialogClick();
break;
}
break;
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
}
}
private void addFriend() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> list = nodeInfo
.findAccessibilityNodeInfosByText("接受");
if (list != null && list.size() > 0) {
for (AccessibilityNodeInfo n : list) {
n.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
} else {
performBackClick();
}
}
}
private void verifyFriend() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
//获得用户名
if (nodeInfo != null) {
userName = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/d0n").get(0).getText().toString();
AccessibilityNodeInfo finishNode = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/hd").get(0);
finishNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
private void openGroup() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/ca5");
for (AccessibilityNodeInfo info : nodes) {
if (info.getText().toString().equals("通讯录")) {
info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
handler.postDelayed(new Runnable() {
@Override
public void run() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j5");
for (AccessibilityNodeInfo info : nodes) {
if (info.getText().toString().equals("群聊")) {
info.getParent().getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
}
}
}
}, 500L);
}
}
}
}
private void inviteGroup() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/a9v");
for (AccessibilityNodeInfo info : nodes) {
if (info.getText().toString().equals("小猪的Python学习交流群")) {
info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
}
}
}
private void openGroupSetting() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/he").get(0).performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
private void addToGroup() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> listNodes = nodeInfo.findAccessibilityNodeInfosByViewId("android:id/list");
if(listNodes != null && listNodes.size() > 0) {
AccessibilityNodeInfo listNode = listNodes.get(0);
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
final AccessibilityNodeInfo scrollNodeInfo = getRootInActiveWindow();
if (scrollNodeInfo != null) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
List<AccessibilityNodeInfo> nodes = scrollNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/d0b");
for (AccessibilityNodeInfo info : nodes) {
if (info.getContentDescription().toString().equals("添加成员")) {
info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
}
}
},1000L);
handler.postDelayed(new Runnable() {
@Override
public void run() {
List<AccessibilityNodeInfo> editNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/arz");
if(editNodes != null && editNodes.size() > 0) {
AccessibilityNodeInfo editNode = editNodes.get(0);
Bundle arguments = new Bundle();
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, userName);
editNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
}
}
}, 2300L);
handler.postDelayed(new Runnable() {
@Override
public void run() {
List<AccessibilityNodeInfo> cbNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/kr");
if(cbNodes != null) {
AccessibilityNodeInfo cbNode = null;
if(cbNodes.size() == 1) {
cbNode = cbNodes.get(0);
} else if(cbNodes.size() == 2) {
cbNode = cbNodes.get(1);
}
if (cbNode != null) {
cbNode.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
AccessibilityNodeInfo sureNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/hd").get(0);
sureNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
}, 3000L);
}
}
}
}
private void dialogClick() {
AccessibilityNodeInfo inviteNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/aln").get(0);
inviteNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
userName = "123";
handler.postDelayed(new Runnable() {
@Override
public void run() {
List<AccessibilityNodeInfo> sureNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/aln");
if(sureNodes != null && sureNodes.size() > 0) {
AccessibilityNodeInfo sureNode = sureNodes.get(0);
sureNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
},1000L);
}
private void performBackClick() {
handler.postDelayed(new Runnable() {
@Override
public void run() {
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
}
}, 300L);
}
//遍历控件的方法
public void recycle(AccessibilityNodeInfo info) {
if (info.getChildCount() == 0) {
Log.i(TAG, "child widget----------------------------" info.getClassName().toString());
Log.i(TAG, "showDialog:" info.canOpenPopup());
Log.i(TAG, "Text:" info.getText());
Log.i(TAG, "windowId:" info.getWindowId());
Log.i(TAG, "desc:" info.getContentDescription());
} else {
for (int i = 0; i < info.getChildCount(); i ) {
if (info.getChild(i) != null) {
recycle(info.getChild(i));
}
}
}
}
@Override
public void onInterrupt() {
}
}
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/136060.html原文链接:https://javaforall.cn