Код для определенного устройства

Система восстановления включает несколько точек вставки кода, относящегося к определенному устройству, чтобы беспроводные обновления могли также обновлять части устройства, отличные от системы Android (например, модем или радиопроцессор).

В следующих разделах и примерах настраивается устройство tardis, произведенное поставщиком yoyodyne.

Карта разделов

Начиная с Android 2.3 платформа поддерживает флеш-устройства eMMC и файловую систему ext4, которая работает на этих устройствах. Также поддерживаются флеш-накопители MTD и файловая система yaffs2 из более ранних версий.

Файл карты разделов задается с помощью переменной TARGET_RECOVERY_FSTAB. Этот файл используется как двоичным файлом восстановления, так и инструментами сборки пакетов. Название файла сопоставления можно указать в файле TARGET_RECOVERY_FSTAB в файле BoardConfig.mk.

Пример файла карты разделов:

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
Файловая система yaffs2 поверх флеш-устройства MTD. "device" должно быть названием раздела MTD и должно быть указано в /proc/mtd.
mtd
Необработанный раздел MTD, используемый для загрузочных разделов, таких как boot и recovery. MTD фактически не монтируется, но точка подключения используется как ключ для поиска раздела. "device" – название раздела MTD в /proc/mtd.
ext4
Файловая система ext4 на флеш-накопителе eMMc. "device" – это путь к блочному устройству.
eMMC
Необработанное блочное устройство eMMc, используемое для загрузочных разделов, таких как boot и recovery. Как и в случае с типом 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. В настоящее время определен только один вариант – length , который позволяет явно указать длину раздела. Этот размер используется при переформатировании раздела (например, раздела userdata при удалении данных или сбросе до заводских настроек или системного раздела при установке полного OTA-пакета). Если значение длины отрицательное, то размер для форматирования определяется путем добавления значения длины к фактическому размеру раздела. Например, если задать значение "length=-16384", то последние 16 КБ раздела не будут перезаписаны при его переформатировании. Это позволяет использовать такие функции, как шифрование раздела userdata (метаданные шифрования хранятся в конце раздела, и их нельзя перезаписать).

Примечание. Поля device2 и options необязательны, что создает неоднозначность при синтаксическом анализе. Если запись в четвертом поле строки начинается с символа "/", она считается записью device2. Если запись не начинается с символа "/", она считается полем options.

Анимированная заставка при запуске

Производители устройств могут настраивать анимацию, которая показывается при загрузке устройства Android. Для этого создайте ZIP-файл, организованный и расположенный в соответствии со спецификациями формата bootanimation.

Если у вас устройство Android Things, вы можете загрузить ZIP-файл в консоль Android Things, чтобы добавить изображения в выбранный продукт.

Примечание. Эти изображения должны соответствовать правилам фирменного оформления Android. Правила фирменного оформления можно найти в разделе Android на сайте Partner Marketing Hub.

Интерфейс восстановления

Чтобы поддерживать устройства с разным аппаратным обеспечением (физическими кнопками, светодиодами, экранами и т. д.), вы можете настроить интерфейс восстановления так, чтобы он показывал статус и предоставлял доступ к скрытым функциям, управляемым вручную, для каждого устройства.

Ваша цель – создать небольшую статическую библиотеку с несколькими объектами 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 кадров/с) соответствует стандартным образам для восстановления. При их использовании функцию Init() указывать не нужно. Подробнее об изображениях в интерфейсе восстановления…

Класс устройства

