1. 程式人生 > >AccessibilityService2016終極解決方案包括(微信搶紅包外掛原理解析和開發實現)

AccessibilityService2016終極解決方案包括(微信搶紅包外掛原理解析和開發實現)

一、前言


自從去年中微信新增搶紅包的功能,微信的電商之旅算是正式開始正式火爆起來。但是作為Android開發者來說,我們在搶紅包的同時意識到了很多問題,就是手動去搶紅包的速度慢了,當然這些有很多原因導致了。或許是網路的原因,而且這個也是最大的原因。但是其他的不可忽略的因素也是要考慮到進去的,比如在手機充電鎖屏的時候,我們並不知道有人已經開始發紅包了,那麼這時候也是讓我們喪失了一大批紅包的原因。那麼關於網路的問題,我們開發者可能用相關技術無法解決(當然在Google和Facebook看來的話,他們的理想是能夠在任何地方都能連線網際網路,當然在偏遠的農村也是,不過我們期待他們有一天能夠普及開來。到時候才是真正的網際網路)。扯得有點遠了。我們迴歸到正題,今天我們來看看使用技術來解決其他非網路問題。在充電鎖屏的時候也可以自動幫我們搶紅包。而且你要知道,機器搶紅包的準確率是100%的,這個也許就是人和機器的區別。那麼保證搶得準確率是100%的話,那就依賴於我們高效準確的演算法實現了。下面就來看看原理實現。 


當去年我看到搶紅包那麼火爆的時候,當時作為一個開發者心裡是多麼渴望開發一個外掛出來,可是當時我們能想到的就是使用: 
adb shell monkey
 
命令去模擬點選螢幕,但是那種方式有一個問題就是是無頭緒的盲目點選,所以幾乎會出現誤點,點選成功率極其低下。所以當時就沒有想到其他方法了,因為最近做了有關輔助功能相關的工作的時候,那麼就發現這個功能可以用於搶紅包。 


其實現在我們可以去各大市場搜尋一下看到,有很多搶紅包的外掛了。當然我們並不是用於商業化,這裡只是來解析一下原理。我們會發現那些外掛都有一個共同的特點是:第一步都是引導使用者去開啟輔助功能。 


二、原理解析


關於輔助功能(AccessibilityService),如果又不瞭解的同學可以去Google一下,這個功能其實很有用的,但是他的出現的出發點是給那些肢體上有障礙的人使用的,比如手指不健全的使用者,怎麼才能滑動螢幕,然後開啟一個應用呢?那麼輔助功能就是幹這些事,他的功能其實就是可以概括兩句話: 
第一、尋找到我們想要的View節點 
第二、然後模擬點選,實現特定功能 
我們知道Android中的View體系是一個樹形結構,那麼每一個View就是一個節點。所以我們可以查詢到指定的節點,那麼我們該如何查詢到我們想要的節點呢?這裡我們先看一下輔助功能(AccessibilityService)的用法

第一步、我們需要整合AccessibilityService類


我們需要自定一個Service然後繼承AccessibilityService,當然還需要在AndroidManifest.xml中宣告這個服務: 




第二步、宣告許可權和配置


這個服務需要註明一個許可權:
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"

當然還要一個meta-data的宣告,這個宣告是對這個AccessibilityService的配置。我們看一下配置檔案內容:
<?xml version="1.0" encoding="utf-8"?>
<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/desc"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm" />

這裡我們看到有很多選項,我們看一下常用的幾個屬性: 
1、android:accessibilityEventTypes="typeAllMask"
看屬性名也差不多可以明白,這個是用來設定響應事件的型別,typeAllMask當然就是響應所有型別的事件了。當然還有單擊、長按、滑動等。 

2、android:accessibilityFeedbackType="feedbackSpoken"
設定回饋給使用者的方式,有語音播出和振動。可以配置一些TTS引擎,讓它實現發音。 

3、android:notificationTimeout="100"
響應時間的設定就不用多說了 


4、android:packageNames="com.example.android.apis"
可以指定響應某個應用的事件,這裡因為要響應所有應用的事件,所以不填,預設就是響應所有應用的事件。比如我們寫一個微信搶紅包的輔助程式,就可以在這裡填寫微信的包名,便可以監聽微信產生的事件了。 


注意: 
1、我們這些配置資訊除了在xml中定義,同樣也可以在程式碼中定義,我們一般都是在onServiceConnected()方法裡進行
 

@Override
protected void onServiceConnected() {
	AccessibilityServiceInfo info = getServiceInfo();
	info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
	info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;  
	info.notificationTimeout = 100;  
	setServiceInfo(info); 
	info.packageNames = new String[]{"xxx.xxx.xxx", "yyy.yyy.yyy","...."};
	setServiceInfo(info);
	super.onServiceConnected();
}

2、這裡我們一般都會在這裡寫上我們需要監聽的應用的包名,但是有時候我們需要監聽多個應用,那麼這時候我們該怎麼辦呢? 
這時候我們可以這麼做: 
第一種:我們在程式碼中註冊多個應用的包名,從而可以監聽多個應用 
@Override
protected void onServiceConnected() {
	AccessibilityServiceInfo info = getServiceInfo();
	//這裡可以設定多個包名,監聽多個應用
	info.packageNames = new String[]{"xxx.xxx.xxx", "yyy.yyy.yyy","...."};
	setServiceInfo(info);
	super.onServiceConnected();
}

第二種:我們在onAccessibilityEvent事件監聽的方法中做包名的過濾(這種方式最常用) 
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
	String pkgName = event.getPackageName().toString();
	if("xxx.xxx.xxx".equals(pkgName)){

	}else if("yyy.yyy.yyy".equals(pkgName)){

	}else if("....".equals(pkgName)){

	}
}



第三步、在onAccessibilityEvent方法中監聽指定的事件


比如我們需要監聽有通知欄訊息的事件: 

@Override
	public void onAccessibilityEvent(AccessibilityEvent event) {
		int eventType = event.getEventType();
		switch (eventType) {
		case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
			//.......
		}
}
這個事件型別很多的,我們可以檢視AccessibilityEvent類的原始碼:
@Deprecated
public static final int MAX_TEXT_LENGTH = 500;

/**
 * Represents the event of clicking on a {@link android.view.View} like
 * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.
 */
public static final int TYPE_VIEW_CLICKED = 0x00000001;

/**
 * Represents the event of long clicking on a {@link android.view.View} like
 * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.
 */
public static final int TYPE_VIEW_LONG_CLICKED = 0x00000002;

/**
 * Represents the event of selecting an item usually in the context of an
 * {@link android.widget.AdapterView}.
 */
public static final int TYPE_VIEW_SELECTED = 0x00000004;

/**
 * Represents the event of setting input focus of a {@link android.view.View}.
 */
public static final int TYPE_VIEW_FOCUSED = 0x00000008;

/**
 * Represents the event of changing the text of an {@link android.widget.EditText}.
 */
public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010;

/**
 * Represents the event of opening a {@link android.widget.PopupWindow},
 * {@link android.view.Menu}, {@link android.app.Dialog}, etc.
 */
public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020;

/**
 * Represents the event showing a {@link android.app.Notification}.
 */
public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040;

/**
 * Represents the event of a hover enter over a {@link android.view.View}.
 */
public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080;

/**
 * Represents the event of a hover exit over a {@link android.view.View}.
 */
public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100;

/**
 * Represents the event of starting a touch exploration gesture.
 */
public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200;

/**
 * Represents the event of ending a touch exploration gesture.
 */
public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400;

/**
 * Represents the event of changing the content of a window and more
 * specifically the sub-tree rooted at the event's source.
 */
public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800;

/**
 * Represents the event of scrolling a view.
 */
public static final int TYPE_VIEW_SCROLLED = 0x00001000;

/**
 * Represents the event of changing the selection in an {@link android.widget.EditText}.
 */
public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000;

/**
 * Represents the event of an application making an announcement.
 */
public static final int TYPE_ANNOUNCEMENT = 0x00004000;

/**
 * Represents the event of gaining accessibility focus.
 */
public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000;

/**
 * Represents the event of clearing accessibility focus.
 */
public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000;

/**
 * Represents the event of traversing the text of a view at a given movement granularity.
 */
public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000;

/**
 * Represents the event of beginning gesture detection.
 */
public static final int TYPE_GESTURE_DETECTION_START = 0x00040000;

/**
 * Represents the event of ending gesture detection.
 */
public static final int TYPE_GESTURE_DETECTION_END = 0x00080000;

/**
 * Represents the event of the user starting to touch the screen.
 */
public static final int TYPE_TOUCH_INTERACTION_START = 0x00100000;

/**
 * Represents the event of the user ending to touch the screen.
 */
public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000;

/**
 * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
 * The type of change is not defined.
 */
public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000;

/**
 * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
 * A node in the subtree rooted at the source node was added or removed.
 */
public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001;

/**
 * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
 * The node's text changed.
 */
public static final int CONTENT_CHANGE_TYPE_TEXT = 0x00000002;

/**
 * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
 * The node's content description changed.
 */
public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004;

這裡有很多事件,這些事件我們通過名字就可以看出來有很多我們可能都知道,比如當視窗發生變化的時候,當某個View被點選了,被滾動了等訊息都是可以知道的。那麼我們有了這些事件我們就可以做我們的事情了,因為我們知道事件觸發了。 


第四步、查詢到我們想要處理的節點View


這裡系統提供了兩個方法讓我們來進行查詢想要的節點View 
第一種是通過節點View的Text內容來查詢 
findAccessibilityNodeInfosByText("查詢內容") 
這種方式查詢,就是像TextView,Button等View有文字內容的,可以使用這種方式快速的找到。 
第二種是通過節點View在xml佈局中的id名稱 
findAccessibilityNodeInfosByViewId("@id/xxx")

這個一般很難知道,但是我們在查詢系統控制元件的時候還是可以做的,因為系統的控制元件的id是可以知道的,而且是統一的。
(關於這兩個方法我們在寫網頁爬蟲程式的時候可能知道,在html中通過tag/name/id等資訊可以找到一個節點,原理都類似) 


第五步、模擬點選指定事件


我們找到我們想要的View節點,呼叫方法模擬事件: 
performAction(AccessibilityNodeInfo.ACTION_CLICK) 
呼叫這個方法即可,當然這裡的引數就是指定事件的名稱,這個和AccessibilityEvent中監聽的那些事件是一一對應的,這裡是模擬點選事件,我們當然可以模擬View的滾動事件,長按事件等。 


三、實戰案例:微信搶紅包外掛


上面我們就介紹了一個輔助功能開發的具體步驟,那麼下面就通過一個簡單的例子,來實戰一下 
例子:微信自動搶紅包外掛 


首先我們來看一下微信搶紅包的流程: 
第一步、我們在通知欄會接收到一個微信紅包的訊息 
我們監聽通知欄事件: 
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED 
然後檢視通知欄的訊息中是否有:[微信紅包]  的文字內容 
是的話,就走進入第二步 


第二步、我們模擬開啟通知欄 
開啟微信如下圖: 


我們查詢包含有:領取紅包 的文字內容的節點View,然後模擬點選,進入第三步: 


第三步、我們點選領取紅包 
如下圖: 
這裡我們在查詢包含有:拆紅包 的文字內容的節點View,然後模擬點選 




下面我們來看一下程式碼中的具體實現: 

package krelve.demo.rob;

import java.util.List;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

public class RobMoney extends AccessibilityService {

	@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();
					Log.i("demo", "text:"+content);
					if (content.contains("[微信紅包]")) {
						//模擬開啟通知欄訊息
						if (event.getParcelableData() != null
								&& 
							event.getParcelableData() instanceof Notification) {
							Notification notification = (Notification) event.getParcelableData();
							PendingIntent pendingIntent = notification.contentIntent;
							try {
								pendingIntent.send();
							} catch (CanceledException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
			break;
		//第二步:監聽是否進入微信紅包訊息介面
		case AccessibilityEvent.TYPE_WINDOW_STATE_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();
			}
			break;
		}
	}

	/**
	 * 查詢到
	 */
	@SuppressLint("NewApi")
	private void openPacket() {
		AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
		if (nodeInfo != null) {
			List<AccessibilityNodeInfo> list = nodeInfo
					.findAccessibilityNodeInfosByText("搶紅包");
			for (AccessibilityNodeInfo n : list) {
				n.performAction(AccessibilityNodeInfo.ACTION_CLICK);
			}
		}

	}

	@SuppressLint("NewApi")
	private void getPacket() {
		AccessibilityNodeInfo rootNode = getRootInActiveWindow();
		recycle(rootNode);
	}
	
