Android构建系统级应用静默授权,静默安装

背景

基于定制平板系统开发应用,能获得安卓源码层的支持。
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.pk8platform.x509.pemsignapk.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.apksigned.apk为输出路径。

方式三:
根据系统方提供的签名文件,生成jks或者keystone文件,后续可直接在AS中打包签名,无需额外再签名。

  1. 生成shared.priv.pem文件

openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out shared.priv.pem -nocrypt

  1. 生成shared.pk12文件

openssl pkcs12 -export -in platform.x509.pem -inkey shared.priv.pem -out shared.pk12 -name moore

  1. 生成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;

然后通过ShellUtilsexecCommand方法执行该命令。

如果有Root权限的话,该方式是可行的,不会有界面,但我们对外放出的设备是不会开放root权限的,所以这条路子只能开发测试的时候用,实际上线操作不了。

模拟系统安装程序流程

介绍前可以先参考这两篇文章,了解系统安装应用的流程和代码逻辑
Android包管理机制(一)PackageInstaller的初始化
Android包管理机制(二)PackageInstaller安装APK

我们要做的,就是分析调起系统安装页面,点击安装之后,逻辑层做了些什么操作,然后用直接在我们应用中去实现这个流程。

大致流程

  1. 定义一个监听回调,接收安装开始、进度、结束的通知
  2. PackageManager获取PackageInstaller对象,获取apk文件File对象
  3. 创建一个安装会话Session,得到会话id
  4. 读取apk File,将文件以流的方式写入session
  5. 创建一个PendingIntent,用于调起PMS安装逻辑
  6. commit这个session,进入PMS安装

PMS安装简介:

  1. 接收到会话commit后,会组装成一个PackageInstallObserverAdapter对象,通过Handler发送该消息交由PMS处理
  2. PackageManagerService的install逻辑和PMSHandlerIMediaContainerService几个类相关
  3. PMSHandler处理绑定消息、新安装消息等逻辑,只有绑定成功才能进行下一步的安装操作
  4. 通过binder绑定服务,对apk进行解析和复制(耗时操作,安卓系统新开一个service进程)
  5. InstallParamsInstallArgs具体执行安装前检查、安装位置确认、copy和move等操作,
  6. 安装成功后,通过PMS分发消息,通知应用所属用户,更新Settings,为新安装的应用程序准备数据。安装失败则删除apk。
  7. 断开binder

参考文章::Android包管理机制(三)PMS处理APK的安装

关键代码

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);
}

如此,就能实现静默安装,安装完成后重启应用。

猜你喜欢

转载自blog.csdn.net/lizebin_bin/article/details/125293599