復原系統包含幾個鉤子,可插入裝置專屬程式碼,讓 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 將「不會」遭到覆寫。這項功能支援加密使用者資料分區等功能 (加密中繼資料會儲存在分區的結尾,且不應覆寫)。
注意:device2 和 options 欄位為選用欄位,會在剖析時造成歧義。如果該行第四個欄位的項目開頭為「/'」字元,則視為「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 以上版本的復原作業介面會使用兩種主要圖片:錯誤圖片和安裝動畫。
![]() 圖 1. icon_error.png |
![]() 圖 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 和更早版本的復原作業介面會使用錯誤圖片 (如上所示) 和安裝動畫,以及多張重疊圖片:
![]() 圖 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
填滿圖片的左端會顯示在空白圖片的右端旁邊,以便製作進度列。兩張圖片之間的邊界位置會變更,以表示進度。舉例來說,如果使用上述成對的輸入圖片,則顯示:

圖 11. 進度列為 1%>

圖 12. 進度列為 10%

圖 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_files
或 ota_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 指令都無法運作 (logcat
、reboot
、push
、pull
、shell
等都會失敗)。 -
您無法在裝置上退出 ADB 側載模式。如要中止,您可以將
/dev/null
(或任何非有效套件) 當作套件傳送,裝置就會無法驗證該套件,並停止安裝程序。RecoveryUI 實作項目的CheckKey()
方法會繼續針對按鍵輸入呼叫,因此您可以提供按鍵序列,讓裝置重新啟動並在 ADB 側載模式下運作。