裝置專屬程式碼

復原系統包含多個插入裝置專屬程式碼的掛鉤,因此 OTA 更新也能更新 Android 系統以外的裝置部分 (例如基頻或無線電處理器)。

以下各節和範例會自訂 yoyodyne 供應商生產的 tardis 裝置。

分區地圖

自 Android 2.3 起,平台支援 eMMc 快閃裝置,以及在這些裝置上執行的 ext4 檔案系統。此外,這個版本也支援舊版中的記憶體技術裝置 (MTD) 快閃裝置和 yaffs2 檔案系統。

分割區對應檔案由 TARGET_RECOVERY_FSTAB 指定,復原二進位檔和套件建構工具都會使用這個檔案。您可以在 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 (選用) 以外,這個範例中的所有掛載點都必須定義 (裝置也可以新增額外的分割區)。支援的檔案系統類型有五種:

yaffs2
MTD 快閃裝置上的 yaffs2 檔案系統。「device」必須是 MTD 分割區的名稱,且必須出現在 /proc/mtd 中。
mtd
原始 MTD 分區,用於可開機分區,例如開機和復原。MTD 實際上並未掛接,但掛接點會做為尋找分割區的金鑰。「device」必須是 /proc/mtd 中的 MTD 分割區名稱。
ext4
eMMc 快閃裝置上的 ext4 檔案系統。「device」必須是區塊裝置的路徑。
emmc
原始 eMMc 區塊裝置,用於開機磁碟分割區,例如開機和復原。與 mtd 類型類似,eMMc 實際上從未掛接,但掛接點字串用於在表格中找出裝置。
vfat
位於區塊裝置頂端的 FAT 檔案系統,通常用於外部儲存空間,例如 SD 卡。裝置是區塊裝置;如果主要裝置的掛接作業失敗,系統會嘗試掛接裝置 2 (與可能已格式化或未格式化分割區表的 SD 卡相容)。

所有磁碟分割區都必須掛接在根目錄中 (也就是說,掛接點值必須以斜線開頭,且不得有其他斜線)。這項限制僅適用於在復原模式中掛接檔案系統;主要系統可隨意掛接檔案系統。/boot/recovery/misc 目錄應為原始類型 (mtd 或 emmc),而 /system/data/cache/sdcard 目錄 (如有) 應為檔案系統類型 (yaffs2、ext4 或 vfat)。

從 Android 3.0 開始,recovery.fstab 檔案會新增一個選用欄位「options」。目前唯一定義的選項是「長度」 ,可讓您明確指定分割區長度。重新格式化分割區時會使用這個長度 (例如,在清除資料/恢復原廠設定作業期間,用於 userdata 分割區;或在安裝完整 OTA 封裝時,用於系統分割區)。如果長度值為負數,則格式化的大小會透過將長度值加到實際分割區大小來取得。舉例來說,設定「length=-16384」表示重新格式化該分區時,最後 16k 的分區「不會」遭到覆寫。這項功能支援使用者資料分割區加密等功能 (加密中繼資料會儲存在分割區結尾,不應覆寫)。

注意:「device2」和「options」欄位為選用項目,因此剖析時可能會產生模稜兩可的情況。如果該行第四個欄位的項目開頭是「/」字元,則視為 device2 項目;如果開頭不是「/」字元,則視為選項欄位。

開機動畫

裝置製造商可以自訂 Android 裝置啟動時顯示的動畫。如要執行這項操作,請根據開機動畫格式的規格,建構並放置 .zip 檔案。

如果是 Android Things 裝置,您可以在 Android Things 控制台中上傳壓縮檔,將圖片納入所選產品。

注意:這些圖片必須符合 Android 品牌宣傳指南。如需品牌宣傳指南,請參閱合作夥伴行銷中心的 Android 專區。

復原 UI

