一次静默安装APK的实践

2017-01-13 15:16:59来源:http://www.jianshu.com/p/cf3a62e6e419作者:_王剑锋人点击

第七城市
一次静默安装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文件,打开它:


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


源码地址: 戳这里




第七城市

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台