После того как вы реализовали RecoveryUI, определите класс устройства (подкласс встроенного класса 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(), с действиями. Для массива элементов в примере с тардис используйте:

   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 и cache, также известные как сброс к заводским настройкам. Прежде чем продолжить, пользователю нужно подтвердить это действие.

Последний метод, WipeData(), является необязательным и вызывается при каждом запуске операции удаления данных (из меню восстановления или когда пользователь выбрал сброс до заводских настроек в основной системе). Этот метод вызывается до того, как будут удалены разделы с данными пользователя и кешем. Если на устройстве пользовательские данные хранятся не в этих двух разделах, их нужно удалить здесь. Чтобы указать, что операция выполнена успешно, нужно вернуть значение 0, а в случае ошибки – другое значение. Однако в настоящее время возвращаемое значение игнорируется. Разделы с пользовательскими данными и кешем будут очищены независимо от того, вернет ли функция значение success или failure.

   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, соберите его и свяжите с режимом восстановления на устройстве. В файле 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

Рисунок 1. icon_error.png

Изображение, показываемое во время установки OTA-обновления

Рисунок 2. icon_installing.png

Анимация установки представлена в виде одного изображения PNG с разными кадрами анимации, чередующимися по строкам (поэтому на рисунке 2 все выглядит сжатым). Например, для анимации 200 x 200 с семью кадрами создайте одно изображение 200 x 1400, где первый кадр – это строки 0, 7, 14, 21 и т. д., второй кадр – строки 1, 8, 15, 22 и т. д. Объединенное изображение включает текстовый фрагмент, в котором указано количество кадров анимации и количество кадров в секунду. Инструмент 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 и более ранних версий используется изображение 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. Локализованный текст для сообщений с просьбой не блокировать рекламу

В тексте для восстановления могут быть следующие сообщения:

  • Установка обновления системы…
  • Ошибка!
  • Удаление… (при удалении данных или сбросе настроек)
  • Нет команды (если пользователь вручную загружает устройство в режиме восстановления)

Приложение для Android в bootable/recovery/tools/recovery_l10n/ создает локализованные версии сообщения и комбинированное изображение. Инструкции по использованию этого приложения приведены в комментариях в файле bootable/recovery/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java.

Если пользователь вручную загружает устройство в режиме восстановления, локаль может быть недоступна и текст не будет отображаться. Не делайте текстовые сообщения критически важными для процесса восстановления.

Примечание. Скрытый интерфейс, в котором отображаются сообщения в журнале и можно выбирать действия из меню, доступен только на английском языке.

Индикаторы выполнения

Индикаторы выполнения могут появляться под основным изображением или анимацией. Индикатор выполнения состоит из двух изображений одинакового размера:

пустой индикатор выполнения;

Рисунок 9. progress_empty.png

заполненный индикатор выполнения

Рисунок 10. progress_fill.png

Левый край изображения fill показывается рядом с правым краем изображения empty, чтобы создать индикатор выполнения. Положение границы между двумя изображениями меняется, чтобы показать прогресс. Например, для приведенных выше пар входных изображений будет показано следующее:

индикатор выполнения на 1 %;

Рисунок 11. Индикатор выполнения на 1%>

индикатор выполнения на 10 %;

Рисунок 12. Индикатор выполнения на 10%

Индикатор выполнения на 50%

Рисунок 13. Индикатор выполнения на 50%

Вы можете предоставить версии этих изображений для разных устройств, поместив их в папку device/yoyodyne/tardis/recovery/res/images (в этом примере). Названия файлов должны совпадать с указанными выше. Если в каталоге найден файл, система сборки использует его вместо соответствующего изображения по умолчанию. Поддерживаются только файлы PNG в формате RGB или RGBA с 8-битной глубиной цвета.

Примечание. В Android 5.x, если языковой стандарт известен среде восстановления и относится к языкам с направлением письма справа налево (например, арабскому или ивриту), индикатор выполнения заполняется справа налево.

Устройства без экрана

Не у всех устройств Android есть экраны. Если у вашего устройства нет экрана или есть только аудиоинтерфейс, вам может потребоваться более глубокая настройка интерфейса восстановления. Вместо того чтобы создавать подкласс ScreenRecoveryUI, создайте подкласс его родительского класса RecoveryUI.

В RecoveryUI есть методы для выполнения операций с интерфейсом более низкого уровня, например "переключить дисплей", "обновить индикатор выполнения", "показать меню", "изменить выбор в меню" и т. д. Вы можете переопределить их, чтобы создать подходящий интерфейс для своего устройства. Возможно, на вашем устройстве есть светодиоды, которые могут мигать разными цветами или с разной частотой, чтобы показывать состояние, или, может быть, вы можете воспроизводить аудио. Возможно, вы не хотите поддерживать меню или режим "Показ текста". В таком случае вы можете запретить доступ к ним, реализовав CheckKey() и HandleMenuKey() так, чтобы они никогда не включали показ и не выбирали пункт меню. В этом случае многие методы RecoveryUI, которые вам нужно предоставить, могут быть пустыми заглушками.)

Чтобы узнать, какие методы необходимо поддерживать, ознакомьтесь с объявлением RecoveryUI в файле bootable/recovery/ui.h. RecoveryUI – это абстрактный класс. Некоторые методы являются чисто виртуальными и должны быть предоставлены подклассами, но он содержит код для обработки входных данных. Вы также можете переопределить это поведение, если на вашем устройстве нет клавиш или вы хотите обрабатывать их по-другому.

