Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 從搶紅包插件談AccessibilityService

從搶紅包插件談AccessibilityService

編輯:關於Android編程

微信紅包自打出世以來就極其受歡迎,搶紅包插件可謂紅極一時.今天,我們重新談談搶紅包插件的哪些事兒.本質上,搶紅包插件的原理不難理解,其過程就是在收到紅包時,自動模擬點擊.做過自動化UI測試的童鞋應該非常熟悉了.

那麼問題來了,我們怎麼知道有沒有紅包,又怎麼模擬點擊操作呢?在PC端我們有按鍵精靈,那麼在Android設備上呢?話說也偶然,Google為了讓Android系統更實用,為用戶提供了無障礙輔助服務—AccessibilityService.

AccessibilityService運行在後台,並且能夠收到由系統發出的一些事件(AccessibilityEvent),比如焦點改變,輸入內容變化,按鈕被點擊了等等,換言之,界面中產生的任何變化都會產生一個時間,並由系統通知給AccessibilityService.這就像監視器監視著界面的一舉一動,一旦界面發生變化,立刻發出警報.另外,該服務不受系統重啟的的影響.除非你手動關閉或者通過第三方安全軟件將其強制關閉.

現在讓我們來看看如何AccessibilityService的基本使用.


基礎使用

1. 創建服務類

和Service類似,要使用該服務,首先創建繼承子AccessibilityService子類:

public class RobService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        //handle 
    }

    @Override
    public void onInterrupt() {

    }

    @Override
    protected void onServiceConnected() {

    }

簡單的介紹這兩個常用的方法:
onServiceConnected():服務啟動成功後執行該方法,通常我們會在這裡做一些初始化的操作.除此之外,也可以在該方法中為AccessibilityServiceInfo設置AccessibilityService參數

onAccessibilityEvent():系統通過sendAccessibiliyEvent()不斷的發送AccessibilityEvent,onAccessibilityEvent()中可以捕獲到這些事件,進而根據不同的事件進行處理.該方法是整個服務的核心方法.

2. 配置服務類

AccessibilityService是一個特殊的Service服務,同樣需要在manifest.xml中聲明該Service.另外在版本4.1之後,該Service需要BIND_ACCESSIBILITY_SERVICE權限,因此其基本配置如下:

 
            
                
            
        

和普通的Service不同,該服務不能通過普通的startService()方法啟動,而是由系統自行識別.為了讓系統能夠該服務,還需要為該服務配置響應的參數信息.在4.0系統之前,我們通過setServiceInfo()方法在運行時為其設置配置信息,而在4.0之後,可以在manifest.xml中通過來為其配置信息.

先來看如何通過setServiceInfo()為其配置信息,這就用到了上面onServiceConnected()方法:

 @Override
    protected void onServiceConnected() {
        AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
        serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
        serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
        serviceInfo.packageNames = new String[]{"com.tencent.mm"}; 
        serviceInfo.notificationTimeout=100;
        setServiceInfo(serviceInfo);
    }

再來看看如何通過來配置:
首先,我們在上面標簽中添加元素,然後通過android:resource指定相應的配置文件(在res目錄下創建xml文件,並在其中創建配置文件accessibility.xml):

 
            
                
            

            
        

接下來我們來看accessibility.xml的相關配置:


為了方便理解,我們快速的對其中的一些重要屬性進行說明:
accessibilityEventTypes:表示該服務對界面中的哪些變化感興趣,即哪些事件通知,比如滑動,焦點變化,長按等.具體的值可以在AccessibilityEvent類中查到,如typeAllMask表示接受所有的事件通知.
accessibilityFeedbackType:表示反饋方式,比如是語音播放,還是震動
canRetrieveWindowContent:表示該服務能否訪問活動窗口中的內容.也就是如果你希望在服務中獲取窗體內容的化,則需要設置其值為true.
notificationTimeout:接受事件的時間間隔,通常將其設置為100即可.
packageNames:表示對該服務是用來監聽哪個包的產生的事件

