最近项目上做了这么一个功能,实现微信消息的自动发送功能。
一顿google后,发现Android提供了辅助服务的方式,可以实现这个功能红包插件想必大家都听过或者使用过,原理实际上也是通过AccessibilityService来实现的。
原理
AccessibilityService运行在后台,并且能够收到由系统发出的一些事件(AccessibilityEvent,这些事件表示用户界面一系列的状态变化),比如焦点改变,输入内容变化,按钮被点击了等等,该种服务能够请求获取当前活动窗口并查找其中的内容.换言之,界面中产生的任何变化都会产生一个时间,并由系统通知给AccessibilityService.这就像监视器监视着界面的一举一动,一旦界面发生变化,立刻发出警报.
参考:http://08643.cn/p/4cd8c109cdfb
http://www.android-doc.com/reference/android/accessibilityservice/AccessibilityService.html
本文主要介绍的是如何通过AccessibilityService实现自动发送微信的功能。
继承AccessibilityService,编写自己的服务类,必须重写onAccessibilityEvent方法,通过这个方法来监听微信界面的变化,还有onInterrupt方法,当AccessibilityService被中断时会调用这个方法。
public class AutoSendMsgService extends AccessibilityService {
/**
* 必须重写的方法,响应各种事件。
*
* @param event
*/
@Override
public void onAccessibilityEvent(final AccessibilityEvent event) {
int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
String currentActivity = event.getClassName().toString();
if (hasSend) {
return;
}
if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI)) {
handleFlow_LaunchUI();
} else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CONTACTINFOUI)) {
handleFlow_ContactInfoUI();
} else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CHATUI)) {
handleFlow_ChatUI();
}
}
break;
}
}
@Override
public void onInterrupt() {
}
}
在清单文件中声明我们的服务类,配置好服务所需参数。
<service
android:name=".AutoSendMsgService"
android:enabled="true"
android:exported="true"
android:label="@string/app_name"
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/auto_reply_service_config"/>
</service>
服务参数通过auto_reply_service_config文件配置,此文件位于res->xml目录下。
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_description"
android:notificationTimeout="100"
//表示监听微信
android:packageNames="com.tencent.mm" />
AccessibilityService 提供两种方式获取界面上的View信息。
a,findAccessibilityNodeInfosByText 通过界面上的文本获取对应的View
b,findAccessibilityNodeInfosByViewId 通过View的id来获取对应的View
这两种方式都会在本文中使用。
自动发送微信步骤
手动开启辅助服务->打开微信->点击通讯录->向下滑动通讯录找到对应的联系人->点击联系人进入到联系人信息界面->点击发消息按钮进入聊天界面->文本框中粘贴需要发送的文本信息->点击发送按钮发送
我们来一步步介绍具体过程
第一步,手动开启辅助服务
辅助服务不能通过代码动态启动,只能手动去设置->无障碍->辅助服务->找到我们的辅助服务->开启,如果不开启,辅助服务是不会生效的,可以通过代码跳转到设置页面
Intent intent = new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);
第二步,启动微信
Intent intent = new Intent();
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
intent.setClassName(WeChatTextWrapper.WECAHT_PACKAGENAME, WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI);
startActivity(intent);
第三步,微信打开之后,AccessibilityService 会回调onAccessibilityEvent方法,eventType会发生改变,AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED代表当前界面的状态发生了变化,通过 event.getClassName().toString()可以获取当前Activity的类名,当前类名如果是“com.tencent.mm.ui.LauncherUI”,说明微信已经来到进入到了首页。此时,我们就可以点击通讯录进行下一步操作了。
if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI)) {
handleFlow_LaunchUI();
}
private void handleFlow_LaunchUI() {
try {
//点击通讯录,跳转到通讯录页面
WechatUtils.findTextAndClick(this, "通讯录");
Thread.sleep(50);
//再次点击通讯录,确保通讯录列表移动到了顶部,微信如果本来就是打开的状态,通讯录可能位于其他位置,从顶部开始查找,这样就能确保遍历到了所有联系人。
WechatUtils.findTextAndClick(this, "通讯录");
Thread.sleep(200);
//遍历通讯录联系人列表,查找联系人
AccessibilityNodeInfo itemInfo = TraversalAndFindContacts();
if (itemInfo != null) {
WechatUtils.performClick(itemInfo);
} else {
SEND_STATUS = SEND_FAIL;
resetAndReturnApp();
}
} catch (Exception e) {
e.printStackTrace();
}
}
第四步,遍历查找通讯录主要是放在TraversalAndFindContacts方法中进行的。通讯录界面本身是一个listview,主要流程是,通过listview的id来得到listview对象,id的可以通过andorid SDK提供的uiautomatorviewer工具获取,使用方法自行google ,AccessibilityService只能拿到屏幕上可见的信息,不能取到不可见的listview item信息,遍历当前屏幕上的listview的item也就是联系人信息,看看是不是能找到联系人,如果找不到,就执行向下翻页操作,直到滚动到底部为止,如果中途找到,直接进入下一步,如果找不到,那么微信就发送失败。
private AccessibilityNodeInfo TraversalAndFindContacts() {
if (allNameList != null) allNameList.clear();
//获取窗体内容
AccessibilityNodeInfo rootNode = getRootInActiveWindow();
//通过id找到listview对象
List<AccessibilityNodeInfo> listview = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_LISTVIEW_ID);
//是否滚动到了底部
boolean scrollToBottom = false;
if (listview != null && !listview.isEmpty()) {
while (true) {
//获取当前屏幕上的联系人信息
List<AccessibilityNodeInfo> nameList = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_NAME_ID);
List<AccessibilityNodeInfo> itemList = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_ITEM_ID);
if (nameList != null && !nameList.isEmpty()) {
for (int i = 0; i < nameList.size(); i++) {
if (i == 0) {
//必须在一个循环内,防止翻页的时候名字发生重复
mRepeatCount = 0;
}
//item代表listview的item,因为
AccessibilityNodeInfo itemInfo = itemList.get(i);
AccessibilityNodeInfo nodeInfo = nameList.get(i);
String nickname = nodeInfo.getText().toString();
Log.d(TAG, "nickname = " + nickname);
//判断是不是要发送的联系人
if (nickname.equals(WechatUtils.NAME)) {
return itemInfo;
}
if (!allNameList.contains(nickname)) {
allNameList.add(nickname);
} else if (allNameList.contains(nickname)) {
Log.d(TAG, "mRepeatCount = " + mRepeatCount);
//判断是不是已经滚动到了底部的方法,这个方法其实有问题,如果微信通信录中有多个重复的联系人,这个判断方法会失效。目前没有找到更好的判断是否滑动到了底部的方法,欢迎各位大神沟通交流。
if (mRepeatCount == 3) {
//表示已经滑动到顶部了
if (scrollToBottom) {
Log.d(TAG, "没有找到联系人");
//此次发消息操作已经完成
hasSend = true;
return null;
}
scrollToBottom = true;
}
mRepeatCount++;
}
}
}
if (!scrollToBottom) {
//向下滚动
listview.get(0).performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
} else {
return null;
}
//必须等待,因为需要等待滚动操作完成
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return null;
}
第五步、如果找到了联系人,点击联系人,到联系人信息页面,此时onAccessibilityEvent会回调,窗体信息发生变化,通过handleFlow_ContactInfoUI方法处理这个消息,此方法比较简单,找到页面上的发消息按钮,点击进入下一步聊天页面。
else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CONTACTINFOUI)) {
handleFlow_ContactInfoUI();
}
private void handleFlow_ContactInfoUI() {
WechatUtils.findTextAndClick(this, "发消息");
}
第六步,同上,进入到聊天页面onAccessibilityEvent会回调,通过handleFlow_ChatUI方法处理此事件。
这个方法的逻辑:判断聊天对象是不是正确的,因为可能我们打开微信的时候,微信已经处于聊天界面中了,此时需要退到主页,再重新执行前面的操作,直到找到正确的联系人即可。WechatUtils.findViewByIdAndPasteContent,这个方法用来向文本框中粘贴需要发送的信息,sendContent方法,点击发送按钮,发送消息。
还有种情况是,聊天页面可能处于语音状态,这样就需要点击切换文本按钮,切换回发送文本状态,才能找到文本框并且粘贴信息,否则会找不到文本框,消息发送失败。
else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CHATUI)) {
handleFlow_ChatUI();
}
private void handleFlow_ChatUI() {
//如果微信已经处于聊天界面,需要判断当前联系人是不是需要发送的联系人
String curUserName = WechatUtils.findTextById(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_USERNAME_ID);
if (!TextUtils.isEmpty(curUserName) && curUserName.equals(WechatUtils.NAME)) {
if (WechatUtils.findViewByIdAndPasteContent(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_EDITTEXT_ID, WechatUtils.CONTENT)) {
sendContent();
} else {
//当前页面可能处于发送语音状态,需要切换成发送文本状态
WechatUtils.findViewIdAndClick(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_SWITCH_ID);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (WechatUtils.findViewByIdAndPasteContent(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_EDITTEXT_ID, WechatUtils.CONTENT)) {
sendContent();
}
}
} else {
//回到主界面
WechatUtils.findViewIdAndClick(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_BACK_ID);
}
}
至此,微信发送完毕,细心的朋友可能已经发现所有的点击,粘贴,查找文本信息等信息等操作都放在WechatUtils这个类中进行的,这是我封装的进行服务操作动作的一个类,主要是方便管理和复用。
贴一个找到文本并且进行点击操作的代码,其他方法感兴趣的自行下载Demo查看。
public static void findTextAndClick(AccessibilityService accessibilityService, String text) {
AccessibilityNodeInfo accessibilityNodeInfo = accessibilityService.getRootInActiveWindow();
if (accessibilityNodeInfo == null) {
return;
}
List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text);
if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
if (nodeInfo != null && (text.equals(nodeInfo.getText()) || text.equals(nodeInfo.getContentDescription()))) {
performClick(nodeInfo);
break;
}
}
}
}