一 . 整体环境说明
1.开发板上的系统是Android 10
2.该系统的对应签名文件,用于通过指令安装更新包
3.在 AndroidManifest.xml 中配置 android:sharedUserId="android.uid.system",否则无法执行cmd安装指令
二.准备生成系统签名
1.platform.pk8和platform.x509.pem两个文件,Android源码目录中的位置是"build/target/product/security"
2.Signapk工具signapk.jar,"build/tools/signapk,这些文件都可以找定制Rom的方案商提供
3.下载keytool-importkeypair工具,链接:https://github.com/getfatday/keytool-importkeypair,也可以通过git下载,如下命令
4.打开终端(Mac),执行指令
git clone https://github.com/getfatday/keytool-importkeypair
cd keytool-importkeypair
# 将platform.pk8,platform.x509.pem,signapk.jar 放在keytool-importkeypair目录下
# demo.jks:生成的签名文件名称
# 123456:签名文件的密码
# demo:签名文件的别名
./keytool-importkeypair -k demo.jks -p 123456 -pk8 platform.pk8 -cert platform.x509.pem -alias demo
上述操作完成后,会得到一个系统签名的文件,为什么需要系统的签名文件呢?因为在没有对设备root的情况下,pm install的指令无法执行,这样就不能通过指令来安装新的apk文件了
三.创建项目,实现app升级后自启动
一共有2种方案,一个是通过指令来达到目的,另一个是通过创建另外一个app,使用广播监听需要升级的app包名的安装状态来启动应用
方案一:使用命令来实现(测试无效),但还是贴上几行代码吧
fun startUpgrade(apkFilePath: String) {
val installCmd = "pm install $apkFilePath"
val restartCmd = "monkey -p $packageName -c android.intent.category.LAUNCHER 1"
// 使用&&表示第一条执行成功后,才执行第二条指令,参考与某位大佬的博客
val finalCmd = "$installCmd && restartCmd"
executeCommand(finalCmd)
}
fun executeCommand(command: String): Boolean {
if (command.isEmpty()) return false
var result = false
var outputStream: DataOutputStream? = null
var errorStream: BufferedReader? = null
try {
// 申请su权限
// cmd type: /system/bin/su、/system/xbin/su、/system/bin/sh
val suCmd = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) "/system/bin/sh" else "su"
val process = Runtime.getRuntime().exec(suCmd)
outputStream = DataOutputStream(process.outputStream)
outputStream.apply {
write(command.toByteArray(Charsets.UTF_8))
flush()
writeBytes("exit\n")
flush()
}
val retCode = process.waitFor()
errorStream = BufferedReader(InputStreamReader(process.errorStream))
val msg = StringBuilder()
var line: String?
// 读取命令的执行结果
while (errorStream.readLine().also { line = it } != null) {
msg.append(line)
}
println("Execute $command result is $msg")
// 如果执行结果中包含Failure字样就认为是执行失败,否则就认为执行成功
if (retCode == 0) result = true
} catch (e: Exception) {
println(e.message)
} finally {
try {
outputStream?.close()
errorStream?.close()
} catch (e: IOException) {
println(e.message)
}
}
return result
}
真正测试的时候发现,除了pm install命令可以用,其他的命令都会提示无权限执行此命令。。。
方案二:创建一个辅助升级的项目来达成目的
1.分别创建2个项目,sampleProject, upgradeHelperProject,这里的sampleProject就是实际工作中的主营业务App
为什么需要使用到2个项目?
a.验证app中使用指令,发现只有pm指令可用,其他指令均失效
b.通过在upgradeHelperProject中添加动态广播,来实现sampleProject安装完成后,打开sampleProject应用
2.upgradeHelperProject中需要的主要代码
private val packageReceiver = PackageReceiver()
// 注册广播
private fun registerBroadcast() {
registerReceiver(packageReceiver, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addDataScheme("package")
})
}
// 广播接收者
internal class PackageReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// 安装、卸载、重装的包的包名
val packageName = intent?.dataString
val schemeName = intent?.data?.schemeSpecificPart ?: ""
Log.i(TAG, "onReceive: packageName=$packageName, $schemeName")
when (intent?.action) {
Intent.ACTION_PACKAGE_REPLACED -> {
Log.i(TAG, "onReceive: Replaced")
restartApp(schemeName)
}
Intent.ACTION_PACKAGE_ADDED -> {
Log.i(TAG, "onReceive: Add new")
restartApp(schemeName)
}
Intent.ACTION_PACKAGE_REMOVED -> {
Log.i(TAG, "onReceive: Delete")
}
else -> {
Log.i(TAG, "onReceive: Other")
}
}
}
}
// 在收到sampleProject应用安装完成后,打开应用
fun restartApp(packageName: String) {
// TODO sampleProject的包名
if (packageName != "sampleProject的包名") {
Log.i(TAG, "restartApp: is`t dst App")
return
}
val intent = packageManager.getLaunchIntentForPackage(dstPackageName)
startActivity(intent)
}
3.sampleProject中需要的主要代码
// 打开辅助升级的apk
fun startUpgradeApp(context: Context, packageName: String = "upgradehelper的报名") {
val pm = context.packageManager
val intent = pm.getLaunchIntentForPackage(packageName)
context.startActivity(intent)
}
// 使用adb命令对apk文件进行安装
fun installApkFile(apkFilePath: String, installType: Int, packageName: String = ""): Boolean {
if (File(apkFilePath).exists().not()) {
return false
}
// 先打开升级辅助类App
//startUpgradeApp()
// 执行安装命令
val installPrefix = when (installType) {
0 -> "pm install -r"
1 -> "pm install -r -d"
2 -> "pm install -r -t"
3 -> "pm install -r -d -t"
else -> "pm install"
}
val installCmd = "$installPrefix $apkFilePath"
return installApkByCmd(installCmd, packageName)
}
// 执行的指令
// Android 10平台测试发现,均不支持 /system/bin/su、/system/xbin/su
private fun installApkByCmd(installCmd: String, packageName: String): Boolean {
val printWriter: PrintWriter?
var process: Process? = null
try {
process = Runtime.getRuntime().exec("/system/bin/sh")
printWriter = PrintWriter(process.outputStream)
printWriter.println(installCmd)
printWriter.flush()
printWriter.close()
val value = process.waitFor()
return value == 0
} catch (e: Exception) {
e.printStackTrace()
} finally {
process?.destroy()
}
return false
}
private fun startUpgrade(context: Context){
// 升级指令执行前先打开辅助升级的apk
val upgradePackageName = "辅助升级的app包名"
startUpgradeApp(context, upgradePackageName)
// 开始执行安装指令
val samplePackageName = "主要功能的app包名"
installApkFile(context, 0, samplePackageName)
}
至此,方案二的主要代码已完成,如何下载新的安装包,得到apk文件的路径就不再贴上来了,此方案已在定制的开发版上验证通过。
PS:
1.这里使用动态广播的原因是,静态广播在Android 10上,发现完全不会触发,而动态广播则能够监听到软件的安装和卸载。
2.具体的升级流程,可能需要判断App是不是更新后的重启,以及其他的一些业务逻辑,就有读者自行实现了
网友评论