多數情況下,我們使用配置文件的方式來配置AccessibilityService即可.

3. 啟動服務

當我們做完以上操作,便可將app安裝到手機.安裝成功後,在設置->輔助功能中便可以找到我們的服務.該服務默認處在關閉狀態,需要手動開啟.

4. 獲取事件信息

上面我們說道,onAccessibilityEvent(AccessibilityEvent event)是該服務的核心方法,其中參數event封裝來自界面相關事件的信息,比如我們可以獲得該事件的事件類型,進而根據起類型選擇不同的處理方式:

 public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            case AccessibilityEvent.TYPE_VIEW_CLICKED:
                //界面點擊
                break;
            case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
                //界面文字改動
                break;
        }
    }

5. 獲取控件信息

僅僅知道事件的信息是不夠的,我們還希望通過事件來獲取發出該事件(事件源)的信息,比如Button按鈕被點擊時它的text.

正如上面所提到的,要想獲取控件的相關信息,在配置AccessibilityService時設置其canRetrieveWindowContent屬性.之後,我們便可以通過event.getSource()來獲取事件源.同樣,我們也可以通過getRootInActiveWindow()來獲取當前活動窗口的根節點.

那麼什麼是活動窗口呢?
所謂的活動窗口是用戶當前觸摸的窗口,或者在沒有觸摸狀態下帶有輸入焦點的窗口.

6. 檢測服務是否開啟

簡單了介紹了一些AccessibilityService的基礎知識,想必到現在已經了解了如何去用了.但是這裡我還是要補充一點關於檢測某個服務是否開啟的知識,通常來說大體有一下兩種方法來檢測:
方法一:借助服務管理器AccessibilityManager來判斷,但是該方法不能檢測app本身開啟的服務.

private boolean enabled(String name) {
        AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
        List serviceInfos = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
        List installedAccessibilityServiceList = am.getInstalledAccessibilityServiceList();
        for (AccessibilityServiceInfo info : installedAccessibilityServiceList) {
            Log.d("MainActivity", "all -->" + info.getId());
            if (name.equals(info.getId())) {
                return true;
            }
        }
        return false;
    }

方法二:我們知道大部分的系統屬性都在settings中進行設置,比如wifi,藍牙狀態等,而這些信息主要是存儲在settings對應的的數據庫中(system表和serure表),同樣我們也可以通過直接讀取setting設置來判斷相關服務是否開啟:

private boolean checkStealFeature1(String service) {
        int ok = 0;
        try {
            ok = Settings.Secure.getInt(getApplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED);
            Log.d("MainActivity", "accessibilityEnabled :" + ok);
        } catch (Settings.SettingNotFoundException e) {
            Log.d("MainActivity", "Error finding setting,default accessibility to not found:" + e.getMessage());
        }

        TextUtils.SimpleStringSplitter ms = new TextUtils.SimpleStringSplitter(':');
        if (ok == 1) {
            String settingValue = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                ms.setString(settingValue);
                while (ms.hasNext()) {
                    String accessibilityService = ms.next();
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        return true;
                    }

                }
            }

到現在有關AccessibilityService的一些知識,我們已經講完,下面我們就看它的具體使用,其中典型的應用就是搶紅包插件.


實戰:紅包插件

先回顧一下搶紅包的的流程:
1. 狀態欄出現”[微信紅包]”的消息提示,點擊進入聊天界面
2. 點擊相應的紅包信息,彈出搶紅包界面
3. 在搶紅包界面點擊”開”,打開紅包
4. 在紅包詳情頁面,查看詳情,點擊返回按鈕返回微信聊天界面.

以上是不在微信聊天界面時的流程.如果你所在的微信聊天窗口出現紅包,則不會執行步驟1,而是直接執行2,3,4.如果是在微信好友列表時,收到紅包,則會在列表項中顯示[微信紅包],需要點即該列表項,進入聊天界面,隨後執行2,3,4.為了方便演示,這裡我們暫時不考慮好友列表時出現紅包的情況.

