设备专用代码

The recovery system includes several hooks for inserting device-specific code so that OTA updates can also update parts of the device other than the Android system (e.g., the baseband or radio processor).

The following sections and examples customize the tardis device produced by the yoyodyne vendor.

Partition map

As of Android 2.3, the platform supports eMMc flash devices and the ext4 filesystem that runs on those devices. It also supports Memory Technology Device (MTD) flash devices and the yaffs2 filesystem from older releases.

The partition map file is specified by TARGET_RECOVERY_FSTAB; this file is used by both the recovery binary and the package-building tools. You can specify the name of the map file in TARGET_RECOVERY_FSTAB in BoardConfig.mk.

A sample partition map file might look like this

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

With the exception of /sdcard, which is optional, all mount points in this example must be defined (devices may also add extra partitions). There are five supported filesystem types

yaffs2
A yaffs2 filesystem atop an MTD flash device. "device" must be the name of the MTD partition and must appear in /proc/mtd.
mtd
A raw MTD partition, used for bootable partitions such as boot and recovery. MTD is not actually mounted, but the mount point is used as a key to locate the partition. "device" must be the name of the MTD partition in /proc/mtd.
ext4
An ext4 filesystem atop an eMMc flash device. "device" must be the path of the block device.
emmc
A raw eMMc block device, used for bootable partitions such as boot and recovery. Similar to the mtd type, eMMc is never actually mounted, but the mount point string is used to locate the device in the table.
vfat
A FAT filesystem atop a block device, typically for external storage such as an SD card. The device is the block device; device2 is a second block device the system attempts to mount if mounting the primary device fails (for compatibility with SD cards which may or may not be formatted with a partition table).

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

从 Android 3.0 开始,recovery.fstab 文件增加了一个额外的可选字段,即选项。目前唯一定义的选项是 length,它允许您显式指定分区的大小。此长度用于重新格式化分区(例如,在数据擦除/恢复出厂设置操作期间用于 userdata 分区,或在安装完整 OTA 软件包期间用于 system 分区)。如果 length 值为负数,则要格式化的大小通过将 length 值加到分区的实际大小来确定。例如,设置“length=-16384”意味着当重新格式化该分区时,该分区的最后 16k 将不会被覆盖。这支持诸如 userdata 分区加密之类的功能(其中加密元数据存储在不应被覆盖的分区末尾)。

注意: device2options 字段是可选的,这会在解析时造成歧义。如果行中第四个字段的条目以“/”字符开头,则将其视为 device2 条目;如果该条目不以“/”字符开头,则将其视为 options 字段。

启动动画

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

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

注意: 这些图像必须符合 Android 品牌指南。有关品牌指南,请参阅 合作伙伴营销中心 的 Android 部分。

恢复界面 (Recovery UI)

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

您的目标是构建一个小型静态库,其中包含几个 C++ 对象,以提供特定于设备的功能。默认情况下使用文件 bootable/recovery/default_device.cpp,在为您的设备编写此文件的版本时,可以将其用作良好的起点进行复制。

注意: 您可能会在此处看到一条消息,提示No Command。要切换文本,请按住电源按钮并同时按下音量增大按钮。如果您的设备没有这两个按钮,请长按任意按钮以切换文本。

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 设备具有屏幕,因此您可以从内置的 ScreenRecoveryUI 实现继承(有关无屏幕设备的说明,请参阅相关部分。)要从 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() 无论恢复的其余部分在做什么都会被调用:当菜单关闭时、当菜单打开时、在软件包安装期间、在 userdata 擦除期间等等。它可以返回四个常量之一:

  • TOGGLE。切换菜单和/或文本日志的显示开/关
  • REBOOT。立即重启设备
  • IGNORE。忽略此按键
  • ENQUEUE。将此按键放入队列中以进行同步消费(即,如果显示已启用,则由恢复菜单系统消费)

CheckKey() 在每次按键按下事件之后,紧跟着同一个按键的按键抬起事件时被调用。(事件序列 A-按下 B-按下 B-抬起 A-抬起 仅导致 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 类子类化)。它应该创建 UI 类的一个实例,并从 GetUI() 函数返回该实例。

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

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

    RecoveryUI* GetUI() { return ui; }

StartRecovery

StartRecovery() 方法在恢复开始时被调用,在 UI 初始化之后以及参数解析之后,但在采取任何操作之前。默认实现不执行任何操作,因此如果您没有任何操作要执行,则无需在您的子类中提供此方法。

   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;
    }

