小米手机MIUI安装APK时自动获取安装权限(自动点击权限框)

  这篇主要是记录一下在完全没学过Android的情况下硬拗完的这个APK,拖了很久查了很多资料才勉强写完,比较垃圾但还是实现功能了。记录的过程我也尽量把知识点贴出来。
一开始是看了一个大佬的分享贴(testerhome的帖子,但是现在论坛封了,等过后再贴大佬的链接),决定改写这个apk。大佬原先写的代码不适用于MIUI系统,稍微改了一下,再增加了跳转获取Accessibility权限部分。(这部分代码也参考了网上的大佬,我有空再找找记得哪个链接就贴哪个,毕竟查了太多资料了)
这个版本现在还很稚嫩,过后有空还会完善。


贴上GitHub地址:https://github.com/congyingHHZ/AutoInstall

有需要的朋友可以自己下载源码


测试设备:小米10
工具:Android Studio 11.0.10
环境:window10

软件介绍

这个软件主要是为了解决MIUI系统(todo:尝试兼容各家定制Android)在APK安装过程中总是需要手动允许才能继续安装流程这个比较烦人的事情。并且也为了以后接入自动化测试,遇到安装APK可以自动完成。
软件大致分成2个部分

  • 判断是否有AccessibilityService权限,如果没有权限则引导跳转到AccessibilityService权限设置页面
  • 利用AccessibilityService对安装过程的弹窗进行自动点击,已完成自动化安装

代码

一、利用AccessibilityService对权限弹窗的“允许”等按钮进行点击(从核心内容开始说起, AccessibilityUtil.java)

1. 创建一个类继承AccessibilityService

无障碍服务AccessibilityService可以主动接收到系统的事件,通过对事件的判断,知道是否有进行安装操作,并且可以对控件进行点击等操作。

public class AutoInstallService extends AccessibilityService{
    // do something
}

2. 当接收到系统事件后,调用onAccessibilityEvent方法

在这个方法里对收到的系统事件进行判断,如果判断为系统正在安装软件那么就调用自己写performInstallation去完成自动点击操作。

public void onAccessibilityEvent(AccessibilityEvent event) {
        /*
         * 回调方法,当事件发生时会从这里进入,在这里判断需要捕获的内容,
         * 可通过下面这句log将所有事件详情打印出来,分析决定怎么过滤。
         */

        log("!!onAccessibilityEvent!!");
        //log(event.toString());
        AccessibilityNodeInfo noteInfo = event.getSource();
        log("===noteInfo!===");
        if (event.getSource() == null) {
            log("<null> event source");
            return;
        }

        AccessibilityNodeInfo rowNode = getRootInActiveWindow();
        log("===rowNode!===");
        //log(rowNode.toString());
        int eventType = event.getEventType();
        log(eventType+"");
        log(event.getPackageName().toString());

        if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
                && (event.getPackageName().equals(PACKAGE_INSTALLER_MIUI) | event.getPackageName().equals(PACKAGE_INSTALLER_MIUI_adb))) {
            boolean r = performInstallation(event);
            log("Action Perform: " + r);
        }else if(eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED &&
                event.getPackageName().equals(PACKAGE_INSTALLER_MIUI)){
            log("!!input TYPE_WINDOW_CONTENT_CHANGED !!");
            boolean r = performInstallation(event);
            log("Action Perform: " + r);
        }
    }

相关知识点:

  1. event.getSource()
    接收的系统事件类型是AccessibilityEvent,通过getSource()方法可以获取到事件信息
    AccessibilityNodeInfo noteInfo = event.getSource();
  2. getRootInActiveWindow()
    获取节点信息
    AccessibilityNodeInfo rowNode = getRootInActiveWindow();
    之后通过节点信息判断页面有没有我们想要的内容,如“允许安装”这些
    3.event.getEventType()
    获取事件的类型,返回值是INT, TYPE_VIEW_CLICKED, TYPE_VIEW_LONG_CLICKED, TYPE_VIEW_SELECTED……
    event.getPackageName()
    获取事件产生的应用,也就是这个事件是哪个应用产生的。
    int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
               && (event.getPackageName().equals(PACKAGE_INSTALLER_MIUI) | event.getPackageName().equals(PACKAGE_INSTALLER_MIUI_adb))){
...
}else if(eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED &&
               event.getPackageName().equals(PACKAGE_INSTALLER_MIUI)){
}