明白了搶紅包流程,之後我們通過AccessibilityService獲取通知欄信息及微信聊天窗口界面,繼而通過模擬點擊實現打開紅包,搶紅包等操作.
AccessibilityService配置如下:


具體實現代碼如下:

public class RobService extends AccessibilityService {


    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
                handleNotification(event);
                break;
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
                String className = event.getClassName().toString();
                if (className.equals("com.tencent.mm.ui.LauncherUI")) {
                    getPacket();
                } else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI")) {
                    openPacket();
                } else if (className.equals("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI")) {
                    close();
                }

                break;
        }
    }

    /**
     * 處理通知欄信息
     *
     * 如果是微信紅包的提示信息,則模擬點擊
     *
     * @param event
     */
    private void handleNotification(AccessibilityEvent event) {
        List 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();
                        } catch (PendingIntent.CanceledException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    /**
     * 關閉紅包詳情界面,實現自動返回聊天窗口
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void close() {
        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        if (nodeInfo != null) {
            //為了演示,直接查看了關閉按鈕的id
            List infos = nodeInfo.findAccessibilityNodeInfosByViewId("@id/ez");
            nodeInfo.recycle();
            for (AccessibilityNodeInfo item : infos) {
                item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
        }
    }

    /**
     * 模擬點擊,拆開紅包
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void openPacket() {
        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        if (nodeInfo != null) {
            //為了演示,直接查看了紅包控件的id
            List list = nodeInfo.findAccessibilityNodeInfosByViewId("@id/b9m");
            nodeInfo.recycle();
            for (AccessibilityNodeInfo item : list) {
                item.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
        }
    }

    /**
     * 模擬點擊,打開搶紅包界面
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void getPacket() {
        AccessibilityNodeInfo rootNode = getRootInActiveWindow();
        AccessibilityNodeInfo node = recycle(rootNode);

        node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
        AccessibilityNodeInfo parent = node.getParent();
        while (parent != null) {
            if (parent.isClickable()) {
                parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                break;
            }
            parent = parent.getParent();
        }

    }

    /**
     * 遞歸查找當前聊天窗口中的紅包信息
     *
     * 聊天窗口中的紅包都存在"領取紅包"一詞,因此可根據該詞查找紅包
     * 
     * @param node
     */
    public AccessibilityNodeInfo recycle(AccessibilityNodeInfo node) {
        if (node.getChildCount() == 0) {
            if (node.getText() != null) {
                if ("領取紅包".equals(node.getText().toString())) {
                    return node;
                }
            }
        } else {
            for (int i = 0; i < node.getChildCount(); i++) {
                if (node.getChild(i) != null) {
                    recycle(node.getChild(i));
                }
            }
        }
        return node;
    }

    @Override
    public void onInterrupt() {

    }

    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
    }


}

上面的代碼簡單演示了搶紅包的原理,為了方便起見,我直接通過findAccessibilityNodeInfosByViewId()獲取制定id控件的信息.在實際中,這種方法不太可靠,到目前為止,微信已經改過幾次相關控件的id了.

有童鞋問,怎麼樣知道該控件的id呢.其實很簡單,android中已經為我們提供了相關的工具:在Android Studio中開啟Android Device Monitor,選擇設備後點擊Dump View Hierarchy for UI Automator,如下:
這裡寫圖片描述

稍等片刻之後,便會出現當前設備的窗口,在該窗口中點擊相關控件,便會顯示該控件的屬性.借助該工具,可以幫我們快速的分析界面結構,幫助我們從其他app布局策略中學習.
這裡寫圖片描述

我們用Dump View Hierarchy fZ喎?/kf/ware/vc/" target="_blank" class="keylink">vciBVSSBBdXRvbWF0b3K31s72wcTM7L3nw+bOotDFuuyw/NDFz6I6PGJyIC8+DQo8aW1nIGFsdD0="這裡寫圖片描述" src="/uploadfile/Collfiles/20160701/20160701091834526.jpg" title="\" />

搶紅包界面:
這裡寫圖片描述


實戰:App自動安裝

