編輯:關於Android編程
微信紅包自打出世以來就極其受歡迎,搶紅包插件可謂紅極一時.今天,我們重新談談搶紅包插件的哪些事兒.本質上,搶紅包插件的原理不難理解,其過程就是在收到紅包時,自動模擬點擊.做過自動化UI測試的童鞋應該非常熟悉了.
那麼問題來了,我們怎麼知道有沒有紅包,又怎麼模擬點擊操作呢?在PC端我們有按鍵精靈,那麼在Android設備上呢?話說也偶然,Google為了讓Android系統更實用,為用戶提供了無障礙輔助服務—AccessibilityService.
AccessibilityService運行在後台,並且能夠收到由系統發出的一些事件(AccessibilityEvent),比如焦點改變,輸入內容變化,按鈕被點擊了等等,換言之,界面中產生的任何變化都會產生一個時間,並由系統通知給AccessibilityService.這就像監視器監視著界面的一舉一動,一旦界面發生變化,立刻發出警報.另外,該服務不受系統重啟的的影響.除非你手動關閉或者通過第三方安全軟件將其強制關閉.
現在讓我們來看看如何AccessibilityService的基本使用.
和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()中可以捕獲到這些事件,進而根據不同的事件進行處理.該方法是整個服務的核心方法.
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即可.
當我們做完以上操作,便可將app安裝到手機.安裝成功後,在設置->輔助功能中便可以找到我們的服務.該服務默認處在關閉狀態,需要手動開啟.
上面我們說道,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;
}
}
僅僅知道事件的信息是不夠的,我們還希望通過事件來獲取發出該事件(事件源)的信息,比如Button按鈕被點擊時它的text.
正如上面所提到的,要想獲取控件的相關信息,在配置AccessibilityService時設置其canRetrieveWindowContent屬性.之後,我們便可以通過event.getSource()來獲取事件源.同樣,我們也可以通過getRootInActiveWindow()來獲取當前活動窗口的根節點.
那麼什麼是活動窗口呢?
所謂的活動窗口是用戶當前觸摸的窗口,或者在沒有觸摸狀態下帶有輸入焦點的窗口.
簡單了介紹了一些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="\" />
搶紅包界面:
講完了微信紅包插件的實現原理,不難發現其本質是根據相關的界面狀態,模擬後續的操作(比如點擊等).
既然這樣,那麼我們完全可以利用該服務實現更多的功能,比如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測試等.
Toast大家都很熟,不多說。直接上圖上代碼。 具體代碼如下:main.xm
WebView組件本身就是一個浏覽器實現,Android5.0增強的WebView基於Chromium M37,直接支持WebRTC、WebAudio、WebGL。開發者
本文實例講述了Android控件之ProgressBar用法。分享給大家供大家參考。具體如下:ProgressBar位於android.widget包下,其繼承於View
Android canvas drawBitmap方法詳解及實例之前自己在自定義view,用到canvas.drawBitmap(Bitmap, SrcRec