Código específico do dispositivo

O sistema de recuperação inclui vários hooks para inserir código específico do dispositivo para 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

Desde o Android 2.3, a plataforma oferece suporte a dispositivos de memória flash eMMC e ao sistema de arquivos ext4 que é executado nesses dispositivos. Ele também oferece suporte a dispositivos flash MTD (Memory Technology Device) e ao sistema de arquivos yaffs2 de versões mais antigas.

O arquivo de mapa de partição é especificado por TARGET_RECOVERY_FSTAB e usado pelo binário de recuperação e pelas ferramentas de criaçã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 ser assim:

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 precisam ser definidos (os dispositivos também podem adicionar partições extras). Há cinco tipos de sistemas de arquivos compatíveis:

yaffs2
Um sistema de arquivos yaffs2 em cima de um dispositivo flash MTD. "device" precisa ser o nome da partição MTD e 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 é montado, mas o ponto de montagem é usado como uma chave para localizar a partição. "device" precisa ser o nome da partição MTD em /proc/mtd.
ext4
Um sistema de arquivos ext4 em cima de um dispositivo flash eMMc. "device" precisa 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. Assim como o tipo mtd, o eMMC nunca é montado, mas a string do ponto de montagem é usada para localizar o dispositivo na tabela.
vfat
Um sistema de arquivos FAT em cima de um dispositivo de bloco, geralmente para armazenamento externo, como um cartão SD. O dispositivo é o dispositivo de bloco. O dispositivo2 é um segundo dispositivo de bloco que o sistema tenta montar se a montagem do dispositivo principal falhar (para compatibilidade com cartões SD que podem ou não ser formatados com uma tabela de partição).

Todas as partições precisam ser montadas no diretório raiz. Ou seja, o valor do ponto de montagem precisa começar com uma barra e não ter outras barras. Essa restrição se aplica apenas à montagem de sistemas de arquivos na recuperação. O sistema principal pode montá-los em qualquer lugar. Os diretórios /boot, /recovery e /misc precisam ser tipos brutos (mtd ou emmc), enquanto os diretórios /system, /data, /cache e /sdcard (se disponíveis) precisam ser tipos de sistema de arquivos (yaffs2, ext4 ou vfat).

A partir do Android 3.0, o arquivo recovery.fstab ganha um campo opcional adicional, options. No momento, 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/restauraçã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 ela for reformatada. Isso oferece suporte a recursos como criptografia da partição userdata (em que os metadados de criptografia são armazenados no final da partição que não deve ser substituída).

Observação:os campos device2 e options são opcionais, o que cria ambiguidade na análise. Se a entrada no quarto campo da linha começar com um caractere "/", ela será considerada uma entrada device2. Se não começar com esse caractere, ela será considerada um campo options.

Animação de inicialização

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

Para dispositivos Android Things, faça upload do arquivo ZIP no console do Android Things para incluir as imagens no produto selecionado.

Observação:essas imagens precisam obedecer às diretrizes da marca Android. Para conferir diretrizes de marca, consulte a seção sobre o Android no Partner Marketing Hub.

Interface de recuperação

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

Seu objetivo é criar 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 desse arquivo para seu dispositivo.

Observação:talvez apareça uma mensagem dizendo Nenhum comando aqui. Para alternar o texto, mantenha pressionado o botão liga/desliga e toque no botão de aumentar o volume. Se o dispositivo não tiver os dois botões, pressione e mantenha pressionado 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" exige 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 mudar/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:as linhas longas são truncadas (não quebradas), então considere a largura da tela do dispositivo.

Personalizar CheckKey

Em seguida, defina a implementação da RecoveryUI do dispositivo. Este exemplo pressupõe que o dispositivo tardis tem uma tela. Assim, é possível herdar da implementação ScreenRecoveryUI integrada. Consulte as instruções para dispositivos sem tela. A única função para personalizar da ScreenRecoveryUI é CheckKey(), que faz o processamento assíncrono inicial de chaves:

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

Constantes KEY

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

  • ATIVAR/DESATIVAR. Ativar ou desativar a exibição do menu e/ou do registro de texto
  • REINICIAR. Reiniciar o dispositivo imediatamente
  • IGNORAR. Ignorar este pressionamento de tecla
  • ENQUEUE. Enfileira esse pressionamento de tecla para ser consumido de forma síncrona (ou seja, pelo sistema de menu de recuperação se a tela estiver ativada).