講完了微信紅包插件的實現原理,不難發現其本質是根據相關的界面狀態,模擬後續的操作(比如點擊等).
既然這樣,那麼我們完全可以利用該服務實現更多的功能,比如apk自動安裝,傳統的安裝過程大概是如下流程:
點擊apk文件,彈出安裝信息界面,在該界面點擊”下一步”,然後在點擊”安裝”,最後在安裝完成界面點擊”完成”.該流程完全可以通過模擬點擊操作完成.現在我們簡單的講一下AccessibilityService在這方面的具體應用.

我們知道系統的安裝程序PackageInstaller的報名是,其package name 是com.android.packageinstaller,那麼我們只需要監聽該package下的安裝信息界面和安裝完成界面,並模擬點擊”下一步”,”安裝”,完成”“操作即可.

AccessibilityService配置如下:



具體實現代碼如下:

public class InstallService extends AccessibilityService {
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        Log.d("InstallService", event.toString());
        checkInstall(event);
    }


    private void checkInstall(AccessibilityEvent event) {
        AccessibilityNodeInfo source = event.getSource();
        if (source != null) {
            boolean installPage = event.getPackageName().equals("com.android.packageinstaller");
            if (installPage) {
                installAPK(event);
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void installAPK(AccessibilityEvent event) {
        AccessibilityNodeInfo source = getRootInActiveWindow();
        List nextInfos = source.findAccessibilityNodeInfosByText("下一步");
        nextClick(nextInfos);
        List installInfos = source.findAccessibilityNodeInfosByText("安裝");
        nextClick(installInfos);
        List openInfos = source.findAccessibilityNodeInfosByText("打開");
        nextClick(openInfos);

        runInBack(event);

    }

    private void runInBack(AccessibilityEvent event) {
        event.getSource().performAction(AccessibilityService.GLOBAL_ACTION_BACK);
    }

    private void nextClick(List infos) {
        if (infos != null)
            for (AccessibilityNodeInfo info : infos) {
                if (info.isEnabled() && info.isClickable())
                    info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private boolean checkTilte(AccessibilityNodeInfo source) {
        List infos = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("@id/app_name");
        for (AccessibilityNodeInfo nodeInfo : infos) {
            if (nodeInfo.getClassName().equals("android.widget.TextView")) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void onInterrupt() {

    }

    @Override
    protected void onServiceConnected() {
        Log.d("InstallService", "auto install apk");
    }
}



實戰:檢測前台服務:

在很多情況下,我們需要檢測自己的app是不是處在前台,借助該服務同樣也能夠完成該檢測操作.
下面,我們就演示一下如何實現:

AccessibilityService配置如下:


具體實現代碼如下:

public class DetectionService extends AccessibilityService {
    private static volatile String foregroundPackageName = "error";

    /**
     * 檢測是否是前台服務
     *
     * @param packagenName
     * @return
     */
    public static boolean isForeground(String packagenName) {
        return foregroundPackageName.equals(packagenName);
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            foregroundPackageName = event.getPackageName().toString();
        }
    }

    @Override
    public void onInterrupt() {

    }
}

在使用時,需要引導用於開啟該服務,然後通過調用DetectionService.isForeground()即可.

竊取信息

上面的所有示例演示的都是AccessibilityService在具體應用中發揮的良好作用.但是該服務也存在一定的風險,很多人利用該服務做一些”壞事”,比如竊取短信驗證碼,竊取短信內容,想要看看自己女朋友最近在和誰聊QQ等等.

你現在是不是想能否借助該服務直接獲取一些app的密碼呢?凡是EditText中設置inputType為password類型的,都無法獲取其輸入值.除此之外,大多數軟件都針對該中風險做了提前的防范.因此,你想要借助該服務來實現竊取密碼還是比較有難度的.

總結

暫時先到這裡,後面再補充其他的吧.其實該服務能做的事情遠不止這些,比如也可以通過該服務獲取微信公眾號的key,進而爬去文章閱讀數,也可借助該服務做自動化UI測試等.

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved