背景
基于定制平板系统开发应用,能获得安卓源码层的支持。
Android8.1系统。
目标
- 默认获取所需全部权限,不再弹窗询问授权。
- 静默升级安装新版本。
实现
获取系统应用权限
通过sharedUserId
这个配置,将当前应用进程配置为系统应用进程,可获取所有权限。
原理
通过Shared User id,拥有同一个UserId的多个APK可以配置成运行在同一个进程中,所以默认就是可以互相访问任意数据,同时可以访问其他APK的数据目录下的数据库和文件,就像访问本程序的数据一样。
为了防止其他应用拿到sharedUserId并滥用,Android系统限制了相同的userId情况下,必须要使用同样的签名才能安装在同一设备上。故,如果要设置为系统共享进程,必须要和系统应用签名一致。
系统给应用签名的逻辑:
系统源码中build/target/product/security
目录中有四组默认签名供Android.mk
在编译APK使用,通过指定LOCAL_CERTIFICATE
字段来实现,分别是:
-
testkey
:普通APK,默认情况下使用。 -
platform
:该APK完成一些系统的核心功能。经过对系统中存在的文件夹的访问测试,这种方式编译出来的APK所在进程的UID为system。 -
shared
:该APK需要和home/contacts进程共享数据。 -
media
:该APK是media/download系统中的一环
普通应用则使用testkey
,若要为系统应用,可指定为platform
。
原理及介绍参考该文章:Android的权限机制之—— “沙箱”机制sharedUserId和签名
实现
参考该文章实现:Android apk 系统签名
1. 前提:
应用签名不能是自定义签名,需要是系统签名。签名文件需要找系统提供厂商提供,包含platform.pk8
、platform.x509.pem
、signapk.jar
等几个文件,如果这一步不能提供,那通过配置sharedUserId
的方式无望。
2、配置sharedUserId
在AndroidManifest.xml
中的manifest
节点增加:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.moore.systemapk"
android:sharedUserId="android.uid.system">
</manifest>
3、签名
方式一:
在系统源码层,将应用Android.mk
中的LOCAL_CERTIFICATE
配置为platform
,即对应原理中的四个默认类型中的system应用类型
方式二:
打好未签名的apk包,通过命令方式,对应用进行签名。
java -jar signapk.jar platform.x509.pem platform.pk8 unsigned.apk signed.apk
把你对apk名和路径,替换unsigned.apk
,signed.apk
为输出路径。
方式三:
根据系统方提供的签名文件,生成jks或者keystone文件,后续可直接在AS中打包签名,无需额外再签名。
- 生成
shared.priv.pem
文件
openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out shared.priv.pem -nocrypt
- 生成
shared.pk12
文件
openssl pkcs12 -export -in platform.x509.pem -inkey shared.priv.pem -out shared.pk12 -name moore
- 生成
jks
或者keystone
文件
keytool -importkeystore -deststorepass android -destkeypass android -destkeystore moore.jks -srckeystore shared.pk12 -srcstoretype PKCS12 -srcstorepass android -alias moore
生成后则可配置到gradle中,或者打包时选择签名所在路径
4、问题
在实际操作过程中,使用命令方式签名,很大几率会出现no conscrypt_openjdk_jni in java.library.path:
的错误,拿到厂商提供的libconscrypt_openjdk_jni.so
,重新签名,报另外一个错误:FATAL ERROR in native method: RegisterNatives failed for 'org/conscrypt/NativeCrypto'
,尝试换电脑、换系统(windows、mac、linux)都无解,厂商那边电脑执行该命令是正常的,遂放弃,采用生成jks签名的方式执行。
5、安装
因更换了签名,若在原安装了该应用的设备上安装,务必提前卸载,同包名不同签名,显然是安装不成功的。
静默安装
应用实现自动更新功能,有几种种安装方式:
- 调用系统安装程序,传入apk路径(需授权,允许安装应用)
- 执行shell命令静默安装apk(需root权限)
- 使用
PackageInstaller
代码安装,模拟系统安装程序流程(需为系统应用)
普通升级安装
首先来说,这种方式是会出现安装页面的,并不符合我们的需求,但在安装过程中发现了一些问题,所以还是罗列一下可以拓展参考。
1、方式
下载好升级包apk后,直接使用Intent调起系统安装意图,即可。需注意8.0以上需使用自定义FileProvider
,否则无法拿到path路径并且会抛出异常FileUriExposedException
。
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri fileUri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//自定义的FileProvider,需要在清单文件中声明
String authority = XUpdate.getContext().getPackageName() + ".updateFileProvider";
fileUri = FileProvider.getUriForFile(XUpdate.getContext(), authority, file);
} else {
fileUri = Uri.fromFile(file);
}
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
context.startActivity(intent)
2、问题
问题来了,按照普通应用流程,这个方式是没问题的,一切正常,但我们的应用,是设置过android:sharedUserId="android.uid.system"
的,那就有点麻烦了。
调起安装的时候,报错:解析软件包时出现问题
确认过下载的安装包无问题,通过文件管理、其他应用等都能调起安装,去除systemUid后也能正常安装,那问题就是这个sharedUserId了。
原因:
通过追踪系统安装流程源码程序,发现如下代码:
final int callingAppId = UserHandle.getAppId(callingUid);
if ((callingAppId == SYSTEM_UID) || (callingAppId == ROOT_UID)) {
if ("com.android.settings.files".equals(grantUri.uri.getAuthority())) {
// Exempted authority for cropping user photos in Settings app
} else {
Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission"
+ " grant to " + grantUri + "; use startActivityAsCaller() instead");
return -1;
}
}
ok,破案,因为我们是设置为SystemUid的应用,但我们的provider并不是com.android.settings.files
,所以直接被return -1
了,而com.android.settings.files
这个provider,是在Settings这个系统应用中定义的,也就是说,只有Settings这个SystemUid的应用能够调起普通的安装流程。
解决方案:
- 我们并不能够将我们自己的
android:authorities
改成上面可用的com.android.settings.files
,因为个是整个系统唯一的一个值,改成一致是无法安装到设备上的。卒。 - 去除systemUid,能够正常安装,能去除就不会花心思去研究这了。卒。
- 修改源码!在如上判断中,增加一个或条件,加上我们自己的
authorities
,但要修改源码,有条件可以使用
该问题详细剖析可参考文章:Android android.uid.system的应用调用安装apk失败
shell命令安装
顾名思义,是直接通过输入命令执行的方式进行安装。众所周知Android内核是linux,所以其原理就是使用root账户直接命令安装。
需要先判断是否有root权限:
public static boolean isDeviceRooted() {
String su = "su";
String[] locations = {"/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/",
"/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/"};
for (String location : locations) {
if (new File(location + su).exists()) {
return true;
}
}
return false;
}
如果有权限的话,拼接命令,filePath
即为apk路径
String command = "pm install -i " + packageName + " --user 0 " + filePath;
然后通过ShellUtils的execCommand
方法执行该命令。
如果有Root权限的话,该方式是可行的,不会有界面,但我们对外放出的设备是不会开放root权限的,所以这条路子只能开发测试的时候用,实际上线操作不了。
模拟系统安装程序流程
介绍前可以先参考这两篇文章,了解系统安装应用的流程和代码逻辑
Android包管理机制(一)PackageInstaller的初始化
Android包管理机制(二)PackageInstaller安装APK
我们要做的,就是分析调起系统安装页面,点击安装之后,逻辑层做了些什么操作,然后用直接在我们应用中去实现这个流程。
大致流程
- 定义一个监听回调,接收安装开始、进度、结束的通知
- 从
PackageManager
获取PackageInstaller
对象,获取apk文件File对象 - 创建一个安装会话
Session
,得到会话id - 读取apk File,将文件以流的方式写入
session
- 创建一个
PendingIntent
,用于调起PMS安装逻辑 - commit这个session,进入PMS安装
PMS安装简介:
- 接收到会话commit后,会组装成一个
PackageInstallObserverAdapter
对象,通过Handler
发送该消息交由PMS处理PackageManagerService
的install逻辑和PMSHandler
、IMediaContainerService
几个类相关PMSHandler
处理绑定消息、新安装消息等逻辑,只有绑定成功才能进行下一步的安装操作- 通过binder绑定服务,对apk进行解析和复制(耗时操作,安卓系统新开一个service进程)
InstallParams
和InstallArgs
具体执行安装前检查、安装位置确认、copy和move等操作,- 安装成功后,通过PMS分发消息,通知应用所属用户,更新Settings,为新安装的应用程序准备数据。安装失败则删除apk。
- 断开binder
关键代码
int count;
int sessionId;
byte[] buffer = new byte[65536];
InputStream inputStream;
OutputStream outputStream;
//拿到apkFile,拿到packageInstaller对象
File apkFile = new File(apkFilePath);
long apkFileLength = apkFile.length();
PackageManager pm = context.getPackageManager();
PackageInstaller packageInstaller = pm.getPackageInstaller();
packageInstaller.registerSessionCallback(忽略回调实现);
//创建会话
PackageInstaller.Session session = null;
PackageInstaller.SessionParams sessionParams;
try {
sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
sessionId = packageInstaller.createSession(sessionParams);
session = packageInstaller.openSession(sessionId);
inputStream = new FileInputStream(apkFile);
outputStream = session.openWrite(apkName, 0, apkFileLength);
//写入
while ((count = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, count);
float progress = ((float) count / (float) apkFileLength);
session.setStagingProgress(progress);
}
session.fsync(outputStream);
inputStream.close();
outputStream.flush();
outputStream.close();
//创建PendingIntent
Intent intent = new Intent();
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
//绑定并提交
session.commit(pendingIntent.getIntentSender());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (session != null) {
session.abandon();
}
}
至此,就能实现静默安装了,但,还有一个问题,正常来说,我们这种系统升级,是需要在升级完成后,重启应用,但是,因为安装过程我们的应用是被Kill掉的,所以,在创建会话时设置的回调,是接收不到安装完成的回调的!!
安装完成重启
思路:
在开始安装的时候,创建一个AlarmManager任务,在10s(视apk大小自己定)后重新打开该应用。
Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
PendingIntent restartIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
mgr.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10000, restartIntent);
}
如此,就能实现静默安装,安装完成后重启应用。