设备专属代码

恢复系统包括一些用于插入设备专属代码的钩子,以便 OTA 更新还可以更新设备中除 Android 系统以外的其他部分(例如基带或无线处理器)。

以下各部分及其中的示例将对供应商 yoyodyne 生产的设备 tardis 进行自定义。

分区映射

自 Android 2.3 版本起,该平台就开始支持 eMMc 闪存设备以及在这些设备上运行的 ext4 文件系统。此外,该平台还支持 MTD(内存技术设备)闪存设备以及之前的版本就支持的 yaffs2 文件系统。

分区映射文件由 TARGET_RECOVERY_FSTAB 指定;recovery 二进制文件和更新包构建工具均使用该文件。您可以在 BoardConfig.mk 中的 TARGET_RECOVERY_FSTAB 中指定映射文件的名称。

分区映射文件示例可能如下所示:

device/yoyodyne/tardis/recovery.fstab
# mount point       fstype  device       [device2]        [options (3.0+ only)]

/sdcard     vfat    /dev/block/mmcblk0p1 /dev/block/mmcblk0
/cache      yaffs2  cache
/misc       mtd misc
/boot       mtd boot
/recovery   emmc    /dev/block/platform/s3c-sdhci.0/by-name/recovery
/system     ext4    /dev/block/platform/s3c-sdhci.0/by-name/system length=-4096
/data       ext4    /dev/block/platform/s3c-sdhci.0/by-name/userdata

/sdcard(可选)之外,本示例中的所有装载点都必须进行定义(设备也可以添加额外的分区)。支持下列 5 种文件系统类型:

yaffs2
yaffs2 文件系统构架于 MTD 闪存设备之上。MTD 分区的名称必须是“device”,且该名称必须显示在 /proc/mtd 中。
mtd
原始 MTD 分区,用于可引导分区(例如 boot 和 recovery)。MTD 实际上并未装载,但其装载点会被用作定位分区的键。/proc/mtd 中 MTD 分区的名称必须是“device”。
ext4
ext4 文件系统构架于 eMMc 闪存设备之上。块设备的路径必须是“device”。
emmc
原始 eMMc 块设备,用于可引导分区(例如 boot 和 recovery)。与 mtd 类型相似,eMMc 从未实际装载,但其装载点字符串会被用于在表中定位设备。
vfat
FAT 文件系统构架于块设备之上,通常用于外部存储空间(如 SD 卡)。块设备的名称是 device;device2 则是系统装载主设备失败时尝试装载的第二个块设备(这么做是为了与 SD 卡兼容,SD 卡可能使用分区表进行了格式化,也可能没有格式化)。

所有分区都必须装载到根目录下(即装载点值必须以斜线开头,且不含其他斜线)。此限制仅适用于在 recovery 中装载文件系统;主系统可随意将其装载在任何位置。目录 /boot/recovery/misc 应该是原始类型(mtd 或 emmc),而目录 /system/data/cache/sdcard(如果有)应为文件系统类型(yaffs2、ext4 或 vfat)。

从 Android 3.0 开始,recovery.fstab 文件新添了额外的可选字段,即“options”。目前,唯一定义的选项是“length”,它可以让您明确指定分区的长度。 对分区重新进行格式化(例如,在执行数据清除/恢复出厂设置操作过程中对用户数据分区进行格式化,或在安装完整 OTA 软件包的过程中对系统分区进行格式化)时会使用此长度。如果长度值为负数,则将长度值与真正的分区大小相加,即可得出要格式化的大小。例如,设置“length=-16384”即表示在对该分区重新进行格式化时,该分区的最后 16k 将不会被覆盖。该选项支持加密 userdata 分区(在这里,加密元数据会存储在分区中不应被覆盖的末尾部分)等功能。

注意device2options 字段均为选填字段,在解析时会产生歧义。如果该行第 4 个字段中的条目以“/”字符开头,则被视为 device2 条目;如果该条目不是以“/”字符开头,则被视为 options 字段。

启动动画

设备制造商可以自定义 Android 设备在启动时显示的动画。为此,请构建一个根据 bootanimation 格式规范而整理和放置的 .zip 文件。

对于 Android Things 设备,您可以在 Android Things 控制台中上传压缩文件,以便将图片加入到所选产品中。

注意:这些图片必须符合 Android 品牌推广指南

恢复界面

要支持配备不同可用硬件(物理按钮、LED、屏幕等)的设备,您可以自定义恢复界面以显示状态,并访问每台设备上已隐藏的手动操作功能。

您的目标是构建一个包含几个 C++ 对象的小型静态库,以提供特定于设备的功能。默认情况下,系统会使用 bootable/recovery/default_device.cpp 文件,当您编写此文件的设备专属版本时,可以从先复制该文件入手。

device/yoyodyne/tardis/recovery/recovery_ui.cpp
#include <linux/input.h>

#include "common.h"
#include "device.h"
#include "screen_ui.h"

标头和项函数

