系统应用如何ota之后自启动(静默安装)

438 阅读2分钟

背景

最近负责的一个应用(带系统签名),接到一个需求: 应用需要支持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)