欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android-Accessibility(辅助功能/无障碍,自动安装APP)

程序员文章站 2022-11-20 15:34:44
android-accessibility(辅助功能/无障碍,自动安装app) 。 一.android accessibility 简介 对于那些失明或低视力,色盲,耳聋或听力受损,以及运动技能...

android-accessibility(辅助功能/无障碍,自动安装app) 。

一.android accessibility 简介

对于那些失明或低视力,色盲,耳聋或听力受损,以及运动技能受限的用户,
android提供了accessibility(辅助功能/无障碍)更加简单地操作设备,
包括文字转语音、触觉反馈、手势操作、轨迹球和手柄操作等。

在android 4.0以前,accessibility功能单一,仅能过单向获取窗口信息(获取输入框内容);
在android 4.1以后,accessibility增加了与窗口元素的双向交互,可以操作窗口元素(点击按钮)。

近期项目需要在小米/华为手机上自动安装/升级app(不能root),市面上大部分的应用市场app都通过辅助功能实现免root自动安装,
于是想借鉴一下方案,试用了5个app:豌豆荚,360手机助手,百度手机助手,腾讯应用宝,应用汇。
腾讯应用宝竟然没有实现免root自动安装,必须要root才能自动安装,难道是我没发现设置按钮,找到的麻烦通知一声。。。
在华为上这几个自动安装都是失效的,在小米上只有豌豆荚(要单独下载插件app,估计是对小米单独适配了)。

自动安装原理(accessibility):
    启动"x.x.packageinstaller"系统安装界面,获取"安装"按钮,然后模拟用户点击,直到安装结束。    
技术实现看起来非常简单,麻烦在于国内千奇百怪android系统安装界面,现在只能自己动手适配项目需要的几台手机。。。

二.自动安装的基本步骤

完整:https://github.com/lifegh/autoinstall

1.manifest添加辅助服务, res/xml配置辅助功能

在androidmanifest.xml中

    
    
        
            
        

        
        
    


在res/xml/accessibility_config中
     


    

