Código específico do dispositivo

O sistema de recuperação inclui vários ganchos para inserir código específico do dispositivo, de modo que as atualizações OTA também possam atualizar partes do dispositivo que não sejam o sistema Android (por exemplo, a banda base ou o processador de rádio).

As seções e exemplos a seguir personalizam o dispositivo tardis produzido pelo fornecedor yoyodyne .

Mapa de partição

A partir do Android 2.3, a plataforma oferece suporte a dispositivos flash eMMc e ao sistema de arquivos ext4 executado nesses dispositivos. Ele também suporta dispositivos flash Memory Technology Device (MTD) e o sistema de arquivos yaffs2 de versões mais antigas.

O arquivo de mapeamento de partição é especificado por TARGET_RECOVERY_FSTAB; este arquivo é usado pelo binário de recuperação e pelas ferramentas de construção de pacotes. Você pode especificar o nome do arquivo de mapa em TARGET_RECOVERY_FSTAB em BoardConfig.mk.

Um exemplo de arquivo de mapa de partição pode ter esta aparência:

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

Com exceção de /sdcard , que é opcional, todos os pontos de montagem neste exemplo devem ser definidos (os dispositivos também podem adicionar partições extras). Existem cinco tipos de sistemas de arquivos suportados:

yaffs2
Um sistema de arquivos yaffs2 sobre um dispositivo flash MTD. "device" deve ser o nome da partição MTD e deve aparecer em /proc/mtd .
mtd
Uma partição MTD bruta, usada para partições inicializáveis, como inicialização e recuperação. O MTD não está realmente montado, mas o ponto de montagem é usado como uma chave para localizar a partição. "device" deve ser o nome da partição MTD em /proc/mtd .
ext4
Um sistema de arquivos ext4 sobre um dispositivo flash eMMc. "device" deve ser o caminho do dispositivo de bloco.
emmc
Um dispositivo de bloco eMMc bruto, usado para partições inicializáveis, como inicialização e recuperação. Semelhante ao tipo mtd, o eMMc nunca é realmente montado, mas a sequência do ponto de montagem é usada para localizar o dispositivo na tabela.
vfat
Um sistema de arquivos FAT sobre um dispositivo de bloco, normalmente para armazenamento externo, como um cartão SD. O dispositivo é o dispositivo de bloco; device2 é um segundo dispositivo de bloco que o sistema tenta montar se a montagem do dispositivo primário falhar (para compatibilidade com cartões SD que podem ou não ser formatados com uma tabela de partição).

Todas as partições devem ser montadas no diretório raiz (ou seja, o valor do ponto de montagem deve começar com uma barra e não ter outras barras). Esta restrição aplica-se apenas à montagem de sistemas de arquivos em recuperação; o sistema principal é gratuito para montá-los em qualquer lugar. Os diretórios /boot , /recovery e /misc devem ser do tipo bruto (mtd ou emmc), enquanto os diretórios /system , /data , /cache e /sdcard (se disponível) devem ser do tipo de sistema de arquivos (yaffs2, ext4 ou vfat).

A partir do Android 3.0, o arquivo recovery.fstab ganha um campo opcional adicional, options . Atualmente a única opção definida é length , que permite especificar explicitamente o comprimento da partição. Esse comprimento é usado ao reformatar a partição (por exemplo, para a partição userdata durante uma operação de limpeza de dados/redefinição de fábrica ou para a partição do sistema durante a instalação de um pacote OTA completo). Se o valor do comprimento for negativo, o tamanho a ser formatado será obtido adicionando o valor do comprimento ao tamanho real da partição. Por exemplo, definir "length=-16384" significa que os últimos 16k dessa partição não serão substituídos quando a partição for reformatada. Isso oferece suporte a recursos como criptografia da partição userdata (onde os metadados de criptografia são armazenados no final da partição e não devem ser substituídos).

Nota: Os campos device2 e options são opcionais, criando ambiguidade na análise. Se a entrada no quarto campo da linha começar com um caractere '/', será considerada uma entrada do dispositivo2 ; se a entrada não começar com um caractere '/', será considerada um campo de opções .

Animação de inicialização

Os fabricantes de dispositivos podem personalizar a animação mostrada quando um dispositivo Android está inicializando. Para isso, construa um arquivo .zip organizado e localizado de acordo com as especificações no formato bootanimation .

Para dispositivos Android Things , você pode fazer upload do arquivo compactado no console do Android Things para incluir as imagens no produto selecionado.

Observação: essas imagens devem atender às diretrizes da marca Android.

IU de recuperação

Para oferecer suporte a dispositivos com diferentes hardwares disponíveis (botões físicos, LEDs, telas, etc.), você pode personalizar a interface de recuperação para exibir o status e acessar os recursos ocultos operados manualmente para cada dispositivo.

