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就可以了。