裝置專屬代碼

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

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

分區圖

自 Android 2.3 起,平台便支援 eMMc 快閃裝置,以及在這些裝置上執行的 ext4 檔案系統。它也支援 Memory Technology Device (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 卡。device 是區塊裝置;device2 是系統嘗試掛載主要裝置失敗時,嘗試掛載的第二個區塊裝置 (與 SD 卡相容,可能會或未以分割區表格格式化)。

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

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

注意:device2options 欄位為選用欄位,會在剖析時造成歧義。如果該行第四個欄位的項目開頭為「/'」字元,則視為「device2」項目;如果該項目開頭不是「/'」字元,則視為「options」欄位。

啟動動畫

裝置製造商可以自訂 Android 裝置啟動時顯示的動畫。為此,請根據 bootanimation 格式的規格,建構並排序 .zip 檔案。

針對 Android Things 裝置,您可以在 Android Things 控制台中上傳壓縮檔案,讓所選產品包含圖片。

注意:這些圖片必須符合 Android 品牌規範。如需品牌規範,請參閱 Partner Marketing Hub 的 Android 專區。

復原 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() 會在復原程序的其他部分中呼叫,無論是否已切換關閉選單、開啟選單、安裝套件、清除使用者資料等,都會呼叫 CheckKey()。它可傳回四個常數之一:

  • 切換鈕:開啟或關閉選單和/或文字記錄的顯示功能
  • 重新啟動。立即重新啟動裝置
  • IGNORE。忽略這個按鍵操作
  • 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() 函式。如需圖片相關詳細資訊,請參閱「Recovery UI 圖片」。

裝置類別

完成 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。不採取任何行動。
  • 重新啟動。退出復原模式,然後正常重新啟動裝置。
  • 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 檔案的結尾處加入一些例行程式碼,以便為 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

復原 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 和更早版本的復原作業介面會使用錯誤圖片 (如上所示) 和安裝動畫,以及多張重疊圖片:

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 會顯示一串文字 (例如「Installing system update...」) 和圖片。當主系統啟動至復原模式時,會將使用者的目前語言代碼當做指令列選項傳遞至復原模式。針對要顯示的每則訊息,復原功能會納入第二個組合圖片,其中包含針對該訊息在各語言代碼中預先算繪的文字字串。

復原文字串列的示例圖片:

復原文字圖片

圖 8. 復原訊息的本地化文字

復原文字可顯示下列訊息:

  • 正在安裝系統更新...
  • 錯誤!
  • 清除中... (執行資料抹除/恢復原廠設定時)
  • 無指令 (使用者手動啟動復原模式時)

bootable/recovery/tools/recovery_l10n/ 中的 Android 應用程式會算繪訊息的本地化版本,並建立組合圖片。如要進一步瞭解如何使用這個應用程式,請參閱 bootable/recovery/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java 中的註解。

當使用者手動啟動 Recovery 時,可能無法使用語言代碼,且不會顯示任何文字。請勿將文字訊息視為復原程序的關鍵因素。

注意:隱藏的介面只提供英文版,可顯示記錄訊息,並讓使用者從選單中選取動作。

進度列

進度列可顯示在主要圖片 (或動畫) 下方。進度列是透過結合兩張輸入圖片而成,兩張圖片必須是相同大小:

空白進度列

圖 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 堆疊)。否則,您會取得傳回值的擁有權,並負責最終對其呼叫 FreeValue()

假設函式需要兩個引數:字串值 和 Blob 值 圖片。您可以讀取以下引數:

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

對於多個引數,檢查是否為空值並釋放先前評估的引數可能會變得乏味。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 的常數字串副本 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 中,將其連結至 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

這些檔案之所以稱為無線電檔案,是因為歷史因素;這些檔案可能與裝置無線電 (如有) 無關。這些只是不透明的資料 Blob,由建構系統複製到 OTA 產生工具使用的目標檔案 .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) 即可將文字輸出至指令碼。請務必讓輸出文字以分號結尾,以免與之後發出的指令衝突。

如需 info 物件的詳細資訊,請參閱 Python Software Foundation 的 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 指令碼會包含在目標檔案的 .zip 檔案 (META/releasetools.py ) 中。

執行發布工具 (img_from_target_filesota_from_target_files) 時,如果目標檔案 .zip 中有 releasetools.py 指令碼,系統會優先使用該指令碼,而非 Android 原始碼樹狀結構中的指令碼。您也可以使用 -s (或 --device_specific) 選項,明確指定裝置專屬擴充功能的路徑,該選項具有最高優先順序。這樣一來,您就能修正錯誤,並在 releasetools 擴充功能中進行變更,然後將這些變更套用至舊的目標檔案。

現在,當您執行 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 裝置,Recovery 支援兩種額外的側載機制:從快取分區載入套件,以及使用 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 開發工具,將套件傳送至裝置。叫用這項機制時,Recovery 會啟動自己的迷你版 adbd 守護程序,讓已連線主機電腦上的 ADB 與其通訊。這個迷你版本只支援單一指令:adb sideload filename。系統會將命名檔案從主機傳送至裝置,然後驗證並安裝該檔案,就像是從本機儲存空間安裝一樣。

以下提供幾點注意事項:

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