该方法接受一个按键代码(该代码先前已由 UI 对象的 CheckKey() 方法处理并放入队列),以及菜单/文本日志可见性的当前状态。返回值是一个整数。如果该值大于等于 0,则将其视为菜单项的位置,该菜单项将立即被调用(请参阅下面的 InvokeMenuItem() 方法)。否则,它可以是以下预定义的常量之一:

  • kHighlightUp。将菜单高亮移动到上一个项目
  • kHighlightDown。将菜单高亮移动到下一个项目
  • kInvokeItem。调用当前高亮显示的项目
  • kNoAction。对此按键不执行任何操作

顾名思义,即使菜单不可见,也会调用 HandleMenuKey()。与 CheckKey() 不同,它在恢复正在执行诸如擦除数据或安装软件包之类的操作时不会被调用——它仅在恢复空闲并等待输入时被调用。

轨迹球机制

如果您的设备具有类似轨迹球的输入机制(生成类型为 EV_REL 和代码为 REL_Y 的输入事件),则每当类似轨迹球的输入设备报告 Y 轴上的运动时,恢复就会合成 KEY_UP 和 KEY_DOWN 按键。您需要做的就是将 KEY_UP 和 KEY_DOWN 事件映射到菜单操作。此映射不会发生在 CheckKey() 中,因此您不能使用轨迹球运动作为重启或切换显示的触发器。

修饰键

要检查按键是否作为修饰键被按住,请调用您自己的 UI 对象的 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。仅重新格式化缓存分区。无需确认,因为这相对无害。
  • WIPE_DATA。重新格式化 userdata 和缓存分区,也称为恢复出厂设置。在继续操作之前,系统会要求用户确认此操作。

最后一个方法 WipeData() 是可选的,并且在每次启动数据擦除操作时调用(无论是在恢复模式下通过菜单启动,还是在用户选择从主系统执行恢复出厂设置时启动)。此方法在用户数据和缓存分区被擦除之前调用。如果您的设备在除这两个分区之外的任何位置存储用户数据,则应在此处将其擦除。您应该返回 0 表示成功,返回另一个值表示失败,尽管目前返回值被忽略。无论您返回成功还是失败,用户数据和缓存分区都会被擦除。

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

制作设备

最后,在 recovery_ui.cpp 文件的末尾包含一些样板代码,用于创建和返回您的 Device 类实例的 make_device() 函数。

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

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

完成 recovery_ui.cpp 文件后,构建它并将其链接到您设备上的恢复。在 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

恢复界面图像

恢复用户界面由图像组成。理想情况下,用户永远不会与 UI 交互:在正常更新期间,手机启动进入恢复模式,填充安装进度条,然后启动回新系统,而无需用户输入。如果系统更新出现问题,唯一可以采取的用户操作是致电客户服务。

仅图像界面消除了本地化的需要。但是,从 Android 5.0 开始,更新可以显示文本字符串(例如,“正在安装系统更新...”)以及图像。有关详细信息,请参阅 本地化恢复文本

Android 5.0 及更高版本

Android 5.0 及更高版本的恢复 UI 使用两个主要图像:error 图像和 installing 动画。

image shown during ota error

图 1. icon_error.png

image shown during ota install

图 2. icon_installing.png

安装动画表示为单个 PNG 图像,动画的不同帧按行交错排列(这就是图 2 看起来被压缩的原因)。例如,对于 200x200 的七帧动画,创建一个 200x1400 的图像,其中第一帧是行 0、7、14、21、...;第二帧是行 1、8、15、22、...;等等。组合图像包含一个文本块,指示动画帧数和每秒帧数 (FPS)。工具 bootable/recovery/interlace-frames.py 接受一组输入帧,并将它们组合成恢复所需的复合图像。

默认图像以不同的密度提供,并且位于 bootable/recovery/res-$DENSITY/images 中(例如,bootable/recovery/res-hdpi/images)。要在安装期间使用静态图像,您只需要提供 icon_installing.png 图像并将动画中的帧数设置为 0(错误图标不是动画的;它始终是静态图像)。

Android 4.x 及更早版本

Android 4.x 及更早版本的恢复 UI 使用 error 图像(如上所示)和 installing 动画以及几个叠加图像。

image shown during ota install

图 3. icon_installing.png

image shown as first
overlay

图 4. icon-installing_overlay01.png

image shown as seventh
overlay

图 5. icon_installing_overlay07.png

在安装期间,屏幕显示是通过绘制 icon_installing.png 图像,然后在其顶部以适当的偏移量绘制其中一个叠加帧来构建的。在这里,叠加了一个红色框,以突出显示叠加层放置在基本图像顶部的哪个位置。

composite image of
install plus first overlay

图 6. 安装动画帧 1 (icon_installing.png + icon_installing_overlay01.png)

composite image of
install plus seventh overlay

图 7. 安装动画帧 7 (icon_installing.png + icon_installing_overlay07.png)

后续帧通过在已有的内容之上绘制下一个叠加图像来显示;基本图像不会重新绘制。

