1. 程式人生 > >Android進階——學習AccessibilityService實現微信搶紅包外掛

Android進階——學習AccessibilityService實現微信搶紅包外掛

前言

在你的手機更多設定或者高階設定中,我們會發現有個無障礙的功能,很多人不知道這個功能具體是幹嘛的,其實這個功能是為了增強使用者介面以幫助殘障人士,或者可能暫時無法與裝置充分互動的人們

它的具體實現是通過AccessibilityService服務執行在後臺中,通過AccessibilityEvent接收指定事件的回撥。這樣的事件表示使用者在介面中的一些狀態轉換,例如:焦點改變了,一個按鈕被點選,等等。這樣的服務可以選擇請求活動視窗的內容的能力。簡單的說AccessibilityService就是一個後臺監控服務,當你監控的內容發生改變時,就會呼叫後臺服務的回撥方法

AccessibilityService使用

一、建立服務類

編寫自己的Service類,重寫onServiceConnected()方法、onAccessibilityEvent()方法和onInterrupt()方法

public class QHBAccessibilityService extends AccessibilityService {

    /**
     * 當啟動服務的時候就會被呼叫
     */
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
    }

    /**
     * 監聽視窗變化的回撥
     */
@Override public void onAccessibilityEvent(AccessibilityEvent event) { int eventType = event.getEventType(); //根據事件回撥型別進行處理 } /** * 中斷服務的回撥 */ @Override public void onInterrupt() { } }

下面是對AccessibilityService中常用的方法的介紹

  • disableSelf():禁用當前服務,也就是在服務可以通過該方法停止執行
  • findFoucs(int falg):查詢擁有特定焦點型別的控制元件
  • getRootInActiveWindow():如果配置能夠獲取視窗內容,則會返回當前活動視窗的根結點
  • getSeviceInfo():獲取當前服務的配置資訊
  • onAccessibilityEvent(AccessibilityEvent event):有關AccessibilityEvent事件的回撥函式,系統通過sendAccessibiliyEvent()不斷的傳送AccessibilityEvent到此處
  • performGlobalAction(int action):執行全域性操作,比如返回,回到主頁,開啟最近等操作
  • setServiceInfo(AccessibilityServiceInfo info):設定當前服務的配置資訊
  • getSystemService(String name):獲取系統服務
  • onKeyEvent(KeyEvent event):如果允許服務監聽按鍵操作,該方法是按鍵事件的回撥,需要注意,這個過程發生了系統處理按鍵事件之前
  • onServiceConnected():系統成功繫結該服務時被觸發,也就是當你在設定中開啟相應的服務,系統成功的綁定了該服務時會觸發,通常我們可以在這裡做一些初始化操作
  • onInterrupt():服務中斷時的回撥

二、宣告服務

既然是個後臺服務,那麼就需要我們在manifests中配置該服務資訊

<service
    android:name=".AccessibilityService.QHBAccessibilityService"
    android:enabled="true"
    android:exported="true"
    android:label="@string/label"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
</service>

我們必須注意:任何一個資訊配置錯誤,都會使該服務無反應

  • android:label:在無障礙列表中顯示該服務的名字
  • android:permission:需要指定BIND_ACCESSIBILITY_SERVICE許可權,這是4.0以上的系統要求的
  • intent-filter:這個name是固定不變的

三、配置服務引數

配置服務引數是指:配置用來接受指定型別的事件,監聽指定package,檢索視窗內容,獲取事件型別的時間等等。其配置服務引數有兩種方法:

  • 方法一:安卓4.0之後可以通過meta-data標籤指定xml檔案進行配置
  • 方法二:通過程式碼動態配置引數

1、方法一

在原先的manifests中增加meta-data標籤指定xml檔案

<service
    android:name=".AccessibilityService.QHBAccessibilityService"
    android:enabled="true"
    android:exported="true"
    android:label="@string/label"
    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/accessibility_service_config" />
</service>

接下來是accessibility_service_config檔案的配置

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged|typeWindowsChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/description"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm" />

下面是對xml引數的介紹

  • accessibilityEventTypes:表示該服務對介面中的哪些變化感興趣,即哪些事件通知,比如視窗開啟,滑動,焦點變化,長按等。具體的值可以在AccessibilityEvent類中查到,如typeAllMask表示接受所有的事件通知
  • accessibilityFeedbackType:表示反饋方式,比如是語音播放,還是震動
  • canRetrieveWindowContent:表示該服務能否訪問活動視窗中的內容。也就是如果你希望在服務中獲取窗體內容,則需要設定其值為true
  • description:對該無障礙功能的描述
  • notificationTimeout:接受事件的時間間隔,通常將其設定為100即可
  • packageNames:表示對該服務是用來監聽哪個包的產生的事件,這裡以微信的包名為例

2、方法二

通過程式碼為我們的AccessibilityService配置AccessibilityServiceInfo資訊,這裡我們可以抽取成一個方法進行設定

private void settingAccessibilityInfo() {
    String[] packageNames = {"com.tencent.mm"};
    AccessibilityServiceInfo mAccessibilityServiceInfo = new AccessibilityServiceInfo();
    // 響應事件的型別,這裡是全部的響應事件(長按,單擊,滑動等)
    mAccessibilityServiceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
    // 反饋給使用者的型別,這裡是語音提示
    mAccessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
    // 過濾的包名
    mAccessibilityServiceInfo.packageNames = packageNames;
    setServiceInfo(mAccessibilityServiceInfo);
}

在這裡涉及到了AccessibilityServiceInfo類,AccessibilityServiceInfo類被用於配置AccessibilityService資訊,該類中包含了大量用於配置的常量欄位及用來xml屬性,常見的有:accessibilityEventTypes,canRequestFilterKeyEvents,packageNames等等

四、啟動服務

在無障礙功能裡面手動開啟該項功能,否則無法繼續進行,通過下面程式碼可以開啟系統的無障礙功能列表

Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);