传进来的AccessibilityEvent,可能是发生了点击事件、长按事件等等,但是如果安装软件弹出来权限框肯定是页面变化,所以这里判断even是不是页面变化的类型(TYPE_WINDOW_STATE_CHANGED,TYPE_WINDOW_CONTENT_CHANGED )才执行接下来的操作。
同时因为系统可能还会有其他软件导致的页面变化事件,因此这里还判断了是不是安装软件产生的页面变化。MIUI中安装应用的包名是PACKAGE_INSTALLER_MIUI,通过adb安装应用的包名是PACKAGE_INSTALLER_MIUI_adb。

3. 确定是进入安装流程后开始执行点击获取权限操作,跳转到performInstallation方法

private boolean performInstallation(AccessibilityEvent event) {
        List<AccessibilityNodeInfo> nodeInfoList;
        /*
         * 有的手机会弹2次,有的只弹一次,在替换安装时会出现确定按钮,
         * 为了大而全,下面定义了比较多的内容,可按需增减。
         */
        log("!!performInstallation!!");
        String[] labels = new String[]{"本次允许","允许", "确定", "继续安装", "下一步", "完成","安装"};
        for (String label : labels) {
            log(label);
            nodeInfoList = event.getSource().findAccessibilityNodeInfosByText(label);
            if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
                boolean performed = performClick(nodeInfoList);
                if (performed) return true;
            }
        }
        return false;
    }

这部分没有什么可说的,就是获取页面内容,然后循环判断有没有存在“允许”之类的字符,这些都要要点击的节点。

4. 如果页面有需要点击节点,也就是弹出的权限框,则跳转到performClick()执行点击操作。

 @RequiresApi(api = Build.VERSION_CODES.N)
    private boolean performClick(List<AccessibilityNodeInfo> nodeInfoList) {
        for (AccessibilityNodeInfo node : nodeInfoList) {
            /*
             * 这里还可以根据node的类名来过滤,大多数是button类,这里也是为了大而全,
             * 判断只要是可点击的是可用的就点。
             */

            if (node.isClickable() && node.isEnabled()) {
                return node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
            else if(node.getClassName() == "android.widget.Button"){
                Log.d(TAG,"clickByNode");
                return clickByNode(node);
            }
        }
        return false;
    }

这一段代码逻辑也很简单就是传进来上一步在当前页面发现的所有node,然后遍历这个list,如果是按钮可以点击则点击该node。

相关知识点:

  1. node.isClickable() && node.isEnabled()
    AccessibilityNode如果是按钮就具有isClickable()和isEnabled()属性
  2. node.getClassName() == "android.widget.Button"
    这里比原版代码多加了一个判断,因为小米安装过程弹出权限框的“允许安装”按钮(还是“继续安装”?不记得了,反正就是有个按钮点不了)不是isClickable的,导致无法继续往下走执行点击。但是这个node的类名是"android.widget.Button",所以补充判断再进行点击操作。当然原来isClickable属性的按钮点击方法也用不了了,这里也添加的这种类型node的点击方法。

5. 执行点击操作。这部分是新增的,针对node.performAction()无法实现点击的情况。

public final boolean clickByNode(AccessibilityNodeInfo nodeInfo){
        if (nodeInfo == null){
            return false;
        }
//        if (nodeInfo.getClassName() != "android.widget.Button"){
//            return false;
//        }
        Rect rect = new Rect();
        nodeInfo.getBoundsInScreen(rect);
        int x = (rect.left + rect.right)/2;
        int y = (rect.top + rect.bottom)/2;

        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,100,50));
        //path:路径  startTime:从手势开始到开始笔画的时间
        final GestureDescription gesture = builder.build();

        return dispatchGesture(gesture,
                new GestureResultCallback(){
            @Override
            public void onCompleted(GestureDescription gestureDescription){
                super.onCompleted(gestureDescription);
            }
            @Override
            public void onCancelled(GestureDescription gestureDescription){
                super.onCancelled(gestureDescription);
            }

                },null);

    }

这一段主要是利用了GestureDescription,这个api是Android7.0之后引入的,所以必须在这个方法前增加 @RequiresApi(api = Build.VERSION_CODES.N)。
利用GestureDescription可以实现在不root手机的情况下进行模拟手势操作。

相关知识点:

  1. GestureDescription.dispatchGesture()
posted @ 2022-01-14 18:05  丛影HHZ  阅读(1441)  评论(0编辑  收藏  举报