CheckKey() é chamado sempre que um evento de tecla pressionada é seguido por um evento de tecla liberada para a mesma tecla. A sequência de eventos A-down B-down B-up A-up resulta apenas na chamada de CheckKey(B). CheckKey() pode chamar IsKeyPressed() para descobrir se outras teclas estão pressionadas. Na sequência de eventos de tecla acima, se CheckKey(B) chamasse IsKeyPressed(A), ele retornaria "true".

O CheckKey() pode manter o estado na classe dele, o que é útil para detectar sequências de chaves. Este exemplo mostra uma configuração um pouco mais complexa: a tela é ativada/desativada ao manter pressionado o botão liga/desliga e pressionar o botão de aumentar volume. O dispositivo pode ser reinicializado imediatamente ao pressionar 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;
    }
};

ScreenRecoveryUI

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

Observação:o script interlace-frames.py atual permite armazenar as informações animation_fps na própria imagem. Em versões anteriores do Android, era necessário definir animation_fps por conta própria.

Para definir a variável animation_fps, substitua a função ScreenRecoveryUI::Init() na 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, não é necessário fornecer uma função Init(). Para mais detalhes sobre imagens, consulte Imagens da interface de recuperação.

Classe do dispositivo

Depois de ter uma implementação do RecoveryUI, defina a classe do dispositivo (subclasse da classe Device integrada). Ela precisa criar uma única instância da classe de UI e retornar essa da função GetUI():

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

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

    RecoveryUI* GetUI() { return ui; }

StartRecovery

O método StartRecovery() é chamado no início da recuperação, depois que a interface foi inicializada e os argumentos foram analisados, mas antes de qualquer ação ser realizada. A implementação padrão não faz nada. Portanto, não é necessário fornecer isso na sua subclasse se você não tiver nada a fazer:

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

Fornecer e gerenciar o menu de recuperação

O sistema chama dois métodos para receber a lista de linhas de cabeçalho e a lista de itens. Nesta implementação, ele retorna as matrizes estáticas definidas na parte de cima do arquivo:

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

HandleMenuKey

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

   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 de tecla (que foi processado e enfileirado anteriormente pelo método CheckKey() do objeto da interface) e o estado atual da visibilidade do menu/registro de texto. O valor de retorno é um número inteiro. Se o valor for 0 ou maior, ele será considerado a posição de um item de menu, que é invocado imediatamente (consulte o método InvokeMenuItem() abaixo). Caso contrário, pode ser uma das seguintes constantes predefinidas:

  • kHighlightUp. Mover o destaque do menu para o item anterior
  • kHighlightDown. Mover o destaque do menu para o próximo item
  • kInvokeItem. Invocar o item destacado
  • kNoAction. Não fazer nada com essa tecla

Como implícito pelo argumento visível, HandleMenuKey() é chamado mesmo que o menu não esteja visível. Ao contrário de 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á inativa e aguardando entrada.

Mecanismos de trackball

Se o dispositivo tiver um mecanismo de entrada semelhante a uma trackball (gera eventos de entrada com tipo EV_REL e código REL_Y), a recuperação sintetiza pressionamentos de tecla KEY_UP e KEY_DOWN sempre que o dispositivo de entrada semelhante a uma trackball informa movimento no eixo Y. Basta mapear os eventos KEY_UP e KEY_DOWN para ações do menu. Esse mapeamento não acontece para CheckKey(). Portanto, não é possível usar movimentos da trackball como gatilhos para reiniciar ou ativar/desativar a tela.

Teclas modificadoras

Para verificar se as teclas estão pressionadas como modificadores, chame o método IsKeyPressed() do seu próprio objeto de interface. Por exemplo, em alguns dispositivos, pressionar Alt-W na recuperação iniciava uma limpeza de dados, mesmo que o menu não estivesse visível. Você pode 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
        }
        ...
    }

Observação:se visible for falso, não faz sentido retornar os valores especiais que manipulam o menu (mover destaque, invocar item destacado), já que o usuário não pode ver o destaque. No entanto, é possível retornar os valores, se quiser.