Seu objetivo é construir uma pequena biblioteca estática com alguns objetos C++ para fornecer a funcionalidade específica do dispositivo. O arquivo bootable/recovery/default_device.cpp é usado por padrão e é um bom ponto de partida para copiar ao gravar uma versão deste arquivo para o seu dispositivo.

Observação: você poderá ver uma mensagem dizendo Nenhum comando aqui. Para alternar o texto, mantenha pressionado o botão liga / desliga enquanto pressiona o botão de aumentar volume. Se seus dispositivos não tiverem os dois botões, pressione e segure qualquer botão para alternar o texto.

device/yoyodyne/tardis/recovery/recovery_ui.cpp
#include <linux/input.h>

#include "common.h"
#include "device.h"
#include "screen_ui.h"

Funções de cabeçalho e item

A classe Device requer funções para retornar cabeçalhos e itens que aparecem no menu de recuperação oculto. Os cabeçalhos descrevem como operar o menu (ou seja, controles para alterar/selecionar o item destacado).

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

Observação: linhas longas são truncadas (não quebradas), portanto, lembre-se da largura da tela do seu dispositivo.

Personalizar CheckKey

A seguir, defina a implementação do RecoveryUI do seu dispositivo. Este exemplo pressupõe que o dispositivo tardis tem uma tela, então você pode herdar da implementação interna do ScreenRecoveryUI (consulte as instruções para dispositivos sem tela ). A única função a ser personalizada do ScreenRecoveryUI é CheckKey() , que faz o manuseio inicial da chave assíncrona:

class TardisUI : public ScreenRecoveryUI {
  public:
    virtual KeyAction CheckKey(int key) {
        if (key == KEY_HOME) {
            return TOGGLE;
        }
        return ENQUEUE;
    }
};

Constantes CHAVE

As constantes KEY_* são definidas em linux/input.h . CheckKey() é chamado independentemente do que está acontecendo no restante da recuperação: quando o menu é desativado, quando está ativado, durante a instalação do pacote, durante a limpeza dos dados do usuário, etc. Ele pode retornar uma das quatro constantes:

  • ALTERNAR . Ativar ou desativar a exibição do menu e/ou registro de texto
  • REINÍCIO . Reinicie imediatamente o dispositivo
  • IGNORAR . Ignorar este pressionamento de tecla
  • ENQUEUE . Enfileire esse pressionamento de tecla para ser consumido de forma síncrona (ou seja, pelo sistema de menu de recuperação se a exibição estiver ativada)

CheckKey() é chamado cada vez que um evento key-down é seguido por um evento key-up para a mesma chave. (A sequência de eventos A-down B-down B-up A-up resulta apenas na chamada CheckKey(B) .) CheckKey() pode chamar IsKeyPressed() , para descobrir se outras teclas estão sendo pressionadas. (Na sequência de eventos principais acima, se CheckKey(B) chamasse IsKeyPressed(A) ele teria retornado verdadeiro.)

CheckKey() pode manter o estado em sua classe; isso pode ser útil para detectar sequências de chaves. Este exemplo mostra uma configuração um pouco mais complexa: a tela é alternada mantendo pressionado o botão liga / desliga e aumentando o volume, e o dispositivo pode ser reiniciado imediatamente pressionando o botão liga / desliga cinco vezes seguidas (sem outras teclas intermediárias):

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

UI de recuperação de tela

Ao usar suas próprias imagens (ícone de erro, animação de instalação, barras de progresso) com ScreenRecoveryUI, você pode definir a variável animation_fps para controlar a velocidade em quadros por segundo (FPS) das animações.

Nota: O script interlace-frames.py atual permite armazenar as informações animation_fps na própria imagem. Nas versões anteriores do Android, era necessário definir você mesmo animation_fps .

Para definir a variável animation_fps , substitua a função ScreenRecoveryUI::Init() em sua subclasse. Defina o valor e chame a função parent Init() para concluir a inicialização. O valor padrão (20 FPS) corresponde às imagens de recuperação padrão; ao usar essas imagens você não precisa fornecer uma função Init() . Para obter detalhes sobre imagens, consulte Imagens de UI de recuperação .

Classe de dispositivo

Depois de ter uma implementação do RecoveryUI, defina sua classe de dispositivo (uma subclasse da classe interna Device). Ele deve criar uma única instância de sua classe UI e retorná-la da função GetUI() :

class TardisDevice : public Device {
  private:
    TardisUI* ui;

  public:
    TardisDevice() :
        ui(new TardisUI) {
    }

    RecoveryUI* GetUI() { return ui; }

Iniciar a recuperação

O método StartRecovery() é chamado no início da recuperação, após a inicialização da UI e após a análise dos argumentos, mas antes de qualquer ação ser executada. A implementação padrão não faz nada, então você não precisa fornecer isso na sua subclasse se não tiver nada para fazer:

   void StartRecovery() {
       // ... do something tardis-specific here, if needed ....
    }

Fornecer e gerenciar menu de recuperação

O sistema chama dois métodos para obter a lista de linhas de cabeçalho e a lista de itens. Nesta implementação, ele retorna os arrays estáticos definidos no topo do arquivo:

const char* const* GetMenuHeaders() { return HEADERS; }
const char* const* GetMenuItems() { return ITEMS; }

HandleMenuKey

Em seguida, forneça uma função HandleMenuKey() , que pressiona uma tecla e a visibilidade atual do menu e decide qual ação tomar:

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

O método usa um código-chave (que foi previamente processado e enfileirado pelo método CheckKey() do objeto UI) e o estado atual da visibilidade do menu/log de texto. O valor de retorno é um número inteiro. Se o valor for 0 ou superior, será considerado a posição de um item de menu, que é invocado imediatamente (veja o método InvokeMenuItem() abaixo). Caso contrário, pode ser uma das seguintes constantes predefinidas:

  • kDestacar para cima . Mova o destaque do menu para o item anterior
  • kDestacarPara Baixo . Mova o destaque do menu para o próximo item
  • kInvokeItem . Invocar o item atualmente destacado
  • kNoAction . Não faça nada com esta tecla

Conforme implícito no argumento visível, HandleMenuKey() é chamado mesmo se o menu não estiver visível. Ao contrário CheckKey() , ele não é chamado enquanto a recuperação está fazendo algo como limpar dados ou instalar um pacote – ele é chamado apenas quando a recuperação está ociosa e aguardando entrada.

Mecanismos de trackball

Se o seu dispositivo tiver um mecanismo de entrada semelhante ao trackball (gera eventos de entrada com tipo EV_REL e código REL_Y), a recuperação sintetiza os pressionamentos de tecla KEY_UP e KEY_DOWN sempre que o dispositivo de entrada semelhante ao trackball relatar movimento no eixo Y. Tudo o que você precisa fazer é mapear os eventos KEY_UP e KEY_DOWN nas ações do menu. Esse mapeamento não acontece para CheckKey() , portanto você não pode usar movimentos do trackball como gatilhos para reinicializar ou alternar a exibição.

Teclas modificadoras

Para verificar se as teclas estão pressionadas como modificadores, chame o método IsKeyPressed() do seu próprio objeto de UI. Por exemplo, em alguns dispositivos, pressionar Alt-W na recuperação iniciaria uma limpeza de dados, independentemente de o menu estar visível ou não. Você poderia implementar assim:

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

Nota: Se visível for falso, não faz sentido retornar os valores especiais que manipulam o menu (mover destaque, invocar item destacado), pois o usuário não pode ver o destaque. No entanto, você pode retornar os valores, se desejar.

InvocarMenuItem

A seguir, forneça um método InvokeMenuItem() que mapeie posições inteiras na matriz de itens retornados por GetMenuItems() para ações. Para a matriz de itens no exemplo tardis, use:

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

Este método pode retornar qualquer membro da enumeração BuiltinAction para informar ao sistema para executar essa ação (ou o membro NO_ACTION se você quiser que o sistema não faça nada). Este é o lugar para fornecer funcionalidade de recuperação adicional além do que está no sistema: Adicione um item para ele em seu menu, execute-o aqui quando esse item de menu for invocado e retorne NO_ACTION para que o sistema não faça mais nada.

BuiltinAction contém os seguintes valores:

  • NO_ACTION . Fazer nada.
  • REINÍCIO . Saia da recuperação e reinicie o dispositivo normalmente.
  • APPLY_EXT, APPLY_CACHE, APPLY_ADB_SIDELOAD . Instale um pacote de atualização de vários lugares. Para obter detalhes, consulte Sideload .
  • LIMPAR CACHE . Reformate apenas a partição de cache. Nenhuma confirmação é necessária, pois isso é relativamente inofensivo.
  • WIPE_DATA . Reformate os dados do usuário e as partições de cache, também conhecido como redefinição de dados de fábrica. O usuário é solicitado a confirmar esta ação antes de prosseguir.

O último método, WipeData() , é opcional e é chamado sempre que uma operação de limpeza de dados é iniciada (seja na recuperação por meio do menu ou quando o usuário optou por fazer uma redefinição de dados de fábrica no sistema principal). Este método é chamado antes que os dados do usuário e as partições de cache sejam apagados. Se o seu dispositivo armazena dados do usuário em qualquer lugar que não seja essas duas partições, você deve apagá-los aqui. Você deve retornar 0 para indicar sucesso e outro valor para falha, embora atualmente o valor de retorno seja ignorado. Os dados do usuário e as partições de cache serão apagados independentemente de você retornar sucesso ou falha.

   int WipeData() {
       // ... do something tardis-specific here, if needed ....
       return 0;
    }

Criar dispositivo

Por fim, inclua alguns padrões no final do arquivo recovery_ui.cpp para a função make_device() que cria e retorna uma instância da sua classe Device:

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

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

Depois de concluir o arquivo recovery_ui.cpp, crie-o e vincule-o à recuperação no seu dispositivo. No Android.mk, crie uma biblioteca estática que contenha apenas este arquivo 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)

Em seguida, na configuração da placa deste dispositivo, especifique sua biblioteca estática como o valor de TARGET_RECOVERY_UI_LIB.

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

# device-specific extensions to the recovery UI
TARGET_RECOVERY_UI_LIB := librecovery_ui_tardis

Imagens da IU de recuperação

A interface do usuário de recuperação consiste em imagens. Idealmente, os usuários nunca interagem com a IU: durante uma atualização normal, o telefone inicia a recuperação, preenche a barra de progresso da instalação e reinicializa no novo sistema sem a intervenção do usuário. No caso de um problema de atualização do sistema, a única ação do usuário que pode ser tomada é ligar para o atendimento ao cliente.

Uma interface somente imagem elimina a necessidade de localização. No entanto, a partir do Android 5.0 a atualização pode exibir uma sequência de texto (por exemplo, "Instalando atualização do sistema...") junto com a imagem. Para obter detalhes, consulte Texto de recuperação localizado .

Android 5.0 e posterior

A IU de recuperação do Android 5.0 e posterior usa duas imagens principais: a imagem de erro e a animação de instalação .

imagem mostrada durante erro ota

Figura 1. icon_error.png

imagem mostrada durante a instalação do ota

Figura 2. icon_installing.png

A animação de instalação é representada como uma única imagem PNG com diferentes quadros da animação entrelaçados por linha (é por isso que a Figura 2 aparece comprimida). Por exemplo, para uma animação de sete quadros de 200x200, crie uma única imagem de 200x1400 onde o primeiro quadro são as linhas 0, 7, 14, 21, ...; o segundo quadro são as linhas 1, 8, 15, 22, ...; etc. A imagem combinada inclui um pedaço de texto que indica o número de quadros de animação e o número de quadros por segundo (FPS). A ferramenta bootable/recovery/interlace-frames.py pega um conjunto de quadros de entrada e os combina na imagem composta necessária usada pela recuperação.

As imagens padrão estão disponíveis em diferentes densidades e estão localizadas em bootable/recovery/res-$DENSITY/images (por exemplo, bootable/recovery/res-hdpi/images ). Para usar uma imagem estática durante a instalação, você só precisa fornecer a imagem icon_installing.png e definir o número de quadros na animação como 0 (o ícone de erro não é animado; é sempre uma imagem estática).

Android 4.xe anteriores

A IU de recuperação do Android 4.x e versões anteriores usa a imagem de erro (mostrada acima) e a animação de instalação , além de várias imagens de sobreposição:

imagem mostrada durante a instalação do ota

Figura 3. icon_installing.png

imagem mostrada como primeira sobreposição

Figura 4. icon-installing_overlay01.png

imagem mostrada como sétima sobreposição

Figura 5. icon_installing_overlay07.png

Durante a instalação, a exibição na tela é construída desenhando a imagem icon_installing.png e, em seguida, desenhando um dos quadros de sobreposição sobre ela no deslocamento adequado. Aqui, uma caixa vermelha é sobreposta para destacar onde a sobreposição é colocada no topo da imagem base:

imagem composta de instalação mais primeira sobreposição

Figura 6. Instalando o quadro de animação 1 (icon_installing.png + icon_installing_overlay01.png)

imagem composta de instalação mais sétima sobreposição

Figura 7. Instalando o quadro de animação 7 (icon_installing.png + icon_installing_overlay07.png)

Os quadros subsequentes são exibidos desenhando apenas a próxima imagem de sobreposição sobre o que já está lá; a imagem base não é redesenhada.

O número de quadros na animação, a velocidade desejada e os deslocamentos x e y da sobreposição em relação à base são definidos por variáveis ​​de membro da classe ScreenRecoveryUI. Ao usar imagens personalizadas em vez de imagens padrão, substitua o método Init() em sua subclasse para alterar esses valores para suas imagens personalizadas (para obter detalhes, consulte ScreenRecoveryUI ). O script bootable/recovery/make-overlay.py pode ajudar na conversão de um conjunto de quadros de imagem para o formato "imagem base + imagens de sobreposição" necessário para a recuperação, incluindo o cálculo dos deslocamentos necessários.

As imagens padrão estão localizadas em bootable/recovery/res/images . Para usar uma imagem estática durante a instalação, você só precisa fornecer a imagem icon_installing.png e definir o número de quadros na animação como 0 (o ícone de erro não é animado; é sempre uma imagem estática).

Texto de recuperação localizado