Device 类需要相关函数来返回已隐藏的恢复菜单中出现的标头和项。标头描述了如何操作菜单(例如如何更改/选择目前突出显示的项)。

static const char* HEADERS[] = { "Volume up/down to move highlight;",
                                 "power button to select.",
                                 "",
                                 NULL };

static const char* ITEMS[] =  {"reboot system now",
                               "apply update from ADB",
                               "wipe data/factory reset",
                               "wipe cache partition",
                               NULL };

注意:过长的行会被截断(而非换行),因此请留意您设备的屏幕宽度。

自定义 CheckKey

接下来,请定义您设备的 RecoveryUI 实现。本示例假设 tardis 设备配有屏幕,因此您可以继承内置的 ScreenRecoveryUIimplementation(请参阅针对无屏幕设备的说明)。可通过 ScreenRecoveryUI 自定义的唯一函数是 CheckKey(),该函数会执行初始异步键处理操作:

class TardisUI : public ScreenRecoveryUI {
  public:
    virtual KeyAction CheckKey(int key) {
        if (key == KEY_HOME) {
            return TOGGLE;
        }
        return ENQUEUE;
    }
};

KEY 常量

KEY_* 常量在 linux/input.h 中定义。系统一律会调用 CheckKey() 而不考虑恢复程序的其余部分正在执行什么操作(菜单切换为关闭状态时、菜单处于打开状态时、软件包安装期间以及用户数据清除期间等)。它会返回下列 4 个常量中的一个:

  • TOGGLE:切换菜单的显示状态以及(或者)开启/关闭文本日志
  • REBOOT:立即重新启动设备
  • IGNORE:忽略此次按键操作
  • ENQUEUE:将此次按键操作添加到队列中,以供同步处理(例如在启用了显示屏时供恢复菜单系统使用)

每次在 key-down 事件后执行同一按键的 key-up 事件时都会调用 CheckKey()。(事件 A-down B-down B-up A-up 序列只会调用 CheckKey(B))。CheckKey() 可以调用 IsKeyPressed(),以确定是否有其他键被按下。(在上述键事件的序列中,如果 CheckKey(B) 调用了 IsKeyPressed(A),则会返回 true)。

CheckKey() 可以在其类中保持状态,这有助于检测键的序列。本示例展示的是一个稍微复杂的设置:按住电源键并按下音量提高键可切换显示状态,连续按五次电源按钮可立即重新启动设备(无需使用其他键):

class TardisUI : public ScreenRecoveryUI {
  private:
    int consecutive_power_keys;

  public:
    TardisUI() : consecutive_power_keys(0) {}

    virtual KeyAction CheckKey(int key) {
        if (IsKeyPressed(KEY_POWER) && key == KEY_VOLUMEUP) {
            return TOGGLE;
        }
        if (key == KEY_POWER) {
            ++consecutive_power_keys;
            if (consecutive_power_keys >= 5) {
                return REBOOT;
            }
        } else {
            consecutive_power_keys = 0;
        }
        return ENQUEUE;
    }
};

ScreenRecoveryUI

如果您在 ScreenRecoveryUI 中使用自己的图片(错误图标、安装动画、进度条),则可以设置变量 animation_fps 来控制动画的速度(以每秒帧数 (FPS) 为单位)。

注意:当前的 interlace-frames.py 脚本允许您将 animation_fps 信息存储到图片本身中。在早期版本的 Android 中,您必须自行设置 animation_fps

如需设置变量 animation_fps,请替换子类中的 ScreenRecoveryUI::Init() 函数。设置值,然后调用 parent Init() 函数以完成初始化。默认值 (20 FPS) 对应默认恢复图片;使用这些图片时,无需提供 Init() 函数。有关图片的详细信息,请参阅恢复界面图片

Device 类

构建好 RecoveryUI 实现后,请定义您的 Device 类(由内置 Device 类派生的子类)。它应该创建一个 UI 类的单一实例,并通过 GetUI() 函数返回该实例:

class TardisDevice : public Device {
  private:
    TardisUI* ui;

  public:
    TardisDevice() :
        ui(new TardisUI) {
    }

    RecoveryUI* GetUI() { return ui; }

StartRecovery

系统会在恢复开始时调用 StartRecovery() 方法,具体时间是在界面初始化完毕且参数已得到解析,但尚未执行任何操作时。默认的实现不会执行任何操作,因此,如果您没有可执行的操作,则无需在子类中提供此项。

   void StartRecovery() {
       // ... do something tardis-specific here, if needed ....
    }

提供和管理恢复菜单

系统会调用两种方法来获取标头行列表和项列表。在此实现中,系统会返回文件顶部定义的静态数组:

const char* const* GetMenuHeaders() { return HEADERS; }
const char* const* GetMenuItems() { return ITEMS; }

HandleMenuKey

接下来请提供 HandleMenuKey() 函数。该函数以按键操作和当前菜单可见性为输入参数,并确定要执行哪项操作。