注意:[来源豌豆荚 https://www.infoq.com/cn/articles/android-accessibility-installing ]
    在一些使用虚拟键盘的app中,经常会出现这样的逻辑
    button button = (button) findviewbyid(r.id.button);
    string num = (string) button.gettext();
    在一般情况下,gettext方法的返回值是java.lang.string类的实例,上面这段代码可以正确运行。
    但是在开启accessibility service之后,如果没有指定 packagenames, 系统会对所有app的ui都进行accessible的处理。
    在这个例子中的表现就是gettext方法的返回值变成了android.text.spannablestring类的实例
    (java.lang.string和android.text.spannablestring都实现了java.lang.charsequence接口),进而造成目标app崩溃。
    所以强烈建议在注册accessibility service时指定目标app的packagename,
    以减少手机上其他应用的莫名崩溃(代码中有这样的逻辑的各位,也请默默的改为调用tostring()方法吧)。

2.继承服务accessibilityservice,实现自动安装

public class autoinstallservice extends accessibilityservice {
    private static final string tag = autoinstallservice.class.getsimplename();
    private static final int delay_page = 320; // 页面切换时间
    private final handler mhandler = new handler();

    ......

    @override
    public void onaccessibilityevent(accessibilityevent event) {
        if (event == null || !event.getpackagename().tostring()
                .contains("packageinstaller"))//不写完整包名,是因为某些手机(如小米)安装器包名是自定义的
            return;
        /*
        某些手机安装页事件返回节点有可能为null,无法获取安装按钮
        例如华为mate10安装页就会出现event.getsource()为null,所以取巧改变当前页面状态,重新获取节点。
        该方法在华为mate10上生效,但其它手机没有验证...(目前小米手机没有出现这个问题)
        */
        log.i(tag, "onaccessibilityevent: " + event);
        accessibilitynodeinfo eventnode = event.getsource();
        if (eventnode == null) {
            log.i(tag, "eventnode: null, 重新获取eventnode...");
            performglobalaction(global_action_recents); // 打开最近页面
            mhandler.postdelayed(new runnable() {
                @override
                public void run() {
                    performglobalaction(global_action_back); // 返回安装页面
                }
            }, delay_page);
            return;
        }

        /*
        模拟点击->自动安装,只验证了小米5s plus(miui 9.8.4.26)、小米redmi 5a(miui 9.2)、华为mate 10
        其它品牌手机可能还要适配,适配最可恶的就是出现安装广告按钮,误点安装其它垃圾app(典型就是小米安装后广告推荐按钮,华为安装开始官方安装)
        */
        accessibilitynodeinfo rootnode = getrootinactivewindow(); //当前窗口根节点
        if (rootnode == null)
            return;
        log.i(tag, "rootnode: " + rootnode);
        if (isnotad(rootnode))
            findtxtclick(rootnode, "安装"); //一起执行:安装->下一步->打开,以防意外漏掉节点
        findtxtclick(rootnode, "继续安装");
        findtxtclick(rootnode, "下一步");
        findtxtclick(rootnode, "打开");
        // 回收节点实例来重用
        eventnode.recycle();
        rootnode.recycle();
    }

    // 查找安装,并模拟点击(findaccessibilitynodeinfosbytext判断逻辑是contains而非equals)
    private void findtxtclick(accessibilitynodeinfo nodeinfo, string txt) {
        list nodes = nodeinfo.findaccessibilitynodeinfosbytext(txt);
        if (nodes == null || nodes.isempty())
            return;
        log.i(tag, "findtxtclick: " + txt + ", " + nodes.size() + ", " + nodes);
        for (accessibilitynodeinfo node : nodes) {
            if (node.isenabled() && node.isclickable() && (node.getclassname().equals("android.widget.button")
                    || node.getclassname().equals("android.widget.checkbox") // 兼容华为安装界面的复选框
            )) {
                node.performaction(accessibilitynodeinfo.action_click);
            }
        }
    }

    // 排除广告[安装]按钮
    private boolean isnotad(accessibilitynodeinfo rootnode) {
        return isnotfind(rootnode, "还喜欢") //小米
                && isnotfind(rootnode, "官方安装"); //华为
    }

    private boolean isnotfind(accessibilitynodeinfo rootnode, string txt) {
        list nodes = rootnode.findaccessibilitynodeinfosbytext(txt);
        return nodes == null || nodes.isempty();
    }
}

3.退出”辅助功能/无障碍”设置

public class autoinstallservice extends accessibilityservice {
    private static final string tag = autoinstallservice.class.getsimplename();
    private static final int delay_page = 320; // 页面切换时间
    private final handler mhandler = new handler();

    @override
    protected void onserviceconnected() {
        log.i(tag, "onserviceconnected: ");
        toast.maketext(this, getstring(r.string.aby_label) + "开启了", toast.length_long).show();
        // 服务开启,模拟两次返回键,退出系统设置界面(实际上还应该检查当前ui是否为系统设置界面,但一想到有些厂商可能篡改设置界面,懒得适配了...)
        performglobalaction(global_action_back);
        mhandler.postdelayed(new runnable() {
            @override
            public void run() {
                performglobalaction(global_action_back);
            }
        }, delay_page);
    }

    @override
    public void ondestroy() {
        log.i(tag, "ondestroy: ");
        toast.maketext(this, getstring(r.string.aby_label) + "停止了,请重新开启", toast.length_long).show();
        // 服务停止,重新进入系统设置界面
        accessibilityutil.jumptosetting(this);
    }

    ......
}

4.开启”辅助功能/无障碍”设置

public class accessibilityutil {
    ......
    /**
     * 检查系统设置:是否开启辅助服务
     * @param service 辅助服务
     */
    private static boolean issettingopen(class service, context cxt) {
        try {
            int enable = settings.secure.getint(cxt.getcontentresolver(), settings.secure.accessibility_enabled, 0);
            if (enable != 1)
                return false;
            string services = settings.secure.getstring(cxt.getcontentresolver(), settings.secure.enabled_accessibility_services);
            if (!textutils.isempty(services)) {
                textutils.simplestringsplitter split = new textutils.simplestringsplitter(':');
                split.setstring(services);
                while (split.hasnext()) { // 遍历所有已开启的辅助服务名
                    if (split.next().equalsignorecase(cxt.getpackagename() + "/" + service.getname()))
                        return true;
                }
            }
        } catch (throwable e) {//若出现异常,则说明该手机设置被厂商篡改了,需要适配
            log.e(tag, "issettingopen: " + e.getmessage());
        }
        return false;
    }

    /**
     * 跳转到系统设置:开启辅助服务
     */
    public static void jumptosetting(final context cxt) {
        try {
            cxt.startactivity(new intent(settings.action_accessibility_settings));
        } catch (throwable e) {//若出现异常,则说明该手机设置被厂商篡改了,需要适配
            try {
                intent intent = new intent(settings.action_accessibility_settings);
                intent.addflags(intent.flag_activity_new_task);
                cxt.startactivity(intent);
            } catch (throwable e2) {
                log.e(tag, "jumptosetting: " + e2.getmessage());
            }
        }
    }
}

5.允许”未知来源”设置

public class autoinstallutil {
    ......

    /**
     * 检查系统设置:是否允许安装来自未知来源的应用
     */
    private static boolean issettingopen(context cxt) {
        return settings.secure.getint(cxt.getcontentresolver(), settings.secure.install_non_market_apps, 0) == 1;
    }

    /**
     * 跳转到系统设置:允许安装来自未知来源的应用
     */
    private static void jumptosetting(context cxt) {
        cxt.startactivity(new intent(settings.action_security_settings));
    }

    /**
     * 安装apk
     * @param apkpath apk文件的本地路径
     */
    public static void install(context cxt, string apkpath) {
        try {
            intent intent = new intent(intent.action_view);
            // android高版本安装器不允许直接访问file,需要借助fileprovider(或使用取巧方法:调低targetsdkversion)
            intent.setdataandtype(uri.fromfile(new file(apkpath)), "application/vnd.android.package-archive");
            cxt.startactivity(intent);
        } catch (throwable e) {
            log.e(tag, "install: " + e.getmessage());
        }
    }
}