美文网首页安卓知识项目程序员
谈谈Android自动安装技术

谈谈Android自动安装技术

作者: liucloo | 来源:发表于2016-05-09 20:29 被阅读3169次

    2016年5月9日

    提起应用自动装

    应用自动装一开始给我的感觉就是拥有root权限才能做得事情,毕竟各大市场早期的自动装都需要root权限。而现在不需要root权限的自动装也不是什么新鲜产物了,Android在4.2有了AccessibilityService这个类,他的作用主要是帮助有障碍的人使用Android手机的,他可以做到帮助你操作手机。这项技术主要面向应用自动更新、应用市场、应用SDK提供的自动更新。但自动更新已经有了插件化技术,比较好用比如360的DroidPlus等。关于AccessibilityService市场上也有了一些比较好玩的应用,比如抢红包。不过呢,今天的主题主要是App的一键安装,他的实现原理就是当出现安装页面时候帮你点一下安装那个按钮而已

    技术点

    1. 如何使用AccessibilityService监听应用Android
    2. 如何只监听你自己的应用
    3. 最后说一下Root下自动安装

    关于AccessibilityService

    首先说说这个类:

    这里不讲API,API可以查看这个类的注释,写的很详细

    它是一个辅助服务,他可以帮你做点击、长按等事件(ACTION)。。那么怎么完成这个过程呢。根据我们以往的经验,完成一个事件,首先要明确什么时候做什么事,比如onClick监听,他就表示在这个View被点击的时候,做了方法里面描述的事情。AccessibilityService思路也是一样的,首先你要在AndroidMainfests里面注册这个服务并绑定事件,然后这个类的相应方法就做了某些事儿。给个小例子

    AndroidMainfests.xml:

    <service
        android:name=".MyAccessibilityService"
        android:label="我的自动装"
        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"/>
    </service>
    

    xml/accessibility_service

    <accessibility-service
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:accessibilityEventTypes="typeAllMask"
        android:accessibilityFeedbackType="feedbackGeneric"
        android:accessibilityFlags="flagDefault"
        android:canRetrieveWindowContent="true"
        android:label="@string/title"
        android:description="@string/description"
        android:packageNames="com.android.packageinstaller"
        android:notificationTimeout="100" />
    

    上面绑定是所有类型,如果有com.android.packageinstaller被激活就会执行这个方法。进而回调AccessibilityService类的onAccessibilityEvent方法。但是不要忘记,你需要在设置-辅助功能开启你的辅助功能。还算是比较简单的。如果想理解深刻一点可以查看文章末尾给的Demo

    如果只监听自己的应用(本文重点)

    AccessibilityService是一个服务,他会不断的在后台运行,监听所有App或者用户发起的安装器请求。如果系统安装器一启动,AccessibilityService的onAccessibilityEvent的方法就会回调。那么,试想象一个情景,你同时装有两个有自动装的App A和B,上面注册的服务会监听所有包名为com.android.packageinstaller的Activity。也就是A和B同时都会监听com.android.packageinstaller的状态,当A去发起一个Intent调起它去安装App的时候,这时候B帮你点了安装。这种情况比较恶心。在实际情况中表现就是,在豌豆荚安装一个应用,用户没有开启豌豆荚的应用自动装,然后被你的自动装给装上了。用户会去骂谁,哈哈哈。

    要解决这个问题,首先你需要知道当应用安装器被调起来的时候正在安装的是不是你要安装的应用。他的实现也很简单,AccessibilityService有一个孪生兄弟类叫AccessibilityNodeInfo。他通过AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();获取,在里面保存了View节点的所有信息,只要把所有节点遍历一下,就知道是不是你要安装的了。若果不明白,你就亲自打开一个安装包,然后看着那个安装界面。你就想,不同应用怎么区分呢。然后你就明白了,因为你看到整个界面只有App名称是特有的,剩下都TND一样。

    for (Iterator<String> ite = whiteList.iterator(); ite.hasNext(); ) {
        String appName = ite.next();
        Log.d(TAG, "待安装/卸载的应用:" + appName);
    
        List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByText(appName);
        if (nodes != null && !nodes.isEmpty()) {
            return appName;
        }
    }
    

    whiteList是一个HashSet,他临时保存了你将要安装的App的名称。用这里面的应用名称和nodeInfo的相应信息进行比对,如果你的HashSet有那么帮它点吧。
    然后问题又来了,怎么获取我要安装Apk的名称呢。根据以往的经验,在AndroidMainfests中的Application里面有个label属性,他一般就是App名称。

        ApplicationInfo info = null;
        try {
            info = context.getPackageManager().getApplicationInfo(context.getPackageName(),0);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        ApplicationInfo info = packageInfo.applicationInfo;
        appName = info.loadLabel(context.getPackageManager()).toString();
    

    的确用它可以获取到我们自己App的名称,但是对于其他的App就无能为了。
    那么如果根据Apk获取应用名称呢?答案还是ApplicationInfo,只不过通过其他的方式获取的对应Apk的ApplicationInfo。Android中有这样一个类android.content.pm.PackageParser,他负责把apk中的AndroidMainfests中的信息读取出来,并存到他自己的内部类Package中,这时候我希望你去看一下这个类。在这个类里面保存着ApplicationInfo以及其他信息。那么我们就通过反射让目标Apk的android.content.pm.PackageParse,让其工作起来。这里直接贴代码,都是反射

    private static Object getPackage(String apkPath) throws Exception {
        String PATH_PackageParser = "android.content.pm.PackageParser";
    
        Constructor<?> packageParserConstructor = null;
        Method parsePackageMethod = null;
        Object packageParser = null;
        Class<?>[] parsePackageTypeArgs = null;
        Object[] parsePackageValueArgs = null;
    
        Class<?> pkgParserCls = Class.forName(PATH_PackageParser);
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            packageParserConstructor = pkgParserCls.getConstructor();//PackageParser构造器
            packageParser = packageParserConstructor.newInstance();//PackageParser对象实例
            parsePackageTypeArgs = new Class<?>[]{File.class, int.class};
            parsePackageValueArgs = new Object[]{new File(apkPath), 0};//parsePackage方法参数
        } else {
            Class<?>[] paserTypeArgs = {String.class};
            packageParserConstructor = pkgParserCls.getConstructor(paserTypeArgs);//PackageParser构造器
            Object[] paserValueArgs = {apkPath};
            packageParser = packageParserConstructor.newInstance(paserValueArgs);//PackageParser对象实例
    
            parsePackageTypeArgs = new Class<?>[]{File.class, String.class,
                    DisplayMetrics.class, int.class};
            DisplayMetrics metrics = new DisplayMetrics();
            metrics.setToDefaults();
            parsePackageValueArgs = new Object[]{new File(apkPath), apkPath, metrics, 0};//parsePackage方法参数
    
        }
        parsePackageMethod = pkgParserCls.getDeclaredMethod("parsePackage", parsePackageTypeArgs);
        // 执行pkgParser_parsePackageMtd方法并返回
        return parsePackageMethod.invoke(packageParser, parsePackageValueArgs);
        }
    

    这么一大段东西,无疑就是做了两件事,找到PackageParser对象,调用packageParser()方法获取Package对象。这里面确实有ApplicationInfo对象,但是你把它的applicationinfo.loadLabel(pm).toString()打印出来他是包名,这不是我门想要的。其实在Resource里面其实也可以读到应用名称,我们都知道,Resource要想读取一个值必须给他指定Id,这个Id其实就存在在ApplicationInfo里面,它叫labelRes。这时用resource.getText(applicationinfo.labelRes)去还是取不到,因为你这里的Resource是属于现在这个应用而不是被安装应用的。那应该怎么做呢?
    做过插件化都知道,如果读取出来插件apk的资源呢。有一个类叫AssetManager,用它的addAssetPath的方法可以把一个apk的Resource读到当前Resource对象中,虽然这个方法是public的,但是实际调用时候还是失败,必须用反射获取。具体看代码,反射这个类有点恶心,挺费解的。

    public static String getAppNameByReflection(Context ctx, String apkPath) {
        File apkFile = new File(apkPath);
        if (!apkFile.exists()) {//|| !apkPath.toLowerCase().endsWith(".apk")
            return null;
        }
        String PATH_AssetManager = "android.content.res.AssetManager";
        try {
            Object pkgParserPkg = getPackage(apkPath);
            // pkgParserPkg 为Package对象
            if (pkgParserPkg == null) {
                return null;
            }
            Field appInfoFld = pkgParserPkg.getClass().getDeclaredField(
                    "applicationInfo");
            // 从对象Package对象得到applicationInfo
            if (appInfoFld.get(pkgParserPkg) == null) {
                return null;
            }
            ApplicationInfo info = (ApplicationInfo) appInfoFld.get(pkgParserPkg);
    
            // 反射得到AssetManager
            Class<?> assetMagCls = Class.forName(PATH_AssetManager);
            Object assetMag = assetMagCls.newInstance();
            // 从AssetManager类得到addAssetPath方法
            Class[] typeArgs = new Class[1];
            typeArgs[0] = String.class;
            Method assetMag_addAssetPathMtd = assetMagCls.getDeclaredMethod(
                    "addAssetPath", typeArgs);
            Object[] valueArgs = new Object[1];
            valueArgs[0] = apkPath;
            // 执行addAssetPath方法,加载目标apk资源
            assetMag_addAssetPathMtd.invoke(assetMag, valueArgs);
    
            // 得到本地Resources对象并实例化,有参数
            Resources res = ctx.getResources();
            typeArgs = new Class[3];
            typeArgs[0] = assetMag.getClass();
            typeArgs[1] = res.getDisplayMetrics().getClass();
            typeArgs[2] = res.getConfiguration().getClass();
            //反射得到目标Resource的构造器
            Constructor resCt = Resources.class
                    .getConstructor(typeArgs);
            valueArgs = new Object[3];
            valueArgs[0] = assetMag;
            valueArgs[1] = res.getDisplayMetrics();
            valueArgs[2] = res.getConfiguration();
            //得到组合之后的Resource
            res = (Resources) resCt.newInstance(valueArgs);
    
            PackageManager pm = ctx.getPackageManager();
            // 读取apk文件的信息
            if (info == null) {
                return null;
            }
            String appName;
            if (info.labelRes != 0) {
                appName = (String) res.getText(info.labelRes);
            } else {
                appName = info.loadLabel(pm).toString();
                if (TextUtils.isEmpty(appName)) {
                    appName = apkFile.getName();
                }
            }
    
            return appName;
        } catch (Exception e) {
            Log.e(TAG, "Exception", e);
        }
        return null;
    }
    

    这里把思路屡一下,通过反射PackageParser获取到Package对象,继续反射Package得到ApplicationInfo,取出ApplicationInfo里面的labelRes供Resource使用。接下来是获取Resource,反射AssetManager得到把目标Resource放到本地Apk的Resource里面。调用本地Resource获取应用名称。

    好的,费很大的劲终于把Apk中的名称给读出来,那么把他加到whiteList里面,这样通过比对whiteList里面的内容是否在应用安装器的界面出现过就可以了。

    Root模式怎么做

    Root为什么有那么大权限呢,玩过Shell都懂。当你想在比你权限高或者不属于你的目录移动活删除文件或被拒绝,但是Root就不一样了。Android赋予Root安装免询问功能。
    他的原理就是一条shell命令pm install。具体看代码

    //LD_LIBRARY_PATH 指定链接库位置 指定安装命令
    String command = "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm install " +
            (pmParams == null ? "" : pmParams) +
            " " +
            filePath.replace(" ", "\\ ");
    //以root模式执行
    ShellUtils.CommandResult result = ShellUtils.execCommand(command, true, true);
    if (result.successMsg != null
            && (result.successMsg.contains("Success") || result.successMsg.contains("success"))) {
        Log.i(TAG, "installSilent: success");
    }
    

    卸载也是一样的道理

    String command = "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm uninstall" +
            (isKeepData ? " -k " : " ") +
            packageName.replace(" ", "\\ ");
    ShellUtils.CommandResult result = ShellUtils.execCommand(command, true, true);
    if (result.successMsg != null
            && (result.successMsg.contains("Success") || result.successMsg.contains("success"))) {
        Log.i(TAG, "uninstallSilent: success");
    }
    

    Demo地址:https://github.com/liucloo/InstallAppDemo
    多谢阅读

    相关文章

      网友评论

      • 禅是一枝花_ca9d:您好,我安装完之后还想自动打开,可是我看了你的安装代码不知道怎么改,您可以帮下忙吗我QQ1131312071

        /**
        * root静默安装
        * @param filePath
        * @param pmParams
        */
        private static void installSilent(String filePath, String pmParams){
        if (filePath == null || filePath.length() == 0) {
        Log.i(TAG, "installSilent: error path");
        return;
        }

        File file = new File(filePath);
        if (file.length() <= 0 || !file.exists() || !file.isFile()) {
        Log.i(TAG, "installSilent: error file");
        return;
        }
        //LD_LIBRARY_PATH 指定链接库位置 指定安装命令
        String command = "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm install " +
        (pmParams == null ? "" : pmParams) +
        " " +
        filePath.replace(" ", "\\ ");
        //以root模式执行
        ShellUtils.CommandResult result = ShellUtils.execCommand(command, true, true);
        if (result.successMsg != null
        && (result.successMsg.contains("Success") || result.successMsg.contains("success"))) {
        Log.i(TAG, "installSilent: success");
        }
        }
      • carrie_chh: @liucloo 自动安装到最后一步安装完成后需要点击完成按钮时,这个服务没有继续去获取这个按钮,在5.0和6.0的系统上都有这个问题,请问针对5.0,6.0需要作什么修改达到跟可以点击“安然成”按钮的功能
        hebbkeke:老铁,解决这个问题没有
      • 27872cc45bdc:你好我最近也在做利用辅助功能智能安装。
        你的MyAccessibilityService里的onAccessibilityEvent方法中判断卸载的这句语句我试了下没有生效。
        String className = event.getClassName().toString();
        if ("com.android.packageinstaller.UninstallerActivity".equals(className)

        当我点击卸载弹出卸载框时,event没有获取到UninstallerActivity这个类,是需要在哪里特别设置一下吗?
        27872cc45bdc:@Floatingmuseum 还有一个问题,就是自动安装到最后一步安装完成后需要点击完成按钮时,这个服务没有继续去获取这个按钮,在5.0和6.0的系统上都是这个毛病,4.4的华为可以走完最后一步。我有去看其他市场的安装功能在5.0和6.0上也能走完最后一步,不知道版本升级后哪里有变动
        27872cc45bdc:@liucloo 可是这里没有问题,我有去你的github上看demo。
        liucloo:@Floatingmuseum 首先你要在清单文件里面配置
        ```
        <service
        android:name=".service.MyAccessibilityService"
        android:label="@string/accessibility_title"
        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"/>
        </service>
        ```
        上面指定了meta-data
        ```
        <accessibility-service
        xmlns:android="http://schemas.android.com/apk/res/android&quot;
        android:accessibilityEventTypes="typeAllMask"
        android:accessibilityFeedbackType="feedbackGeneric"
        android:accessibilityFlags="flagDefault"
        android:canRetrieveWindowContent="true"
        android:label="@string/accessibility_title"
        android:description="@string/accessibility_description"
        android:packageNames="com.android.packageinstaller,com.lenovo.safecenter,com.google.android.packageinstaller"
        android:notificationTimeout="100" />
        ```

      本文标题:谈谈Android自动安装技术

      本文链接:https://www.haomeiwen.com/subject/ygmerttx.html