Android微信自動(dòng)回復(fù)功能

Android微信自動(dòng)回復(fù)功能

本文原創(chuàng),轉(zhuǎn)載請(qǐng)經(jīng)過(guò)本人準(zhǔn)許。

寫(xiě)在前面:

最近接到老大的一個(gè)需求,要求在手機(jī)端攔截微信的通知(Notification),從而獲得聯(lián)系人和內(nèi)容。之后將聯(lián)系人和內(nèi)容發(fā)送到我們的硬件產(chǎn)品上,展示出來(lái)之后,再將我們想回復(fù)內(nèi)容傳給微信,并且發(fā)送給相應(yīng)聯(lián)系人。

老大還提示我需要用AccessibilityService去實(shí)現(xiàn)它,當(dāng)然在此之前我并不知道AccessibilityService是什么鬼,不過(guò)沒(méi)關(guān)系, just do IT

AccessibilityService

AccessibilityService官方文檔(需翻墻)

上面這個(gè)鏈接是AccessibilityService的官方文檔,可以翻墻點(diǎn)進(jìn)去了解下,我再給大家總結(jié)一下:

AccessibilityService是Android系統(tǒng)框架提供給安裝在設(shè)備上應(yīng)用的一個(gè)可選的導(dǎo)航反饋特性。AccessibilityService 可以替代應(yīng)用與用戶(hù)交流反饋,比如將文本轉(zhuǎn)化為語(yǔ)音提示,或是用戶(hù)的手指懸停在屏幕上一個(gè)較重要的區(qū)域時(shí)的觸摸反饋等。

如果感覺(jué)上面的描述比較抽象,沒(méi)關(guān)系,也許你見(jiàn)過(guò)下面這張圖:

輔助功能中的服務(wù)

打開(kāi)你手機(jī)的設(shè)置--輔助功能中,有很多APP提供的服務(wù),他們都是基于AccessibilityService編寫(xiě)的,AccessibilityService可以偵聽(tīng)你的點(diǎn)擊,長(zhǎng)按,手勢(shì),通知欄的變化等。并且你可以通過(guò)很多種方式找到窗體中的EditText,Button等組件,去填充他們,去點(diǎn)擊他們來(lái)幫你實(shí)現(xiàn)自動(dòng)化的功能。

像360助手的自動(dòng)安裝功能,它就是偵聽(tīng)著系統(tǒng)安裝的APP,然后找到“安裝”按鈕,實(shí)現(xiàn)了自動(dòng)點(diǎn)擊。微信自動(dòng)搶紅包功能,實(shí)現(xiàn)方式都是如此。

配置AccessibilityService

首先我們?cè)趓es文件夾下創(chuàng)建xml文件夾,然后創(chuàng)建一個(gè)名為auto_reply_service_config的文件,一會(huì)我們會(huì)在清單文件中引用它。

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" />

這個(gè)文件表示我們對(duì)AccessibilityService服務(wù)未來(lái)偵聽(tīng)的行為做了一些配置,比如 typeNotificationStateChangedtypeWindowStateChanged 表示我們需要偵聽(tīng)通知欄的狀態(tài)變化和窗體狀態(tài)改變。
android:packageNames="com.tencent.mm" 這是微信的包名,表示我們只關(guān)心微信這一個(gè)應(yīng)用。

代碼不打算帶著大家一行一行看了,如果有不明白的,去看看文檔,或者下面回復(fù)我,我給大家解答~

創(chuàng)建AccessibilityService

下面貼出AccessibilityService類(lèi)的全部代碼,注釋還算詳盡,如有疑問(wèn),下方回復(fù)。

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();


    /**
     * 必須重寫(xiě)的方法,響應(yīng)各種事件。
     * @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");
                                // 監(jiān)聽(tīng)到微信紅包的notification,打開(kāi)通知
                                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;
        }
    }

    /**
     * 尋找窗體中的“發(fā)送”按鈕,并且點(diǎn)擊。
     */
    @SuppressLint("NewApi")
    private void send() {
        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        if (nodeInfo != null) {
            List<AccessibilityNodeInfo> list = nodeInfo
                    .findAccessibilityNodeInfosByText("發(fā)送");
            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, "正在忙,稍后回復(fù)你");
        }
        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() {

    }

    /**
     * 判斷指定的應(yīng)用是否在前臺(tái)運(yùn)行
     *
     * @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;
    }


    /**
     * 將當(dāng)前應(yīng)用運(yùn)行到前臺(tái)
     */
    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;
            }
        }
    }

    /**
     * 回到系統(tǒng)桌面
     */
    private void back2Home() {
        Intent home = new Intent(Intent.ACTION_MAIN);

        home.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        home.addCategory(Intent.CATEGORY_HOME);

        startActivity(home);
    }


    /**
     * 系統(tǒng)是否在鎖屏狀態(tài)
     *
     * @return
     */
    private boolean isScreenLocked() {
        KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
        return keyguardManager.inKeyguardRestrictedInputMode();
    }

    private void wakeAndUnlock() {
        //獲取電源管理器對(duì)象
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);

        //獲取PowerManager.WakeLock對(duì)象,后面的參數(shù)|表示同時(shí)傳入兩個(gè)值,最后的是調(diào)試用的Tag
        PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "bright");

        //點(diǎn)亮屏幕
        wl.acquire(1000);

        //得到鍵盤(pán)鎖管理器對(duì)象
        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");
            //得到鍵盤(pán)鎖管理器對(duì)象
            kl.reenableKeyguard();
            locked = false;
        }
    }
}

接著配置清單文件,權(quán)限和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應(yīng)該是18

運(yùn)行程序,打開(kāi)服務(wù),看看效果如何把~

打開(kāi)輔助服務(wù)

接著用其他手機(jī)試著發(fā)送給我?guī)讞l微信

自動(dòng)回復(fù)微信

可以看到,自動(dòng)回復(fù)功能就實(shí)現(xiàn)了。

寫(xiě)在后面:

代碼沒(méi)有給大家詳細(xì)講解,不過(guò)看注釋?xiě)?yīng)該可以看懂個(gè)大概。當(dāng)微信程序切換到后臺(tái),或者鎖屏(無(wú)鎖屏密碼)時(shí),只要有通知出現(xiàn),都可以實(shí)現(xiàn)自動(dòng)回復(fù)。

關(guān)于AccessibilityService可以監(jiān)控的行為非常多,所以我覺(jué)得可以實(shí)現(xiàn)各種各樣炫酷的功能,不過(guò)我并不建議你打開(kāi)某些流氓軟件的AccessibilityService服務(wù),因?yàn)楹苡锌赡茉斐梢恍┌踩珕?wèn)題,所以,自己動(dòng)手寫(xiě)就安全多了嘛。

github項(xiàng)目地址:
WcAutoReply

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評(píng)論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,036評(píng)論 4 61
  • 01 前幾天跟爸媽出去吃飯,聽(tīng)說(shuō)了一件事。 叫他李叔吧,李叔跟我爸媽的年齡差不多,家有兩個(gè)孩子,大兒子今年31歲,...
    喵姬閱讀 1,988評(píng)論 0 0
  • 君不見(jiàn),良辰美景月兒圓,清風(fēng)拂面玉滿(mǎn)樽。 君不見(jiàn),高朋滿(mǎn)座齊歡聚,把酒豪飲樂(lè)開(kāi)懷。 相逢相知不相棄,天涯何處不念君...
    良柳如煙閱讀 997評(píng)論 11 8

友情鏈接更多精彩內(nèi)容