一次静默安装APK的实践

一次静默安装APK的实践

研究这些黑科技总是令人兴奋的,最近由于某些原因需要看看静默安装APK可否实现。总得来说,实现了一个小Demo,对于自己理解静默安装的原理有了一个大概的理解。静默安装听起来就是有点流氓,不过不管怎么样,知道多一些知识也是好的,万一要用到了呢。

我这里是刚开始也是对于静默安装一点都不会,那就网上找资料呗。果然发现了几篇有点参考价值的文章。比如:

  • android 实现静默安装、卸载
  • Android 无需root实现apk的静默安装

其实静默安装分为在有Root权限和没有Root权限这两种情况,在Root的情况下实现起来比较简单直接使用命令行执行pm命令大概就完事了。然而这种方式很明显只能自娱自乐一下,因为大部分手机都是没有Root权限的。关于Root情况下的静默安装我就不多介绍了,网上搜索一大把。本文主要研究没有Root情况下的静默安装过程。

从上面列出的两篇文章中知道了系统安装APK最终都是调用了PackageManagerinstallPackage()方法,其声明如下:

 public abstract void installPackage(
            Uri packageURI, IPackageInstallObserver observer, int flags,
            String installerPackageName);

这个方法是加了@hide注解的,表示隐藏的api,我们无法访问到。这个类在源码中的目录为:

\frameworks\base\core\java\android\content\pm

其实PackageManger里面还提供了一些其他方法用来做卸载应用等其他操作的,现在我们只关心安装。有兴趣的童鞋可以自己查看一下他的源码。

我们再来分析这里的installPackage方法中有一个参数为IPackageInstallObserver类型的。看到这个名字,有没有很熟悉的赶脚,其实这个东西是一个AIDL接口,用来监听APK安装结果的。恩,原理分析完了。接下来就开始实践了。

分析

我们可以知道,系统提供了一个IPackageInstallObserverAIDL接口,理论上我们可以直接使用这个接口启动系统的服务,然后通过调用相应得方法就可以实现。

实现原理大概说一下:首先通过反射获取系统的ServiceManager,然后通过getService方法获取 PackageService,这个Service就是一个IBinder对象,接下来就可以获得IPackageManager了,用这个来调用installPackage方法。下面有一段从网上抄来的代码:

 Class clazz = Class.forName("android.os.ServiceManager");  
            Method method = clazz.getMethod("getService", String.class);  
            IBinder iBinder = (IBinder) method.invoke(null, "package");  
            IPackageManager ipm = IPackageManager.Stub.asInterface(iBinder);  
            @SuppressWarnings("deprecation")  
            VerificationParams verificationParams = new VerificationParams(null, null, null, VerificationParams.NO_UID, null);  
                        // 执行安装(方法及详细参数,可能因不同系统而异)  
            ipm.installPackage(fileName, new PackageInstallObserver(), 2, null, verificationParams, ""); 

我这里采用的方式是直接把PackageManager源码拷贝过来,然后做一些巧妙的处理就能调用到隐藏的api,下面会说清楚是如何实现的。

第一步:拷贝源码

  • 拷贝IPackageInstallObserver.aidl 到我们的app/src/main/aidl/目录中 记住包名一定要为android.content.pm。(这个了解过AIDL原理的都知道为什么了)
  • 拷贝PackageManager到我们 app/src/main/java/目录。包名也是android.content.pm

这样基本环境就配置好了。

第二步,撸代码。

首先我们需要Build一下工程,这样AIDL才能正确引用。

接着就要写一个接受安装结果的回调信息了。

编写如下代码:

class MyPackageInstallObserver extends IPackageInstallObserver.Stub {
        Context ctx;
        String appname;
        String filename;
        String pkname;

        public MyPackageInstallObserver(Context context, String appname, String filename, String pkname) {
            this.ctx = context;
            this.appname = appname;
            this.filename = filename;
            this.pkname = pkname;
        }

        @Override
        public void packageInstalled(String packageName, int returnCode) throws RemoteException {
            Log.i(TAG, "packageInstalled returnCode = " + returnCode);
            if (returnCode == 1) {
                //TODO install success
            }

        }
    }

这里的代码很简单,我只是在安装操作之后的回调中打印了一下returnCode

接着继续写安装的代码。

public void autoInstallApk(Context context, File file, String packageName, String APPName) {
        Log.i(TAG, "auto install apk packageName = " + packageName + ", fileName = " + file.getAbsolutePath());
        int installFlag = 0;
        if (!file.exists()) {
            //TODO file not exists
            Log.i(TAG,"file is not exists :" + file.getAbsolutePath());
            return;
        }
        installFlag |= PackageManager.INSTALL_REPLACE_EXISTING;  //已经安装的话就替换
        /**
         * 在模拟器安装的时候老是返回 -18 ,通过查看PackageManager源码得出,这个码的意思是SDCARD不能安装应用。所以我这里去掉了
         */
//        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//            installFlag |= PackageManager.INSTALL_EXTERNAL;
//        }
        try {
            PackageManager pm = context.getPackageManager();
            IPackageInstallObserver observer = new MyPackageInstallObserver(context, APPName, file.getAbsolutePath(), packageName);
            pm.installPackage(Uri.fromFile(file), observer, installFlag, packageName);
        } catch (Exception e) {
            Log.getStackTraceString(e);
        }
    }