如要支援具有不同可用硬體 (實體按鈕、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 裝置有螢幕,因此您可以從內建的 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():選單關閉時、選單開啟時、封裝安裝期間、清除使用者資料期間等。這個函式可以傳回四個常數之一:

  • 切換。開啟或關閉選單和/或文字記錄的顯示設定
  • 重新啟動。立即重新啟動裝置
  • 略過。忽略這個按鍵動作
  • ENQUEUE。將這個按鍵事件加入佇列,以同步取用 (也就是說,如果顯示器已啟用,則由復原選單系統取用)

每次按鍵按下事件後,如果同一按鍵發生按鍵抬起事件,系統就會呼叫 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() 函式。如需圖片詳細資料,請參閱「復原 UI 圖片」。

裝置類別

實作 RecoveryUI 後,請定義裝置類別 (從內建裝置類別建立子類別)。這個函式應建立 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。不採取任何行動。
  • 重新啟動。退出復原模式,然後正常重新啟動裝置。
  • 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 圖片

復原使用者介面由圖片組成。理想情況下,使用者完全不需要與 UI 互動:在正常更新期間,手機會啟動復原模式、填滿安裝進度列,然後啟動新系統,使用者完全不需要輸入任何內容。如果系統更新發生問題,使用者唯一能做的就是致電客戶服務中心。

全圖像介面不需要進行本地化。不過,從 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 會取得一組輸入影格,並將這些影格合併為復原作業所需的合成影像。

預設圖片提供不同密度,位於 bootable/recovery/res-$DENSITY/images (例如bootable/recovery/res-hdpi/images)。如要在安裝期間使用靜態圖片,您只需要提供 icon_installing.png 圖片,並將動畫中的影格數設為 0 (錯誤圖示不會有動畫,一律是靜態圖片)。

Android 4.x 以下版本

Android 4.x 和更早版本的復原 UI 會使用「error」圖片 (如上所示) 和「installing」動畫,以及多張重疊圖片:

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 指令碼可協助將一組圖片影格轉換為復原所需的「基礎圖片 + 疊加圖片」形式,包括計算必要的偏移。

預設圖片位於 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

「填滿」圖片的左端會顯示在「空白」圖片的右端旁邊,形成進度列。兩張圖片之間的邊界位置會變更,表示進度。舉例來說,如果使用上述成對的輸入圖片,顯示結果如下:

進度列顯示 1%

圖 11. 進度列顯示 1%>

進度列顯示 10%

圖 12. 進度列顯示 10%

進度列顯示 50%

圖 13. 進度列顯示 50%

您可以將這些圖片放入 (本例中的) device/yoyodyne/tardis/recovery/res/images,提供裝置專屬版本。檔案名稱必須與上述名稱相符;如果建構系統在該目錄中找到檔案,就會優先使用該檔案,而非對應的預設圖片。僅支援 RGB 或 RGBA 格式的 PNG,且顏色深度為 8 位元。

注意:在 Android 5.x 中,如果復原程序知道語言代碼,且該語言代碼是從右到左 (RTL) 的語言 (阿拉伯文、希伯來文等),進度列會從右到左填滿。

沒有螢幕的裝置

並非所有 Android 裝置都有螢幕。如果裝置是無螢幕的家電,或是只有音訊介面,您可能需要更廣泛地自訂復原 UI。請直接建立父類別 RecoveryUI 的子類別, 而非建立 ScreenRecoveryUI 的子類別。

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* 所指向的任何資料 (尤其是資料成員) 的擁有權。

在本例中,您要傳回 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,使用程式碼建構靜態程式庫。(這是用來在上一節中自訂復原 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 中,將這些函式連結至更新程式,而不必呼叫 (不存在的) 註冊函式。舉例來說,如果裝置專屬程式碼想使用 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

基於歷史因素,這些檔案稱為無線電檔案,但可能與裝置無線電 (如有) 無關。這些只是不透明的資料 Blob,建構系統會複製到 OTA 生成工具使用的 target-files .zip 中。建構時,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()
在指令碼產生作業結束時呼叫,此時已發出更新開機和系統分割區的指令碼指令。您也可以發出其他指令,進行裝置專屬的更新。
IncrementalOTA_Assertions()
FullOTA_Assertions() 類似,但會在產生增量更新套件時呼叫。
IncrementalOTA_VerifyBegin()
在裝置狀態的所有判斷皆通過後,但尚未進行任何變更前呼叫。您可以發出裝置專屬更新的指令,這些指令必須在裝置上的任何其他項目變更前執行。
IncrementalOTA_VerifyEnd()
在驗證階段結束時呼叫,此時指令碼已確認要處理的檔案具有預期的起始內容。此時裝置上的任何內容都未變更。您也可以發出其他裝置專屬的驗證碼。
IncrementalOTA_InstallBegin()
在確認要修補的檔案處於預期之前狀態後,但在進行任何變更之前呼叫。您可以發出指令,針對裝置專屬更新項目執行作業,這些更新項目必須在裝置上的任何其他項目變更前執行。
IncrementalOTA_InstallEnd()
與完整 OTA 套件對應項目類似,這個函式會在指令碼產生作業結束時呼叫,也就是在發出更新開機和系統分割區的指令碼指令後呼叫。您也可以發出其他指令,進行裝置專屬的更新。

注意:如果裝置沒電,OTA 安裝程序可能會從頭開始。請準備好處理已執行部分或全部這些指令的裝置。

將函式傳遞至資訊物件

將函式傳遞至包含各種實用項目的單一資訊物件:

  • info.input_zip。(僅限完整 OTA) 輸入目標檔案 .zip 的 zipfile.ZipFile 物件。
  • info.source_zip。(僅限增量 OTA) 來源目標檔案 .zip 的 zipfile.ZipFile 物件 (安裝增量套件時,裝置上已有的建構版本)。
  • info.target_zip:(僅限增量 OTA) 目標目標檔案 .zip 的 zipfile.ZipFile 物件 (增量套件置於裝置上的建構版本)。
  • info.output_zip。正在建立的套件;已開啟 zipfile.ZipFile 物件以供寫入。使用 common.ZipWriteStr(info.output_zip, filename, data) 將檔案新增至套件。
  • info.script。您可以將指令附加至的指令碼物件。呼叫 info.script.AppendExtra(script_text),將文字輸出至指令碼。請確保輸出文字以半形分號結尾,以免與後續發出的指令發生衝突。

如要瞭解資訊物件的詳細資料,請參閱 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) 時,系統會優先使用目標檔案 .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 裝置,復原功能支援兩種額外的側載機制:從快取分割區載入套件,以及使用 adb 透過 USB 載入套件。

如要叫用各個側載機制,裝置的 Device::InvokeMenuItem() 方法可以傳回下列 BuiltinAction 值:

  • APPLY_EXT。從外部儲存空間 ( /sdcard 目錄) 側載更新套件。recovery.fstab 必須定義 /sdcard 掛接點。如果裝置使用符號連結模擬 SD 卡,則無法使用這項功能。/data/data 通常無法復原,因為可能已加密。復原 UI 會顯示 /sdcard 中的 .zip 檔案選單,並讓使用者選取其中一個檔案。
  • APPLY_CACHE。這與從 /sdcard 載入套件類似,但會改用 /cache 目錄 (一律可供復原)。在一般系統中,只有具備權限的使用者可以寫入 /cache ,如果裝置無法開機,則完全無法寫入 /cache 目錄 (這使得這項機制用途有限)。
  • APPLY_ADB_SIDELOAD。允許使用者透過 USB 傳輸線和 adb 開發工具,將套件傳送到裝置。啟動這項機制後,復原程序會啟動自己的 adbd 精靈迷你版,讓連線主機電腦上的 adb 與其通訊。這個迷你版只支援單一指令:adb sideload filename。主機會將具名檔案傳送至裝置,裝置會驗證並安裝該檔案,就像檔案位於本機儲存空間一樣。

請注意以下幾點:

  • 僅支援 USB 傳輸。
  • 如果復原程序正常執行 adbd (通常適用於 userdebug 和 eng 建構版本),系統會在裝置處於 adb sideload 模式時關閉 adbd,並在 adb sideload 接收完套件後重新啟動 adbd。在 adb 側載模式下,除了 sideload 以外,所有 adb 指令都無法運作 ( logcatrebootpushpullshell 等都會失敗)。
  • 無法在裝置上退出 adb 側載模式。如要中止,可以傳送 /dev/null (或任何無效的套件) 做為套件,裝置就會無法驗證,並停止安裝程序。系統會繼續為按鍵呼叫 RecoveryUI 實作的 CheckKey() 方法,因此您可以提供重新啟動裝置的按鍵序列,並在 adb 側載模式下運作。