最近工作中遇到在定制设备上自更新home的运用,而且需求自发动,特此整理出来,供咱们参看。
一 . 整体环境说明
- 开发板上的系统是Android 10;
- 该Android系统的对应签名文件,用于通过指令设备更新包;
- 在 AndroidManifest.xml 中配置 android:sharedUserId=”android.uid.system”,不然无法实行cmd设备指令
二.预备生成系统签名
- platform.pk8和platform.x509.pem两个文件,Android源码目录中的方位是”build/target/product/security”
- Signapk东西signapk.jar,”build/tools/signapk,这些文件都能够找定制Rom的方案商提供
- 下载keytool-importkeypair东西,链接:github.com/getfatday/k…
- 翻开终端(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指令能够用,其他的指令都会提示无权限实行此指令。。。
方案二:创立一个辅佐晋级的项目来达成目的
- 别离创立2个项目,sampleProject, upgradeHelperProject,这儿的sampleProject便是实践工作中的主营业务App;
- 为什么需求运用到2个项目?
- 验证app中运用指令,发现只要pm指令可用,其他指令均失效;
- 通过在upgradeHelperProject中添加动态广播,来完结sampleProject设备完结后,翻开sampleProject运用。
- 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)
}
- 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)
}
- 完结晋级的流程便是
- sampleProject 在实行设备更新包的指令实行前,先翻开upgradeHelperProject生成的apk,这样在实行设备指令的时分,upgradeHelperProject的apk以及处于前台,动态注册的广播会监听系统卸载和设备的包,在检测到sampleProject的apk现已设备完结后,就能够翻开sampleProject的apk了。
- 这个过程中,或许会有人问,你这还需求预先设备好2个app,多费事。这儿能够在sampleProject中加一个设备upgradeHelperProject的apk的代码,sampleProject首次发动就能够通过指令把upgradeHelperProject的apk设备好,这样预先植入到系统的apk只需求主运用(也便是sampleProject)就能够了。
至此,方案二的首要代码已完结,如何下载新的设备包,得到apk文件的途径就不再贴上来了,此方案已在定制的开发版上验证通过。
PS:
- 1.这儿运用动态广播的原因是,静态广播在Android 10上,发现完全不会触发,而动态广播则能够监听到软件的设备和卸载。
- 2.详细的晋级流程,或许需求判别App是不是更新后的重启,以及其他的一些业务逻辑,就由读者自行完结了