OTA升级那些事-Recovery升级和AB升级

提示:本文以MTK为例,实战+根据个人理解总结 ,讲解 OTA升级中的两种升级方式,作为总结和结论,方便理解OTA升级流程和后续工作直接拿来即用,快速实现客需需求。

文章目录

  • [前言 - 需求](#前言 - 需求)
  • [一、 OTA升级两种方式](#一、 OTA升级两种方式)
    • [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平台 整包升级相关知识点)
  • 三、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 ADBApply 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 模块升级: 这里就是更为坑爹的,需要分区要求的 Cache misc分区,如下你自己看有木有 ,所以本身无解。
  • 原厂沟通:原厂基本都不支持 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分钟左右,过程蛮多。
    -
相关推荐
坂田民工1 个月前
RK3566 AB升级功能
linux·rk3566·buildroot·ab升级
星野云联AIoT技术洞察2 个月前
ESP32 Edge AI 架构设计:固件、OTA 与端侧推理的完整实践
深度学习·esp32·模型部署·aiot·esp-idf·ota升级·固件开发
一枝小雨5 个月前
【OTA专题】2 初级bootloader架构和基础工程移植
stm32·单片机·嵌入式·ota·bootloader·固件升级·加密升级
云卓SKYDROID7 个月前
无人机传感器技术要点与难点解析
人工智能·数码相机·无人机·高科技·云卓科技·固件升级
不断提高7 个月前
多种适用于 MCU 固件的 OTA 升级方案
单片机·mcu·ota升级·双分区升级
慧都小项10 个月前
微服务测试困境?Parasoft SOAtest的自动化、虚拟化与智能分析来袭!
自动化测试·parasoft·智能修复·服务虚拟化·ota升级·微服务测试
achirandliu2 年前
汽车有FOTA升级,FOTA与OTA有什么差异? 做FOTA,有那些注意事项?
网络·汽车·fota升级