   int HandleMenuKey(int key, int visible) {
        if (visible) {
            switch (key) {
              case KEY_VOLUMEDOWN: return kHighlightDown;
              case KEY_VOLUMEUP:   return kHighlightUp;
              case KEY_POWER:      return kInvokeItem;
            }
        }
        return kNoAction;
    }

该方法会提取按键代码(之前已通过界面对象的 CheckKey() 方法进行处理并加入队列),以及菜单/文本日志可见性的当前状态。返回值为整数。如果值不小于 0,则被视为会立即调用的菜单项的位置(请参阅下方的 InvokeMenuItem() 方法)。否则,它可能是以下预设常量之一:

  • kHighlightUp:突出显示菜单中的上一项
  • kHighlightDown:突出显示菜单中的下一项
  • kInvokeItem:激活当前突出显示的项
  • kNoAction:不因此次按键执行任何操作

您可以根据 visible 参数而猜到,即使在菜单不可见时,系统也会调用 HandleMenuKey()。与 CheckKey() 不同的是,当恢复系统执行清除数据或安装软件包等操作时,系统不会调用该函数,仅当恢复系统处于闲置状态并等待输入时,系统才会调用该函数。

轨迹球机制

如果您的设备采用类似于轨迹球的输入机制(生成类型为 EV_REL、代码为 REL_Y 的输入事件),那么,只要类似于轨迹球的输入设备报告 Y 轴的动作,恢复系统就会合成 KEY_UP 和 KEY_DOWN 按键。您只需将 KEY_UP 和 KEY_DOWN 事件映射到相应的菜单操作即可。由于无法针对 CheckKey() 实现此映射,因此您不能将轨迹球运动用作重新启动或切换显示状态的触发器。

辅助键

如需查看作为辅助键按下的键,请调用您自己的界面对象的 IsKeyPressed() 方法。例如,在某些设备上,在恢复系统中按 Alt-W 会启动数据清除(无论菜单是否可见)。您可以按如下方式实现:

   int HandleMenuKey(int key, int visible) {
        if (ui->IsKeyPressed(KEY_LEFTALT) && key == KEY_W) {
            return 2;  // position of the "wipe data" item in the menu
        }
        ...
    }

注意:如果 visible 为 false,则返回用于操控菜单(移动突出显示亮标、调用突出显示项)的特殊值将毫无意义,因为用户看不到突出显示亮标。不过,您可以视需要返回相应的值。

InvokeMenuItem

接下来,提供 InvokeMenuItem() 方法,将由 GetMenuItems() 返回的项数组中的整数位置映射到相应的操作。对于 tardis 示例中的项数组,请使用:

   BuiltinAction InvokeMenuItem(int menu_position) {
        switch (menu_position) {
          case 0: return REBOOT;
          case 1: return APPLY_ADB_SIDELOAD;
          case 2: return WIPE_DATA;
          case 3: return WIPE_CACHE;
          default: return NO_ACTION;
        }
    }

该方法可以返回 BuiltinAction 枚举的任何成员,以指示系统执行相应的操作(如果您不希望系统执行任何操作,则返回 NO_ACTION 成员)。您可以在这里提供除系统功能以外的其他恢复功能:在您的菜单中为其添加项,在调用菜单项时在此处执行此项,以及返回 NO_ACTION 以便让系统不执行其他任何操作。

BuiltinAction 包含以下值:

  • NO_ACTION:不进行任何操作。
  • REBOOT:退出恢复系统,并正常重新启动设备。
  • APPLY_EXT、APPLY_CACHE、APPLY_ADB_SIDELOAD:从不同的位置安装更新程序包。如需了解详情,请参阅旁加载
  • WIPE_CACHE:仅将 cache 分区重新格式化。无需确认,因为此操作相对来说没有什么不良后果。
  • WIPE_DATA:将 userdata 和 cache 分区重新格式化,又称为恢复出厂设置。用户需要先确认这项操作,然后才能继续。

最后一种方法 WipeData() 是可选的,每当启动数据清除操作时(通过菜单从恢复系统执行,或当用户选择从主系统恢复出厂设置时)都会调用。该方法在清除 userdata 和 cache 分区之前调用。如果您的设备将用户数据存储在这两个分区之外的其他位置,您应在此处清空数据。您应返回 0 以表示成功,返回其他值以表示失败,不过目前系统会忽略返回值。无论您返回成功还是失败,userdata 和 cache 分区都会被清除。