O Android 5.x exibe uma sequência de texto (por exemplo, "Instalando atualização do sistema...") junto com a imagem. Quando o sistema principal inicializa na recuperação, ele passa a localidade atual do usuário como uma opção de linha de comando para recuperação. Para cada mensagem a ser exibida, a recuperação inclui uma segunda imagem composta com sequências de texto pré-renderizadas para essa mensagem em cada localidade.

Exemplo de imagem de strings de texto de recuperação:

imagem do texto de recuperação

Figura 8. Texto localizado para mensagens de recuperação

O texto de recuperação pode exibir as seguintes mensagens:

  • Instalando atualização do sistema...
  • Erro!
  • Apagando... (ao fazer uma limpeza de dados/redefinição de fábrica)
  • Nenhum comando (quando um usuário inicia a recuperação manualmente)

O aplicativo Android em bootable/recovery/tools/recovery_l10n/ renderiza localizações de uma mensagem e cria a imagem composta. Para obter detalhes sobre como usar este aplicativo, consulte os comentários em bootable/recovery/tools/recovery_l10n/src/com/android/recovery_l10n/Main.java .

Quando um usuário inicializa manualmente na recuperação, a localidade pode não estar disponível e nenhum texto é exibido. Não torne as mensagens de texto críticas para o processo de recuperação.

Nota: A interface oculta que exibe mensagens de log e permite ao usuário selecionar ações no menu está disponível somente em inglês.

Barras de progresso

Barras de progresso podem aparecer abaixo da imagem principal (ou animação). A barra de progresso é feita combinando duas imagens de entrada, que devem ser do mesmo tamanho:

barra de progresso vazia

Figura 9. progress_empty.png

barra de progresso completa

Figura 10. progress_fill.png

A extremidade esquerda da imagem de preenchimento é exibida próxima à extremidade direita da imagem vazia para formar a barra de progresso. A posição do limite entre as duas imagens é alterada para indicar o progresso. Por exemplo, com os pares de imagens de entrada acima, exiba:

barra de progresso em 1%

Figura 11. Barra de progresso em 1%>

barra de progresso em 10%

Figura 12. Barra de progresso em 10%

barra de progresso em 50%

Figura 13. Barra de progresso em 50%

Você pode fornecer versões específicas do dispositivo dessas imagens colocando-as (neste exemplo) device/yoyodyne/tardis/recovery/res/images . Os nomes dos arquivos devem corresponder aos listados acima; quando um arquivo é encontrado nesse diretório, o sistema de compilação o utiliza em preferência à imagem padrão correspondente. Somente PNGs no formato RGB ou RGBA com profundidade de cores de 8 bits são suportados.

Observação: no Android 5.x, se a localidade for recuperada e for um idioma da direita para a esquerda (RTL) (árabe, hebraico etc.), a barra de progresso será preenchida da direita para a esquerda.

Dispositivos sem telas

Nem todos os dispositivos Android possuem telas. Se o seu dispositivo for um dispositivo headless ou tiver uma interface somente de áudio, talvez seja necessário fazer uma personalização mais extensa da IU de recuperação. Em vez de criar uma subclasse de ScreenRecoveryUI, crie uma subclasse de sua classe pai RecoveryUI diretamente.

RecoveryUI possui métodos para lidar com operações de UI de nível inferior, como "alternar a exibição", "atualizar a barra de progresso", "mostrar o menu", "alterar a seleção do menu" etc. para o seu dispositivo. Talvez o seu dispositivo tenha LEDs onde você pode usar diferentes cores ou padrões de intermitência para indicar o estado, ou talvez você possa reproduzir áudio. (Talvez você não queira oferecer suporte a um menu ou ao modo de "exibição de texto"; você pode evitar acessá-los com implementações CheckKey() e HandleMenuKey() que nunca ativam a exibição ou selecionam um item de menu. Neste caso , muitos dos métodos RecoveryUI que você precisa fornecer podem ser apenas stubs vazios.)

Consulte bootable/recovery/ui.h para obter a declaração de RecoveryUI para ver quais métodos você deve suportar. RecoveryUI é abstrato – alguns métodos são puramente virtuais e devem ser fornecidos por subclasses – mas contém o código para processar as principais entradas. Você também pode substituir isso, se o seu dispositivo não tiver chaves ou se quiser processá-las de maneira diferente.

Atualizador

Você pode usar código específico do dispositivo na instalação do pacote de atualização, fornecendo suas próprias funções de extensão que podem ser chamadas no script do atualizador. Aqui está um exemplo de função para o dispositivo tardis:

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

#include "edify/expr.h"

Cada função de extensão possui a mesma assinatura. Os argumentos são o nome pelo qual a função foi chamada, um cookie State* , o número de argumentos recebidos e uma matriz de ponteiros Expr* representando os argumentos. O valor de retorno é um Value* recém-alocado.

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