动画中的帧数、所需速度以及叠加层相对于基本图像的 x 和 y 偏移量由 ScreenRecoveryUI 类的成员变量设置。当使用自定义图像而不是默认图像时,请覆盖子类中的 Init() 方法以更改自定义图像的这些值(有关详细信息,请参阅 ScreenRecoveryUI)。脚本 bootable/recovery/make-overlay.py 可以帮助将一组图像帧转换为恢复所需的“基本图像 + 叠加图像”形式,包括计算必要的偏移量。

默认图像位于 bootable/recovery/res/images 中。要在安装期间使用静态图像,您只需要提供 icon_installing.png 图像并将动画中的帧数设置为 0(错误图标不是动画的;它始终是静态图像)。

本地化恢复文本

Android 5.x 显示文本字符串(例如,“正在安装系统更新...”)以及图像。当主系统启动进入恢复模式时,它会将用户的当前区域设置作为命令行选项传递给恢复。对于要显示的每条消息,恢复都包含第二个复合图像,其中包含每种区域设置的该消息的预渲染文本字符串。

恢复文本字符串的示例图像

image of recovery text

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

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

  • 正在安装系统更新...
  • 错误!
  • 正在擦除...(在执行数据擦除/恢复出厂设置时)
  • No command(当用户手动启动进入恢复模式时)

bootable/recovery/tools/recovery_l10n/ 中的 Android 应用程序渲染消息的本地化版本并创建复合图像。有关使用此应用程序的详细信息,请参阅 bootable/recovery/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java 中的注释。

当用户手动启动进入恢复模式时,区域设置可能不可用,并且不显示文本。不要使文本消息对恢复过程至关重要。

注意: 显示日志消息并允许用户从菜单中选择操作的隐藏界面仅以英语提供。

进度条

进度条可以显示在主图像(或动画)下方。进度条通过组合两个输入图像来制作,这两个输入图像必须大小相同。

empty progress bar

图 9. progress_empty.png

full progress bar

图 10. progress_fill.png

填充图像的左端显示在图像的右端旁边,以制作进度条。更改两个图像之间边界的位置以指示进度。例如,使用上述输入图像对,显示:

progress bar at 1%

图 11. 进度条在 1%> 时

progress bar at 10%

图 12. 进度条在 10% 时

progress bar at 50%

图 13. 进度条在 50% 时

您可以通过将设备特定版本的这些图像放置到(在本示例中)device/yoyodyne/tardis/recovery/res/images 中来提供它们。文件名必须与上面列出的文件名匹配;当在该目录中找到文件时,构建系统将优先使用它而不是相应的默认图像。仅支持 RGB 或 RGBA 格式且颜色深度为 8 位的 PNG。

注意: 在 Android 5.x 中,如果恢复已知区域设置并且该区域设置为从右到左 (RTL) 的语言(阿拉伯语、希伯来语等),则进度条从右向左填充。

无屏幕设备

并非所有 Android 设备都具有屏幕。如果您的设备是无头设备或具有仅音频界面,您可能需要对恢复 UI 进行更广泛的自定义。与其创建 ScreenRecoveryUI 的子类,不如直接子类化其父类 RecoveryUI。