   int WipeData() {
       // ... do something tardis-specific here, if needed ....
       return 0;
    }

生成 Device 类

最后,在 recovery_ui.cpp 文件的末尾添加一些用于 make_device() 函数的样板文件,该函数创建并返回 Device 类实例:

class TardisDevice : public Device {
   // ... all the above methods ...
};

Device* make_device() {
    return new TardisDevice();
}

完成 recovery_ui.cpp 文件后,编译该文件并将其链接到您设备上的 recovery 分区。在 Android.mk 中,创建一个只包含此 C++ 文件的静态库:

device/yoyodyne/tardis/recovery/Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := eng
LOCAL_C_INCLUDES += bootable/recovery
LOCAL_SRC_FILES := recovery_ui.cpp

# should match TARGET_RECOVERY_UI_LIB set in BoardConfig.mk
LOCAL_MODULE := librecovery_ui_tardis

include $(BUILD_STATIC_LIBRARY)

然后,在该设备的板配置中,将静态库指定为 TARGET_RECOVERY_UI_LIB 的值。

device/yoyodyne/tardis/BoardConfig.mk
 [...]

# device-specific extensions to the recovery UI
TARGET_RECOVERY_UI_LIB := librecovery_ui_tardis

恢复界面图片

恢复用户界面由图片组成。在理想情况下,用户绝不会与该界面互动:在正常更新过程中,手机会启动进入恢复模式,填充安装进度条,并在无需用户做任何输入的情况下启动返回新系统。如果系统更新出现问题,唯一可以执行的用户操作是呼叫客服中心。

只含图片的界面无需进行本地化。不过,自 Android 5.0 起,更新会显示一串文本(如“正在安装系统更新…”)以及图片。如需了解详情,请参阅经过本地化的恢复文本

Android 5.0 及更高版本

Android 5.0 及更高版本的恢复界面采用两种主要图片:错误图片和正在安装动画。

在 ota 错误期间显示的图片

图 1. icon_error.png

在 ota 安装期间显示的图片

图 2. icon_installing.png

“正在安装”动画由一张 PNG 图片表示,动画的各帧行行交错(这就是图 2 呈现挤压效果的原因)。例如,为 200x200 的七帧动画创建一张 200x1400 的图片,其中第一帧对应第 0、7、14、21…行,第二帧对应第 1、8、15、22...行,以此类推。合并的图片包含表示动画帧数和每秒帧数 (FPS) 的文本块。bootable/recovery/interlace-frames.py 工具需要处理一组输入帧,并将其合并到 recovery 所用的必要合成图片中。

默认图片提供多种不同密度的版本,所在位置是 bootable/recovery/res-$DENSITY/images (例如,bootable/recovery/res-hdpi/images)。如需在安装过程中使用静态图片,您只需提供 icon_installing.png 图片,并将动画中的帧数设置为 0(错误图标不是动画;该图片一律为静态图片)即可。

Android 4.x 及更低版本

Android 4.x 及更低版本的恢复界面会采用错误图片(如上图所示)、正在安装动画以及几张叠加图片:

在 ota 安装期间显示的图片

图 3. icon_installing.png

作为首次叠加显示的图片

图 4. icon-installing_overlay01.png

作为第七次叠加显示的图片

图 5. icon_installing_overlay07.png

在安装过程中,屏幕显示通过绘制 icon_installing.png 图片进行构建,然后在适当的偏移量处绘制其中一张叠加帧。图中叠加的红色方框用来突出显示叠加帧在基本图片上的放置位置:

安装加首次叠加的合成图片

图 6. “正在安装”动画帧 1 (icon_installing.png + icon_installing_overlay01.png)

安装加第七次叠加的合成图片

图 7. “正在安装”动画帧 7 (icon_installing.png + icon_installing_overlay07.png)

后续帧通过只绘制下一张已位于顶部的叠加图片显示;基本图片不会重新绘制。

动画中的帧数、所需速度以及叠加图片相对于基本图片的 x 轴和 y 轴偏移量均通过 ScreenRecoveryUI 类的成员变量来设置。如果您使用的是自定义图片而不是默认图片,请替换子类中的 Init() 方法,以便更改自定义图片的这些值(如需了解详情,请参阅 ScreenRecoveryUI)。bootable/recovery/make-overlay.py 脚本可协助将一组图片帧转为 recovery 所需的“基本图片 + 叠加图片”,其中包括计算所需的偏移量。

默认图片位于以下位置:bootable/recovery/res/images。如需在安装过程中使用静态图片,您只需提供 icon_installing.png 图片,并将动画中的帧数设置为 0(错误图标不是动画;该图片一律为静态图片)即可。

经过本地化的恢复文本

Android 5.x 会显示一串文本(例如,正在安装系统更新…”)以及图片。如果主系统启动进入恢复模式,系统会将用户当前的语言区域作为命令行选项传递到恢复系统。对于每条要显示的消息,恢复系统都会为每个语言区域中的相应消息添加另一张带有预呈现文本字符串的合成图片。

恢复文本字符串的示例图片:

恢复文字的图片

图 8. 恢复消息的本地化文本

恢复文本会显示以下消息:

