提示:本文以MTK为例,实战+根据个人理解总结 ,讲解 OTA升级中的两种升级方式,作为总结和结论,方便理解OTA升级流程和后续工作直接拿来即用,快速实现客需需求。
文章目录
- [前言 - 需求](#前言 - 需求)
- [一、 OTA升级两种方式](#一、 OTA升级两种方式)
-
- [Recovery 升级](#Recovery 升级)
-
- 需求
- 实现方案
- 源码分析
- Recovery升级标准流程
-
- [1. 正常系统侧(重启前)](#1. 正常系统侧(重启前))
- [2、Recovery 模式侧(升级执行)](#2、Recovery 模式侧(升级执行))
- Recovery相关总结
- AB升级
-
- [一、AB 升级核心原理](#一、AB 升级核心原理)
-
- [1. 硬件分区设计(双槽位)](#1. 硬件分区设计(双槽位))
- [2. 核心逻辑](#2. 核心逻辑)
- [二、AB 升级完整流程(4 步)](#二、AB 升级完整流程(4 步))
-
- [1. 后台下载升级包](#1. 后台下载升级包)
- [2. 后台静默刷写(核心步骤)](#2. 后台静默刷写(核心步骤))
- [3. 切换启动槽位 + 重启](#3. 切换启动槽位 + 重启)
- [4. 失败自动回滚(安全机制)](#4. 失败自动回滚(安全机制))
- [三、AB 升级 关键优点(最重要)](#三、AB 升级 关键优点(最重要))
-
- [✅ 1. 真正无缝升级](#✅ 1. 真正无缝升级)
- [✅ 2. 绝对安全,不会变砖](#✅ 2. 绝对安全,不会变砖)
- [✅ 3. 升级速度快](#✅ 3. 升级速度快)
- [✅ 4. 支持增量升级、差分包](#✅ 4. 支持增量升级、差分包)
- [✅ 5. 适合车机、IoT、无人设备](#✅ 5. 适合车机、IoT、无人设备)
- [四、AB 升级 缺点(必须知道)](#四、AB 升级 缺点(必须知道))
-
- [❌ 1. 占用双倍存储空间](#❌ 1. 占用双倍存储空间)
- [❌ 2. 不支持 OTA 包本地缓存解密(uncrypt 相关)](#❌ 2. 不支持 OTA 包本地缓存解密(uncrypt 相关))
- [❌ 3. 结构复杂,适配成本高](#❌ 3. 结构复杂,适配成本高)
- [❌ 4. data 分区共用,有极小概率数据冲突](#❌ 4. data 分区共用,有极小概率数据冲突)
- [五、AB 升级 vs 传统 Recovery 升级(对比表)](#五、AB 升级 vs 传统 Recovery 升级(对比表))
- [二、MTK平台 整包升级相关知识点](#二、MTK平台 整包升级相关知识点)
-
- [MTK 制作OTA 整包方法](#MTK 制作OTA 整包方法)
-
- [制作OTA 包命令](#制作OTA 包命令)
- 生成的全量OTA包位置
- 制作OTA包坑点
- [第三方广升-艾拉比 OTA升级方案](#第三方广升-艾拉比 OTA升级方案)
- 三、AB升级实战
- 总结
前言 - 需求
升级相关介绍
- OTA升级方式有两种,整包OTA升级和差分包升级,我们这里讨论的是整包升级。
- 借助第三方实现: OTA升级基本上被第三方公司垄断->艾拉比 和 广升 两家,那升级很简单直接用第三方。第三方后台创建产品项目账号,直接用第三方app。 打包固件时候集成第三方ota 相关信息即可。
- 自己实现OTA升级,利用各个
Android芯片原厂的要求,自己打包编译整包、差分包。升级包放到自己服务器,自己下载到本地,然后调用系统接口或者按照系统要求进行升级操作 - 升级方式其实有两种:
Recovery升级和AB升级。对于Recovery升级大家很熟悉的,其实就是大家偶尔会见到的或者早些年前经常简单 收集变砖哪个界面、那个模块。
一、 OTA升级两种方式
Recovery 升级
需求
直接看需求 :厂商需要确保上面这个方法可用
java
import android.os.RecoverySystem;
RecoverySystem.installPackage(context,file);
实现方案
所以正常情况下,应用端只需要把OTA包,下载到本地,然后调用系统API 即可,机器会自动重启然后进行固件更新,完成升级。
自己之前在RK平台,Android7.1 、Android8.1 、Android 10、Android11 都是这么干的,系统都不用改。应用层直接调用系统提供的API 即可完成OTA升级。
但是在Android12 , MTK 平台上面,自己完全走不动,掉到坑里面无法爬起来,后面才发现。 MTK 平台已经废弃掉了,没有人这么干的。 需要芯片远程支持和个人对分区的理解和分区的改动,或者说高版本已经不推荐这个方案了。
源码分析
framework 层 涉及到的类:
java
/frameworks/base/core/java/android/os/RecoverySystem.java
/frameworks/base/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
java
/**
* Reboots the device in order to install the given update
* package.
* Requires the {@link android.Manifest.permission#REBOOT} permission.
*
* @param context the Context to use
* @param packageFile the update package to install. Must be on
* a partition mountable by recovery. (The set of partitions
* known to recovery may vary from device to device. Generally,
* /cache and /data are safe.)
*
* @throws IOException if writing the recovery command file
* fails, or if the reboot itself fails.
*/
@RequiresPermission(android.Manifest.permission.RECOVERY)
public static void installPackage(Context context, File packageFile)
throws IOException {
Log.d(TAG,"==========installPackage==============");
installPackage(context, packageFile, false);
}
/**
* If the package hasn't been processed (i.e. uncrypt'd), set up
* UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the
* reboot.
*
* @param context the Context to use
* @param packageFile the update package to install. Must be on a
* partition mountable by recovery.
* @param processed if the package has been processed (uncrypt'd).
*
* @throws IOException if writing the recovery command file fails, or if
* the reboot itself fails.
*
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.RECOVERY)
public static void installPackage(Context context, File packageFile, boolean processed)
throws IOException {
synchronized (sRequestLock) {
LOG_FILE.delete();
// Must delete the file in case it was created by system server.
UNCRYPT_PACKAGE_FILE.delete();
String filename = packageFile.getCanonicalPath();
Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
// If the package name ends with "_s.zip", it's a security update.
boolean securityUpdate = filename.endsWith("_s.zip");
// If the package is on the /data partition, the package needs to
// be processed (i.e. uncrypt'd). The caller specifies if that has
// been done in 'processed' parameter.
if (filename.startsWith("/data/")) {
if (processed) {
if (!BLOCK_MAP_FILE.exists()) {
Log.e(TAG, "Package claimed to have been processed but failed to find "
+ "the block map file.");
throw new IOException("Failed to find block map file");
}
} else {
FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE);
try {
uncryptFile.write(filename + "\n");
} finally {
uncryptFile.close();
}
// UNCRYPT_PACKAGE_FILE needs to be readable and writable
// by system server.
if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false)
|| !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) {
Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE);
}
BLOCK_MAP_FILE.delete();
}
// If the package is on the /data partition, use the block map
// file as the package name instead.
filename = "@/cache/recovery/block.map";
}
final String filenameArg = "--update_package=" + filename + "\n";
final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() + "\n";
final String securityArg = "--security\n";
String command = filenameArg + localeArg;
if (securityUpdate) {
command += securityArg;
}
Log.d(TAG,"==========installPackage==============command:"+command);
RecoverySystem rs = (RecoverySystem) context.getSystemService(
Context.RECOVERY_SERVICE);
if (!rs.setupBcb(command)) {
Log.d(TAG,"==========installPackage==Setup BCB failed============");
throw new IOException("Setup BCB failed");
}
try {
if (!rs.allocateSpaceForUpdate(packageFile)) {
Log.d(TAG,"==========installPackage==Failed to allocate space for update============");
throw new IOException("Failed to allocate space for update "
+ packageFile.getAbsolutePath());
}
} catch (RemoteException e) {
Log.d(TAG,"==========installPackage==============rethrowAsRuntimeException");
e.rethrowAsRuntimeException();
}
// Having set up the BCB (bootloader control block), go ahead and reboot
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
String reason = PowerManager.REBOOT_RECOVERY_UPDATE;
Log.d(TAG,"==========installPackage== On TV, reboot quiescently if the screen is off============reason:"+reason);
// On TV, reboot quiescently if the screen is off
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
DisplayManager dm = context.getSystemService(DisplayManager.class);
if (dm.getDisplay(DEFAULT_DISPLAY).getState() != Display.STATE_ON) {
reason += ",quiescent";
}
}
Log.d(TAG,"==========installPackage== On TV, reboot quiescently if the screen is off============");
pm.reboot(reason);
throw new IOException("Reboot failed (no permissions?)");
}
}
这里讲解的是需要几个过程,几个核心流程点:通过源码即可了解,但是通过实践操作 更能说明走过的流程,然后一步一步理解吧。
Recovery升级标准流程
1. 正常系统侧(重启前)
- 下载与准备:OTA 包下载到 /data(主流)或 /cache,校验完整性与签名。
- 触发升级:上层调用
RecoverySystem.installPackage(),写入升级命令到/cache/recovery/command,路径形如--update_package=/data/update.zip。 - uncrypt 处理(核心):
java
启动 uncrypt 进程(setup-bcb 服务)。
读取 /data 下的 OTA 包,生成 block map(块映射) 写入 /cache/recovery/block.map。
若 /data 加密(FBE/FDE),解密并回写到原物理块,绕过文件系统层。
更新命令为 --update_package=@/cache/recovery/block.map,写入 BCB(Bootloader Control Block,misc 分区)。
- 重启进入 Recovery:系统重启,Bootloader 读取 BCB 进入 Recovery 模式。
2、Recovery 模式侧(升级执行)
- 初始化:挂载 /cache、/misc,不挂载 /data(安全与加密兼容)。
- 解析命令:从 BCB 或 /cache/recovery/command 读取升级指令。
- 读取升级包:通过 block.map 直接访问物理块,读取已解密的 OTA 包数据。
- 包验证:校验签名、完整性、版本兼容性(verifier.cpp)。
- 执行升级:
java
解压 OTA 包,执行 META-INF/com/google/android/update-script。
刷写 boot、system、vendor 等分区,执行数据迁移、权限修复。
- 清理与重启:升级成功后清理临时文件,重启到新系统;失败则回滚或提示错误。
Recovery相关总结
Recovery界面介绍和相关升级方案介绍
假若你还没有接触过,不了解,现在直接给出实例图来,大家就有个大概了解了。


之前自己在接触OTA升级之前,对Recovery 升级有一个莫名的恐惧,后来发现也就那回事。 这里整理几点方便了解:
- 我们做ota升级,Recovery升级,那么最起码先保证本地升级OK吧。
- 那么Recovery升级界面有两个特别重要的是:
Apply update from ADB、Apply update from SD card两种升级方式,你本地先跑通验证OTA升级没问题,再进行云端下载进行OTA升级呀。
遇到的天坑
如上做了一些相关介绍,这里直接把自己做项目花了一两天时间收获的天坑规整说明一下,MTK Android12 平台。
SystemServer相关的Selinux权限: 接口调用installPackage,进程是SystemServer进程,要保证SystemServer访问具体固件存放目录的权限,也就是SeLinux权限。MTK ota固件目录:经历过无数次的实践,发现MTK平台有自己专门存放ota 固件的目录/data/ota_package。 不要想着自定义目录,随意存放。 你永远也绕不开 SELinux 权限,无解。uncrypt进程Selinux权限问题:uncrypt本身是守护进程,用来对OTA 包加密解密验证的,结果发现它也是要有访问目录权限的。Recovery模块升级: 这里就是更为坑爹的,需要分区要求的Cachemisc分区,如下你自己看有木有 ,所以本身无解。

- 原厂沟通:原厂基本都不支持
Recovery升级了,直接让用AB升级验证。
AB升级
这里对于大多数做应用和framework层的工程师是比较陌生的,但是这种方式实际在Android12 及12 后已经是主流的方式了。
AB 升级是 Android 7.0 引入的无缝升级方案,核心是两套独立系统分区(A 槽 / B 槽),升级过程不影响当前使用的系统,升级失败自动回滚,是目前高端手机、车机、Android 智能设备的主流升级方案。
一、AB 升级核心原理
1. 硬件分区设计(双槽位)
- 设备内置两套完全一样的系统分区:
A 槽(Slot A):当前正在运行的系统(主系统)
B 槽(Slot B):空闲、备用的系统(副系统) - 两套分区包含:boot、system、vendor、odm 等所有启动必需分区,完全独立
2. 核心逻辑
- 手机正常开机跑 A 槽
- 后台下载升级包,直接刷写到 B 槽
- 刷写完成后,切换启动槽位
- 重启 → 直接进入已升级完成的 B 槽
- 升级失败 → 自动切回 A 槽,数据完全无损
这就是无缝升级:升级时你还能正常用手机,不会进入黑屏 Recovery 界面。
二、AB 升级完整流程(4 步)
1. 后台下载升级包
- OTA 包下载到 /data
- 不影响使用,可息屏、可后台运行
2. 后台静默刷写(核心步骤)
- 系统调用 update_engine 服务
- 直接把新系统写入 B 槽所有分区
- 全程不关机、不重启、不进入 Recovery
- 支持断点续刷、网络中断恢复
3. 切换启动槽位 + 重启
- 刷写校验成功后,修改 Bootloader 启动优先级:B 槽优先
- 提示用户重启
- 重启一次就完成升级
4. 失败自动回滚(安全机制)
- 如果 B 槽启动失败(无法开机、崩溃)
- Bootloader 自动切回 A 槽
- 用户无感知,数据完全不丢
- 不会变砖
三、AB 升级 关键优点(最重要)
✅ 1. 真正无缝升级
- 不用进入 Recovery 黑屏界面
- 升级过程手机正常使用
- 重启一次就完成(传统升级要等 5~10 分钟)
✅ 2. 绝对安全,不会变砖
- 升级不破坏当前系统
- 失败自动回滚到老系统
- 断电、死机、升级包损坏都不怕
✅ 3. 升级速度快
- 后台静默写入,不占用用户时间
- 重启即新系统,无等待
✅ 4. 支持增量升级、差分包
- 流量消耗小
- 写入速度快
✅ 5. 适合车机、IoT、无人设备
- 不能停机、不能变砖
- 远程升级必须用 AB
四、AB 升级 缺点(必须知道)
❌ 1. 占用双倍存储空间
- 必须存两套系统(A + B)
- 系统分区空间几乎翻倍
- 成本更高(低端机一般不用)
❌ 2. 不支持 OTA 包本地缓存解密(uncrypt 相关)
- AB 升级不需要 uncrypt,因为:
- 它不通过 Recovery 读取 /data 里的包
- 它是系统后台直接写分区
- 所以没有 Recovery 无法读取 /data 的问题
❌ 3. 结构复杂,适配成本高
- Bootloader 必须支持双槽位
- 分区表必须重新设计
- 老设备无法兼容
❌ 4. data 分区共用,有极小概率数据冲突
- 新老系统共用用户数据
- 极端版本跨代可能出现兼容问题
五、AB 升级 vs 传统 Recovery 升级(对比表)
| 特性 | AB 无缝升级 | 传统 Recovery 升级 |
|---|---|---|
| 分区 | 双槽 A/B | 单槽 |
| 升级界面 | 后台静默,无黑屏 | 必须进 Recovery 黑屏 |
| 失败风险 | 无,自动回滚 | 可能变砖 |
| 重启次数 | 1 次 | 1 次但等待很久 |
| 空间占用 | 高(双倍系统) | 低 |
| 适用设备 | 高端手机、车机、IoT | 中低端手机、老设备 |
| 是否需要 uncrypt | 不需要 | 必须需要 |
二、MTK平台 整包升级相关知识点
如上,了解了Recovery升级和 AB升级基本知识点,说白了现在的机器性能都比较高端 所以基本都支持推荐AB升级,特别像 MTK平台做手机方案的,推荐的就是AB升级。
MTK 制作OTA 整包方法
制作OTA 包命令
其实就是几条命令
java
source build/envsetup.sh
lunch full_k69v1_64_k419-userdebug
make otapackage -j16
生成的全量OTA包位置
java
\out\target\product\k69v1_64_k419 目录下有一个ota 开头的.zip 包就是OTA包。

制作OTA包坑点
在MTK平台,我们自己制作OTA包,平台会给提供的命令,如下:
java
python out_sys/target/product/mssi_64_cn/images/split_build.py --system-dir out_sys/target/product/mssi_64_cn/images --vendor-dir out/target/product/k69v1_64_k419/images --kernel-dir out/target/product/k69v1_64_k419/images --output-dir ${FS_SW_VERSION}/${FS_SW_VERSION}/ --otapackage
所以我们在镜像包里面会有 otapackage.zip

特别提醒: 千万千万别用这个包,别搞混了。
第三方广升-艾拉比 OTA升级方案
Room配置
在平台上面创建项目,然后在系统里面创建属性如下:
java
广升
广升的项目没有秘钥,可以先出软件,再在网站上管理添加项目。
示例:
ro.fota.platform=MTK6769_13.0 \
ro.fota.type=pad \
ro.fota.id=sn \
ro.fota.app= \
ro.fota.battery= \
ro.fota.oem= \
ro.fota.device= \
ro.fota.version=1.0.0.1 \
ro.fota.display=2 \
艾拉比
所有艾拉比管理的项目都需要先在网站上建立相应项目,获得该项目的秘钥之后将其编入固件。
示例:
ro.fota.platform=MT6769 \
ro.fota.type=box \
ro.fota.oem= \
ro.fota.device= \
ro.fota.version=1.0.0.1 \
ro.fota.productId= \
ro.fota.productSecret=
OTA包制作
####全量包制作
- 全量包:直接上传 下载之后覆盖更新,就是上面脚本配置的
otapackage.zip包 - 差分包:
差分包:a版本升到b版本 需要两个版本的特征文件target_files 加上当前软件的编译环境制作
差分包制作通用指令 在源码根目录:out/host/linux-x86/bin/ota_from_target_files -v --block --path
java
out/host/linux-x86/ -i a.zip b.zip update.zip
out/host/linux-x86/bin/ota_from_target_files -v --block --path out/host/linux-x86/ -i 1.zip 2.zip 1-2.zip
有些服务器python版本不对,只能把out目录下fota相关的文件移到python版本对的服务器 再把两个targetfile移到对应的位置进行差分包制作。位置没有固定,通过指令指向到文件就行,上文的制作指令默认路径在源码根目录,两个targetfile都在根目录下,分别为a.zip b.zip

三、AB升级实战
以上讲了大多数的概念性内容,基础中的基础,在理解基础后,下面以实际案例来实现。
需求-对接客户AB升级-系统端提供接口
java
persist.sys.cmcc.ota.mode值为ab或other的设备,固件包由系统安装。固件包下载完成后,通过 Android ContentProvider 的"call"方法触发固件自定义升级的统一接口。
应用仅负责传递 OTA 文件路径(可由persist.cmcc.ota.path字段定义)与 MD5,系统负责后续的校验、升级、重启等全流程。
5.1.1.1 ContentProvider定义
Authority:com.cmhi.rom.update.provider
Uri:
触发升级:content://com.cmhi.rom.update.provider/update
查询/监听状态:content://com.cmhi.rom.update.provider/status
5.1.1.2 触发升级
Uri:content://com.cmhi.rom.update.provider/update
调用方法:ContentResolver.call()
Method 名称:startRomUpdate
参数传递:通过"Bundle"传入
返回值:通过"Bundle"返回执行结果
1、参数
调用时传入以下参数(存放在"Bundle"中):
参数名 类型 必填 说明
path String 是 OTA 文件的绝对路径(例如"/data/ota/update.zip")
md5 String 是 OTA 文件的 MD5 校验值
2、返回值
ContentProvider需返回一个"Bundle",包含以下字段:
参数名 类型 说明
result boolean true:任务成功接收并开始处理,false:接收失败
message String 失败原因或提示信息(可选)
3、固件侧实现要求
固件侧需实现一个 ContentProvider,并在"call"方法中处理升级逻辑。
Java代码参考:
1.@Override
2.public Bundle call(String method, String arg, Bundle extras) {
3. if ("startRomUpdate".equals(method)) {
4. String path = extras.getString("path");
5. String md5 = extras.getString("md5");
6.
7. Bundle result = new Bundle();
8. if (path != null && md5 != null && verifyFile(path, md5)) {
9. startUpgrade(path, md5);
10. result.putBoolean("result", true);
11. result.putString("message", "任务已接收");
12. } else {
13. result.putBoolean("result", false);
14. result.putString("message", "参数错误或校验失败");
15. }
16. return result;
17. }
18. return super.call(method, arg, extras);
19.}
5.1.1.3 查询升级状态
statusUri:content://com.cmhi.rom.update.provider/status
调用方法:ContentResolver.query()
返回值:int类型 status、progress
应用层直接query status表:
1.Cursor cursor = getContentResolver().query(
2. Uri.parse("content://com.cmhi.rom.update.provider/status"),
3. null, null, null, null
4.);
5.
6.if (cursor != null && cursor.moveToFirst()) {
7. int status = cursor.getInt(cursor.getColumnIndexOrThrow("status"));
8. int progress = cursor.getInt(cursor.getColumnIndexOrThrow("progress"));
9. cursor.close();
10.}
1、表字段定义
●status:任务状态(Int类型)
0 → 空闲
1 → 启动安装
2 → 安装中(进度)
3 → 安装完成初始化中
4 → 初始化完成待重启
5 → 安装失败
●progress:安装进度百分比(0--100,Int类型)
2、监听任务状态
应用层通过 ContentObserver 监听 status Uri 的变化,固件侧要求每次状态变化或安装进度变化时写入 progress 字段(如 5, 10, 15...100),调用 getContext().getContentResolver().notifyChange(statusUri, null) 通知应用层。
5.1.1.4 权限控制
固件方必须限制调用来源,定义自有权限:
1.<permission android:name="com.cmhi.permission.ROM_UPDATE" />
Provider 声明:
1.<provider
2. android:name=".RomUpdateProvider"
3. android:authorities="com.cmhi.rom.update.provider"
4. android:exported="true"
5. android:permission="com.cmhi.permission.ROM_UPDATE" />
需求实现
接口实现-对外提供Provider
java
package com.cmhi.rom.update;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.UpdateEngine;
import android.os.UpdateEngineCallback;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class RomUpdateProvider extends ContentProvider {
private static final String TAG = "RomUpdateProvider";
private UpdateEngine mUpdateEngine;
// ====================== 你要求的 URI 定义 ======================
public static final String AUTHORITY = "com.cmhi.rom.update.provider";
public static final Uri URI_UPDATE = Uri.parse("content://" + AUTHORITY + "/update");
public static final Uri URI_STATUS = Uri.parse("content://" + AUTHORITY + "/status");
// ====================== call 方法名 ======================
public static final String METHOD_START_ROM_UPDATE = "startRomUpdate";
// ====================== 入参 KEY ======================
public static final String KEY_PATH = "path";
public static final String KEY_MD5 = "md5";
// ====================== 返回值 KEY ======================
public static final String KEY_RESULT = "result";
public static final String KEY_MESSAGE = "message";
// ====================== 查询字段 ======================
public static final String COLUMN_STATUS = "status";
public static final String COLUMN_PROGRESS = "progress";
// ====================== 状态定义 ======================
public static final int STATUS_IDLE = 0; // 空闲
public static final int STATUS_START = 1; // 启动安装
public static final int STATUS_INSTALLING = 2; // 安装中
public static final int STATUS_INIT = 3; // 初始化
public static final int STATUS_READY_REBOOT = 4; // 待重启
public static final int STATUS_FAILED = 5; // 失败
// 0 - 空闲,没有升级任务
public static final int UPDATE_STATUS_IDLE = 0;
// 1 - 检查系统更新中
public static final int UPDATE_STATUS_CHECKING_FOR_UPDATE = 1;
// 2 - 可用更新,等待下载
public static final int UPDATE_STATUS_UPDATE_AVAILABLE = 2;
// 3 - 正在下载更新包
public static final int UPDATE_STATUS_DOWNLOADING = 3;
// 4 - 正在验证、校验下载完成的包
public static final int UPDATE_STATUS_VERIFYING = 4;
// 5 - 正在刷入(patch)系统分区
public static final int UPDATE_STATUS_FINALIZING = 5;
// 6 - 升级完成,等待重启
public static final int UPDATE_STATUS_UPDATED_NEED_REBOOT = 6;
// 7 - 升级失败
public static final int UPDATE_STATUS_FAILED = 7;
// 8 - 报告状态错误(不常用)
public static final int UPDATE_STATUS_REPORTING_ERROR_EVENT = 8;
// 全局状态
private int mCurrentStatus = STATUS_IDLE;
private int mCurrentProgress = 0;
private Handler mHandler = new Handler(Looper.getMainLooper());
RomUpdateProvider mContext;
@Override
public boolean onCreate() {
mCurrentStatus = STATUS_IDLE;
mCurrentProgress = 0;
mContext =this;
return true;
}
// ====================== 5.1.1.2 触发升级:call() ======================
@Override
public Bundle call(String method, String arg, Bundle extras) {
// 完全按你给的代码逻辑
Log.d(TAG,"===========call=====method:"+method);
if (METHOD_START_ROM_UPDATE.equals(method)) {
String path = extras.getString(KEY_PATH);
//String path ="/data/ota_package/full_k69v1_64_k419-ota-1rck61v164bspP17.zip";// extras.getString(KEY_PATH);
String md5 = extras.getString(KEY_MD5);
Log.d(TAG,"===================path:="+path+" md5:"+md5);
Bundle bundle = new Bundle();
if (path != null && md5 != null && verifyFile(path, md5)) {
startUpgrade(path, md5);
bundle.putBoolean(KEY_RESULT, true);
bundle.putString(KEY_MESSAGE, "任务已接收");
} else {
bundle.putBoolean(KEY_RESULT, false);
bundle.putString(KEY_MESSAGE, "参数错误或校验失败");
}
return bundle;
}
return super.call(method, arg, extras);
}
// ====================== 5.1.1.3 查询状态:query() ======================
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Log.d(TAG,"==========query==查询状态:query()");
if (!URI_STATUS.equals(uri)) {
Log.d(TAG,"==========query==URI_STATUS not return null ");
return null;
}
// 返回一行数据:status + progress
String[] columns = {COLUMN_STATUS, COLUMN_PROGRESS};
MatrixCursor cursor = new MatrixCursor(columns);
Log.d(TAG,"==========query============"+COLUMN_STATUS+" :"+mCurrentStatus+" "+COLUMN_PROGRESS+": "+mCurrentProgress);
cursor.addRow(new Object[]{mCurrentStatus, mCurrentProgress});
return cursor;
}
// ====================== 文件 MD5 校验 ======================
private boolean verifyFile(String path, String md5) {
try {
Log.d(TAG,"==========verifyFile==文件 MD5 校验========path:"+path+" md5:"+md5);
FileInputStream fis = new FileInputStream(path);
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
digest.update(buffer, 0, len);
}
fis.close();
byte[] bytes = digest.digest();
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
String fileMd5 = sb.toString().toLowerCase();
return fileMd5.equals(md5.toLowerCase());
} catch (Exception e) {
Log.e(TAG, "校验失败", e);
return false;
}
}
// ====================== 启动升级(Framework 实际调用系统 OTA 接口) ======================
private void startUpgrade(String path, String md5) {
Log.d(TAG,"===========startUpgrade====== path:"+path+" md5:"+md5);
// 更新状态:启动安装
mCurrentStatus = STATUS_START;
mCurrentProgress = 0;
notifyStatusChange();
// 模拟升级流程(真实项目替换成系统 Recovery 升级接口)
mHandler.postDelayed(() -> {
mCurrentStatus = STATUS_INSTALLING;
notifyStatusChange();
//startSimulateProgress();
startFullOtaUpdate(mContext.getContext(), path);
}, 1000);
}
// 对外调用
public void startFullOtaUpdate(Context context, String zipFilePath) {
Log.d(TAG,"=========startFullOtaUpdate=========");
File zipFile = new File(zipFilePath);
if (!zipFile.exists()) {
Log.e(TAG, "OTA包不存在");
return;
}
// 解压到系统公共路径
String outDir = "/data/ota_package/";
// String outDir = zipFilePath+"/";
File outFolder = new File(outDir);
if (!outFolder.exists()) outFolder.mkdirs();
try {
unzipPayloadFiles(zipFilePath, outDir);
} catch (Exception e) {
e.printStackTrace();
return;
}
String payloadBinPath = outDir + "payload.bin";
String payloadPropPath = outDir + "payload_properties.txt";
startUpdate(context, payloadBinPath, payloadPropPath);
}
// 解压
private static void unzipPayloadFiles(String zipPath, String outDirPath) throws Exception {
ZipFile zipFile = new ZipFile(zipPath);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.equals("payload.bin") || name.equals("payload_properties.txt")) {
File outFile = new File(outDirPath, name);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try (InputStream in = zipFile.getInputStream(entry);
OutputStream out = Files.newOutputStream(outFile.toPath())) {
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}
}
}
zipFile.close();
// 加权限
try {
Runtime.getRuntime().exec("chmod 777 " + outDirPath + "*").waitFor();
Runtime.getRuntime().exec("chmod 755 /data/ota_package") ;
Runtime.getRuntime().exec("chown system:system /data/ota_package") ;
Runtime.getRuntime().exec("chown system:system /data/ota_package") ;
// 2. payload.bin 必须给 644 + system(重要)
Runtime.getRuntime().exec("chmod 644 /data/ota_package/payload.bin");
Runtime.getRuntime().exec("chown system:system /data/ota_package/payload.bin");
// 3. payload_properties.txt 必须给 644 + system
Runtime.getRuntime().exec("chmod 644 /data/ota_package/payload_properties.txt");
Runtime.getRuntime().exec("chown system:system /data/ota_package/payload_properties.txt");
// 4. 最关键一步:恢复 selinux 上下文(必须加)
Runtime.getRuntime().exec("restorecon -R /data/ota_package");
} catch (Exception e) {
e.printStackTrace();
}
}
// 核心升级
private void startUpdate(Context context, String payloadBinPath, String payloadPropPath) {
File payloadFile = new File(payloadBinPath);
if (!payloadFile.exists()) return;
// ✅ 全局变量
try{
mUpdateEngine = new UpdateEngine();
UpdateEngineCallback callback = new UpdateEngineCallback() {
@Override
public void onStatusUpdate(int status, float percent) {
Log.d(TAG, "===== 升级进度 status:" + status + " percent:" + percent);
switch (status){
case UPDATE_STATUS_IDLE:
mCurrentStatus = STATUS_IDLE;
notifyStatusChange();
break;
case UPDATE_STATUS_DOWNLOADING:
mCurrentStatus = STATUS_INSTALLING;
mCurrentProgress= (int) (percent*100);
notifyStatusChange();
break;
case UPDATE_STATUS_VERIFYING:
mCurrentStatus = STATUS_INIT;
notifyStatusChange();
break;
case UPDATE_STATUS_FINALIZING:
mCurrentStatus = STATUS_INIT;
notifyStatusChange();
break;
}
}
@Override
public void onPayloadApplicationComplete(int errorCode) {
Log.d(TAG, "===== 升级完成 errorCode:" + errorCode);
if (errorCode == 0) {
Log.d(TAG, "✅ 升级成功,需要重启设备");
mCurrentStatus = STATUS_READY_REBOOT;
notifyStatusChange();
} else {
Log.d(TAG, "❌ 升级失败");
mCurrentStatus = STATUS_FAILED;
notifyStatusChange();
}
}
};
// 绑定服务
mUpdateEngine.bind(callback);
// 读取属性
List<String> propList = new ArrayList<>();
try {
BufferedReader br = new BufferedReader(new FileReader(payloadPropPath));
String line;
while ((line = br.readLine()) != null) {
propList.add(line);
}
br.close();
} catch (Exception e) {}
String[] headers = propList.toArray(new String[0]);
// 开始升级
mUpdateEngine.applyPayload(
"file://" + payloadBinPath,
0,
0,
headers
);
}catch (Exception e){
Log.d(TAG," 已经升级过 等待重启...........");
e.printStackTrace();
}
Log.d(TAG, "✅ A/B 升级已启动");
}
// ====================== 通知应用层状态变化 ======================
private void notifyStatusChange() {
if (getContext() != null) {
getContext().getContentResolver().notifyChange(URI_STATUS, null);
}
}
// ====================== 不需要实现的方法 ======================
@Override
public String getType(Uri uri) { return null; }
@Override
public Uri insert(Uri uri, ContentValues values) { return null; }
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; }
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; }
}
配置文件
java
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CMHIRomUpdate"
tools:targetApi="31"
android:process="com.cmhi.rom.update"
>
<!-- android:persistent="true"
-->
<!-- 你要求的 Provider 声明 -->
<provider
android:name="com.cmhi.rom.update.RomUpdateProvider"
android:authorities="com.cmhi.rom.update.provider"
android:exported="true"
android:permission="com.cmhi.permission.ROM_UPDATE"
tools:ignore="Instantiatable" />
</application>
总结
如上实现了OTA升级方案,通过两到三天的不断摸索,知识点:
- 解决各种SELinux 权限问题
- MTK 不支持Recovery升级,主流都是支持AB 升级方案
- 对于需求,直接用AI实现了。但是 逻辑、原理 AB升级 方案实现思路特别重要。
- 这里只是针对MTK,rk 平台可以遇到需求自行实现。
- 自己对AB升级有点无感,缺点就是耗时很久,一个整包1个多G,需要耗费16分钟左右,过程蛮多。
-