InvokeMenuItem

Em seguida, 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;
        }
    }

Esse método pode retornar qualquer membro da enumeração BuiltinAction para informar ao sistema que ele precisa realizar essa ação (ou o membro NO_ACTION se você não quiser que o sistema faça nada). É aqui que você fornece funcionalidade de recuperação adicional além do que está no sistema: adicione um item para isso no 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. Não fazer nada.
  • REINICIAR. 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 mais detalhes, consulte Sideloading.
  • WIPE_CACHE. Reformatar apenas a partição de cache. Nenhuma confirmação é necessária, já que isso é relativamente inofensivo.
  • WIPE_DATA. Reformatar as partições userdata e cache, também conhecida como redefinição de dados de fábrica. O usuário precisa confirmar essa ação antes de continuar.

O último método, WipeData(), é opcional e é chamado sempre que uma operação de limpeza de dados é iniciada (pela recuperação no menu ou quando o usuário escolhe fazer uma redefinição de dados de fábrica no sistema principal). Esse método é chamado antes que os dados do usuário e as partições de cache sejam apagados. Se o dispositivo armazenar dados do usuário em qualquer outro lugar que não seja essas duas partições, apague-os aqui. Retorne 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 são apagados, seja qual for o resultado.

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

Criar dispositivo

Por fim, inclua um modelo 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 e vincule-o à recuperação no seu dispositivo. Em 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 para esse 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 interface de recuperação

A interface do usuário de recuperação consiste em imagens. O ideal é que os usuários nunca interajam com a interface: durante uma atualização normal, o smartphone inicializa a recuperação, preenche a barra de progresso da instalação e inicializa novamente o novo sistema sem entrada do usuário. Em caso de um problema de atualização do sistema, a única ação que o usuário pode realizar é entrar em contato com o atendimento ao cliente.

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

Android 5.0 e mais recente

A interface de recuperação do Android 5.0 e versões mais recentes usa duas imagens principais: a imagem de erro e a animação de instalação.

imagem mostrada durante um erro de 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 frames da animação entrelaçados por linha. Por isso, a Figura 2 parece achatada. Por exemplo, para uma animação de sete frames de 200 x 200, crie uma única imagem de 200 x 1.400 em que o primeiro frame seja as linhas 0, 7, 14, 21 etc., o segundo frame seja as linhas 1, 8, 15, 22 etc. A imagem combinada inclui um bloco de texto que indica o número de frames da animação e o número de frames por segundo (FPS). A ferramenta bootable/recovery/interlace-frames.py usa um conjunto de frames 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, basta fornecer a imagem icon_installing.png e definir o número de frames na animação como 0. O ícone de erro não é animado, ele é sempre uma imagem estática.

Android 4.x e versões anteriores

A interface 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 a primeira
sobreposição

Figura 4.icon-installing_overlay01.png

imagem mostrada como a sétima
sobreposição

Ícone Figura 5 icon_installing_overlay07.png

Durante a instalação, a tela é construída desenhando a imagem icon_installing.png e um dos frames de sobreposição em cima dela no deslocamento adequado. Aqui, uma caixa vermelha é sobreposta para destacar onde a sobreposição é colocada em cima da imagem de base:

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

Figura 6. Instalando o frame 1 da animação (icon_installing.png + icon_installing_overlay01.png)

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

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

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

O número de frames 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 das padrão, substitua o método Init() na subclasse para mudar esses valores nas imagens personalizadas. Para mais detalhes, consulte ScreenRecoveryUI. O script bootable/recovery/make-overlay.py pode ajudar a converter um conjunto de frames de imagem no formato "imagem de 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, basta fornecer a imagem icon_installing.png e definir o número de frames na animação como 0. O ícone de erro não é animado, ele é sempre uma imagem estática.

Texto de recuperação localizado