  • 正在安装系统更新…
  • 出错了!
  • 正在清除…(执行数据清除/恢复出厂设置时)
  • 无命令(用户手动启动进入恢复模式时)

bootable/recovery/tools/recovery_l10n/ 中的 Android 应用会呈现经过本地化的消息并创建合成图片。如需详细了解如何使用此应用,请参阅 bootable/recovery/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java 中的注释。

如果用户手动启动进入恢复模式,则语言区域可能不可用,且不会显示任何文本。不要让文本消息对恢复流程产生太多制约影响。

注意:隐藏界面(可显示日志消息并允许用户从菜单中选择操作)仅提供英文版。

进度条

进度条会显示在主要图片(或动画)的下方。进度条由两张输入图片(大小必须相同)合并而成:

进度为零的进度条

图 9. progress_empty.png

完全完成的进度条

图 10. progress_fill.png

fill 图片的左端显示在 empty 图片右端的旁边,从而形成进度条。两张图片之间的边界位置会不时变更,以表示相应的进度。以上述几对输入图片为例,显示效果为:

进度为 1% 的进度条

图 11. 进度条显示为 1%>

进度为 10% 的进度条

图 12. 进度为 10% 的进度条

进度为 50% 的进度条

图 13. 进度为 50% 的进度条

您可以将这些图片的设备专属版本放入(在本例中)device/yoyodyne/tardis/recovery/res/images 中,以提供这类版本的图片。文件名必须与上面列出的文件名相符;如果可在该目录下找到文件,则编译系统会优先使用该文件,而非对应的默认图片。仅支持采用 8 位色深的 RGB 或 RGBA 格式的 PNG 文件。

注意:在 Android 5.x 中,如果恢复模式下的语言区域是已知的,且采用从右至左 (RTL) 的语言模式(例如阿拉伯语、希伯来语等),则进度条将会按照从右向左的顺序进行填充。

没有屏幕的设备

并非所有 Android 设备都有屏幕。如果您的设备是无头装置或采用纯音频界面,那么您可能需要对恢复界面进行更多自定义设置。请勿创建 ScreenRecoveryUI 的子类,而是直接针对其父类 RecoveryUI 创建子类。

RecoveryUI 具有处理低级界面操作(如“切换显示”、“更新进度条”、“显示菜单”、“更改菜单选项”等)的方法。您可以替换这些操作以提供适合您设备的界面。也许您的设备有 LED,这样您可以使用不同的颜色或闪烁图案来指示状态;或许您还可以播放音频(也许您根本不想支持菜单或“文本显示”模式;您可以使用 CheckKey()HandleMenuKey() 实现来阻止访问它们,这些实现永远不会切换显示或选择菜单项。在这种情况下,您需要提供的很多 RecoveryUI 方法都可以只是空的存根)。

请参阅 bootable/recovery/ui.h 了解 RecoveryUI 声明,以查看您必须支持哪些方法。RecoveryUI 是抽象的(有些方法是纯虚拟的,必须由子类提供),但它包含处理键输入内容的代码。如果您的设备没有键或者您希望通过其他方式处理这些内容,也可以将其替换掉。

更新程序

您可以提供自己的扩展函数(可从您的更新程序脚本中调用),从而在安装更新程序包的过程中使用设备专属代码。以下是适用于 tardis 设备的示例函数:

device/yoyodyne/tardis/recovery/recovery_updater.c
#include <stdlib.h>
#include <string.h>

#include "edify/expr.h"

每个扩展函数都采用相同的签名。具体参数即调用函数时所用的名称,State* Cookie、传入参数的数量和表示参数的 Expr* 指针数组。返回值是新分配的 Value*

Value* ReprogramTardisFn(const char* name, State* state, int argc, Expr* argv[]) {
    if (argc != 2) {
        return ErrorAbort(state, "%s() expects 2 args, got %d", name, argc);
    }

您的参数在您调用函数时尚未求值,函数的逻辑决定了会对哪些参数求值以及求值多少次。因此,您可以使用扩展函数来实现自己的控制结构。Call Evaluate() 可用来对 Expr* 参数求值,返回 Value*。如果 Evaluate() 返回 NULL,您应该释放所持有的所有资源,并立即返回 NULL(此操作会将 abort 传播到 edify 堆栈中)。否则,您将获得所返回 Value 的所有权,并负责最终对其调用 FreeValue()

假设该函数需要两种参数:值为字符串的 key 和值为 blob 的 image。您可能会看到如下参数:

   Value* key = EvaluateValue(state, argv[0]);
    if (key == NULL) {
        return NULL;
    }
    if (key->type != VAL_STRING) {
        ErrorAbort(state, "first arg to %s() must be string", name);
        FreeValue(key);
        return NULL;
    }
    Value* image = EvaluateValue(state, argv[1]);
    if (image == NULL) {
        FreeValue(key);    // must always free Value objects
        return NULL;
    }
    if (image->type != VAL_BLOB) {
        ErrorAbort(state, "second arg to %s() must be blob", name);
        FreeValue(key);
        FreeValue(image)
        return NULL;
    }

为多个参数检查 NULL 并释放之前求值的参数可能会很繁琐。ReadValueArgs() 函数会让此变得更简单。您可以不使用上面的代码,而是写入下面的代码:

   Value* key;
    Value* image;
    if (ReadValueArgs(state, argv, 2, &key, &image) != 0) {
        return NULL;     // ReadValueArgs() will have set the error message
    }
    if (key->type != VAL_STRING || image->type != VAL_BLOB) {
        ErrorAbort(state, "arguments to %s() have wrong type", name);
        FreeValue(key);
        FreeValue(image)
        return NULL;
    }

ReadValueArgs() 不会执行类型检查,因此您必须在这里执行这项检查;使用一个 if 语句执行这项检查更方便,不过这样做也有一个弊端,那就是,如果操作失败,所显示的错误消息不够具体。但是,如果求值失败,ReadValueArgs() 会处理每个参数的求值操作,并释放之前求值的所有参数(以及设置有用的错误消息)。您可以使用 ReadValueVarArgs() 便捷函数对数量不定的参数进行求值(它会返回 Value* 数组)。

对参数进行求值后,执行以下函数:

   // key->data is a NUL-terminated string
    // image->data and image->size define a block of binary data
    //
    // ... some device-specific magic here to
    // reprogram the tardis using those two values ...

返回值必须是 Value* 对象;此对象的所有权将传递给调用程序。调用程序将获得此 Value* 所指向的所有数据的所有权,特别是数据成员。

在这种情况下,您需要返回 true 或 false 值来表示成功。请记住以下惯例:空字符串为 false,所有其他字符串均为 true。您必须使用要返回的常量字符串的经过 malloc 处理的副本来对 Value 对象进行 malloc 处理,因为调用程序会 free() 这两者。别忘了通过对参数求值在获得的对象上调用 FreeValue()

   FreeValue(key);
    FreeValue(image);

    Value* result = malloc(sizeof(Value));
    result->type = VAL_STRING;
    result->data = strdup(successful ? "t" : "");
    result->size = strlen(result->data);
    return result;
}

便捷函数 StringValue() 会将字符串封装到新的 Value 对象中。使用此函数可以更精简地编写上述代码:

   FreeValue(key);
    FreeValue(image);

    return StringValue(strdup(successful ? "t" : ""));
}

如需将函数挂接到 edify 解释器中,请提供函数 Register_foo(其中 foo 是该代码所在静态库的名称)。调用 RegisterFunction() 可注册各个扩展函数。按照惯例,您需要对设备专属函数 device.whatever 进行命名,以免与将来添加的内置函数发生冲突。

void Register_librecovery_updater_tardis() {
    RegisterFunction("tardis.reprogram", ReprogramTardisFn);
}

现在,您可以配置 makefile,以使用您的代码编译静态库(此 makefile 即是在之前的部分中自定义恢复界面时使用的 makefile;您设备的两个静态库可能都是在此处定义的)。

device/yoyodyne/tardis/recovery/Android.mk
include $(CLEAR_VARS)
LOCAL_SRC_FILES := recovery_updater.c
LOCAL_C_INCLUDES += bootable/recovery

静态库的名称必须与其中包含的 Register_libname 函数的名称相匹配。

LOCAL_MODULE := librecovery_updater_tardis
include $(BUILD_STATIC_LIBRARY)

最后,配置 recovery 的编译版本以拉入您的库。将您的库添加到 TARGET_RECOVERY_UPDATER_LIBS(它可能包含多个库;所有库均已注册)。如果您的代码依赖于本身不是 edify 扩展的其他静态库(即它们没有 Register_libname 函数),您可以在 TARGET_RECOVERY_UPDATER_EXTRA_LIBS 中列出这些库,以便将其关联到更新程序,而无需调用其(不存在的)注册函数。例如,如果您的设备专属代码需要使用 zlib 解压缩数据,您可以在此处包含 libz。

device/yoyodyne/tardis/BoardConfig.mk
 [...]

# add device-specific extensions to the updater binary
TARGET_RECOVERY_UPDATER_LIBS += librecovery_updater_tardis
TARGET_RECOVERY_UPDATER_EXTRA_LIBS +=

您的 OTA 更新包中的更新程序脚本现已可以像其他脚本一样调用您的函数。如需重新对您的 tardis 设备进行编程,更新脚本可能包含:tardis.reprogram("the-key", package_extract_file("tardis-image.dat")) 。它会使用单参数版本的内置函数 package_extract_file(),该函数会将从更新程序包中提取的文件内容作为 blob 返回,从而为新的扩展函数生成第二个参数。

生成 OTA 更新包

最终的组件是获取 OTA 更新包生成工具以了解您的设备专属数据,并发出 (emit) 包含对您的扩展函数进行调用的更新程序脚本。

首先,让编译系统了解设备专属数据 blob。假设您的数据文件位于 device/yoyodyne/tardis/tardis.dat 中,请在设备的 AndroidBoard.mk 中声明以下内容:

device/yoyodyne/tardis/AndroidBoard.mk
  [...]

$(call add-radio-file,tardis.dat)