	/**
	 * 列印一個節點的結構
	 * @param info
	 */
	@SuppressLint("NewApi")
	public void recycle(AccessibilityNodeInfo info) {  
        if (info.getChildCount() == 0) { 
        	if(info.getText() != null){
        		if("領取紅包".equals(info.getText().toString())){
        			//這裡有一個問題需要注意,就是需要找到一個可以點選的View
                	Log.i("demo", "Click"+",isClick:"+info.isClickable());
                	info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                	AccessibilityNodeInfo parent = info.getParent();
                	while(parent != null){
                		Log.i("demo", "parent isClick:"+parent.isClickable());
                		if(parent.isClickable()){
                			parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                			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() {
	}

	
}
程式碼沒什麼好說的了,按照我們之前說的三個步驟來就可以了,但是這裡需要注意點細節上的問題: 
1、我們在監聽到通知欄的訊息的時候,呼叫如下程式碼來進行通知欄的訊息點選 

if (content.contains("[微信紅包]")) {
	//模擬開啟通知欄訊息
	if (event.getParcelableData() != null
			&& 
			event.getParcelableData() instanceof Notification) {
		Notification notification = (Notification) event.getParcelableData();
		PendingIntent pendingIntent = notification.contentIntent;
		try {
			pendingIntent.send();
		} catch (CanceledException e) {
			e.printStackTrace();
		}
	}
}

2、我們在模擬點選通知欄訊息之後,還是需要監聽:AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED 這個事件,這個事件我們以後會經常用到,這個事件就是在視窗發生改變的時候發出來的事件,很常用的,比如我們可以通過這個事件來監聽TopActivity,然後得到包名,這也是一個實現應用鎖的一個原理。 


3、我們在查詢領取紅包的時候,模擬點選的時候做了一個工作,就是從“領取紅包”文字的控制元件View網上查詢,查詢到一個可以點選的View出來,然後模擬點選
 

if(info.getText() != null){
	if("領取紅包".equals(info.getText().toString())){
		//這裡有一個問題需要注意,就是需要找到一個可以點選的View
		Log.i("demo", "Click"+",isClick:"+info.isClickable());
		info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
		AccessibilityNodeInfo parent = info.getParent();
		while(parent != null){
			Log.i("demo", "parent isClick:"+parent.isClickable());
			if(parent.isClickable()){
				parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
				break;
			}
			parent = parent.getParent();
		}

	}
}

這裡為什麼這麼做,其實原理很簡單,因為我們不知道微信他的介面佈局,也不知道他對哪個View進行了setOnClickListener。我們可以寫一個例子,performAction方法只對呼叫了setOnClickListener方法的View模擬點選才有效,其實看View的原始碼也是可以看出來的.這裡就不多解釋了。所以我們就需要得到一個View節點之後,從下往上找,直到找到一個可以click的View為止。 


專案下載:http://download.csdn.net/detail/jiangwei0910410003/9156347 


四、延展


關於微信搶紅包的原理解析上面已經做了分析了,但是要想做到極致,這裡還有很多問題的,比如我們還需要過濾一些已經領取過的紅包,這樣的話效率也是很高的。這個都是演算法精確的問題了,我想在這裡說的是,我們不僅可以用輔助功能來實現搶紅包,還可以實現很多功能,比如 
1、靜默安裝 
關於靜默安裝的實現,之前的做法是,參見這篇文章: 
http://blog.csdn.net/jiangwei0910410003/article/details/36427963 
在這篇文章中我介紹了很多方法來實現靜默安裝,但是都是有一個限制,那就是root,或者是獲取到systemId。但是對於這兩個要求,我們或許很難得到,那麼現在如果有了輔助功能,我們就好做了: 


我們可以監聽系統的這個安裝介面,然後得到安裝節點View,然後模擬點選即可,解除安裝也是同樣的原理 


2、強制停止應用 
我們知道Android中停止應用有很多方法,kill程序,stopService,但是這些方法,有一些應用它們都是有對策的,那麼我們之前用到的強制停止的方法是獲取root許可權呼叫系統的forceStop的api來停止,但是前提還是有root。那麼現在如果我們有了輔助功能的話,我們可以這麼做: 


我們可以監聽系統的應用詳情頁面,然後找到:結束執行的節點View,然後模擬點選即可 


當然上面我就說了兩個簡單的例子,還有很多輔助功能都是可以做的。他的好處就是不需要root許可權。但是他也是需要使用者授權的: 


如果使用者沒有授權的話,那麼所有的工作都沒辦法開始了,所以說這個方法也不是萬能的。當然說句題外話:有了輔助功能的話,他的危險性比root之後的危險性更大,比如我們上面的搶紅包外掛,其實我們稍作修改,就可以獲取微信通訊錄資訊,微信支付的密碼。這些事都是可以做的,所以說,我們在作為使用者的時候,進行授權的時候還是需要三思而後行。 


五、總結


關於輔助功能,之前沒有太多的接觸,是在一次工作中用到了這個功能,就去學習了一下,作為自己的興趣,就延展了學習瞭如何寫一個微信搶紅包的外掛,同時可以考慮了使用輔助功能能夠做我們之前需要root做的事情。當然輔助功能是google對於肢體上有障礙的人開發出來的一個功能,我們開發者或許使用這個功能,可以做一下產品的拓展功能,當然這些是google沒有想到的事情,但是這個至少是我們開發者在以後的開發道路上的一個解決問題的一個辦法和途徑,謹記此功能! 

-=-=-=------------------------=-=-=

public class QQHongbaoService extends AccessibilityService {
 private static final String WECHAT_OPEN_EN = "Open";  
 private static final String WECHAT_OPENED_EN = "You've opened";  
 private final static String QQ_DEFAULT_CLICK_OPEN = "點選拆開"; 
 private final static String QQ_HONG_BAO_PASSWORD = "口令紅包";  
 private final st