五、處理事件資訊

由於我們監聽了事件的通知欄和介面等資訊,當我們指定packageNames的通知欄或者介面發生變化時,會通過onAccessibilityEvent回撥我們的事件,接著進行事件的處理

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    int eventType = event.getEventType();
    //根據事件回撥型別進行處理
    switch (eventType) {
        //當通知欄發生改變時
        case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:

            break;
        //當視窗的狀態發生改變時
        case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:

            break;
    }
}

當我們微信收到通知時,狀態列會有一條推送資訊到達,這個時候就會被TYPE_NOTIFICATION_STATE_CHANGED監聽,執行裡面的內容,當我們切換微信介面時,或者使用微信時,這個時候就會被TYPE_WINDOW_STATE_CHANGED監聽,執行裡面的內容

AccessibilityEvent的方法

  • getEventType():事件型別
  • getSource():獲取事件源對應的結點資訊
  • getClassName():獲取事件源對應類的型別,比如點選事件是有某個Button產生的,那麼此時獲取的就是Button的完整類名
  • getText():獲取事件源的文字資訊,比如事件是有TextView發出的,此時獲取的就是TextView的text屬性。如果該事件源是樹結構,那麼此時獲取的是這個樹上所有具有text屬性的值的集合
  • isEnabled():事件源(對應的介面控制元件)是否處在可用狀態
  • getItemCount():如果事件源是樹結構,將返回該樹根節點下子節點的數量

六、獲取節點資訊

獲取了介面視窗變化後,這個時候就要獲取控制元件的節點。整個視窗的節點本質是個樹結構,通過以下操作節點資訊

1、獲取視窗節點(根節點)

AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();

2、獲取指定子節點(控制元件節點)

//通過文字找到對應的節點集合
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(text);
//通過控制元件ID找到對應的節點集合,如com.tencent.mm:id/gd
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(clickId);

七、模擬節點點選

當我們獲取了節點資訊之後,對控制元件節點進行模擬點選、長按等操作,AccessibilityNodeInfo類提供了performAction()方法讓我們執行模擬操作,具體操作可看官方文件介紹,這裡列舉常用的操作

//模擬點選
accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
//模擬長按
accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
//模擬獲取焦點
accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
//模擬貼上
accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE);

搶紅包外掛實現

一、原理分析

  1. 收到微信紅包的推送資訊,在推送資訊中判斷是否出現”[微信紅包]”的訊息提示,如果出現則點選進入聊天介面
  2. 通過遍歷視窗樹節點,發現帶有”領取紅包”字樣的節點,則點選進入,即紅包,彈出搶紅包介面
  3. 在搶紅包介面,通過ID獲取”開”按鈕的節點,則開啟紅包
  4. 在紅包詳情頁面,通過ID獲取返回鍵按鈕的節點,點選並返回微信聊天介面

二、注意事項

  1. 由於微信每個版本的按鈕ID都是不一樣的,在我們的程式中是需要去修改按鈕ID,以達到版本的適配
  2. 在獲取控制元件ID的時候,注意其佈局是否可點選,否則獲取不可點選的控制元件,會使程式無反應

