├── README.md └── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── ileja │ └── autoreply │ └── ApplicationTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── ileja │ │ └── autoreply │ │ ├── AutoReplyService.java │ │ └── MainActivity.java └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── auto_reply_service_config.xml └── test └── java └── com └── ileja └── autoreply └── ExampleUnitTest.java /README.md: -------------------------------------------------------------------------------- 1 | # WcAutoReply 2 | 3 | Android 微信自动回复功能,使用的知识为 AccessibilityService,未来有时间会重新研究下各种 rom 下的适配,以及优化下代码 :) 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | defaultConfig { 7 | applicationId "com.ileja.autoreply" 8 | minSdkVersion 19 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | productFlavors { 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(dir: 'libs', include: ['*.jar']) 25 | testCompile 'junit:junit:4.12' 26 | compile 'com.android.support:appcompat-v7:23.4.0' 27 | compile 'com.jakewharton:butterknife:7.0.1' 28 | compile 'com.jakewharton.timber:timber:2.4.2' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/chentao/Documents/android/develop/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ileja/autoreply/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.ileja.autoreply; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/ileja/autoreply/AutoReplyService.java: -------------------------------------------------------------------------------- 1 | package com.ileja.autoreply; 2 | 3 | import android.accessibilityservice.AccessibilityService; 4 | import android.annotation.SuppressLint; 5 | import android.app.ActivityManager; 6 | import android.app.KeyguardManager; 7 | import android.app.Notification; 8 | import android.app.PendingIntent; 9 | import android.content.ClipData; 10 | import android.content.ClipboardManager; 11 | import android.content.ComponentName; 12 | import android.content.Context; 13 | import android.content.Intent; 14 | import android.os.Bundle; 15 | import android.os.Handler; 16 | import android.os.PowerManager; 17 | import android.text.TextUtils; 18 | import android.view.KeyEvent; 19 | import android.view.accessibility.AccessibilityEvent; 20 | import android.view.accessibility.AccessibilityNodeInfo; 21 | 22 | import java.io.IOException; 23 | import java.util.List; 24 | 25 | public class AutoReplyService extends AccessibilityService { 26 | private final static String MM_PNAME = "com.tencent.mm"; 27 | boolean hasAction = false; 28 | boolean locked = false; 29 | boolean background = false; 30 | private String name; 31 | private String scontent; 32 | AccessibilityNodeInfo itemNodeinfo; 33 | private KeyguardManager.KeyguardLock kl; 34 | private Handler handler = new Handler(); 35 | 36 | /** 37 | * 必须重写的方法,响应各种事件。 38 | * @param event 39 | */ 40 | @Override 41 | public void onAccessibilityEvent(final AccessibilityEvent event) { 42 | int eventType = event.getEventType(); 43 | android.util.Log.d("maptrix", "get event = " + eventType); 44 | switch (eventType) { 45 | case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:// 通知栏事件 46 | android.util.Log.d("maptrix", "get notification event"); 47 | List texts = event.getText(); 48 | if (!texts.isEmpty()) { 49 | for (CharSequence text : texts) { 50 | String content = text.toString(); 51 | if (!TextUtils.isEmpty(content)) { 52 | if (isScreenLocked()) { 53 | locked = true; 54 | wakeAndUnlock(); 55 | android.util.Log.d("maptrix", "the screen is locked"); 56 | if (isAppForeground(MM_PNAME)) { 57 | background = false; 58 | android.util.Log.d("maptrix", "is mm in foreground"); 59 | sendNotifacationReply(event); 60 | handler.postDelayed(new Runnable() { 61 | @Override 62 | public void run() { 63 | sendNotifacationReply(event); 64 | if (fill()) { 65 | send(); 66 | } 67 | } 68 | }, 1000); 69 | } else { 70 | background = true; 71 | android.util.Log.d("maptrix", "is mm in background"); 72 | sendNotifacationReply(event); 73 | } 74 | } else { 75 | locked = false; 76 | android.util.Log.d("maptrix", "the screen is unlocked"); 77 | if (isAppForeground(MM_PNAME)) { 78 | background = false; 79 | android.util.Log.d("maptrix", "is mm in foreground"); 80 | sendNotifacationReply(event); 81 | handler.postDelayed(new Runnable() { 82 | @Override 83 | public void run() { 84 | if (fill()) { 85 | send(); 86 | } 87 | } 88 | }, 1000); 89 | } else { 90 | background = true; 91 | android.util.Log.d("maptrix", "is mm in background"); 92 | sendNotifacationReply(event); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | break; 99 | case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: 100 | android.util.Log.d("maptrix", "get type window down event"); 101 | if (!hasAction) break; 102 | itemNodeinfo = null; 103 | String className = event.getClassName().toString(); 104 | if (className.equals("com.tencent.mm.ui.LauncherUI")) { 105 | if (fill()) { 106 | send(); 107 | }else { 108 | if(itemNodeinfo != null){ 109 | itemNodeinfo.performAction(AccessibilityNodeInfo.ACTION_CLICK); 110 | handler.postDelayed(new Runnable() { 111 | @Override 112 | public void run() { 113 | if (fill()) { 114 | send(); 115 | } 116 | back2Home(); 117 | release(); 118 | hasAction = false; 119 | } 120 | }, 1000); 121 | break; 122 | } 123 | } 124 | } 125 | //bring2Front(); 126 | back2Home(); 127 | release(); 128 | hasAction = false; 129 | break; 130 | } 131 | } 132 | 133 | /** 134 | * 寻找窗体中的“发送”按钮,并且点击。 135 | */ 136 | @SuppressLint("NewApi") 137 | private void send() { 138 | AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); 139 | if (nodeInfo != null) { 140 | List list = nodeInfo 141 | .findAccessibilityNodeInfosByText("发送"); 142 | if (list != null && list.size() > 0) { 143 | for (AccessibilityNodeInfo n : list) { 144 | if(n.getClassName().equals("android.widget.Button") && n.isEnabled()){ 145 | n.performAction(AccessibilityNodeInfo.ACTION_CLICK); 146 | } 147 | } 148 | 149 | } else { 150 | List liste = nodeInfo 151 | .findAccessibilityNodeInfosByText("Send"); 152 | if (liste != null && liste.size() > 0) { 153 | for (AccessibilityNodeInfo n : liste) { 154 | if(n.getClassName().equals("android.widget.Button") && n.isEnabled()){ 155 | n.performAction(AccessibilityNodeInfo.ACTION_CLICK); 156 | } 157 | } 158 | } 159 | } 160 | pressBackButton(); 161 | } 162 | } 163 | /** 164 | * 模拟back按键 165 | */ 166 | private void pressBackButton(){ 167 | Runtime runtime = Runtime.getRuntime(); 168 | try { 169 | runtime.exec("input keyevent " + KeyEvent.KEYCODE_BACK); 170 | } catch (IOException e) { 171 | e.printStackTrace(); 172 | } 173 | } 174 | /** 175 | * 拉起微信界面 176 | * @param event 177 | */ 178 | private void sendNotifacationReply(AccessibilityEvent event) { 179 | hasAction = true; 180 | if (event.getParcelableData() != null 181 | && event.getParcelableData() instanceof Notification) { 182 | Notification notification = (Notification) event 183 | .getParcelableData(); 184 | String content = notification.tickerText.toString(); 185 | String[] cc = content.split(":"); 186 | name = cc[0].trim(); 187 | scontent = cc[1].trim(); 188 | 189 | android.util.Log.i("maptrix", "sender name =" + name); 190 | android.util.Log.i("maptrix", "sender content =" + scontent); 191 | 192 | 193 | PendingIntent pendingIntent = notification.contentIntent; 194 | try { 195 | pendingIntent.send(); 196 | } catch (PendingIntent.CanceledException e) { 197 | e.printStackTrace(); 198 | } 199 | } 200 | } 201 | 202 | @SuppressLint("NewApi") 203 | private boolean fill() { 204 | AccessibilityNodeInfo rootNode = getRootInActiveWindow(); 205 | if (rootNode != null) { 206 | return findEditText(rootNode, "正在忙,稍后回复你"); 207 | } 208 | return false; 209 | } 210 | 211 | 212 | private boolean findEditText(AccessibilityNodeInfo rootNode, String content) { 213 | int count = rootNode.getChildCount(); 214 | 215 | android.util.Log.d("maptrix", "root class=" + rootNode.getClassName() + ","+ rootNode.getText()+","+count); 216 | for (int i = 0; i < count; i++) { 217 | AccessibilityNodeInfo nodeInfo = rootNode.getChild(i); 218 | if (nodeInfo == null) { 219 | android.util.Log.d("maptrix", "nodeinfo = null"); 220 | continue; 221 | } 222 | 223 | android.util.Log.d("maptrix", "class=" + nodeInfo.getClassName()); 224 | android.util.Log.e("maptrix", "ds=" + nodeInfo.getContentDescription()); 225 | if(nodeInfo.getContentDescription() != null){ 226 | int nindex = nodeInfo.getContentDescription().toString().indexOf(name); 227 | int cindex = nodeInfo.getContentDescription().toString().indexOf(scontent); 228 | android.util.Log.e("maptrix", "nindex=" + nindex + " cindex=" +cindex); 229 | if(nindex != -1){ 230 | itemNodeinfo = nodeInfo; 231 | android.util.Log.i("maptrix", "find node info"); 232 | } 233 | } 234 | if ("android.widget.EditText".equals(nodeInfo.getClassName())) { 235 | android.util.Log.i("maptrix", "=================="); 236 | Bundle arguments = new Bundle(); 237 | arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, 238 | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD); 239 | arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, 240 | true); 241 | nodeInfo.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, 242 | arguments); 243 | nodeInfo.performAction(AccessibilityNodeInfo.ACTION_FOCUS); 244 | ClipData clip = ClipData.newPlainText("label", content); 245 | ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 246 | clipboardManager.setPrimaryClip(clip); 247 | nodeInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE); 248 | return true; 249 | } 250 | 251 | if (findEditText(nodeInfo, content)) { 252 | return true; 253 | } 254 | } 255 | 256 | return false; 257 | } 258 | 259 | @Override 260 | public void onInterrupt() { 261 | 262 | } 263 | 264 | /** 265 | * 判断指定的应用是否在前台运行 266 | * 267 | * @param packageName 268 | * @return 269 | */ 270 | private boolean isAppForeground(String packageName) { 271 | ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); 272 | ComponentName cn = am.getRunningTasks(1).get(0).topActivity; 273 | String currentPackageName = cn.getPackageName(); 274 | if (!TextUtils.isEmpty(currentPackageName) && currentPackageName.equals(packageName)) { 275 | return true; 276 | } 277 | 278 | return false; 279 | } 280 | 281 | 282 | /** 283 | * 将当前应用运行到前台 284 | */ 285 | private void bring2Front() { 286 | ActivityManager activtyManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); 287 | List runningTaskInfos = activtyManager.getRunningTasks(3); 288 | for (ActivityManager.RunningTaskInfo runningTaskInfo : runningTaskInfos) { 289 | if (this.getPackageName().equals(runningTaskInfo.topActivity.getPackageName())) { 290 | activtyManager.moveTaskToFront(runningTaskInfo.id, ActivityManager.MOVE_TASK_WITH_HOME); 291 | return; 292 | } 293 | } 294 | } 295 | 296 | /** 297 | * 回到系统桌面 298 | */ 299 | private void back2Home() { 300 | Intent home = new Intent(Intent.ACTION_MAIN); 301 | 302 | home.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 303 | home.addCategory(Intent.CATEGORY_HOME); 304 | 305 | startActivity(home); 306 | } 307 | 308 | 309 | /** 310 | * 系统是否在锁屏状态 311 | * 312 | * @return 313 | */ 314 | private boolean isScreenLocked() { 315 | KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 316 | return keyguardManager.inKeyguardRestrictedInputMode(); 317 | } 318 | 319 | private void wakeAndUnlock() { 320 | //获取电源管理器对象 321 | PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 322 | 323 | //获取PowerManager.WakeLock对象,后面的参数|表示同时传入两个值,最后的是调试用的Tag 324 | PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "bright"); 325 | 326 | //点亮屏幕 327 | wl.acquire(1000); 328 | 329 | //得到键盘锁管理器对象 330 | KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 331 | kl = km.newKeyguardLock("unLock"); 332 | 333 | //解锁 334 | kl.disableKeyguard(); 335 | 336 | } 337 | 338 | private void release() { 339 | 340 | if (locked && kl != null) { 341 | android.util.Log.d("maptrix", "release the lock"); 342 | //得到键盘锁管理器对象 343 | kl.reenableKeyguard(); 344 | locked = false; 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /app/src/main/java/com/ileja/autoreply/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.ileja.autoreply; 2 | 3 | import android.app.KeyguardManager; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.os.PowerManager; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.view.WindowManager; 9 | 10 | public class MainActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_main); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsMelo/WcAutoReply/b5d868495e9c032c4fffccffa6fc2e60ebcaad15/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsMelo/WcAutoReply/b5d868495e9c032c4fffccffa6fc2e60ebcaad15/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsMelo/WcAutoReply/b5d868495e9c032c4fffccffa6fc2e60ebcaad15/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsMelo/WcAutoReply/b5d868495e9c032c4fffccffa6fc2e60ebcaad15/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsMelo/WcAutoReply/b5d868495e9c032c4fffccffa6fc2e60ebcaad15/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WxAutoReply 3 | autoreply_service 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/auto_reply_service_config.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/test/java/com/ileja/autoreply/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.ileja.autoreply; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } --------------------------------------------------------------------------------