Auto.js是什么?
Github链接:https://github.com/hyb1996/Auto.js
1.由无障碍服务实现的简单易用的自动操作函数。
2.悬浮窗录制和运行更专业&强大的选择器API,提供对屏幕上的控件的寻找、遍历、获取信息、操作等。类似于Google的UI测试框架UiAutomator,您也可以把他当做移动版UI测试框架使用。
以上摘自Github简介,通俗来说它是通过AccessibilityService的服务,结合javascript的脚本来实现类似于按键精灵的一款可编写脚本的安卓辅助软件。
这篇文章的目的?
这篇文章将通过分析Auto.js 与 AccessibilityService相关部分的代码来学习AccessibilityService的进阶知识,通过这篇文章的学习,我们来更好的掌握AccessibilityService的主动操作知识,比如实现手势轨迹,模拟下拉通知栏(非调用系统API),双指捏合,三指下滑等。
First Step
了解AccessibilityService,我们可以通过其他博主的文章来了解,不过我更推荐使用官方文档来看,附上链接:https://developer.android.com/reference/android/accessibilityservice/AccessibilityService 也可以学习我的微信自动回复的文章来了解,去博客里找一下不附链接了。
通过学习需要掌握关于AccessibilityService的基本使用,可以自己写一个Demo安装在自己的手机中,在onServiceConnected()方法中实现点击开启服务后弹出一个Toast,这样就算走完的第一步。
Second Step
做完了第一步,接下来我们再回到官方文档,来看看几个重要的方法和类。
Public methods
返回值 | 方法名(描述) |
final boolean | dispatchGesture(GestureDescription gesture, AccessibilityService.GestureResultCallback callback, Handler handler)Dispatch a gesture to the touch screen. |
解释 | 上面这个dispatchGesture方法是把手势分发,手势可以模拟用户全部操作,如手指点击、滑动。进行中的手势只允许有一个,多次发送手势,之前正在进行的手势会被取消。留下一个问题,这个可以实现三指下滑吗? |
final boolean | performGlobalAction(int action)Performs a global action. |
解释 | 发送点击,发送文本改变,发送获取焦点,发送home、back,复制粘贴等操作,具体的可以AccessibilityNodeInfo类中查找。 |
了解了以上知识,我们已经可以开始对Auto.js下手了。下载源码后我们找到GlobalActionAutomator.class,这里Auto.js的作者通过AccessibilityService实现了类似按键精灵的操作,用户在不用root的情况下也可以使用模拟点击,我们来看看代码。
学习dispatchGesture
//这句话要求api版本是N(7.0)以上
@RequiresApi(api = Build.VERSION_CODES.N)
//函数gesture传入了三个参数分别是延时,执行时间和一个int的二维数组。=
public boolean gesture(long start, long duration, int[]... points) {
//这里通过下面的pointsToPath方法将points数组转换为路径。
Path path = pointsToPath(points);
这里调用gestures函数,这个函数等下来看
return gestures(new GestureDescription.StrokeDescription(path, start, duration));
}
//解析函数传入一个二维数组,事实上是一个int[n][2]的函数。
private Path pointsToPath(int[][] points) {
Path path = new Path();
//moveTo(float x, float y) Set the beginning of the next contour to the point (x,y).
//这个方法是将触点移动到下个动作的开始位置
path.moveTo(scaleX(points[0][0]), scaleY(points[0][1]));
//lineTo(float x, float y)Add a line from the last point to the specified point (x,y).
//这个方法是从上个点到这个点直接划线,相当于在屏幕上划一条直线。
//通过循环将各个点之间连起来
for (int i = 1; i < points.length; i++) {
int[] point = points[i];
path.lineTo(scaleX(point[0]), scaleY(point[1]));
}
//最后返回路径。
return path;
}
上面的代码很简单,其实就是将int的二维数组转化为路径然后调用了gestures函数,这个函数我们还没有看,接着走。
public boolean gestures(GestureDescription.StrokeDescription... strokes) {
//这里的mService其实就是我们的AccessibilityService
if (mService == null)
return false;
//使用GestureDescription.Builder().addStroke()方法添加GestureDescription.StrokeDescription类。
//这里addStroke()方法是可以多次调用的,可以加入不同的路径同时模拟。
//这样就可以通过这个方法传入多个path来实现双指捏合,三指下滑。
GestureDescription.Builder builder = new GestureDescription.Builder();
for (GestureDescription.StrokeDescription stroke : strokes) {
builder.addStroke(stroke);
}
//下面都是不同的构造方法,自己看看就行了。
if (mHandler == null) {
return gesturesWithoutHandler(builder.build());
} else {
return gesturesWithHandler(builder.build());
}
}
这个类其实就分析完了,很简单,Auto.js的作者通过一个int二维数组转路径的方法就实现了路径的转存工作。下面附上一个路径执行的代码转换,可以更好的理解。
accessibilityService
.dispatchGesture(new GestureDescription.Builder()
.addStroke(new GestureDescription.
StrokeDescription(path, 0, 500)).build()
, 回调函数(可以为null), 回调的线程(null表示主线程));
上面代码等价于下面的代码,我是从Auto.js作者的角度进行分解的。
GestureDescription.Builder builder = new GestureDescription.Builder();
Path path = new Path();
GestureDescription.StrokeDescription stroke =
new GestureDescription.StrokeDescription(path,0,500);
builder.addStroke(stroke);
accessibilityService.dispatchGesture(builder.build(),null,null);
学习performGlobalAction
这个部分的相关内容在SimpleActionAutomator.java文件内,我们可以通过学习这个类来学习performGlobalAction,话不多说看代码。
public boolean back() {
return performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
}
先来个简单的,这个方法很简单,就是直接调用了系统给返回方法,使用performGlobalAction,就可以模拟点击返回按钮。当然了,系统提供了很多方法,比如点击Home键,下拉通知栏等等,这里大家可以去看官方文档,就不多说了。
public boolean click(ActionTarget target) {
return performAction(target.createAction(AccessibilityNodeInfo.ACTION_CLICK));
}
看看这段代码,好像很简单啊,其实不然,Auto.js作者在这里把控件的方法执行进行的分装,我们要一步一步去找,那就开始找吧。我们的着手点事target,也就是ActionTarget类。打开之后可以看见其实是个接口,里面封装了几个内部类。随便找一个来看看。
class TextActionTarget implements ActionTarget {
String mText;
int mIndex;
public TextActionTarget(String text, int i) {
mText = text;
mIndex = i;
}
@Override
public SimpleAction createAction(int action, Object... params) {
return ActionFactory.createActionWithTextFilter(action, mText, mIndex);
}
}
这是接口ActionTarget的一个内部类,它实现了ActionTarget方法,我们看createAction方法,事实上它又调用了ActionFactory的createActionWithTextFilter方法,那接着找呗。
public static SimpleAction createActionWithTextFilter(int action, String text, int index) {
if (searchUpAction.containsKey(action))
return new SearchUpTargetAction(action, new FilterAction.TextFilter(text, index));
else
return new DepthFirstSearchTargetAction(action, new FilterAction.TextFilter(text, index));
}
找到了ActionFactory类里的createActionWithTextFilter方法,它判断了我们要执行的Action来判断新建哪个类,下面是执行new一个SearchUpTargetAction类时Action的类型列表
private static Map<Integer, Object> searchUpAction = new MapEntries<Integer, Object>()
.entry(AccessibilityNodeInfo.ACTION_CLICK, null)
.entry(AccessibilityNodeInfo.ACTION_LONG_CLICK, null)
.entry(AccessibilityNodeInfo.ACTION_SELECT, null)
.entry(AccessibilityNodeInfo.ACTION_FOCUS, null)
.entry(AccessibilityNodeInfo.ACTION_SELECT, null)
.entry(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, null)
.entry(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, null)
.map();
Ⅰ 、路线SearchUpTargetAction
当执行这些Action时会new SearchUpTargetAction(),我们去找这个类,看看它是如何做的。
@Override
public boolean perform(List<UiObject> nodes) {
boolean performed = false;
for (UiObject node : nodes) {
node = searchTarget(node);
if (node != null && performAction(node)) {
performed = true;
}
}
return performed;
}
protected boolean performAction(UiObject node) {
return node.performAction(mAction);
}
public int getAction() {
return mAction;
}
public UiObject searchTarget(UiObject node) {
return node;
}
这个类里的关键方法就是perform()它又调用了performAction()方法,其实到这里思路已经基本清楚了,performAction()方法相当于执行了我们target组件的相关方法,包括点击,长点击等操作。
Ⅱ 路线DepthFirstSearchTargetAction
public UiObject searchTarget(UiObject n) {
if (n == null)
return null;
if (mAble.isAble(n))
return n;
for (int i = 0; i < n.getChildCount(); i++) {
UiObject child = n.child(i);
if (child == null)
continue;
UiObject node = searchTarget(child);
if (node != null)
return node;
}
return null;
}
这个类的主要方法就是这个,其实就是判断别的操作是否可以在这个控件上执行。
Third
到这里其实就把Auto.js里的有关AccessibilityService的基础部分进行了分析,那我们就根据第二部分学的相关知识来写一个AutoAc的Demo来巩固一下相关知识。一共有四个类附上代码:
MainActivity:
public class MainActivity extends AppCompatActivity {
private Button button, button1;
private AccessibilityService accessibilityService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initAutoAcService();
accessibilityService = AutoAcService.getAutoACService();
Log.d("accessibilityService", accessibilityService.toString());
button = (Button) findViewById(R.id.button);
button1 = (Button) findViewById(R.id.button2);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
pullDownNotification();
}
});
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
viewClick();
}
});
}
private void initAutoAcService() {
if (!AutoAcUtil.isServiceON(this, AutoAcService.class.getName())) {
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
private void pullDownNotification() {
Path path = new Path();
Path path1 = new Path();
Path path2 = new Path();
path.moveTo(300, 20);
path.lineTo(300, 1500);
for (int i = 0; i < 1; i++) {
if (accessibilityService != null) {
accessibilityService.dispatchGesture(new GestureDescription.Builder().addStroke(new GestureDescription.StrokeDescription(path, 0
, 500)).build(), null, null);
}
}
}
private void viewClick() {
AccessibilityNodeInfo nodeInfo = AutoAcService.getNodeInfo();
if (nodeInfo == null) {
AutoAcUtil.toastShow(this,"nodeInfo is null",1);
return;
}
AccessibilityNodeInfo target = findNodeInfosByText(nodeInfo, "BUTTON");
target.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
public AccessibilityNodeInfo findNodeInfosByText(AccessibilityNodeInfo nodeInfo, String text) {
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(text);
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
}
有两个按钮第一个实现了dispatchGesture方法来实现一个简单的下拉通知栏的操作。第二个按钮这是实现了performAction方法,通过查找找到第一个按钮进行点击,也会下拉通知栏。附上剩下的几个类。
AutoAcService类:
public class AutoAcService extends AccessibilityService {
private static AutoAcService autoACService;
public static AccessibilityNodeInfo getNodeInfo() {
return nodeInfo;
}
private static AccessibilityNodeInfo nodeInfo;
public AutoAcService() {
super();
autoACService = this;
}
public static AutoAcService getAutoACService(){
if (autoACService != null){
return autoACService;
}
return null;
}
@Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
nodeInfo = getRootInActiveWindow();
}
@Override
public void onInterrupt() {
}
}
AutoAcUtil类:
public class AutoAcUtil {
public static boolean isServiceON(Context context,String className) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(context.ACTIVITY_SERVICE);
List<ActivityManager.RunningServiceInfo>
runningServices = activityManager.getRunningServices(100);
if (runningServices.size() < 0) {
return false;
}
for (int i = 0; i < runningServices.size(); i++) {
ComponentName service = runningServices.get(i).service;
if (service.getClassName().contains(className)) {
return true;
}
}
return false;
}
public static void toastShow(Context context, String msg, int time) {
if (time == 0) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
} else if (time == 1) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
}
}
当然了这些类提供的方法你们也可以自己看一下,最后在Manifest里声明一下服务和权限就可以运行了。
Fourth
以上就是通过分析Auto.js来学习AccessibilityService,当然了这里的思路并不一定是Auto.js作者的,但是我们能实现相关方法了解AccessibilityService就可以了。