Seus argumentos não foram avaliados no momento em que sua função é chamada — a lógica da sua função determina quais deles serão avaliados e quantas vezes. Assim, você pode usar funções de extensão para implementar suas próprias estruturas de controle. Call Evaluate() para avaliar um argumento Expr* , retornando um Value* . Se Evaluate() retornar NULL, você deverá liberar todos os recursos que estiver mantendo e retornar imediatamente NULL (isso propaga aborta a pilha edify). Caso contrário, você assumirá a propriedade do Valor retornado e será responsável por eventualmente chamar FreeValue() nele.

Suponha que a função precise de dois argumentos: uma chave com valor de string e uma imagem com valor de blob. Você poderia ler argumentos como este:

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

Verificar NULL e liberar argumentos avaliados anteriormente pode ser entediante para vários argumentos. A função ReadValueArgs() pode tornar isso mais fácil. Em vez do código acima, você poderia ter escrito isto:

   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() não faz verificação de tipo, então você deve fazer isso aqui; é mais conveniente fazer isso com uma instrução if ao custo de produzir uma mensagem de erro um pouco menos específica quando ela falhar. Mas ReadValueArgs() cuida da avaliação de cada argumento e da liberação de todos os argumentos avaliados anteriormente (bem como da definição de uma mensagem de erro útil) se alguma das avaliações falhar. Você pode usar uma função de conveniência ReadValueVarArgs() para avaliar um número variável de argumentos (ela retorna uma matriz de Value* ).

Após avaliar os argumentos, faça o trabalho da função:

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

O valor de retorno deve ser um objeto Value* ; a propriedade deste objeto passará para o chamador. O chamador assume a propriedade de todos os dados apontados por este Value* —especificamente o membro de dados.

Neste caso, você deseja retornar um valor verdadeiro ou falso para indicar sucesso. Lembre-se da convenção de que a string vazia é falsa e todas as outras strings são verdadeiras . Você deve malloc um objeto Value com uma cópia malloc da string constante para retornar, já que o chamador irá free() ambos. Não se esqueça de chamar FreeValue() nos objetos obtidos avaliando seus argumentos!

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

A função de conveniência StringValue() envolve uma string em um novo objeto Value. Use para escrever o código acima de forma mais sucinta:

   FreeValue(key);
    FreeValue(image);

    return StringValue(strdup(successful ? "t" : ""));
}

Para conectar funções ao interpretador edify, forneça a função Register_ foo onde foo é o nome da biblioteca estática que contém este código. Chame RegisterFunction() para registrar cada função de extensão. Por convenção, nomeie funções específicas do dispositivo device . whatever para evitar conflitos com futuras funções integradas adicionadas.

void Register_librecovery_updater_tardis() {
    RegisterFunction("tardis.reprogram", ReprogramTardisFn);
}

Agora você pode configurar o makefile para construir uma biblioteca estática com seu código. (Este é o mesmo makefile usado para personalizar a UI de recuperação na seção anterior; seu dispositivo pode ter ambas as bibliotecas estáticas definidas aqui.)

device/yoyodyne/tardis/recovery/Android.mk
include $(CLEAR_VARS)
LOCAL_SRC_FILES := recovery_updater.c
LOCAL_C_INCLUDES += bootable/recovery

O nome da biblioteca estática deve corresponder ao nome da função Register_ libname contida nela.

LOCAL_MODULE := librecovery_updater_tardis
include $(BUILD_STATIC_LIBRARY)

Por fim, configure o build de recuperação para extrair sua biblioteca. Adicione sua biblioteca a TARGET_RECOVERY_UPDATER_LIBS (que pode conter múltiplas bibliotecas; todas elas são registradas). Se o seu código depende de outras bibliotecas estáticas que não são extensões edify (ou seja, elas não têm uma função Register_ libname ), você pode listá-las em TARGET_RECOVERY_UPDATER_EXTRA_LIBS para vinculá-las ao atualizador sem chamar sua função de registro (inexistente). Por exemplo, se o código específico do seu dispositivo quisesse usar zlib para descompactar dados, você incluiria libz aqui.

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 +=

Os scripts atualizadores em seu pacote OTA agora podem chamar sua função como qualquer outra. Para reprogramar seu dispositivo tardis, o script de atualização pode conter: tardis.reprogram("the-key", package_extract_file("tardis-image.dat")) . Isso usa a versão de argumento único da função integrada package_extract_file() , que retorna o conteúdo de um arquivo extraído do pacote de atualização como um blob para produzir o segundo argumento para a nova função de extensão.

Geração de pacotes OTA

O componente final é fazer com que as ferramentas de geração de pacotes OTA conheçam os dados específicos do seu dispositivo e emitam scripts de atualização que incluem chamadas para suas funções de extensão.

Primeiro, faça com que o sistema de compilação conheça um blob de dados específico do dispositivo. Supondo que seu arquivo de dados esteja em device/yoyodyne/tardis/tardis.dat , declare o seguinte no AndroidBoard.mk do seu dispositivo:

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

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

