一次静默安装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操作应该就可以跑起来。
源码地址:
戳这里