Melo's Blog

Android微信自动回复功能

Android微信自动回复功能

写在前面:
最近接到老大的一个需求,要求在手机端拦截微信的通知(Notification),从而获得联系人和内容。之后将联系人和内容发送到我们的硬件产品上,展示出来之后,再将我们想回复内容传给微信,并且发送给相应联系人。

老大还提示我需要用AccessibilityService去实现它,当然在此之前我并不知道AccessibilityService是什么鬼,不过没关系, just do IT

AccessibilityService

AccessibilityService官方文档(需翻墙)

上面这个链接是AccessibilityService的官方文档,可以翻墙点进去了解下,我再给大家总结一下:

AccessibilityService是Android系统框架提供给安装在设备上应用的一个可选的导航反馈特性。AccessibilityService 可以替代应用与用户交流反馈,比如将文本转化为语音提示,或是用户的手指悬停在屏幕上一个较重要的区域时的触摸反馈等。

如果感觉上面的描述比较抽象,没关系,也许你见过下面这张图:

辅助功能中的服务

打开你手机的设置–辅助功能中,有很多APP提供的服务,他们都是基于AccessibilityService编写的,AccessibilityService可以侦听你的点击,长按,手势,通知栏的变化等。并且你可以通过很多种方式找到窗体中的EditText,Button等组件,去填充他们,去点击他们来帮你实现自动化的功能。

像360助手的自动安装功能,它就是侦听着系统安装的APP,然后找到“安装”按钮,实现了自动点击。微信自动抢红包功能,实现方式都是如此。

配置AccessibilityService

首先我们在res文件夹下创建xml文件夹,然后创建一个名为auto_reply_service_config的文件,一会我们会在清单文件中引用它。

AccessibilityService配置文件

代码:

1
2
3
4
5
6
7
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_description"
android:notificationTimeout="100"
android:packageNames="com.tencent.mm" />

这个文件表示我们对AccessibilityService服务未来侦听的行为做了一些配置,比如 typeNotificationStateChangedtypeWindowStateChanged 表示我们需要侦听通知栏的状态变化和窗体状态改变。
android:packageNames=”com.tencent.mm” 这是微信的包名,表示我们只关心微信这一个应用。

代码不打算带着大家一行一行看了,如果有不明白的,去看看文档,或者下面回复我,我给大家解答~

创建AccessibilityService