这里很巧秒的把Context获得的PackageManager替换成我们自己代码的PackageManager了,这样就可以调用隐藏的api了(感觉有点耍赖)

在写这部分代码的时候可能会有一些问题,什么问题呢。嘿嘿。当你写到 PackageManager.INSTALL_REPLACE_EXISTING 这句的时候,发现编译器会报错,报的是没有找到这个变量,为什么呢,自己打开源码中的PackageManager明显是有这个属性的。其实原因是开发APP的时候,因为你本地源码有android.content.pm.PackageManager这个类,但是Android SDK中同样有这个类的。它默认引用了SDK中的这个类。然后你点进去看,其实SDK中的这个属性也是存在的,不过也是添加的@hide注解,所以你引用不到。

那么我们如何让Studio先加载我们本地的代码,再从SDK里面找呢?如果是Eclipse的环境的话就可以这样做:

右键工程名->properties->java build path -> order and export 把 src up到顶部。

但是我的是Studio怎么办。我上网找到一个方法,在我们的module根目录有一个app.iml文件,打开它:

  type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
  type="sourceFolder" forTests="false" />

你会看到有两行这样的东西,把这两行的位置调换一下。然后在build一下module就行了。(此方法在我电脑可以的,但是其他studio不确定能不能成功,不行的话,只能把sdk中的PackageManager删除掉了)

这样的话,我们在代码中就能引用自己的那个`PackageManager“了。

好了,写完上面的代码之后,我们就可以调用一下了。

autoInstallApk(this,new File("/data/app/autoinstall.apk"),"com.analysis","Analysis");

第一个参数为Context,第二个参数为你存放静默安装的apk路径,第三个参数为静默安装的apk的包名(要写对),第四个位应用名称,这个应该是可以随便写的。

这样就行了,no no no ! 我们需要把这个应用声明为系统级别的app,这样才能进行安装操作,还需要声明一些安装的权限,这些操作都是在AndroidManifest里面实现的,在manifest节点添加一行 android:sharedUserId:"android.uid.system" ,如下:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.silentinstaller"
    android:installLocation="auto"
    android:sharedUserId="android.uid.system">


    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.DELETE_PACKAGES" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            intent-filter>
        activity>
    application>

manifest>

到了这里还没完。既然我们声明了这个应用是系统级别的,但是Android系统似乎不承认它,这样我们在安装的时候就会报INSTALL_FAILED_SHARED_USER_INCOMPATIBLE异常,似乎还是不能运行。

这种情况的解决方法就是找到源码的签名文件对这个apk进行签名。我的步骤是这样的:

  • 找到这3个文件

    • SignApk.jar 目录:/out/host/linux-x86/framework/signapk.jar
    • platform.x509.pem 目录:/build/target/product/security/platform.x509.pem
    • platform.pk8 目录:/build/target/product/security/platform.pk8
  • 在根目录建立一个文件夹 /5.0(因为我这里的Android 5.0的源码)

  • 把上面3个文件拷贝到/5.0目录下,再生成一个apk,放到/5.0目录下。

  • 打开Terminal,进入/5.0目录,执行下面命令

    java -jar SignApk.jar platform.x509.pem  platform.pk8 旧的apk.apk 生成的apk.apk  
    
  • 执行完命令之后会生成一个apk,在把这个apk安装到模拟器上面,然后我们把一个需要静默安装的apk放到模拟器的/data/app目录下(因为我前面写的代码是这个目录) 然后启动应用,点击安装,之后查看logcat 输出 returnCode0,回到模拟器主界面的时候发现apk已经安装上去了。下面是这里操作的命令

    #导入需要静默安装的apk
    adb push autoinstall.apk /data/app/  
    #导入apk
    adb push 生成的apk.apk /data/app/
    #安装
    abd shell
    adb pm install -r /data/app/生成的apk.apk

这样就成功实现了一个静默安装的Demo了。

总结

大体上对静默安装有了个了解,这里其实我也是参考别人的方法来做了一遍,其实自己研究过的东西并不多,感觉做完这个Demo之后,发现静默安装要实现起来并不简单。首先这个能实现静默安装的APK需要用对应的API源码的签名文件进行签名才能够正常安装。这就很尴尬了(Android有这么多版本,要把所有源码下载一遍然后把自己的APK签名一次,这就很蛋疼了),其次应用需要声明为系统级别的应用,这样的话,安装的时候在系统默认弹出的安装界面上会弹出几百个权限,不知道是不是我自己手机的特殊问题。反正我一看见这么多权限都不敢安装了(题外话~),最后我在自己的小米4s手机上运行并不成功,仅仅在5.0的模拟器上面运行成功了,原因是我的手机Android版本为5.1.1。我没有对应的签名文件,安装不上。就这样看来,这些东西运行在模拟器上是可以的,但是运行在各大品牌的手机上就显得有点吃力了。因为不知道那些手机改系统的时候签名文件有没有改过,所以要做大量的兼容测试。不过总的来说,这也是一种实现静默安装的思路,还是存在其参考价值。

这里我提供了一个Demo,按照里面的Readme.md操作应该就可以跑起来。

源码地址: 戳这里

你可能感兴趣的:(一次静默安装APK的实践)