背景
最近负责的一个应用(带系统签名),接到一个需求: 应用需要支持OTA,用户无需感知ota动作并且OTA之后需要自启动。一开始觉得替换安装都把自己给杀死,怎么能够自己启动自己,脑瓜疼。
设备情况:Android 13 (EDLA 认证 )
实现步骤
声明权限
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
静态注册广播
<receiver android:name=".receive.UpdateReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
/**
* @category apk替换广播
* @Description 启动自己需要的广播
*/
class UpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val packageName = intent.data?.schemeSpecificPart
if (intent.action == Intent.ACTION_PACKAGE_REPLACED
&& packageName == context.packageName) {
context.startService(Intent(context, MqttService::class.java))
}
}
}
遇到的问题
1、使用公司中间提供的安装应用方法,无法收到apk替换广播 <action android:name="android.intent.action.PACKAGE_REPLACED" />
2、不能给应用的application 标签加持久化属性 android:persistent="true"
加了这个属性kill应用进程会重新启动,但是不能替换安装。需要push才能替换 这与我们的ota 不符。
3、我通过 adb 安装接收不到这个替换广播
领导建议自己写一下应用安装的方法再去试一下,可能这边中间件的安装接口可能稍微有点问题,再不行就其他应用监听应用替换广播然后启动你的服务。最后还真发现是中间件的问题。
最后使用PackageInstaller
class InstallHelp {
/**
* 静默安装 (机器登录谷歌账号还是会提示不安全内容)
* Silent installation
* @param apkFilePath apk文件路径
*/
fun installSilent(apkFilePath: String, context: Context = MyApplication.instance) {
val apkFile = File(apkFilePath)
val packageInstaller = context.packageManager.packageInstaller
val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL)
sessionParams.setSize(apkFile.length())
val sessionId = createSession(packageInstaller, sessionParams)
if (sessionId != -1) {
val copySuccess = copyInstallFile(packageInstaller, sessionId, apkFilePath)
if (copySuccess) {
execInstallCommand(context, packageInstaller, sessionId)
}
}
}
private fun createSession(
packageInstaller: PackageInstaller,
sessionParams: SessionParams
): Int {
var sessionId = -1
try {
sessionId = packageInstaller.createSession(sessionParams)
} catch (e: IOException) {
e.printStackTrace()
}
return sessionId
}
@SuppressLint("RestrictedApi")
private fun copyInstallFile(
packageInstaller: PackageInstaller,
sessionId: Int, apkFilePath: String
): Boolean {
var input: InputStream? = null
var out: OutputStream? = null
var session: PackageInstaller.Session? = null
var success = false
try {
val apkFile = File(apkFilePath)
session = packageInstaller.openSession(sessionId)
out = session.openWrite("base.apk", 0, apkFile.length())
input = Files.newInputStream(apkFile.toPath())
var c: Int
val buffer = ByteArray(65536)
while (input.read(buffer).also { c = it } != -1) {
out.write(buffer, 0, c)
}
session.fsync(out)
success = true
} catch (e: IOException) {
e.printStackTrace()
} finally {
TypefaceCompatUtil.closeQuietly(out)
TypefaceCompatUtil.closeQuietly(input)
TypefaceCompatUtil.closeQuietly(session)
}
return success
}
@SuppressLint("RestrictedApi")
private fun execInstallCommand(
context: Context,
packageInstaller: PackageInstaller,
sessionId: Int
) {
var session: PackageInstaller.Session? = null
try {
session = packageInstaller.openSession(sessionId)
val intent = Intent(context, InstallResultReceiver::class.java)
val pendingIntent =
PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_IMMUTABLE)
session.commit(pendingIntent.intentSender)
} catch (e: IOException) {
e.printStackTrace()
} finally {
TypefaceCompatUtil.closeQuietly(session)
}
}
/**
* 使用 FileProvider 提供 URI,以适配 Android 7.0 以上的版本对文件权限的要求
* 需要申请权限 android:name="android.permission.REQUEST_INSTALL_PACKAGES"
* 注意:
* 不能添加权限android:sharedUserId="android.uid.system" 会报错
*
* @param context 上下文对象,用于启动安装和访问文件
*/
fun startAppInstall(context: Context, filePath: String) {
val installIntent = Intent(Intent.ACTION_VIEW)
installIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
val contentUri = FileProvider.getUriForFile(
context,
context.packageName,
File(filePath)
)
installIntent.setDataAndType(contentUri, "application/vnd.android.package-archive")
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(installIntent)
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
}
}
}
class InstallResultReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val status = intent?.getIntExtra(
PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE
) ?: PackageInstaller.STATUS_FAILURE
if (status == PackageInstaller.STATUS_SUCCESS) {
println("PackageInstaller.STATUS_SUCCESS")
} else {
println("PackageInstaller.STATUS_FAILURE")
}
}
}
当下载完ota 包之后使用以下方法,最后会在UpdateReceiver 收到广播。
val installHelp= InstallHelp()
installHelp.installSilent("path",context)