三、獲取控制元件ID

當我們手機接入USB線時,在Android Device Monitor中的選擇裝置並開啟Dump View Hierarchy for UI Automator工具,通過它可以獲取控制元件資訊

獲取”開”按鈕ID和返回按鈕ID

四、程式碼實現

注意:這裡使用的是微信最新6.3.30版本的控制元件ID,如果是其他版本的請自行適配

/**
 * =====作者=====
 * 許英俊
 * =====時間=====
 * 2016/11/19.
 */
public class QHBAccessibilityService extends AccessibilityService {

    private List<AccessibilityNodeInfo> parents;

    /**
     * 當啟動服務的時候就會被呼叫
     */
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        parents = new ArrayList<>();
    }

    /**
     * 監聽視窗變化的回撥
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            //當通知欄發生改變時
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                List<CharSequence> texts = event.getText();
                if (!texts.isEmpty()) {
                    for (CharSequence text : texts) {
                        String content = text.toString();
                        if (content.contains("[微信紅包]")) {
                            //模擬開啟通知欄訊息,即開啟微信
                            if (event.getParcelableData() != null &&
                                    event.getParcelableData() instanceof Notification) {
                                Notification notification = (Notification) event.getParcelableData();
                                PendingIntent pendingIntent = notification.contentIntent;
                                try {
                                    pendingIntent.send();
                                    Log.e("demo","進入微信");
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                }
                break;
            //當視窗的狀態發生改變時
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                String className = event.getClassName().toString();
                if (className.equals("com.tencent.mm.ui.LauncherUI")) {
                    //點選最後一個紅包
                    Log.e("demo","點選紅包");
                    getLastPacket();
                } else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI")) {
                    //開紅包
                    Log.e("demo","開紅包");
                    inputClick("com.tencent.mm:id/bg7");
                } else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI")) {
                    //退出紅包
                    Log.e("demo","退出紅包");
                    inputClick("com.tencent.mm:id/gd");
                }
                break;
        }
    }

    /**
     * 通過ID獲取控制元件,並進行模擬點選
     * @param clickId
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void inputClick(String clickId) {
        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        if (nodeInfo != null) {
            List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByViewId(clickId);
            for (AccessibilityNodeInfo item : list) {
                item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
        }
    }

    /**
     * 獲取List中最後一個紅包,並進行模擬點選
     */
    private void getLastPacket() {
        AccessibilityNodeInfo rootNode = getRootInActiveWindow();
        recycle(rootNode);
        if(parents.size()>0){
            parents.get(parents.size() - 1).performAction(AccessibilityNodeInfo.ACTION_CLICK);
        }
    }

    /**
     * 迴歸函式遍歷每一個節點,並將含有"領取紅包"存進List中
     *
     * @param info
     */
    public void recycle(AccessibilityNodeInfo info) {
        if (info.getChildCount() == 0) {
            if (info.getText() != null) {
                if ("領取紅包".equals(info.getText().toString())) {
                    if (info.isClickable()) {
                        info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    }
                    AccessibilityNodeInfo parent = info.getParent();
                    while (parent != null) {
                        if (parent.isClickable()) {
                            parents.add(parent);
                            break;
                        }
                        parent = parent.getParent();
                    }
                }
            }
        } else {
            for (int i = 0; i < info.getChildCount(); i++) {
                if (info.getChild(i) != null) {
                    recycle(info.getChild(i));
                }
            }
        }
    }

    /**
     * 中斷服務的回撥
     */
    @Override
    public void onInterrupt() {

    }
}

當收到紅包傳送的時候,Log的列印資訊

11-21 13:53:06.275 2909-2909/com.handsome.boke2 E/demo: 進入微信
11-21 13:53:06.921 2909-2909/com.handsome.boke2 E/demo: 點選紅包
11-21 13:53:07.883 2909-2909/com.handsome.boke2 E/demo: 開紅包
11-21 13:53:08.732 2909-2909/com.handsome.boke2 E/demo: 退出紅包

你可能會想到做一些竊取資訊的軟體,比如獲取QQ密碼、支付寶密碼等等,凡是EditText中設定inputType為password型別的,都無法獲取其輸入值

宣告

本來是不想修改文章的,不過挺多人遇到這個問題問我,我不得不解釋一下

1、這個程式存在一個bug:開紅包後退出紅包介面,還是會一直迴圈進入這個紅包,你只要一直按退出就可以了
2、解決方法:對每個紅包加個標記flag,在點選開紅包之前判斷一下其flag即可,交給你們自己去解決哈

原始碼下載