Updater

Вы можете использовать код, относящийся к определенному устройству, при установке пакета обновлений, добавив собственные функции расширения, которые можно вызывать из скрипта обновления. Вот пример функции для устройства tardis:

device/yoyodyne/tardis/recovery/recovery_updater.c
#include <stdlib.h>
#include <string.h>

#include "edify/expr.h"

Все функции расширения имеют одинаковую сигнатуру. Аргументы – это название, по которому была вызвана функция, файл cookie State*, количество входящих аргументов и массив указателей 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().

Предположим, функции требуются два аргумента: строка key и объект 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.

В этом случае вам нужно вернуть значение "Истина" или "Ложь", чтобы указать, успешно ли выполнена операция. Помните, что пустая строка считается false, а все остальные строки – true. Вы должны выделить память для объекта 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, который использовался для настройки интерфейса восстановления в предыдущем разделе. В вашем устройстве могут быть определены обе статические библиотеки.)

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.reprogram("the-key", package_extract_file("tardis-image.dat")) . В этом случае используется версия встроенной функции package_extract_file() с одним аргументом, которая возвращает содержимое файла, извлеченного из пакета обновления, в виде объекта BLOB, чтобы создать второй аргумент для новой функции расширения.

Создание пакета OTA

Последний компонент – это инструменты для создания пакетов OTA, которые должны знать о данных, относящихся к вашему устройству, и создавать скрипты обновления, включающие вызовы ваших функций расширения.

Сначала нужно сообщить системе сборки о блобе данных, относящемся к определенному устройству. Если файл данных находится в каталоге device/yoyodyne/tardis/tardis.dat, объявите в файле AndroidBoard.mk устройства следующее:

device/yoyodyne/tardis/AndroidBoard.mk
  [...]

$(call add-radio-file,tardis.dat)

Вы также можете поместить его в Android.mk, но тогда его нужно будет защитить проверкой устройства, поскольку все файлы Android.mk в дереве загружаются независимо от того, какое устройство собирается. (Если в дереве несколько устройств, файл tardis.dat нужно добавить только при создании устройства tardis.)

device/yoyodyne/tardis/Android.mk
  [...]

# an alternative to specifying it in AndroidBoard.mk
ifeq (($TARGET_DEVICE),tardis)
  $(call add-radio-file,tardis.dat)
endif

Такие файлы называются радиофайлами по историческим причинам. Они могут быть никак не связаны с радиомодулем устройства (если он есть). Это просто непрозрачные блоки данных, которые система сборки копирует в ZIP-архив target-files, используемый инструментами создания OTA-обновлений. При сборке файл tardis.dat сохраняется в target-files.zip как RADIO/tardis.dat. Вы можете вызвать функцию 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 изменился между двумя сборками.

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()
Вызывается после того, как файлы, которые нужно исправить, были проверены на соответствие ожидаемому состоянию before, но до того, как были внесены какие-либо изменения. Вы можете отправлять команды для обновлений, относящихся к определенному устройству, которые должны выполняться до того, как на устройстве будут изменены другие параметры.
IncrementalOTA_InstallEnd()
Как и в случае с полным пакетом OTA, этот метод вызывается в конце скрипта, после того как будут выполнены команды для обновления разделов загрузки и системы. Вы также можете отправлять дополнительные команды для обновлений, относящихся к определенным устройствам.

Примечание. Если устройство разрядится, установка OTA может начаться заново. Будьте готовы к тому, что на некоторых устройствах эти команды уже были выполнены полностью или частично.

Передача функций объектам информации

Передайте функции в один объект info, который содержит различные полезные элементы:

  • info.input_zip. (Только для полных OTA-обновлений) Объект zipfile.ZipFile для входного целевого файла ZIP.
  • info.source_zip. (Только для дополнительных обновлений OTA) Объект zipfile.ZipFile для исходного ZIP-файла target-files (версия, уже установленная на устройстве, когда устанавливается дополнительный пакет).
  • info.target_zip. (Только для дополнительных OTA-обновлений.) Объект zipfile.ZipFile для целевого файла ZIP (дополнительный пакет, который устанавливается на устройство).
  • info.output_zip. Создаваемый пакет; объект zipfile.ZipFile , открытый для записи. Используйте common.ZipWriteStr(info.output_zip, filename, data), чтобы добавить файл в пакет.
  • info.script. Объект скрипта, к которому можно добавлять команды. Вызовите метод info.script.AppendExtra(script_text), чтобы добавить текст в скрипт. Убедитесь, что выходной текст заканчивается точкой с запятой, чтобы он не сливался с командами, которые будут выведены после него.

