预备知识:熟悉Android开发环境,linux/mac环境,设备推荐使用pixel。
写在前面
AccessibilityService是Android系统提供的辅助功能,可以让我们实时监听系统的各种事件,并做出相应的操作。事件包括:屏幕activity变化,按钮点击,界面切换等;可执行的操作包括:点击,滑动,对话框输入等等。因此我们可以使用这功能来实现一些自动化操作。本文以实现抖音的自动点赞,关注,评论功能作为实战例子,详细介绍AccessibilityService的用法。
服务注册与权限申请
App使用Android AccessibilityService服务首先需要申请相应的权限,以及注册对应的服务可供系统回调。在AndroidManifest.xml文件中配置信息如下:
<service
android:name=".AutoAccessibilityService"
android:exported="true"
android:label="@string/app_name"
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/accessibility_service_config" />
</service>
android.permission.BIND_ACCESSIBILITY_SERVICE是需要申请的权限。
.AutoAccessibilityService是自定义的服务类名称,可自定义命名。
@xml/accessibility_service_config自定义配置的辅助服务信息,需要在res文件下创建accessibility_service_config.xml文件,文件事例如下:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_service_description"
android:packageNames="com.android.automeng, com.ss.android.ugc.aweme"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagReportViewIds"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true"
android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
/>
- android:description:服务的描述,出现在辅助中心列表中,引用字符串资源。
- android:packageNames:监听的应用包名,用逗号隔开,这里监听的包名为com.android.automeng,com.ss.android.ugc.aweme这两个应用触发的事件。
- android:accessibilityEventTypes:指明要监听的事件类型,这里使用
typeAllMask表示监听所有类型的辅助事件。 - android:accessibilityFlags:事件回调中报告视图ID,这样服务可以找到触发事件的具体视图。
- ndroid:accessibilityFeedbackType:提供语音反馈。
- android:notificationTimeout:事件通知的超时时间,单位ms。如果超过这个时间服务没有响应,该事件将被丢弃。
- android:canRetrieveWindowContent:服务可以检索窗口内容,如果为false,onAccessibilityEvent()中的事件无法获取子View信息。
- android:canPerformGestures:服务可以执行手势操作,如双击、滑动等。
- android:settingsActivity服务的设置界面Activity,允许用户设置服务相关选项。
创建辅助服务
创建辅助服务类需要继承 android.accessibilityservice.AccessibilityService 并重载onAccessibilityEvent 及 onInterrupt 方法,让系统在触发到我们accessibility_service_config.xml文件中注册的事件时,能够回调我们重载的辅助服务代码。
public class AutoAccessibilityService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
//任意事件触发都会调用该函数,因此可以在此实现辅助功能代码。
}
@Override
public void onInterrupt() {
//退出时调用。
}
实现点击事件
要实现模拟点击,我们首先需要获取需要点击的点坐(x,y)。在此可以利用AccessibilityServier中的AccessibilityEvent对象来获取,通过遍历当前设备Activity界面中各个view来得到我们所要点击的view的坐标。
//根据veiwID拿到对应的节点信息。
static AccessibilityNodeInfo searchNodeInfoByViewId(AccessibilityEvent event, String viewId){
AccessibilityNodeInfo nodeInfo = event.getSource();
if (nodeInfo == null) return null;
List<AccessibilityNodeInfo> nodeList = nodeInfo.findAccessibilityNodeInfosByViewId(viewId);
if(nodeList == null){
for(int i=0; i < nodeInfo.getChildCount(); ++i){
AccessibilityNodeInfo node = searchNodeInfoByViewId(nodeInfo.getChild(i), viewId);
if(node != null){
return node;
}
}
}else if(nodeList != null && nodeList.size() > 0){
for(AccessibilityNodeInfo node : nodeList){
if(node.isVisibleToUser()){ //搜索到可见的view则返回。
return node;
}
}
}
return null;
}
//根据节点信息可获得对应的x,y坐标
static Point getPointtByNode(AccessibilityNodeInfo node){
if (node == null){
return new Point(0, 0);
}
Rect rect = new Rect();
node.getBoundsInScreen(rect);
printNodeInfo(node, "- ");
Point point = new Point(rect.centerX(), rect.centerY());
return point;
}
利用GestureDescription实现点击操作。
//实现对(x,y)坐标进行点击操作。
public static boolean clickByNode(AccessibilityService service, int x, int y) {
if (service == null) {
AutoLog.e("clickByNode, service is NULL.");
return false;
}
Point point = new Point(x, y);
GestureDescription.Builder builder = new GestureDescription.Builder();
Path path = new Path();
path.moveTo(point.x, point.y);
builder.addStroke(new GestureDescription.StrokeDescription(path, 0L, 200L));
GestureDescription gesture = builder.build();
boolean isDispatched = service.dispatchGesture(gesture, new AccessibilityService.GestureResultCallback() {
@Override
public void onCompleted(GestureDescription gestureDescription) {
super.onCompleted(gestureDescription);
AutoLog.d("dispatchGesture click onCompleted.");
}
@Override
public void onCancelled(GestureDescription gestureDescription) {
super.onCancelled(gestureDescription);
AutoLog.d("dispatchGesture click onCancelled.");
}
}, null);
return isDispatched;
}
在我们得知dou音app中点赞位置的viewid为"com.ss.android.ugc.aweme:id/dxk"后,即可利用上面代码实现自动点击操作。
public class AutoAccessibilityService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
//任意事件触发都会调用该函数,因此可以在此实现辅助功能代码。
String viewId = "com.ss.android.ugc.aweme:id/dxk"; //该id为点赞view的id。
AccessibilityNodeInfo node = searchNodeInfoByViewId(accessibilityEvent, viewId);
Point p = getPointtByNode(node);
clickByNode(this, p.x, p.y);
}
@Override
public void onInterrupt() {
//退出时调用。
}
如何获取viewid?最简单的就是通过对所有view进行打印,然后根据getContentDescription或者getText返回的view描述信息来猜测对应的view。下面为打印出来的部分AccessibilityNodeInfo信息,从中不难看出来“关注”的viewid为com.ss.android.ugc.aweme:id/vf4。
- className[android.widget.TextView], viewIdResourceName[null], text[NULL], contentDescription[探索,按钮], stateDescription[NULL], top[63], bottom[178], left[145], right[303], centerX[224], centerY[120]
---- className[android.widget.TextView], viewIdResourceName[com.ss.android.ugc.aweme:id/vf4], text[探索], contentDescription[NULL], stateDescription[NULL], top[90], bottom[151], left[179], right[269], centerX[224], centerY[120]
--- className[android.widget.TextView], viewIdResourceName[null], text[NULL], contentDescription[同城,按钮], stateDescription[NULL], top[63], bottom[178], left[303], right[461], centerX[382], centerY[120]
---- className[android.widget.TextView], viewIdResourceName[com.ss.android.ugc.aweme:id/vf4], text[同城], contentDescription[NULL], stateDescription[NULL], top[90], bottom[151], left[337], right[427], centerX[382], centerY[120]
--- className[android.widget.TextView], viewIdResourceName[null], text[NULL], contentDescription[关注,按钮有未读消息], stateDescription[NULL], top[63], bottom[178], left[461], right[619], centerX[540], centerY[120]
---- className[android.widget.TextView], viewIdResourceName[com.ss.android.ugc.aweme:id/vf4], text[关注], contentDescription[NULL], stateDescription[NULL], top[90], bottom[151], left[495], right[585], centerX[540], centerY[120]
获取viewId还可以通过使用Androidstudio中的Layout Inspection来查看页面布局信息,但是该方法只能对debug包或者处于debug状态的系统才能生效。如果对于已发布的app则需要使用apktool等工具来反编译修改Manifest中的debuggable信息。或者直接修改系统的debuggable属性。因为与本文章主题不太一致,在此不做详细描述。不建议使用该方法,因为对于视频类app来说界面变化太快,Layout Inspection会反应不过来,经常出现卡死状态。
实现滑动事件
与“点击事件”类似,同样利用GestureDescription实现点击操作,执行向上滑动操作,代码如下。
private boolean scrolUp(AccessibilityService service, int center_X, int center_Y) {
if (service == null) {
AutoLog.e("ScrolUp, service is NULL.");
return false;
}
AutoLog.d("center position:" + "(" + center_X + "," + center_Y + ")");
final android.graphics.Path path = new Path();
path.moveTo((int) (center_X), (int) (center_Y * 1.5)); //起点坐标。
path.lineTo((int) (center_X), (int) (center_Y * 0.5)); //终点坐标。
GestureDescription.Builder builder = new GestureDescription.Builder();
GestureDescription gestureDescription = builder.addStroke(
new GestureDescription.StrokeDescription(path, 0, 800)
).build();
boolean isDispatched = service.dispatchGesture(gestureDescription, new AccessibilityService.GestureResultCallback() {
@Override
public void onCompleted(GestureDescription gestureDescription) {
super.onCompleted(gestureDescription);
AutoLog.d("dispatchGesture ScrollUp onCompleted.");
path.close();
}
@Override
public void onCancelled(GestureDescription gestureDescription) {
super.onCancelled(gestureDescription);
AutoLog.d("dispatchGesture ScrollUp cancel.");
}
}, null);
return isDispatched;
}
实现评论事件
评论操作需要往文本框中设置文本内容,并点击发送操作,即可实现自动评论操作。
//往评论框输入文本内容。
String commentEditViewId = "com.ss.android.ugc.aweme:id/csa"; //评论文本框viewId
AccessibilityNodeInfo node = searchNodeInfoByViewId(accessibilityEvent, commentEditViewId);
if (node != null) {
Bundle bundle = new Bundle();
String commentMsg = "hello world";
bundle.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
commentMsg);
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle);
}
Thread.sleep(2000); //等待2秒
//点击发送按钮,发送评论内容。
String sendViewId = "com.ss.android.ugc.aweme:id/ct_";
AccessibilityNodeInfo node2 = AccessibilityUtils.searchNodeInfoByViewId(rootNodeInfo, sendViewId);
if (node2 != null) {
Point p = getPointtByNode(node2);
boolean ret = clickByNode(this, p.x, p.y);
}
最终效果
将上面的内容组合起来即可实现某音的自动化操作,示例视频如下,其中使用的抖音23.1.0版本包。
注:示例代码中,的viewid均为Android端抖音23.1.0版本包。