Você também pode colocá-lo em um Android.mk, mas ele deve ser protegido por uma verificação de dispositivo, já que todos os arquivos Android.mk na árvore são carregados, independentemente do dispositivo que está sendo construído. (Se sua árvore inclui vários dispositivos, você deseja adicionar apenas o arquivo tardis.dat ao criar o dispositivo 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

Estes são chamados de arquivos de rádio por razões históricas; eles podem não ter nada a ver com o rádio do dispositivo (se houver). Eles são simplesmente blocos opacos de dados que o sistema de compilação copia nos arquivos de destino .zip usados ​​pelas ferramentas de geração OTA. Quando você faz uma compilação, tardis.dat é armazenado em target-files.zip como RADIO/tardis.dat . Você pode chamar add-radio-file várias vezes para adicionar quantos arquivos desejar.

Módulo Python

Para estender as ferramentas de lançamento, escreva um módulo Python (deve ser denominado releasetools.py) que as ferramentas podem chamar, se estiverem presentes. Exemplo:

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"));""")

Uma função separada trata do caso de geração de um pacote OTA incremental. Para este exemplo, suponha que você precise reprogramar o tardis somente quando o arquivo tardis.dat for alterado entre duas compilações.

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"));""")

Funções do módulo

Você pode fornecer as seguintes funções no módulo (implementar apenas as que você precisa).

FullOTA_Assertions()
Chamado próximo ao início da geração de um OTA completo. Este é um bom local para emitir afirmações sobre o estado atual do dispositivo. Não emita comandos de script que façam alterações no dispositivo.
FullOTA_InstallBegin()
Chamado após todas as asserções sobre o estado do dispositivo terem passado, mas antes de qualquer alteração ter sido feita. Você pode emitir comandos para atualizações específicas do dispositivo que devem ser executadas antes que qualquer outra coisa no dispositivo seja alterada.
FullOTA_InstallEnd()
Chamado no final da geração do script, após os comandos de script para atualizar a inicialização e as partições do sistema terem sido emitidos. Você também pode emitir comandos adicionais para atualizações específicas do dispositivo.
IncrementalOTA_Assertions()
Semelhante a FullOTA_Assertions() mas chamado ao gerar um pacote de atualização incremental.
IncrementalOTA_VerifyBegin()
Chamado após todas as asserções sobre o estado do dispositivo terem sido aprovadas, mas antes de qualquer alteração ter sido feita. Você pode emitir comandos para atualizações específicas do dispositivo que devem ser executadas antes que qualquer outra coisa no dispositivo seja alterada.
IncrementalOTA_VerifyEnd()
Chamado no final da fase de verificação, quando o script terminar de confirmar se os arquivos que irá tocar possuem o conteúdo inicial esperado. Neste ponto, nada no dispositivo foi alterado. Você também pode emitir código para verificações adicionais específicas do dispositivo.
IncrementalOTA_InstallBegin()
Chamado depois que os arquivos a serem corrigidos foram verificados como tendo o estado anterior esperado, mas antes de quaisquer alterações serem feitas. Você pode emitir comandos para atualizações específicas do dispositivo que devem ser executadas antes que qualquer outra coisa no dispositivo seja alterada.
IncrementalOTA_InstallEnd()
Semelhante ao pacote OTA completo, ele é chamado no final da geração do script, após a emissão dos comandos de script para atualizar a inicialização e as partições do sistema. Você também pode emitir comandos adicionais para atualizações específicas do dispositivo.

Nota: Se o dispositivo ficar sem energia, a instalação OTA poderá ser reiniciada desde o início. Esteja preparado para lidar com dispositivos nos quais esses comandos já foram executados, total ou parcialmente.

Passar funções para objetos de informação

Passe funções para um único objeto de informação que contém vários itens úteis:

  • info.input_zip . (Somente OTAs completos) O objeto zipfile.ZipFile para os arquivos de destino de entrada .zip.
  • info.source_zip . (Somente OTAs incrementais) O objeto zipfile.ZipFile para os arquivos de destino de origem .zip (a compilação já no dispositivo quando o pacote incremental está sendo instalado).
  • info.target_zip . (Somente OTAs incrementais) O objeto zipfile.ZipFile para os arquivos de destino .zip (a compilação que o pacote incremental coloca no dispositivo).
  • info.output_zip . Pacote sendo criado; um objeto zipfile.ZipFile aberto para gravação. Use common.ZipWriteStr(info.output_zip, filename , data ) para adicionar um arquivo ao pacote.
  • info.script . Objeto de script ao qual você pode anexar comandos. Chame info.script.AppendExtra( script_text ) para gerar texto no script. Certifique-se de que o texto de saída termine com ponto e vírgula para que não seja executado em comandos emitidos posteriormente.