Подробные сведения об объекте info приведены в документации Python Software Foundation по ZIP-архивам.

Укажите местоположение модуля

Укажите местоположение скрипта 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 включается в ZIP-файл target-files (META/releasetools.py ).

Когда вы запускаете инструменты выпуска (img_from_target_files или ota_from_target_files), скрипт releasetools.py в ZIP-архиве target-files, если он там есть, используется вместо скрипта из дерева исходного кода Android. Вы также можете явно указать путь к расширениям для определенного устройства с помощью параметра -s (или --device_specific), который имеет наивысший приоритет. Это позволяет исправлять ошибки и вносить изменения в расширения releasetools, а затем применять эти изменения к старым целевым файлам.

Теперь при выполнении команды ota_from_target_files модуль для определенного устройства автоматически выбирается из ZIP-файла target_files и используется при создании пакетов 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.

Механизм установки из сторонних источников

В режиме восстановления есть механизм установки из неизвестного источника, позволяющий вручную установить пакет обновления без скачивания по беспроводной сети основной системой. Установка из неизвестного источника полезна для отладки или внесения изменений на устройствах, где основная система не может быть загружена.

Раньше установка из неизвестного источника выполнялась путем загрузки пакетов с SD-карты устройства. Если устройство не загружается, пакет можно поместить на SD-карту с помощью другого компьютера, а затем вставить карту в устройство. Чтобы обеспечить возможность загрузки на устройствах Android без съемного внешнего накопителя, в режиме восстановления поддерживаются два дополнительных механизма установки из неизвестного источника: загрузка пакетов из раздела кеша и загрузка через USB с помощью adb.

Чтобы вызвать каждый механизм параллельной загрузки, метод Device::InvokeMenuItem() вашего устройства может возвращать следующие значения BuiltinAction:

  • APPLY_EXT. Установить пакет обновления с внешнего накопителя (каталог /sdcard). В файле recovery.fstab должна быть определена точка подключения /sdcard . Этот параметр нельзя использовать на устройствах, которые эмулируют SD-карту с помощью символической ссылки на /data или аналогичного механизма. /data обычно нельзя восстановить, так как он может быть зашифрован. В интерфейсе восстановления показывается меню ZIP-файлов в /sdcard, и пользователь может выбрать один из них.
  • APPLY_CACHE. Похоже на загрузку пакета из /sdcard, но вместо этого используется каталог /cache (который всегда доступен для восстановления). В обычной системе записывать данные в каталог /cache могут только пользователи с расширенными правами. Если устройство не загружается, то запись в каталог /cache невозможна, что ограничивает полезность этого механизма.
  • APPLY_ADB_SIDELOAD. Позволяет пользователю отправлять пакет на устройство через USB-кабель и инструмент разработки adb. Когда этот механизм вызывается, процесс восстановления запускает собственную мини-версию демона adbd, чтобы adb на подключенном хост-компьютере мог взаимодействовать с ней. Эта мини-версия поддерживает только одну команду: adb sideload filename. Указанный файл отправляется с хост-компьютера на устройство, которое затем проверяет и устанавливает его так же, как если бы он находился в локальном хранилище.

Обратите внимание на следующее:

  • Поддерживается только передача данных через USB.
  • Если восстановление запускает adbd обычным образом (как правило, это верно для сборок userdebug и eng), он будет отключен, пока устройство находится в режиме adb sideload, и перезапущен, когда adb sideload завершит получение пакета. В режиме боковой загрузки adb работают только команды sideload. Другие команды adb ( logcat, reboot, push, pull, shell и т. д.) не выполняются.
  • Не удается выйти из режима adb sideload на устройстве. Чтобы прервать процесс, отправьте /dev/null (или любой другой недопустимый пакет). Устройство не сможет его проверить и остановит установку. Метод CheckKey() в реализации RecoveryUI по-прежнему будет вызываться при нажатии клавиш, поэтому вы можете задать сочетание клавиш, которое перезагрузит устройство и будет работать в режиме adb sideload.