您也可以将其放在 Android.mk 中,但是之后必须通过设备检查提供保护,因为无论构建什么设备,树中的所有 Android.mk 文件都会加载。(如果您的树中包含多个设备,那么您只需要在构建 tardis 设备时添加 tardis.dat 文件即可)。

device/yoyodyne/tardis/Android.mk
  [...]

# an alternative to specifying it in AndroidBoard.mk
ifeq (($TARGET_DEVICE),tardis)
  $(call add-radio-file,tardis.dat)
endif

由于历史原因,这些文件被称为无线电文件,但它们可能与设备无线电(如果存在)没有任何关系。它们只是编译系统复制到 OTA 生成工具所用的 target-files .zip 中的模糊数据 blob。在您执行编译时,tardis.dat 会作为 RADIO/tardis.dat 存储在 target-files.zip 中。您可以多次调用 add-radio-file,以根据需要添加任意数量的文件。

Python 模块

要扩展发布工具,请编写工具(如果有)可以调用的 Python 模块(必须命名为 releasetools.py)。例如:

device/yoyodyne/tardis/releasetools.py
import common

def FullOTA_InstallEnd(info):
  # copy the data into the package.
  tardis_dat = info.input_zip.read("RADIO/tardis.dat")
  common.ZipWriteStr(info.output_zip, "tardis.dat", tardis_dat)

  # emit the script code to install this data on the device
  info.script.AppendExtra(
      """tardis.reprogram("the-key", package_extract_file("tardis.dat"));""")

独立的函数可以处理生成增量 OTA 更新包的情况。在本例中,假设您只需要在两个版本号之间的 tardis.dat 文件发生更改时重新编程 tardis。

def IncrementalOTA_InstallEnd(info):
  # copy the data into the package.
  source_tardis_dat = info.source_zip.read("RADIO/tardis.dat")
  target_tardis_dat = info.target_zip.read("RADIO/tardis.dat")

  if source_tardis_dat == target_tardis_dat:
      # tardis.dat is unchanged from previous build; no
      # need to reprogram it
      return

  # include the new tardis.dat in the OTA package
  common.ZipWriteStr(info.output_zip, "tardis.dat", target_tardis_dat)

  # emit the script code to install this data on the device
  info.script.AppendExtra(
      """tardis.reprogram("the-key", package_extract_file("tardis.dat"));""")

模块函数

您可以在模块中提供以下函数(仅实现所需函数)。

FullOTA_Assertions()
在即将开始生成完整 OTA 时调用。此时非常适合发出 (emit) 关于设备当前状态的断言。请勿发出 (emit) 对设备进行更改的脚本命令。
FullOTA_InstallBegin()
在关于设备状态的断言都已传递但尚未进行任何更改时调用。您可以发出 (emit) 用于设备专属更新的命令(必须在设备上的其他任何内容发生更改之前运行)。
FullOTA_InstallEnd()
在脚本生成流程结束且已发出 (emit) 脚本命令(用于更新 boot 和 system 分区)后调用。您还可以发出 (emit) 用于设备专属更新的其他命令。
IncrementalOTA_Assertions()
FullOTA_Assertions() 类似,但在生成增量更新包时调用。
IncrementalOTA_VerifyBegin()
在关于设备状态的断言都已传递但尚未进行任何更改时调用。您可以发出 (emit) 用于设备专属更新的命令(必须在设备上的其他任何内容发生更改之前运行)。
IncrementalOTA_VerifyEnd()
在验证阶段结束且脚本确认即将接触的文件具有预期开始内容时调用。此时,设备上的内容尚未发生任何更改。您还可以发出 (emit) 用于其他设备专属验证的代码。
IncrementalOTA_InstallBegin()
在要修补的文件已被验证为具有预期 before 状态但尚未进行任何更改时调用。您可以发出 (emit) 用于设备专属更新的命令(必须在设备上的其他任何内容发生更改之前运行)。
IncrementalOTA_InstallEnd()
与其完整的 OTA 更新包类似的是,这项函数在脚本生成结束阶段且已发出 (emit) 用于更新 boot 和 system 分区的脚本命令后调用。您还可以发出 (emit) 用于设备专属更新的其他命令。

注意:如果设备电量耗尽了,OTA 安装可能会从头重新开始。请准备好针对已全部或部分运行这些命令的设备进行相应的操作。

将函数传递到 info 对象

将函数传递到包含各种实用项的单个 info 对象:

  • info.input_zip:(仅限完整 OTA)输入 target-files .zip 的 zipfile.ZipFile 对象。
  • info.source_zip:(仅限增量 OTA)源 target-files .zip 的 zipfile.ZipFile 对象(安装增量更新包时已在设备上的编译版本)
  • info.target_zip:(仅限增量 OTA)目标 target-files .zip 的 zipfile.ZipFile 对象(增量更新包置于设备上的编译版本)。
  • info.output_zip:正在创建更新包;打开一个 zipfile.ZipFile 对象,以进行写入。使用 common.ZipWriteStr(info.output_zip、filename、data)将文件添加到更新包。
  • info.script:可以附加命令的目标脚本对象。调用 info.script.AppendExtra(script_text) 以将文本输出到脚本中。请确保输出文本以英文分号结尾,这样就不会遇到随后发出 (emit) 的命令。