Para obter detalhes sobre o objeto info, consulte a documentação da Python Software Foundation para arquivos ZIP .

Especifique a localização do módulo

Especifique a localização do script releasetools.py do seu dispositivo no arquivo BoardConfig.mk:

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

TARGET_RELEASETOOLS_EXTENSIONS := device/yoyodyne/tardis

Se TARGET_RELEASETOOLS_EXTENSIONS não estiver definido, o padrão é o diretório $(TARGET_DEVICE_DIR)/../common ( device/yoyodyne/common neste exemplo). É melhor definir explicitamente a localização do script releasetools.py. Ao construir o dispositivo tardis, o script releasetools.py é incluído no arquivo .zip dos arquivos de destino ( META/releasetools.py ).

Quando você executa as ferramentas de lançamento ( img_from_target_files ou ota_from_target_files ), o script releasetools.py no .zip dos arquivos de destino, se presente, tem preferência sobre aquele da árvore de origem do Android. Você também pode especificar explicitamente o caminho para as extensões específicas do dispositivo com a opção -s (ou --device_specific ), que tem prioridade máxima. Isso permite corrigir erros e fazer alterações nas extensões do releasetools e aplicar essas alterações a arquivos de destino antigos.

Agora, quando você executa ota_from_target_files , ele seleciona automaticamente o módulo específico do dispositivo do arquivo .zip target_files e o usa ao gerar pacotes 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

Como alternativa, você pode especificar extensões específicas do dispositivo ao executar 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

Nota: Para obter uma lista completa de opções, consulte os comentários ota_from_target_files em build/make/tools/releasetools/ota_from_target_files .

Mecanismo de carregamento lateral

O Recovery possui um mecanismo de sideload para instalar manualmente um pacote de atualização sem baixá-lo pelo ar pelo sistema principal. O sideload é útil para depurar ou fazer alterações em dispositivos onde o sistema principal não pode ser inicializado.

Historicamente, o sideload tem sido feito através do carregamento de pacotes do cartão SD do dispositivo; no caso de um dispositivo que não inicializa, o pacote pode ser colocado no cartão SD usando algum outro computador e, em seguida, o cartão SD inserido no dispositivo. Para acomodar dispositivos Android sem armazenamento externo removível, a recuperação oferece suporte a dois mecanismos adicionais de carregamento lateral: carregar pacotes da partição de cache e carregá-los por USB usando adb.

Para invocar cada mecanismo de sideload, o método Device::InvokeMenuItem() do seu dispositivo pode retornar os seguintes valores de BuiltinAction:

  • APLICAR_EXT . Carregue lateralmente um pacote de atualização do armazenamento externo (diretório /sdcard ). Sua recuperação.fstab deve definir o ponto de montagem /sdcard . Isso não é utilizável em dispositivos que imitam um cartão SD com um link simulado para /data (ou algum mecanismo semelhante). /data normalmente não estão disponíveis para recuperação porque podem ser criptografados. A interface do usuário de recuperação exibe um menu de arquivos .zip em /sdcard e permite que o usuário selecione um.
  • APLIC_CACHE . Semelhante ao carregar um pacote de /sdcard , exceto que o diretório /cache (que está sempre disponível para recuperação) é usado. No sistema regular, /cache é gravável apenas por usuários privilegiados e, se o dispositivo não for inicializado, o diretório /cache não poderá ser gravado (o que torna esse mecanismo de utilidade limitada).
  • APLIC_ADB_SIDELOAD . Permite que o usuário envie um pacote para o dispositivo por meio de um cabo USB e a ferramenta de desenvolvimento do ADB. Quando esse mecanismo é chamado, a recuperação inicia sua própria mini versão do daemon ADBD para deixar o ADB em um computador host conectado conversar com ele. Esta mini versão suporta apenas um único comando: adb sideload filename . O arquivo nomeado é enviado da máquina host para o dispositivo, que verifica e instala -o como se estivesse no armazenamento local.

Algumas advertências:

  • Somente transporte USB é suportado.
  • Se a sua recuperação executar o ADBD normalmente (geralmente é verdadeiro para o UserDebug e o ENG Builds), isso será desligado enquanto o dispositivo estiver no modo Sideload Adb e será reiniciado quando o ADB Sideload terminar de receber um pacote. Enquanto estiver no modo Adb SideLoad, nenhum comandos de ADB além do trabalho sideload ( logcat , reboot , push , pull , shell , etc. Todos falham).
  • Você não pode sair do modo ADB Sideload no dispositivo. Para abortar, você pode enviar /dev/null (ou qualquer outra coisa que não seja um pacote válido) como o pacote e, em seguida, o dispositivo falhará ao verificar e interromper o procedimento de instalação. O método CheckKey() da implementação da Recoverui continuará a ser chamado para pressionamentos de chave, para que você possa fornecer uma sequência de chaves que reinicie o dispositivo e funcione no modo ADB Sideload.