O Android 5.x mostra uma string de texto (por exemplo, Instalando atualização do sistema...") junto com a imagem. Quando o sistema principal é inicializado na recuperação, ele transmite a localidade atual do usuário como uma opção de linha de comando para a recuperação. Para cada mensagem a ser exibida, a recuperação inclui uma segunda imagem composta com strings 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 mostrar as seguintes mensagens:

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

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

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

Observação:a interface oculta que mostra mensagens de registro e permite que o usuário selecione ações no menu está disponível apenas em inglês.

Barras de progresso

As barras de progresso podem aparecer abaixo da imagem (ou animação) principal. A barra de progresso é feita combinando duas imagens de entrada, que precisam ter o 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 é mostrada ao lado da extremidade direita da imagem de vazio para criar a barra de progresso. A posição do limite entre as duas imagens muda para indicar o progresso. Por exemplo, com os pares de imagens de entrada acima, mostre:

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 em (neste exemplo) device/yoyodyne/tardis/recovery/res/images. Os nomes dos arquivos precisam corresponder aos listados acima. Quando um arquivo é encontrado nesse diretório, o sistema de build o usa em vez da imagem padrão correspondente. Somente PNGs nos formatos RGB ou RGBA com profundidade de cor de 8 bits são aceitos.

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

Dispositivos sem tela

Nem todos os dispositivos Android têm telas. Se o dispositivo for um eletrodoméstico sem tela ou tiver uma interface somente de áudio, talvez seja necessário personalizar mais a interface de recuperação. Em vez de criar uma subclasse de ScreenRecoveryUI, crie uma subclasse da classe mãe RecoveryUI diretamente.

A RecoveryUI tem métodos para lidar com operações de interface de nível inferior, como "alternar a tela", "atualizar a barra de progresso", "mostrar o menu", "mudar a seleção do menu" etc. Você pode substituir essas opções para fornecer uma interface adequada ao seu dispositivo. Talvez seu dispositivo tenha LEDs em que você pode usar cores ou padrões de piscagem diferentes para indicar o estado, ou talvez seja possível tocar áudio. Talvez você não queira oferecer suporte a um menu ou ao modo de "exibição de texto". É possível impedir o acesso a eles com implementações CheckKey() e HandleMenuKey() que nunca ativam a exibição nem selecionam um item de menu. Nesse caso, muitos dos métodos RecoveryUI que você precisa fornecer podem ser apenas stubs vazios.)

Consulte bootable/recovery/ui.h para a declaração de RecoveryUI e saiba quais métodos você precisa oferecer suporte. A RecoveryUI é abstrata. Alguns métodos são virtuais puros e precisam ser fornecidos por subclasses, mas ela contém o código para processar entradas importantes. Você também pode substituir isso se o dispositivo não tiver chaves ou se quiser processá-las de maneira diferente.

Updater

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. Confira 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"

Todas as funções de extensão têm 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* que representam 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 são avaliados quando a função é chamada. A lógica da função determina quais deles são avaliados e quantas vezes. Assim, é possível 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, libere todos os recursos que você está mantendo e retorne NULL imediatamente (isso propaga as interrupções na pilha do edify). Caso contrário, você assume a propriedade do valor retornado e é responsável por 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ê pode ler argumentos assim:

   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 ficar cansativo para vários argumentos. A função ReadValueArgs() pode facilitar isso. 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;
    }

O ReadValueArgs() não faz verificação de tipos, então você precisa fazer isso aqui. É mais conveniente fazer isso com uma instrução if, mesmo que isso produza uma mensagem de erro um pouco menos específica quando falha. No entanto, ReadValueArgs() processa a avaliação de cada argumento e libera todos os argumentos avaliados anteriormente (além de definir uma mensagem de erro útil) se alguma das avaliações falhar. É possível usar uma função de conveniência ReadValueVarArgs() para avaliar um número variável de argumentos (ela retorna uma matriz de Value*).

Depois de 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 precisa ser um objeto Value*. A propriedade desse objeto será transferida para o caller. O autor da chamada assume a propriedade de todos os dados apontados por esse Value*, especificamente o datamember.

Nesse caso, você quer 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ê precisa alocar um objeto Value com uma cópia alocada da string constante para retornar, já que o chamador vai free() os dois. Não se esqueça de chamar FreeValue() nos objetos que você recebeu ao avaliar 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() encapsula uma string em um novo objeto Value. Use para escrever o código acima de forma mais concisa:

   FreeValue(key);
    FreeValue(image);

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