RecoveryUI 具有用于处理较低级别的 UI 操作的方法,例如“切换显示”、“更新进度条”、“显示菜单”、“更改菜单选择”等。您可以覆盖这些方法,为您的设备提供适当的界面。也许您的设备具有 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(这会将中止传播到 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* 指向的任何数据的所有权——特别是 datamember。

在此实例中,您希望返回 true 或 false 值以指示成功。请记住约定,空字符串为false,所有其他字符串为true。您必须 malloc 一个 Value 对象,其中包含要返回的常量字符串的 malloc'd 副本,因为调用者将 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 以构建包含您的代码的静态库。(这与上一节中用于自定义恢复 UI 的 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)

最后,配置恢复的构建以拉取您的库。将您的库添加到 TARGET_RECOVERY_UPDATER_LIBS(可能包含多个库;它们都会被注册)。如果您的代码依赖于其他本身不是 edify 扩展的静态库(即,它们没有 Register_libname 函数),您可以在 TARGET_RECOVERY_UPDATER_EXTRA_LIBS 中列出这些库,以便将它们链接到 updater,而无需调用它们(不存在的)注册函数。例如,如果您的设备特定代码想要使用 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 软件包生成工具了解您的设备特定数据并发出包含对您的扩展函数调用的更新器脚本。

首先,让构建系统了解设备特定的数据 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

由于历史原因,这些被称为 radio 文件;它们可能与设备 radio(如果存在)无关。它们只是构建系统复制到 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 的开始附近调用。这是发出有关设备当前状态的断言的好地方。不要发出对设备进行更改的脚本命令。
FullOTA_InstallBegin()
在关于设备状态的所有断言都通过之后,但在进行任何更改之前调用。您可以发出设备特定更新的命令,这些命令必须在设备上的任何其他内容发生更改之前运行。
FullOTA_InstallEnd()
在脚本生成结束时调用,在发出用于更新 boot 和 system 分区的脚本命令之后。您还可以发出用于设备特定更新的附加命令。
IncrementalOTA_Assertions()
类似于 FullOTA_Assertions(),但在生成增量更新软件包时调用。
IncrementalOTA_VerifyBegin()
在关于设备状态的所有断言都通过之后,但在进行任何更改之前调用。您可以发出设备特定更新的命令,这些命令必须在设备上的任何其他内容发生更改之前运行。
IncrementalOTA_VerifyEnd()
在验证阶段结束时调用,当脚本完成确认它将要接触的文件具有预期的起始内容时。此时,设备上的任何内容都尚未更改。您还可以发出用于其他设备特定验证的代码。
IncrementalOTA_InstallBegin()
在要修补的文件已被验证为具有预期的之前状态之后,但在进行任何更改之前调用。您可以发出设备特定更新的命令,这些命令必须在设备上的任何其他内容发生更改之前运行。
IncrementalOTA_InstallEnd()
与其完整 OTA 软件包对应项类似,这在脚本生成结束时调用,在发出用于更新 boot 和 system 分区的脚本命令之后。您还可以发出用于设备特定更新的附加命令。

注意: 如果设备断电,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。您可以向其追加命令的 Script 对象。调用 info.script.AppendExtra(script_text) 将文本输出到脚本中。确保输出文本以分号结尾,这样它就不会与之后发出的命令冲突。

有关 info 对象的详细信息,请参阅 Python 软件基金会关于 ZIP 存档的文档

指定模块位置

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

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)选项显式指定设备特定扩展的路径,该选项具有最高优先级。这使您能够纠正错误并更改 releasetools 扩展,并将这些更改应用于旧的 target-files。

现在,当您运行 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

注意: 有关选项的完整列表,请参阅 ota_from_target_files 中的注释,路径为 build/make/tools/releasetools/ota_from_target_files

侧加载机制

Recovery 具有侧加载机制,用于手动安装更新包,而无需通过主系统无线下载。侧加载对于在主系统无法启动的设备上进行调试或进行更改非常有用。

从历史上看,侧加载是通过从设备的 SD 卡加载软件包来完成的;对于无法启动的设备,可以使用其他计算机将软件包放入 SD 卡,然后将 SD 卡插入设备。为了适应没有可移动外部存储设备的 Android 设备,recovery 支持两种额外的侧加载机制:从缓存分区加载软件包,以及使用 adb 通过 USB 加载软件包。

要调用每种侧加载机制,您设备的 Device::InvokeMenuItem() 方法可以返回以下 BuiltinAction 值

  • APPLY_EXT。从外部存储( /sdcard 目录)侧加载更新包。您的 recovery.fstab 必须定义 /sdcard 挂载点。这不适用于使用指向 /data 的符号链接(或某些类似机制)来模拟 SD 卡的设备。/data 通常对 recovery 不可用,因为它可能已加密。recovery UI 会在 /sdcard 中显示 .zip 文件菜单,并允许用户选择一个。
  • APPLY_CACHE。类似于从 /sdcard 加载软件包,不同之处在于使用 /cache 目录(该目录始终可供 recovery 使用)。从常规系统来看,/cache 仅可由特权用户写入,如果设备不可启动,则根本无法写入 /cache 目录(这使得此机制的实用性有限)。
  • APPLY_ADB_SIDELOAD。允许用户通过 USB 电缆和 adb 开发工具将软件包发送到设备。当调用此机制时,recovery 会启动其自己的 adbd 守护程序的迷你版本,以允许连接的主机计算机上的 adb 与其通信。此迷你版本仅支持一个命令:adb sideload filename。指定的文件从主机发送到设备,然后设备验证并安装它,就像它位于本地存储中一样。

一些注意事项

  • 仅支持 USB 传输。
  • 如果您的 recovery 正常运行 adbd(通常对于 userdebug 和 eng 版本是这样),则当设备处于 adb 侧加载模式时,adbd 将被关闭,并在 adb 侧加载完成接收软件包后重新启动。在 adb 侧加载模式下,除了 sideload 之外,没有其他 adb 命令起作用( logcatrebootpushpullshell 等均失败)。
  • 您无法在设备上退出 adb 侧加载模式。要中止,您可以发送 /dev/null(或任何其他无效软件包)作为软件包,然后设备将无法验证它并停止安装过程。RecoveryUI 实现的 CheckKey() 方法将继续为按键调用,因此您可以提供一个按键序列,该序列可以重启设备并在 adb 侧加载模式下工作。