Android后臺模擬點(diǎn)擊探索(附源碼)
工作中我們需要自制一套工具,其中遇到需要模擬點(diǎn)擊事件的需求,類似按鍵精靈的功能,支持后臺持續(xù)運(yùn)行,滿足觸發(fā)條件時完成點(diǎn)擊。
經(jīng)過一番探索,一共整理出兩種不同的方案:AccessibilityService 和 adb shell命令,讀者可自行選擇合適的場景。
AccessibilityService
無障礙模式是我首先想到的方案,對于不知道Android無障礙模式的,可自行百度。這里簡單說明一下,AccessibilityService是Android為殘障人士提供的貼心功能,比如可以報出當(dāng)前頁面有哪些按鈕balabala。使用官方提供的一些列API,我們還可以完成一些自動運(yùn)行的“黑科技”操作,比如早些年的紅包插件、微信自動回復(fù)插件、自動點(diǎn)贊插件等。
本方案原理比較簡單:掃描當(dāng)前頁面的View樹,找到目標(biāo)控件,模擬點(diǎn)擊操作,下面詳細(xì)闡述。
添加配置文件
首先需要在res目錄下建立配置文件:accessible_service_config.xml ,名字隨意取。
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagReportViewIds" android:canRetrieveWindowContent="true" android:notificationTimeout="100" android:description="@string/description" android:packageNames="目標(biāo)包名"/>
accessibilityEventTypes:設(shè)置響應(yīng)事件的類型,這里設(shè)置typeAllMask,就是響應(yīng)全部類型的事件。
accessibilityFeedbackType:設(shè)置回饋給用戶的方式,有語音播出和振動,這里使用通用類型。
notificationTimeout:設(shè)置響應(yīng)時間。
packageNames:目標(biāo)包名,比如紅包插件就要設(shè)置微信包名,關(guān)于包名如何獲取,下文會提到。
繼承AccessibilityService編碼
接著我們繼承AccessibilityService新建AutoClickAccessibilityService,重寫onAccessibilityEvent(AccessibilityEvent event)。
public class AutoClickAccessibilityService extends AccessibilityService { private static final String TAG = "GK"; @Override public void onAccessibilityEvent(AccessibilityEvent event) { ztLog("===start==="); try { //拿到根節(jié)點(diǎn) AccessibilityNodeInfo rootInfo = getRootInActiveWindow(); if (rootInfo == null) { return; //開始遍歷,這里拎出來細(xì)講,直接往下看正文 if (rootInfo.getChildCount() != 0) { …… } } catch (Exception e) { ztLog("Exception:" + e.getMessage(), true); } }
拿到根節(jié)點(diǎn)以后,我們有兩種方式開始尋找目標(biāo)節(jié)點(diǎn):
- 根據(jù)View id:findAccessibilityNodeInfosByViewId
- 根據(jù)控件文案:findAccessibilityNodeInfosByText
這里我們拿魅族手機(jī)自帶的音樂App做例子,假如我們需要自動點(diǎn)擊下圖的 專欄 :
使用findAccessibilityNodeInfosByViewId尋找目標(biāo)
我們可以使用findAccessibilityNodeInfosByViewId(),通過id找到目標(biāo)節(jié)點(diǎn),關(guān)于View id,可以使用DDMS中的Dump View Hierarchy for UI Automator,就是點(diǎn)擊下圖按鈕(不知道如何打開eclipse或者AS的DDMS的同學(xué)可以自行百度):
稍等片刻,生成屏幕快照,并解析出View樹,從右下的屬性框就可以找到id,同時仔細(xì)看,包名也可以獲取到啦~
這里很有可能因?yàn)槟繕?biāo)apk混淆嚴(yán)重而讀不到id,比如是個?,那么可以嘗試第二個方法。
使用findAccessibilityNodeInfosByText尋找目標(biāo)
使用findAccessibilityNodeInfosByText("最熱MV"),顧名思義,就是根據(jù)文案找控件。
找到控件以后,就可以執(zhí)行點(diǎn)擊操作了,但是且慢,這里有個坑。
因?yàn)樽⒁饪催@里的view樹:
無論我們根據(jù)id還是文案,找到的可能只是一個TextView或者Button,但是根據(jù)我們?nèi)粘=?jīng)驗(yàn),我們肯定是給其父布局設(shè)置的點(diǎn)擊事件,也就是這里的LinearLayout或者FrameLayout。
所以我的方案是根據(jù)View樹的結(jié)構(gòu),自行遍歷。比如這里的View樹結(jié)構(gòu)如下:
我先做深度優(yōu)先遍歷找到GridView,然后遍歷它所有孩子直至找到專欄這個TextView,為什么我不直接DFS找到專欄呢?因?yàn)槲乙涗浰母腹?jié)點(diǎn)甚至爺爺節(jié)點(diǎn),方便接下來執(zhí)行點(diǎn)擊操作。
如果有同學(xué)使用這種方案,建議根據(jù)實(shí)際View樹的結(jié)構(gòu),自行遍歷尋找,我的代碼如下:
/** * 深度優(yōu)先遍歷尋找目標(biāo)節(jié)點(diǎn) */ private void DFS(AccessibilityNodeInfo rootInfo) { if (rootInfo == null || TextUtils.isEmpty(rootInfo.getClassName())) { return; } if (!"android.widget.GridView".equals(rootInfo.getClassName())) { ztLog(rootInfo.getClassName().toString()); for (int i = 0; i < rootInfo.getChildCount(); i++) { DFS(rootInfo.getChild(i)); } } else { ztLog("==find gridView=="); final AccessibilityNodeInfo GridViewInfo = rootInfo; for (int i = 0; i < GridViewInfo.getChildCount(); i++) { final AccessibilityNodeInfo frameLayoutInfo = GridViewInfo.getChild(i); //細(xì)心的同學(xué)會發(fā)現(xiàn),我代碼里的遍歷的邏輯跟View樹里顯示的結(jié)構(gòu)不一樣, //快照顯示的FrameLayout下明明該是LinearLayout,我這里卻是TextView, //這個我也不知道,實(shí)際調(diào)試出來的就是這樣……所以大家實(shí)操過程中也要注意了 final AccessibilityNodeInfo childInfo = frameLayoutInfo.getChild(0); String text = childInfo.getText().toString(); if (text.equals("專欄")) { performClick(frameLayoutInfo); } else { ztLog(text); } } } } private void performClick(AccessibilityNodeInfo targetInfo) { targetInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK); }
AndroidManifest文件添加Service配置
AccessibilityService也是一個Servcie,所以要在AndroidManifest配置一下。
<service android:name=".AutoClickService" android:exported="false" <!-- label就是在手機(jī)設(shè)置中的無障礙里,顯示的標(biāo)簽 --> android:label="自動點(diǎn)擊Demo" <!-- 注意這里的android:permission是在service結(jié)構(gòu)里面的??! --> android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" > <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <!-- 配置服務(wù)服務(wù)配置文件路徑 --> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessible_service_config" /> </service>
至此無障礙模式方案就講完了,運(yùn)行之后,需要在手機(jī)設(shè)置中的無障礙里打開對應(yīng)的開關(guān):
打開以后,自動點(diǎn)擊功能可以自動后臺運(yùn)行了,不想用時可以在上圖開關(guān)那里關(guān)閉即可。
以后需要先運(yùn)行App,再打開開關(guān),開啟功能。
無障礙模式雖然用著挺舒服,但是在很多廠商的系統(tǒng)里,已經(jīng)打開的無障礙模式隔一段時間經(jīng)常會被自動關(guān)閉,比如MIUI系統(tǒng)里就要給App加開機(jī)運(yùn)行的權(quán)限。
而廠商自帶的無障礙就沒事,猜測系統(tǒng)里內(nèi)置了處理,這也是無障礙模式的一個坑吧。
小結(jié)
最后總結(jié)一下,AccessibilityService是一個很有趣的功能,發(fā)揮想象力可以做很多事,但是要小心踩坑:
- 通過findAccessibilityNodeInfosByViewId或者findAccessibilityNodeInfosByText找到的目標(biāo)控件不一定是你想要的點(diǎn)擊控件
- 各家廠商系統(tǒng)可能對無障礙模式內(nèi)置了屏蔽處理
adb shell命令
adb可以方便我們直接高效的操作真機(jī),比如安裝apk,批量安裝apk,復(fù)制文件等,而模擬點(diǎn)擊事件也是可以通過adb命令完成的。
我是突然想到,前陣子看過網(wǎng)上流傳的一個“微信跳一跳”的輔助,使用python + adb完成。
原理就是adb負(fù)責(zé)截圖,python負(fù)責(zé)圖像識別像素計算距離,最后再由adb模擬點(diǎn)擊。
如果我們需要點(diǎn)擊的目標(biāo),坐標(biāo)相對確定,那我們直接在代碼里執(zhí)行adb命令模擬點(diǎn)擊即可。
真機(jī)實(shí)驗(yàn)
我們先用USB連接真機(jī),在cmd命令行工具里:
adb shell shell@PRO6:/ $ input tap 125 521 shell@PRO6:/ $
這里的意思就是點(diǎn)擊屏幕上 (x, y) = (125, 521)的地方。果然手機(jī)響應(yīng)了,缺點(diǎn)就是響應(yīng)時間略長,感覺有1秒左右。
同理其他手勢操作也可以完成,這里不作詳解,感興趣的可以自行搜索。
下面我們需要做的就是在代碼里完成上述操作,并且可以持續(xù)在后臺運(yùn)行。這里我也是踩坑無數(shù),聽我慢慢吐槽。
尋找后臺執(zhí)行adb命令的方案
ProcessBuilder — OUT
沒什么好說的,直接看代碼:
int x = 0, y = 0; String[] order = { "input", "tap", " ", x + "", y + "" }; try { new ProcessBuilder(order).start(); } catch (IOException e) { Log.i("GK", e.getMessage()); e.printStackTrace(); }
這種版本,在Activity中可行,但是切后臺不行……這肯定無法滿足需求,再找!
Instrumentation — OUT
try { Instrumentation inst = new Instrumentation(); inst.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0)); inst.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0)); Log.i("GK", "模擬點(diǎn)擊" + x + ", " + y); } catch (Exception e) { Log.e("Exception when sendPointerSync", e.toString()); }
這種版本和上一個一模一樣,不能后臺,差評??!
救世主Runtime登場
private OutputStream os; /** * 執(zhí)行ADB命令: input tap 125 340 */ private final void exec(String cmd) { try { if (os == null) { os = Runtime.getRuntime().exec("su").getOutputStream(); } os.write(cmd.getBytes()); os.flush(); } catch (Exception e) { e.printStackTrace(); Log.e("GK", e.getMessage()); } }
后臺問題迎刃而解!
添加合適的時機(jī)
目前我們把核心功能做完了,最后需要做的就是找到合適的時機(jī),執(zhí)行操作。
首先我們的容器肯定是一個Service,然后后臺不斷的判斷當(dāng)前app是否是目標(biāo)app,如果是的話,再執(zhí)行自動點(diǎn)擊操作。
所以我們需要判斷當(dāng)前前臺app的包名或者Activity的名字是否是我們的目標(biāo)。
/** * 如果前臺APP是目標(biāo)apk */ private boolean isCurrentAppIsTarget() { String name = getForegroundAppPackageName(); if (!TextUtils.isEmpty(name) && PACKAGE_NAME.equalsIgnoreCase(name)) { return true; } return false; } /** * 獲取前臺程序包名 */ public String getForegroundAppPackageName() { ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); List<RunningAppProcessInfo> lr = am.getRunningAppProcesses(); if (lr == null) { return null; } for (RunningAppProcessInfo ra : lr) { if (ra.importance == RunningAppProcessInfo.IMPORTANCE_VISIBLE || ra.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { Log.i("GK", ra.processName); return ra.processName; } } return ""; }
以上就是adb shell方案,這種方案缺陷也比較明顯,就是要求 自動點(diǎn)擊的位置不能改變。
對于如何獲取點(diǎn)擊位置的坐標(biāo),可以打開開發(fā)者選項(xiàng)中的指針位置:
直接查看坐標(biāo)。
總結(jié)
模擬點(diǎn)擊這種需求,我們一般都不會用到,也有點(diǎn)歪門邪道的意思。但是無論什么需求,中間的探索過程才最珍貴。技術(shù)也是人,不是每次都會有說干就干的決心和勇氣,保持一顆好奇心,珍惜每次探索的機(jī)會,學(xué)有所得,小有收獲,也未嘗不是一種自我認(rèn)可。
最后附上源碼:AutoClickService
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
android 自定義ScrollView實(shí)現(xiàn)背景圖片伸縮的實(shí)現(xiàn)代碼及思路
本文純屬個人見解,是對前面學(xué)習(xí)的總結(jié),如有描述不正確的地方還請高手指正~,首先還是按照通例給大家看下示例.2013-05-05Android中AsyncTask與handler用法實(shí)例分析
這篇文章主要介紹了Android中AsyncTask與handler用法,以實(shí)例形式較為詳細(xì)的分析了Android中AsyncTask與handler的功能、用法與相關(guān)注意事項(xiàng),并附帶完整實(shí)例源碼供讀者下載,需要的朋友可以參考下2015-10-10Android自定義帶水滴的進(jìn)度條樣式(帶漸變色效果)
這篇文章主要介紹了Android自定義帶水滴的進(jìn)度條樣式(帶漸變色效果)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-12-12Android ScrollView實(shí)現(xiàn)橫向和豎向拖動回彈效果
這篇文章主要為大家詳細(xì)介紹了Android ScrollView實(shí)現(xiàn)橫向和豎向拖動回彈效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-09-09Android編程實(shí)現(xiàn)文件瀏覽功能的方法【類似于FileDialog的功能】
這篇文章主要介紹了Android編程實(shí)現(xiàn)文件瀏覽功能的方法,可實(shí)現(xiàn)類似于FileDialog的功能,涉及Android針對文件與目錄操作的相關(guān)技巧,需要的朋友可以參考下2016-11-11Kotlin使用flow實(shí)現(xiàn)倒計時功能(示例詳解)
這篇文章主要介紹了Kotlin使用flow實(shí)現(xiàn)倒計時功能,本文通過圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-02-02詳解android 中animation-list 動畫的應(yīng)用
本篇文章主要介紹了詳解android 中animation-list 動畫的應(yīng)用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12