天天看點

OTA更新Google官方文檔翻譯——裝置專屬代碼

原文:https://source.android.com/devices/tech/ota/device_code

恢複系統包括一些用于插入裝置專屬代碼的鈎子,以便 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 卡)。該裝置是塊裝置;device2 是系統裝載主裝置失敗時嘗試裝載的第二個塊裝置(旨在與可能(或可能沒有)使用分區表進行格式化的 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 分區(在這裡,加密中繼資料會存儲在不得被覆寫的分區的末尾部分)等功能。

注意:device2 和 options 字段均為可選字段,在解析時會産生歧義。如果該行第 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"
      

Headers 和 Item 函數

Device 類需要相關函數來傳回隐藏恢複菜單中出現的标頭和項。Headers 介紹了如何操作菜單(例如,用于更改/選擇突出顯示項的控制功能)。

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:不使用此按鍵執行任何操作

由于 

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 類執行個體的 

make_device()

 函數的 recovery_ui.cpp 檔案末尾包含一些樣闆檔案。

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

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

編譯并連結到裝置 recovery 分區

完成 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更新Google官方文檔翻譯——裝置專屬代碼
圖 1. icon_error.png
OTA更新Google官方文檔翻譯——裝置專屬代碼
圖 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更新Google官方文檔翻譯——裝置專屬代碼
圖 3. icon_installing.png
OTA更新Google官方文檔翻譯——裝置專屬代碼
圖 4. icon-installing_overlay01.png
OTA更新Google官方文檔翻譯——裝置專屬代碼
圖 5. icon_installing_overlay07.png

在安裝過程中,螢幕顯示通過繪制 icon_installing.png 圖檔進行建構,然後在适當的偏移量處繪制其中一張疊加幀。圖中疊加的紅色方框是用來突出顯示疊加幀在基本圖檔上的放置位置:

OTA更新Google官方文檔翻譯——裝置專屬代碼
圖 6. “正在安裝”動畫幀 1 (icon_installing.png + icon_installing_overlay01.png)
OTA更新Google官方文檔翻譯——裝置專屬代碼
圖 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 會顯示一串文本(如“正在安裝系統更新…”)以及圖檔。如果主系統啟動進入恢複模式,系統會将使用者目前的語言區域作為指令行選項傳遞到恢複系統。對于每條要顯示的消息,恢複系統都會為每個語言區域中的相應消息添加第二張帶有預呈現文本字元串的合成圖檔。

恢複文本字元串的示例圖檔:

OTA更新Google官方文檔翻譯——裝置專屬代碼

圖 8. 恢複消息的本地化文本

恢複文本會顯示以下消息:

  • 正在安裝系統更新…
  • 出錯了!
  • 正在清除…(執行資料清除/恢複出廠設定時)
  • 無指令(使用者手動啟動進入恢複模式時)

development/tools/recovery_l10/

 中的 Android 應用會呈現經過本地化的消息并建立合成圖檔。要詳細了解如何使用該應用,請參閱 

development/tools/recovery_l10n/ src/com/android/recovery_l10n/Main.java

 中的注解。

如果使用者手動啟動進入恢複模式,則語言區域可能不可用,且不會顯示任何文本。不要讓文本消息對恢複流程産生太多制約影響。

注意:隐藏界面(可顯示日志消息并允許使用者從菜單中選擇操作)僅提供英文版。

進度條

進度條會顯示在主要圖檔(或動畫)的下方。進度條由兩張輸入圖檔(大小必須相同)合并而成:

OTA更新Google官方文檔翻譯——裝置專屬代碼

圖 9. progress_empty.png

OTA更新Google官方文檔翻譯——裝置專屬代碼

圖 10. progress_fill.png

fill 圖檔的左端顯示在 empty 圖檔右端的旁邊,進而形成進度條。兩張圖檔之間的邊界位置會不時變更,以表示相應的進度。以上述幾對輸入圖檔為例,顯示效果為:

OTA更新Google官方文檔翻譯——裝置專屬代碼

圖 11. 進度條顯示為 1%>

OTA更新Google官方文檔翻譯——裝置專屬代碼

圖 12. 進度條顯示為 10%

OTA更新Google官方文檔翻譯——裝置專屬代碼

圖 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 對象,因為調用程式會 

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 和 boot 分區)後調用。您還可以發出 (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_files

 或 

ota_from_target_files

)時,target-files .zip 中的 releasetools.py 腳本(如果存在)将優先于 Android 源代碼樹中的腳本而執行。您還可以通過優先級最高的 

-s

(或 

--device_specific

)選項明确指定裝置專屬擴充程式的路徑。這樣一來,您就可以在釋出工具擴充程式中更正錯誤及做出更改,并将這些更改應用于舊的目标檔案。

現在,當您運作 

ota_from_target_files

 時,它會自動從 target_files .zip 檔案擷取裝置專屬子產品,并在生成 OTA 更新包時使用該子產品:

% ./build/tools/releasetools/ota_from_target_files \
    -i PREVIOUS-tardis-target_files.zip \
    dist_output/tardis-target_files.zip incremental_ota_update.zip
unzipping target target-files...
using device-specific extensions from target_files
unzipping source target-files...
   [...]
done.
      

或者,您可以在運作 

ota_from_target_files

 時指定裝置專屬擴充程式。

% ./build/tools/releasetools/ota_from_target_files \
    -s device/yoyodyne/tardis \  # specify the path to device-specific extensions
    -i PREVIOUS-tardis-target_files.zip \
    dist_output/tardis-target_files.zip incremental_ota_update.zip
unzipping target target-files...
loaded device-specific extensions from device/yoyodyne/tardis
unzipping source target-files...
   [...]
done.
      

注意:如需檢視完整的選項清單,請參閱 

build/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

     指令可以發揮作用(

    logcat

    reboot

    push

    pull

    shell

     等都不起作用)。
  • 您無法在裝置上退出 adb 旁加載模式。要終止,您可以将 

    /dev/null

    (或有效更新包以外的其他任何檔案)作為更新包進行發送,然後裝置将無法對其進行驗證,并會停止安裝程式。RecoveryUI 實作的 

    CheckKey()

     方法将繼續為按鍵所調用,是以,您可以提供可重新啟動裝置并在 adb 旁加載模式下運作的按鍵序列。