有关 info 对象的详细信息,请参阅针对 ZIP 归档的 Python 软件基础文档

指定模块位置

指定您设备的 releasetools.py 脚本在 BoardConfig.mk 文件中的位置:

device/yoyodyne/tardis/BoardConfig.mk
 [...]

TARGET_RELEASETOOLS_EXTENSIONS := device/yoyodyne/tardis

如果未设置 TARGET_RELEASETOOLS_EXTENSIONS,则默认位置为 $(TARGET_DEVICE_DIR)/../common 目录(在本例中为 device/yoyodyne/common )。最好明确指定 releasetools.py 脚本的位置。编译 tardis 设备时,releasetools.py 脚本会包含在 target-files .zip 文件 (META/releasetools.py ) 中。

当您运行发布工具(img_from_target_filesota_from_target_files)时,target-files .zip 中的 releasetools.py 脚本(如果存在)优先于 Android 源代码树中的脚本而执行。您还可以通过优先级最高的 -s(或 --device_specific)选项明确指定设备专属扩展程序的路径。这样一来,您就可以在发布工具扩展程序中更正错误及做出更改,并将这些更改应用于旧的目标文件。

现在,当您运行 ota_from_target_files 时,它会自动从 target_files .zip 文件获取设备专属模块,并在生成 OTA 更新包时使用该模块:

./build/make/tools/releasetools/ota_from_target_files \
    -i PREVIOUS-tardis-target_files.zip \
    dist_output/tardis-target_files.zip \
    incremental_ota_update.zip

或者,您可以在运行 ota_from_target_files 时指定设备专属扩展程序。

./build/make/tools/releasetools/ota_from_target_files \
    -s device/yoyodyne/tardis \
    -i PREVIOUS-tardis-target_files.zip \
    dist_output/tardis-target_files.zip \
    incremental_ota_update.zip

注意:如需查看完整的选项列表,请参阅 build/make/tools/releasetools/ota_from_target_files 中的 ota_from_target_files 注释。

旁加载

恢复系统提供旁加载机制,可手动安装更新包(无需主系统通过无线方式下载)。旁加载有助于在主系统无法启动的设备上进行调试或更改。

在过去,旁加载都是通过将更新包下载到设备的 SD 卡上而完成,如果设备无法启动,则可以使用其他计算机将更新包写入 SD 卡中,然后将 SD 卡插入设备中。为了支持没有可拆卸外部存储设备的 Android 设备,恢复系统还支持另外两种旁加载机制:从 cache 分区加载更新包,以及使用 adb 通过 USB 进行加载。

如需调用各种旁加载机制,您设备的 Device::InvokeMenuItem() 方法可以返回以下 BuiltinAction 值:

  • APPLY_EXT:从外部存储空间( /sdcard 目录)旁加载更新包。您的 recovery.fstab 必须定义 /sdcard 装载点。此方法在通过符号链接到 /data 来模拟 SD 卡(或其他类似机制)的设备上不可用。/data 通常不可用于恢复系统,因为它可能会被加密。恢复界面会显示 /sdcard 中的 .zip 文件菜单,以便用户进行选择。
  • APPLY_CACHE:类似于从 /sdcard 加载更新包,不过使用的是 /cache 目录(始终可供恢复系统使用)。在常规系统中,/cache 只能由特权用户写入;如果设备不可启动,则完全无法写入 /cache 目录(这样一来,该机制的效用就会有所限制)。
  • APPLY_ADB_SIDELOAD:允许用户通过 USB 数据线和 adb 开发工具将更新包发送到设备。调用此机制时,恢复系统将启动自身的迷你版 adbd 守护程序,以便已连接的主机上的 adb 与其进行通信。该迷你版守护程序仅支持一个命令:adb sideload filename。已命名的文件会从主机发送到设备,然后接受验证并进行安装(如同文件在本地存储区中一样)。

一些注意事项:

  • 仅支持 USB 传输。
  • 如果您的恢复系统可以正常运行 adbd(对于 userdebug 和 eng 版本来说通常是这样),则会在设备处于 adb 旁加载模式时关闭,并将在 adb 旁加载完成接收更新包后重新启动。在 adb 旁加载模式下,只有 sideload 命令可以发挥作用(logcatrebootpushpullshell 等都不起作用)。
  • 您无法在设备上退出 adb 旁加载模式。如需终止,您可以发送 /dev/null(或其他不是有效更新包的其他任何内容)作为更新包,然后设备将无法对其进行验证,并停止安装过程。对于按键事件,系统会继续调用 RecoveryUI 实现的 CheckKey() 方法,因此,您可以提供可重新启动设备并在 adb 旁加载模式下运行的按键序列。