從零開始無障礙服務
文章目錄
- 從零開始無障礙服務
- 前言
- 一、新建專案-選擇Empty Activity
- 二、新建BaseService類和AccessService類
- 1. BaseService類
- 2. AccessService類
- 三、修改AndroidManifest.xml
- 1. 添加AccessibilityService配置
- 2. 添加allocation.xml組態檔
- 四、修改MainActivity類
- 五、實作1+2+3=6
- 1. 獲取APP包名
- 2. 查找并點擊1、2、3、+、=
- 3. 運行效果
前言
以前安卓root權限很容易獲取的時候,可以寫一些日常作業批處理的助手工具,而現在的安卓手機權限管理越來越嚴,root權限越來越難獲取,于是就開始使用安卓自帶的無障礙服務來實作自己的操作了,雖然也有限制,但是總體的操作也是很符合預期,本文將從零撰寫一個無障礙服務的工具,不適合安卓初學者,請小白自行百度詳細,
一、新建專案-選擇Empty Activity
Minimun SDK我直接選擇API 26,省去一些雜七雜八的問題,讀者有更低版本兼容需求的話,請自行填坑,

二、新建BaseService類和AccessService類
1. BaseService類
BaseService繼承AccessibilityService,里面封裝了一些常用的查找、定位、手勢方法,
代碼如下:
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.GestureDescription;
import android.content.Context;
import android.content.Intent;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/*
* 基類,封裝了查找定位、點擊、手勢方法
* */
public class BaseService extends AccessibilityService {
private AccessibilityManager mAccessibilityManager;
private Context mContext;
private static BaseService mInstance;
public void init(Context context) {
mContext = context.getApplicationContext();
mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
public static BaseService getInstance() {
if (mInstance == null) {
mInstance = new BaseService();
}
return mInstance;
}
/**
* Check當前輔助服務是否啟用
*
* @param serviceName serviceName
* @return 是否啟用
*/
public boolean checkAccessibilityEnabled(String serviceName) {
List<AccessibilityServiceInfo> accessibilityServices =
mAccessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
for (AccessibilityServiceInfo info : accessibilityServices) {
if (info.getId().equals(serviceName)) {
return true;
}
}
return false;
}
/**
* 前往開啟輔助服務界面
*/
public void goAccess() {
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
/**
* 模擬點擊事件,如果該node不能點擊,則點擊父node,將點擊事件一直向父級傳遞,直至到根node或者找到一個可以點擊的node
*
* @param nodeInfo nodeInfo
*/
public void performViewClick(AccessibilityNodeInfo nodeInfo) {
if (nodeInfo == null) {
return;
}
while (nodeInfo != null) {
if (nodeInfo.isClickable()) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
nodeInfo = nodeInfo.getParent();
}
}
/**
* 模擬回傳操作
*/
public void performBackClick() {
performGlobalAction(GLOBAL_ACTION_BACK);
}
/**
* 模擬下滑操作
*/
public void performScrollBackward() {
performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
}
/**
* 模擬上滑操作
*/
public void performScrollForward() {
performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
}
/**
* 查找對應文本的View,無論該node能不能點擊
*
* @param text text
* @return View
*/
public AccessibilityNodeInfo findViewByText(String text) {
AccessibilityNodeInfo viewByText = findViewByText(text, true);
if (viewByText == null) {
viewByText = findViewByText(text, false);
}
return viewByText;
}
/**
* 查找對應文本的View
*
* @param text text
* @param clickable 該View是否可以點擊
* @return View
*/
public AccessibilityNodeInfo findViewByText(String text, boolean clickable) {
AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();
if (accessibilityNodeInfo == null) {
return null;
}
List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text);
if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
if (nodeInfo != null && (nodeInfo.isClickable() == clickable)) {
return nodeInfo;
}
}
}
return null;
}
/**
* 查找對應ID的View
*
* @param id id
* @return View
*/
public AccessibilityNodeInfo findViewByID(String id) {
AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();
if (accessibilityNodeInfo == null) {
return null;
}
List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id);
if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
Log.d("dd", "findViewByID: " + nodeInfoList.size());
for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
if (nodeInfo != null) {
Log.d("dd", "findViewByID: " + nodeInfo.toString());
return nodeInfo;
}
}
}
return null;
}
/**
* 點擊對應文本的一個view,前提是這個view能夠點擊,即 clickable == true,
*
* @param text 要查找的文本
*/
public void clickViewByText(String text) {
AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();
if (accessibilityNodeInfo == null) {
return;
}
List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text);
if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
if (nodeInfo != null) {
performViewClick(nodeInfo);
break;
}
}
}
}
/**
* 點擊對應id的一個view,前提是這個view能夠點擊,即 clickable == true,
*
* @param id 要查找的id
*/
public void clickViewByID(String id) {
AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();
if (accessibilityNodeInfo == null) {
return;
}
List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id);
if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
if (nodeInfo != null) {
performViewClick(nodeInfo);
break;
}
}
}
}
/**
* 遞回遍歷node及其子node,點擊文本相同的節點,全點擊
*
* @param text
* @param parentNode
*/
public void clickNodesByText(String text, AccessibilityNodeInfo parentNode) {
if (parentNode == null) {
return;
}
int childCount = parentNode.getChildCount();
if (childCount == 0) { //葉節點
if (parentNode.getText() == null) {
return;
}
if (!text.equals(parentNode.getText().toString())) {
return;
}
Rect rect = new Rect();
parentNode.getBoundsInScreen(rect);
int moveToX = (rect.left + rect.right) / 2;
int moveToY = (rect.top + rect.bottom) / 2;
int lineToX = (rect.left + rect.right) / 2;
int lineToY = (rect.top + rect.bottom) / 2;
gesture(moveToX, moveToY, lineToX, lineToY, 100L, 400L);
return;
}
for (int i = 0; i < childCount; i++) {
AccessibilityNodeInfo child = parentNode.getChild(i);
clickNodesByText(text, child);
}
}
/**
* 根據文本查找節點
*
* @param text 要查找的文本
* @return 與文本相同的節點串列,找不到則回傳空
*/
public List<AccessibilityNodeInfo> findNodesByText(String text) {
List<AccessibilityNodeInfo> accessibilityNodeInfos = new ArrayList<>();
Stack<AccessibilityNodeInfo> nodeStack = new Stack<>();
AccessibilityNodeInfo node = getRootInActiveWindow();
nodeStack.add(node);
while (!nodeStack.isEmpty()) {
node = nodeStack.pop();
if (node != null && node.getText() != null && node.getText().toString().equals(text)) {
accessibilityNodeInfos.add(node);
}
if (node == null || node.getChildCount() == 0) {
continue;
}
//獲得節點的子節點,對于二叉樹就是獲得節點的左子結點和右子節點
int childCount = node.getChildCount();
for (int i = 0; i < childCount; i++) {
AccessibilityNodeInfo child = node.getChild(i);
if (child != null) {
nodeStack.push(child);
}
}
}
if (accessibilityNodeInfos.size() > 0) {
return accessibilityNodeInfos;
} else {
return null;
}
}
/**
* 模擬輸入,低版本的輸入有所不同,讀者請自行百度
*
* @param nodeInfo nodeInfo
* @param text text
*/
public void inputText(AccessibilityNodeInfo nodeInfo, String text) {
Bundle arguments = new Bundle();
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
}
/**
* 手勢操作,因為path不能小于0,因此小于則直接回傳,不操作,另外如果有需求,可以自行修改小于則設定為0或者螢屏的寬高
*
* @param moveToX
* @param moveToY
* @param lineToX
* @param lineToY
* @param startTime
* @param duration
*/
public void gesture(int moveToX, int moveToY, int lineToX, int lineToY, long startTime, long duration) {
if (moveToX < 0 || moveToY < 0 || lineToX < 0 || lineToY < 0) {
Log.e("path", "path nagative");
return;
}
GestureDescription.Builder builder = new GestureDescription.Builder();
Path path = new Path();
path.moveTo(moveToX, moveToY);
path.lineTo(lineToX, lineToY);
GestureDescription gestureDescription = builder
.addStroke(new GestureDescription.StrokeDescription(path, startTime, duration, false))
.build();
dispatchGesture(gestureDescription, new AccessibilityService.GestureResultCallback() {
@Override
public void onCompleted(GestureDescription gestureDescription) {
super.onCompleted(gestureDescription);
}
@Override
public void onCancelled(GestureDescription gestureDescription) {
}
}, new Handler(Looper.getMainLooper()));
}
protected void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) { }
@Override
public void onInterrupt() { }
@Override
protected void onServiceConnected() { super.onServiceConnected(); }
}
2. AccessService類
AccessService則繼承BaseService,具體的無障礙處理邏輯都在這個類里面實作,
代碼如下:
import android.view.accessibility.AccessibilityEvent;
/**
* 操作類,在這里實作具體邏輯
*/
public class AccessService extends BaseService {
private String appPackageName = "xxx.xxx.xxx";
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
String packageName = event.getPackageName() == null ? "" : event.getPackageName().toString();
if (!packageName.equals(appPackageName)) {// 如果活動APP不是目標APP則不回應
return;
}
int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:// 捕獲視窗內容改變事件
break;
default:
break;
}
}
}
三、修改AndroidManifest.xml
1. 添加AccessibilityService配置
在application節點下添加service節點,為程式配置AccessibilityService的屬性
內容如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.anheimoxin.access">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 添加AccessibilityService配置 -->
<service
android:name="com.anheimoxin.access.service.AccessService"
android: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/allocation" />
</service>
</application>
</manifest>
2. 添加allocation.xml組態檔
在res目錄下新建xml檔案夾,并在新建的xml目錄下新建一個名為allocation.xml的檔案
檔案內容如下:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagRequestEnhancedWebAccessibility|flagReportViewIds"
android:canPerformGestures="true"
android:canRetrieveWindowContent="true"
android:canRequestFilterKeyEvents="true"
android:canRequestEnhancedWebAccessibility="true"
android:notificationTimeout="300" />
檔案結構如下圖:

四、修改MainActivity類
修改MainActivity類,讓程式打開后就檢查檢查是否開啟無障礙服務并跳轉到設定頁面,
代碼如下:
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import com.anheimoxin.access.service.BaseService;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BaseService instance = BaseService.getInstance();
instance.init(this);
if (!instance.checkAccessibilityEnabled("暗黑魔心的無障礙服務")) {
instance.goAccess();
}
}
}
專案當前的運行效果如下:

五、實作1+2+3=6
萬變不離其宗,這里就舉個簡單的例子,實作打開計算器后自動點擊1+2+3=6的操作,
1. 獲取APP包名
首先要獲取到計算器的包名,可以將手機設定除錯模式連接到電腦,使用ADB指令獲取,
查看當前活動的APP的包名的指令如下:
adb logcat | findstr Displayed

2. 查找并點擊1、2、3、+、=
獲取到了包名就可以直接操作了,很簡單的實作,先查找定位,然后就可以點擊了,如果要詳細分析APP的布局,可以使用android studio自帶的工具UI Automator Viewer,路徑在SDK檔案夾下面的tools/bin/中


實作代碼如下:
import android.graphics.Rect;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import java.util.List;
/**
* 操作類,在這里實作具體邏輯
*/
public class AccessService extends BaseService {
private String appPackageName = "com.huawei.calculator";
private boolean refresh = true; // 控制在未處理完邏輯前不要進入邏輯空間
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
String packageName = event.getPackageName() == null ? "" : event.getPackageName().toString();
if (!packageName.equals(appPackageName)) {// 如果活動APP不是目標APP則不回應
return;
}
int eventType = event.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:// 捕獲視窗內容改變事件
if (packageName.equals(appPackageName)) {
if (refresh) {
refresh = false;
AccessibilityNodeInfo nodeOne = findViewByText("1");
if (nodeOne != null) {
performViewClick(nodeOne);
sleep(500);
}
// 有些view是沒有text的,就可以通過ID、類名等屬性來獲取
AccessibilityNodeInfo nodeAdd = findViewByID("com.huawei.calculator:id/op_add");
if (nodeOne != null) {
performViewClick(nodeAdd);
sleep(500);
}
// 查找所有的2,并點擊
List<AccessibilityNodeInfo> nodeOneList = findNodesByText("2");
if (nodeOneList != null && nodeOneList.size() != 0) {
for (int i = 0; i < nodeOneList.size(); i++) {
AccessibilityNodeInfo node = nodeOneList.get(i);
if (node != null) {
Rect rect = new Rect();
node.getBoundsInScreen(rect);
int moveToX = (rect.left + rect.right) / 2;
int moveToY = (rect.top + rect.bottom) / 2;
int lineToX = (rect.left + rect.right) / 2;
int lineToY = (rect.top + rect.bottom) / 2;
// 有些View是不能點擊,這時候可以用手勢來處理
gesture(moveToX, moveToY, lineToX, lineToY, 100L, 400L);
sleep(500);
}
}
}
nodeAdd = findViewByID("com.huawei.calculator:id/op_add");
if (nodeOne != null) {
performViewClick(nodeAdd);
sleep(500);
}
// getRootInActiveWindow回傳整個view的root節點,深度優先遍歷查找所有的3,并點擊
clickNodesByText("3", getRootInActiveWindow());
sleep(500);
AccessibilityNodeInfo nodeEq = findViewByID("com.huawei.calculator:id/eq");
if (nodeOne != null) {
performViewClick(nodeEq);
sleep(500);
}
// 更多的操作請看BaseService,或者自行百度
refresh = true;
}
}
break;
default:
break;
}
}
}
3. 運行效果
至此整個教程的內容就完了,如果有疑問也可以掃碼關注我的微信公眾號與我聯系,下面就是本次專案的效果動圖、微信公眾號二維碼和原始碼下載鏈接

需要專案原始碼的話,可關注公眾號回復【安卓無障礙服務原始碼】獲取百度云盤鏈接,或者點擊此處下載

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/294499.html
標籤:其他
上一篇:Flutter Android 端 FlutterView 相關流程原始碼分析
下一篇:Flutter版聚合廣告插件