下面贴出AccessibilityService类的全部代码,注释还算详尽,如有疑问,下方回复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
package com.ileja.autoreply;
import android.accessibilityservice.AccessibilityService;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import java.io.IOException;
import java.util.List;
public class AutoReplyService extends AccessibilityService {
private final static String MM_PNAME = "com.tencent.mm";
boolean hasAction = false;
boolean locked = false;
boolean background = false;
private String name;
private String scontent;
AccessibilityNodeInfo itemNodeinfo;
private KeyguardManager.KeyguardLock kl;
private Handler handler = new Handler();
/**
* 必须重写的方法,响应各种事件。
* @param event
*/
@Override
public void onAccessibilityEvent(final AccessibilityEvent event) {
int eventType = event.getEventType();
android.util.Log.d("maptrix", "get event = " + eventType);
switch (eventType) {
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:// 通知栏事件
android.util.Log.d("maptrix", "get notification event");
List<CharSequence> texts = event.getText();
if (!texts.isEmpty()) {
for (CharSequence text : texts) {
String content = text.toString();
if (!TextUtils.isEmpty(content)) {
if (isScreenLocked()) {
locked = true;
wakeAndUnlock();
android.util.Log.d("maptrix", "the screen is locked");
if (isAppForeground(MM_PNAME)) {
background = false;
android.util.Log.d("maptrix", "is mm in foreground");
sendNotifacationReply(event);
handler.postDelayed(new Runnable() {
@Override
public void run() {
sendNotifacationReply(event);
if (fill()) {
send();
}
}
}, 1000);
} else {
background = true;
android.util.Log.d("maptrix", "is mm in background");
sendNotifacationReply(event);
}
} else {
locked = false;
android.util.Log.d("maptrix", "the screen is unlocked");
// 监听到微信红包的notification,打开通知
if (isAppForeground(MM_PNAME)) {
background = false;
android.util.Log.d("maptrix", "is mm in foreground");
sendNotifacationReply(event);
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (fill()) {
send();
}
}
}, 1000);
} else {
background = true;
android.util.Log.d("maptrix", "is mm in background");
sendNotifacationReply(event);
}
}
}
}
}
break;
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
android.util.Log.d("maptrix", "get type window down event");
if (!hasAction) break;
itemNodeinfo = null;
String className = event.getClassName().toString();
if (className.equals("com.tencent.mm.ui.LauncherUI")) {
if (fill()) {
send();
}else {
if(itemNodeinfo != null){
itemNodeinfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (fill()) {
send();
}
back2Home();
release();
hasAction = false;
}
}, 1000);
break;
}
}
}
//bring2Front();
back2Home();
release();
hasAction = false;
break;
}
}
/**
* 寻找窗体中的“发送”按钮,并且点击。
*/
@SuppressLint("NewApi")
private void send() {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo != null) {
List<AccessibilityNodeInfo> list = nodeInfo
.findAccessibilityNodeInfosByText("发送");
if (list != null && list.size() > 0) {
for (AccessibilityNodeInfo n : list) {
if(n.getClassName().equals("android.widget.Button") && n.isEnabled())
{
n.performAction(AccessibilityNodeInfo.ACTION_CLICK);}
}
} else {
List<AccessibilityNodeInfo> liste = nodeInfo
.findAccessibilityNodeInfosByText("Send");
if (liste != null && liste.size() > 0) {
for (AccessibilityNodeInfo n : liste) {
if(n.getClassName().equals("android.widget.Button") && n.isEnabled())
{
n.performAction(AccessibilityNodeInfo.ACTION_CLICK);}
}
}
}
}
pressBackButton();
}
}
/**
* 模拟back按键
*/
private void pressBackButton(){
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("input keyevent " + KeyEvent.KEYCODE_BACK);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
*
* @param event
*/
private void sendNotifacationReply(AccessibilityEvent event) {
hasAction = true;
if (event.getParcelableData() != null
&& event.getParcelableData() instanceof Notification) {
Notification notification = (Notification) event
.getParcelableData();
String content = notification.tickerText.toString();
String[] cc = content.split(":");
name = cc[0].trim();
scontent = cc[1].trim();
android.util.Log.i("maptrix", "sender name =" + name);
android.util.Log.i("maptrix", "sender content =" + scontent);
PendingIntent pendingIntent = notification.contentIntent;
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
}
@SuppressLint("NewApi")
private boolean fill() {
AccessibilityNodeInfo rootNode = getRootInActiveWindow();
if (rootNode != null) {
return findEditText(rootNode, "正在忙,稍后回复你");
}
return false;
}
private boolean findEditText(AccessibilityNodeInfo rootNode, String content) {
int count = rootNode.getChildCount();
android.util.Log.d("maptrix", "root class=" + rootNode.getClassName() + ","+ rootNode.getText()+","+count);
for (int i = 0; i < count; i++) {
AccessibilityNodeInfo nodeInfo = rootNode.getChild(i);
if (nodeInfo == null) {
android.util.Log.d("maptrix", "nodeinfo = null");
continue;
}
android.util.Log.d("maptrix", "class=" + nodeInfo.getClassName());
android.util.Log.e("maptrix", "ds=" + nodeInfo.getContentDescription());
if(nodeInfo.getContentDescription() != null){
int nindex = nodeInfo.getContentDescription().toString().indexOf(name);
int cindex = nodeInfo.getContentDescription().toString().indexOf(scontent);
android.util.Log.e("maptrix", "nindex=" + nindex + " cindex=" +cindex);
if(nindex != -1){
itemNodeinfo = nodeInfo;
android.util.Log.i("maptrix", "find node info");
}
}
if ("android.widget.EditText".equals(nodeInfo.getClassName())) {
android.util.Log.i("maptrix", "==================");
Bundle arguments = new Bundle();
arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD);
arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
true);
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
arguments);
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
ClipData clip = ClipData.newPlainText("label", content);
ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClip(clip);
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE);
return true;
}
if (findEditText(nodeInfo, content)) {
return true;
}
}
return false;
}
@Override
public void onInterrupt() {
}
/**
* 判断指定的应用是否在前台运行
*
* @param packageName
* @return
*/
private boolean isAppForeground(String packageName) {
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ComponentName cn = am.getRunningTasks(1).get(0).topActivity;
String currentPackageName = cn.getPackageName();
if (!TextUtils.isEmpty(currentPackageName) && currentPackageName.equals(packageName)) {
return true;
}
return false;
}
/**
* 将当前应用运行到前台
*/
private void bring2Front() {
ActivityManager activtyManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> runningTaskInfos = activtyManager.getRunningTasks(3);
for (ActivityManager.RunningTaskInfo runningTaskInfo : runningTaskInfos) {
if (this.getPackageName().equals(runningTaskInfo.topActivity.getPackageName())) {
activtyManager.moveTaskToFront(runningTaskInfo.id, ActivityManager.MOVE_TASK_WITH_HOME);
return;
}
}
}
/**
* 回到系统桌面
*/
private void back2Home() {
Intent home = new Intent(Intent.ACTION_MAIN);
home.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
home.addCategory(Intent.CATEGORY_HOME);
startActivity(home);
}
/**
* 系统是否在锁屏状态
*
* @return
*/
private boolean isScreenLocked() {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
return keyguardManager.inKeyguardRestrictedInputMode();
}
private void wakeAndUnlock() {
//获取电源管理器对象
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
//获取PowerManager.WakeLock对象,后面的参数|表示同时传入两个值,最后的是调试用的Tag
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "bright");
//点亮屏幕
wl.acquire(1000);
//得到键盘锁管理器对象
KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
kl = km.newKeyguardLock("unLock");
//解锁
kl.disableKeyguard();
}
private void release() {
if (locked && kl != null) {
android.util.Log.d("maptrix", "release the lock");
//得到键盘锁管理器对象
kl.reenableKeyguard();
locked = false;
}
}
}

接着配置清单文件,权限和service的配置比较重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ileja.autoreply">
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".AutoReplyService"
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>
</application>
</manifest>

为了使用某些必要的API,最低API level应该是18

运行程序,打开服务,看看效果如何把~

打开辅助服务

接着用其他手机试着发送给我几条微信

自动回复微信

可以看到,自动回复功能就实现了。

写在后面:

代码没有给大家详细讲解,不过看注释应该可以看懂个大概。当微信程序切换到后台,或者锁屏(无锁屏密码)时,只要有通知出现,都可以实现自动回复。

关于AccessibilityService可以监控的行为非常多,所以我觉得可以实现各种各样炫酷的功能,不过我并不建议你打开某些流氓软件的AccessibilityService服务,因为很有可能造成一些安全问题,所以,自己动手写就安全多了嘛。