一次静默安装APK的实践

  • Post author:
  • Post category:其他


一次静默安装APK的实践

研究这些黑科技总是令人兴奋的,最近由于某些原因需要看看静默安装APK可否实现。总得来说,实现了一个小

Demo

,对于自己理解静默安装的原理有了一个大概的理解。静默安装听起来就是有点流氓,不过不管怎么样,知道多一些知识也是好的,万一要用到了呢。

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

其实静默安装分为在有

Root

权限和没有

Root

权限这两种情况,在

Root

的情况下实现起来比较简单直接使用命令行执行

pm

命令大概就完事了。然而这种方式很明显只能自娱自乐一下,因为大部分手机都是没有

Root

权限的。关于

Root

情况下的静默安装我就不多介绍了,网上搜索一大把。本文主要研究没有

Root

情况下的静默安装过程。

从上面列出的两篇文章中知道了系统安装

APK

最终都是调用了

PackageManager



installPackage()

方法,其声明如下:

 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

安装结果的。恩,原理分析完了。接下来就开始实践了。

分析

我们可以知道,系统提供了一个

IPackageInstallObserver



AIDL

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

实现原理大概说一下:首先通过反射获取系统的

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

文件,打开它:

  <orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
  <orderEntry 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"

,如下:

<?xml version="1.0" encoding="utf-8"?>
<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

    输出

    returnCode



    0

    ,回到模拟器主界面的时候发现

    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操作应该就可以跑起来。

源码地址:

戳这里



版权声明:本文为u012943767原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。