Para conectar funções ao interpretador do edify, forneça a função Register_foo, em que foo é o nome da biblioteca estática que contém esse 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 é possível configurar o makefile para criar uma biblioteca estática com seu código. (Este é o mesmo makefile usado para personalizar a interface de recuperação na seção anterior. Seu dispositivo pode ter as duas 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 precisa ser igual 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 várias bibliotecas; todas são registradas). Se o código depender de outras bibliotecas estáticas que não sejam extensões do Edify (por exemplo, Se eles não tiverem uma função Register_libname, liste-os em TARGET_RECOVERY_UPDATER_EXTRA_LIBS para vincular ao atualizador sem chamar a função de registro (inexistente). Por exemplo, se o código específico do 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 de atualização no seu pacote OTA agora podem chamar sua função como qualquer outra. Para reprogramar o 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 da nova função de extensão.

Geração de pacotes OTA

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

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

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

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

Você também pode colocar em um Android.mk, mas ele precisa ser protegido por uma verificação de dispositivo, já que todos os arquivos Android.mk na árvore são carregados, não importa qual dispositivo esteja sendo criado. Se a árvore incluir vários dispositivos, você só vai querer o arquivo tardis.dat adicionado 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

Eles são chamados de arquivos de rádio por motivos históricos, mas podem não ter nada a ver com o rádio do dispositivo (se houver). São apenas blobs opacos de dados que o sistema de build copia para o arquivo .zip de destino usado pelas ferramentas de geração de OTA. Quando você faz um build, o tardis.dat é armazenado no target-files.zip como RADIO/tardis.dat. Você pode chamar add-radio-file várias vezes para adicionar quantos arquivos quiser.

Módulo Python

Para estender as ferramentas de lançamento, escreva um módulo do Python (precisa ser chamado de 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 processa a geração de um pacote OTA incremental. Neste exemplo, suponha que você precise reprogramar o tardis somente quando o arquivo tardis.dat for alterado entre dois builds.

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 (implemente apenas as que você precisa).

FullOTA_Assertions()
Chamado perto do início da geração de uma OTA completa. Esse é um bom lugar para emitir declarações sobre o estado atual do dispositivo. Não emita comandos de script que façam mudanças no dispositivo.
FullOTA_InstallBegin()
Chamado depois que todas as declarações sobre o estado do dispositivo foram aprovadas, mas antes de qualquer mudança ter sido feita. Você pode emitir comandos para atualizações específicas do dispositivo que precisam ser executadas antes de qualquer outra coisa no dispositivo ser alterada.
FullOTA_InstallEnd()
Chamado no final da geração de script, depois que os comandos de script para atualizar as partições de inicialização e do sistema foram emitidos. Também é possível emitir outros comandos 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 depois que todas as declarações sobre o estado do dispositivo foram aprovadas, mas antes de qualquer mudança ser feita. Você pode emitir comandos para atualizações específicas do dispositivo que precisam ser executadas antes de qualquer outra coisa no dispositivo.
IncrementalOTA_VerifyEnd()
Chamado ao final da fase de verificação, quando o script termina de confirmar que os arquivos que ele vai tocar têm o conteúdo inicial esperado. Neste momento, nada no dispositivo foi alterado. Também é possível emitir código para outras verificações específicas do dispositivo.
IncrementalOTA_InstallBegin()
Chamado depois que os arquivos a serem corrigidos são verificados como tendo o estado before esperado, mas antes que qualquer mudança seja feita. Você pode emitir comandos para atualizações específicas do dispositivo que precisam ser executadas antes de qualquer outra coisa ser alterada nele.
IncrementalOTA_InstallEnd()
Semelhante ao pacote OTA completo, esse comando é chamado no final da geração do script depois que os comandos do script para atualizar as partições de inicialização e do sistema foram emitidos. Também é possível emitir outros comandos para atualizações específicas do dispositivo.

Observação:se o dispositivo perder a energia, a instalação OTA poderá ser reiniciada do início. Esteja preparado para lidar com dispositivos em que esses comandos já foram executados, total ou parcialmente.

Transmitir funções para objetos de informações

Transmita funções para um único objeto de informações que contenha vários itens úteis:

  • info.input_zip. (Somente OTAs completas) O objeto zipfile.ZipFile para o .zip de arquivos de destino de entrada.
  • info.source_zip. (Somente OTAs incrementais) O objeto zipfile.ZipFile para o .zip de arquivos de destino de origem (o build já no dispositivo quando o pacote incremental está sendo instalado).
  • info.target_zip. (Somente OTAs incrementais) O objeto zipfile.ZipFile para o .zip de arquivos de destino (o build 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. Verifique se o texto de saída termina com um ponto e vírgula para não entrar em conflito com comandos emitidos depois.

Para mais detalhes sobre o objeto de informações, consulte a documentação da Python Software Foundation para arquivos ZIP.

Especificar o local do módulo

Especifique o local do script releasetools.py do 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 será o diretório $(TARGET_DEVICE_DIR)/../common (device/yoyodyne/common neste exemplo). É melhor definir explicitamente o local do script releasetools.py. Ao criar o dispositivo tardis, o script releasetools.py é incluído no arquivo .zip target-files (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 de arquivos de destino, se presente, é preferível ao do diretório de origem do Android. Também é possível especificar explicitamente o caminho para as extensões específicas do dispositivo com a opção -s (ou --device_specific), que tem a prioridade máxima. Isso permite que você corrija erros e faça mudanças nas extensões do releasetools e aplique essas mudanças a arquivos de destino antigos.

Agora, quando você executa ota_from_target_files, ele seleciona automaticamente o módulo específico do dispositivo no 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

Outra opção é 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

Observação:para 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 sideload

A recuperação tem um mecanismo de sideload para instalar manualmente um pacote de atualização sem fazer o download dele por transmissão sem fio pelo sistema principal. O sideload é útil para depurar ou fazer mudanças em dispositivos em que o sistema principal não pode ser inicializado.

Historicamente, o sideloading era feito carregando 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 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 sideload: carregar pacotes da partição de cache e carregá-los por USB usando o adb.

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

  • APPLY_EXT. Transferir um pacote de atualização por sideload do armazenamento externo (diretório /sdcard ). O recovery.fstab precisa definir o ponto de montagem /sdcard . Isso não pode ser usado em dispositivos que emulam um cartão SD com um symlink para /data (ou algum mecanismo semelhante). Normalmente, /data não está disponível para recuperação porque pode estar criptografado. A interface de recuperação mostra um menu de arquivos .zip em /sdcard e permite que o usuário selecione um.
  • APPLY_CACHE. Semelhante ao carregamento de um pacote de /sdcard, exceto que o diretório /cache (que está sempre disponível para recuperação) é usado. No sistema regular, /cache só pode ser gravado por usuários privilegiados, e se o dispositivo não for inicializável, o diretório /cache não poderá ser gravado de forma alguma, o que torna esse mecanismo de utilidade limitada.
  • APPLY_ADB_SIDELOAD. Permite que o usuário envie um pacote para o dispositivo usando um cabo USB e a ferramenta de desenvolvimento adb. Quando esse mecanismo é invocado, a recuperação inicia a própria miniversão do daemon adbd para permitir que o adb em um computador host conectado se comunique com ele. Esta mini versão só aceita um comando: adb sideload filename. O arquivo nomeado é enviado da máquina host para o dispositivo, que o verifica e instala como se estivesse no armazenamento local.

Algumas ressalvas:

  • Há suporte apenas para transporte USB.
  • Se a recuperação executar o adbd normalmente (geralmente verdadeiro para builds userdebug e eng), ele será desligado enquanto o dispositivo estiver no modo adb sideload e será reiniciado quando o adb sideload terminar de receber um pacote. No modo de sideload do adb, nenhum comando do adb funciona, exceto sideload ( logcat, reboot, push , pull, shell etc. falham).
  • Não é possível sair do modo de sideload do adb no dispositivo. Para cancelar, envie /dev/null (ou qualquer outra coisa que não seja um pacote válido) como o pacote. Assim, o dispositivo não vai conseguir verificar e vai interromper o procedimento de instalação. O método CheckKey() da implementação do RecoveryUI continuará sendo chamado para pressionamentos de tecla. Assim, você pode fornecer uma sequência de teclas que reinicia o dispositivo e funciona no modo de sideload do adb.