Android微信自动回复功能

作者: MeloDev | 来源:发表于2016-07-15 10:17 被阅读11130次

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配置文件

代码:

<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类的全部代码,注释还算详尽,如有疑问,下方回复。

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的配置比较重要。

<?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服务,因为很有可能造成一些安全问题,所以,自己动手写就安全多了嘛。

github项目地址:
WcAutoReply

相关文章

网友评论

  • 橙味菌:我想知道车萝卜是什么?
  • 七号大蒜:能介绍下你家老大吗
  • PeanutZYH:66666
  • 17adf804b72c:新手入门要写一个类似功能的app,然后在研读大神的代码,求问为什么会报了很多Error:(172, 13) 错误: 需要class, interface或enum的错误呀,不知道过了这么久回复能不能看到……求教!
    17adf804b72c:@MeloDev 嗯嗯,因为刚上手已经解决了,非常感谢,请问想要实现回复图片应该怎么办啊
    MeloDev:@当归_eca1 这个是初级问题吧,导包,R文件什么的报错导致的
  • 缑晓东:一年的帖子,不知道还会不会回复,我想问一下,在微信自动回复里,怎么去做精准回复呢? 我这里有好几个同时给我发消息就会乱了,只发到一个好友里面了
  • 王元_Trump:这个能获取大群消息的群名么
  • b502d9d35cf2:有办法区别群和用户吗?
  • bac47fee786c:你好,如果想让屏幕自动滚动应该使用什么事件呢?
  • 张贤同学:要是用户没开这个辅助功能,那怎么玩~~~
  • 0f8879052428:你好..我用gerRootInActiveWindow命令遍历控件. 但是发现微信的聊天记录列表取不到.. 亲对这个了解么? 求帮助.
  • xVkWzE:大神,wxid_xxxxxx 如此的微信id是否能够获取到?
  • King杨:很棒!学习了。这个只能做响应是吗?请教一下如果想要自动做一些操作该如何实现呢,比如自动给所有好友发消息
  • 淡忘LQX:很不错,大神,问个问题,网上的那些微信一键转发朋友圈视频的软件也是使用这个AccessibilityService实现的吗
    MeloDev: @堕落星辰03 很有可能呦
  • hackware:模拟back键没有权限问题?
  • Yuven:还有怎么拿到webview中的控件,我需要模拟点击webview :pray:
  • Yuven:博主我想问下怎么知道控件的类型是textview还是radiobutton
  • 3dbb328e8015:使用了两部手机测试作者的代码,小米max手机不能自动回复,魅族3手机能够自动回复
  • ccc48ba4d41c:你那个判断微信是否在前台运行到了Android 22就不能用了 :sob: :sob:
    lastingyang: @跘廿誓唁 Accessibilty 就能判断出前台焦点是谁
    MeloDev:@跘廿誓唁 嗯啊,适配差是真的 - -
  • 王洪贺:还有其他方式在不打开微信界面的情况下拦截收到的消息,也能回复语音
    我也在研究这个方向,你可以下载一个叫云驾的软件,里面有个微信助手,我反编译代码感觉他用的是微信分享的sdk,但是他用的一些方法在腾讯官网上并没有,希望有时间共同探讨一下
  • 47fe3088500c: :+1: :anguished: 还可以再屌一点吗?炫酷得一塌糊涂
    MeloDev:@47fe3088500c 过奖了
  • 47fe3088500c:干货硬菜 牛得一塌糊涂... :joy:
  • FredKang:好像自动化测试那样
  • FredKang:好玩,抢红包也是这个
  • xiawe_i:抢红包没成功?没弄对。。求告知
  • ec95b5891948: :+1: 研究得不错!
    MeloDev: @D_clock爱吃葱花 谢葱花哥夸奖哈
  • FlynnHai: sendNotifacationReply(event);
    handler.postDelayed(new Runnable() {
    @Override
    public void run() {
    sendNotifacationReply(event);
    if (fill()) {
    send();
    }
    }
    }, 1000); 为什么2次sendNotifacationReply(event)
  • 2dd9afb6d615:问个问题,自动回复消息的时候,打开了微信应用,那么在这个时间间隙,就收不到新的消息通知了,这个问题怎么解决?
    MeloDev:@wins22237 所有事件的开始都是有通知开启的,在聊天界面,你收到对方的消息,没有通知,这个功能就没法使用了~
  • qzwlf:能不能把Demo上传下 :grin:
    MeloDev:@qzwlf github已经上传了
  • 幸福就是不断的经历:我调试了,通知栏有信息都不触发事件啊..源码我就改了这里 android:description="@string/accessibility_description"..换成string里面有的了
    MeloDev:@幸福就是不断的经历 <string name="accessibility_description">autoreply_service</string>

    字符串引用这个试试吧,我的手机就跑成功啦,不过也许有的手机不可以,没有做太多测试,只是提供一个功能思路
  • 幸福就是不断的经历:把大神,代码复制,运行成功了,辅助功能也打开了,通知栏里面收到信息了,但没有自动回复..是怎么回事,,小米手机..有成功的吗?代码我来运行看看
    674fa1c36ad3:@幸福就是不断的经历 垃圾小米
  • 封不然:我是服务端的,不过我很好奇,这个不会被杀毒软件判定为流氓软件吗?他有搜集用户信息的潜在风险
    MeloDev:@neek_huang 不会,因为它本身不是病毒啊,就是权限大了一点,会弹出询问框的
  • 捡淑:大写的服!
  • 02ff3d94682c:牛逼,求关注
  • 431d0e86df61:能不能运行?新建一个项目把代码复制过去就行了?
    MeloDev:@Sam丶Yu 回复内容可以自己定制哦
    431d0e86df61:可以了,姿势不对 :smile:
  • fe2a5bcdf6ea:你们老大的需求不错
  • HelloTu:不错
  • b637fe356585:为什么我照着写却没效果?服务和辅助都开启了
    MeloDev:@SunmengCoco 手机是不是4.3以上?打印log好好看看过程吧
  • 21bda5c08e47:i get it.it is so good
  • RoseauHan:求安装包😏😏😏
  • SZhua:Mark
  • RoadToGeek:没想到AccessibilityService这么强大,又给了我学习它的动力
    RoadToGeek:@MeloDev 哈哈,刚刚看到这篇文章就开始读起文档了,谢谢大神的干货 :kissing_heart:
    MeloDev:@RoadToGeek 配合上好的产品想法,能做出来很多炫酷的服务,建议读一读官方文档
  • 王元_Trump:有点意思 我要试试
    王元_Trump: @MeloDev 这个除了通知栏其它这不是都可以监听么
    王元_Trump: @MeloDev 嗯 已经跑起来了
    MeloDev:@王元_Trump 跑起来试试,也可以自己改改
  • 太妹儿:赞~大神
  • 秋染蒹葭:第一次看见如此黑科技,赞~\(≧▽≦)/~
  • 84a26e609a78:我大魅族
  • 63aef557a0a6:你家老大的需求真不赖~
  • 9fc668f60f07:可以再教一下我吗?看不懂
  • 4263999de9bc:魅族手机😂看出来了
  • atomic_volatile:你们家老大深不可测啊~
  • 69d0c9b219a5:魅族手机
  • libmi:还能这么玩。
  • 4661b8b82818:不错可以学学
  • 329c6f7a2399:厉害,这都能实现
  • shone:老司机,良心作,good
    MeloDev:@shone 感谢支持hhh
  • karonl:可以可以
  • 为梦拼命的攻城狮:是不是必须微信得有通知栏?我的三星下载最新版本的微信是不行的....
    MeloDev:@为梦拼命的攻城狮 恩是的,因为我一直在侦听着 Notification 的状态 没通知栏就不知道来微信了
  • nbpzjy:厉害!初入门表示还看不懂。但可拷下来试试👍
    MeloDev:@nbpzjy 然后可以慢慢啃嘛,自己修修改改,完成想要的功能,加油喽!
  • 岁月留痕:前面有看过一篇文章讲360静默安装,想不到还能实现这么多功能,涨经验了
  • 黑丫山上小旋风:评论好6啊
  • f38c7cf0b2dc:直接把代码复制到手机就可以了吗
    幸福就是不断的经历:我运行起来了也打开辅助了,但没有自动回复啊,大神,小米手机
    f38c7cf0b2dc: @MeloDev 我试试,不懂的再请教你
    MeloDev:@马兰 不是。。。需要Android Studio跑到手机上。。。主要是面对开发者的教程,因为这个就算一个demo,仅仅在我手机上跑通了,没有在其他手机上试过

本文标题:Android微信自动回复功能

本文链接:https://www.haomeiwen.com/subject/rwryjttx.html