从零开始无障碍服务
文章目录
前言
以前安卓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 (nodeAdd != 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. 运行效果
至此整个教程的内容就完了,如果有疑问也可以扫码关注我的微信公众号与我联系,下面就是本次项目的效果动图、微信公众号二维码和源码下载链接
需要项目源码的话,可关注公众号【编程杂绪】回复【安卓无障碍服务源码】获取百度云盘链接,或者点击此处下载