Código específico do dispositivo

O sistema de recuperação inclui vários ganchos para inserir código específico do dispositivo para que as atualizações OTA também possam atualizar partes do dispositivo além do 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 suporta dispositivos flash eMMc e o sistema de arquivos ext4 executado nesses dispositivos. Ele também suporta dispositivos flash MTD (Memory Technology Device) e o sistema de arquivos yaffs2 de versões mais antigas.

O arquivo de mapa de partição é especificado por TARGET_RECOVERY_FSTAB; esse arquivo é 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 arquivo de mapa de partição de amostra 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 sistema 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 chave para localizar a partição. "device" deve ser o nome da partição MTD em /proc/mtd .
ramal 4
Um sistema de arquivos ext4 sobre um dispositivo flash eMMc. "dispositivo" 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 string 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). Essa restrição se aplica apenas à montagem de sistemas de arquivos em recuperação; o sistema principal é livre para montá-los em qualquer lugar. Os diretórios /boot , /recovery e /misc devem ser tipos brutos (mtd ou emmc), enquanto os diretórios /system , /data , /cache e /sdcard (se disponíveis) devem 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 . 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 essa 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 que não deve ser substituído).

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 '/', ela será considerada uma entrada de dispositivo2; se a entrada não começar com um caractere '/', ela será considerada um campo de opções .

Animação de inicialização

Os fabricantes de dispositivos podem personalizar a animação exibida quando um dispositivo Android é inicializado. 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 desse arquivo para o seu dispositivo.

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: as linhas longas são truncadas (não quebradas), portanto, lembre-se da largura da tela do dispositivo.

Personalizando a Chave de Verificação

Em seguida, defina a implementação do RecoveryUI do seu dispositivo. Este exemplo assume que o dispositivo tardis tem uma tela, então você pode herdar da implementação integrada de ScreenRecoveryUI CheckKey() consulte as instruções para dispositivos sem tela .)

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 está acontecendo no resto da recuperação: quando o menu está desligado, quando está ligado, durante a instalação do pacote, durante a limpeza de dados do usuário, etc. Ele pode retornar uma das quatro constantes:

  • ALTERNAR . Ativar ou desativar a exibição do menu e/ou log de texto
  • REINICIAR . Reinicie imediatamente o dispositivo
  • IGNORAR . Ignorar este pressionamento de tecla
  • ENQUADRAR . Enfileirar este 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 sempre que um evento key-down é seguido por um evento key-up para a mesma chave. (A seqüência de eventos A-down B-down B-up A-up resulta apenas em CheckKey(B) sendo chamado.) CheckKey() pode chamar IsKeyPressed() , para descobrir se outras teclas estão sendo pressionadas. (Na sequência de eventos de teclas acima, se CheckKey(B) chamasse IsKeyPressed(A) teria retornado true.)

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 pressionando o aumento de volume, e o dispositivo pode ser reinicializado 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;
    }
};

ScreenRecoveryUI

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 atual interlace-frames.py permite que você armazene as informações de 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 da interface do usuário de recuperação .

Classe do dispositivo

Depois de ter uma implementação de RecoveryUI, defina sua classe de dispositivo (subclasse da classe de dispositivo integrada). Ele deve criar uma única instância de sua classe de interface do usuário e retornar isso 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 interface do usuário 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 em sua subclasse se não tiver nada para fazer:

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

Fornecendo e gerenciando o 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 na parte superior do arquivo:

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

HandleMenuKey

Em seguida, forneça uma função HandleMenuKey() , que leva um pressionamento de tecla e a visibilidade do menu atual e decide qual ação executar:

   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 recebe um código de chave (que foi previamente processado e enfileirado pelo método CheckKey() do objeto UI) e o estado atual da visibilidade do log de menu/texto. O valor de retorno é um número inteiro. Se o valor for 0 ou superior, isso é 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 atualmente destacado
  • kNoAção . Não faça nada com este pressionamento de tecla

Conforme implícito no argumento visible, 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 - é chamado apenas quando a recuperação está ociosa e aguardando entrada.

Mecanismos de Trackball

Se o seu dispositivo tiver um mecanismo de entrada do tipo trackball (gera eventos de entrada com tipo EV_REL e código REL_Y), a recuperação sintetiza as teclas KEY_UP e KEY_DOWN sempre que o dispositivo de entrada do tipo trackball relata 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.

Chaves modificadoras

Para verificar as teclas que estão sendo pressionadas como modificadores, chame o método IsKeyPressed() de seu próprio objeto de interface do usuário. 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 visible for false, 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, você pode retornar os valores, se desejar.

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 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 chamado e retorne NO_ACTION para que o sistema não faça mais nada.

BuiltinAction contém os seguintes valores:

  • NO_ACTION . 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 obter detalhes, consulte Sideload .
  • WIPE_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 conhecidas como redefinição de dados de fábrica. O usuário é solicitado a confirmar esta ação antes de continuar.

O último método, WipeData() , é opcional e é chamado sempre que uma operação de limpeza de dados é iniciada (seja da 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). Esse método é chamado antes que os dados do usuário e as partições de cache sejam apagados. Se o seu dispositivo armazenar 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 são apagados se você retornar com sucesso ou falha.

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

Fazer dispositivo

Por fim, inclua alguns clichês no final do arquivo recovery_ui.cpp para a função make_device() que cria e retorna uma instância de 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 em 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 este 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 interface do usuário: durante uma atualização normal, o telefone inicializa na recuperação, preenche a barra de progresso da instalação e inicializa novamente no novo sistema sem a entrada 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 de imagem evita 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 a 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 o erro ota

Figura 1. icon_error.png

imagem mostrada durante a instalação 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 compactada). Por exemplo, para uma animação de sete quadros de 200 x 200, crie uma única imagem de 200 x 1400 em que o primeiro quadro seja 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ê precisa apenas fornecer a imagem icon_installing.png e definir o número de quadros na animação para 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.xe anterior 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 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 em cima dela no deslocamento adequado. Aqui, uma caixa vermelha é sobreposta para destacar onde a sobreposição é colocada na parte superior da imagem base:

imagem composta da instalação mais a primeira sobreposição

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

imagem composta da instalação mais a 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ê precisa apenas fornecer a imagem icon_installing.png e definir o número de quadros na animação para 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 a 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 strings de texto pré-renderizadas para essa mensagem em cada localidade.

Imagem de amostra 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 a atualização do sistema...
  • Erro!
  • Apagando... (ao fazer uma limpeza de dados/redefinição de fábrica)
  • Nenhum comando (quando um usuário inicializa na recuperação manualmente)

O aplicativo Android em bootable/recovery/tools/recovery_l10n/ renderiza as 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 que o usuário selecione ações no menu está disponível apenas 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 ao lado da extremidade direita da imagem vazia para criar 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 em (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 de preferência à imagem padrão correspondente. Apenas PNGs no formato RGB ou RGBA com profundidade de cor de 8 bits são suportados.

Observação: no Android 5.x, se a localidade for conhecida por recuperação 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 têm telas. Se o seu dispositivo for um dispositivo sem periféricos ou tiver uma interface somente de áudio, talvez seja necessário fazer uma personalização mais ampla da interface do usuário de recuperação. Em vez de criar uma subclasse de ScreenRecoveryUI, subclasse sua classe pai RecoveryUI diretamente.

O RecoveryUI tem métodos para lidar com operações de interface do usuário de nível inferior, como "alternar a exibição", "atualizar a barra de progresso", "mostrar o menu", "alterar a seleção do menu" etc. Você pode substituí-los para fornecer uma interface apropriada para o seu dispositivo. Talvez o seu dispositivo tenha LEDs onde você possa usar diferentes cores ou padrões de piscar para indicar o estado, ou talvez você possa reproduzir áudio. (Talvez você não queira suportar um menu ou o modo "exibição de texto"; você pode evitar acessá-los com as 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 oferecer suporte. RecoveryUI é abstrato—alguns métodos são puramente virtuais e devem ser fornecidos por subclasses—mas contém o código para fazer o processamento das principais entradas. Você também pode substituir isso, se o seu dispositivo não tiver chaves ou se desejar processá-las de maneira diferente.

Atualizador

Você pode usar o 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 de dentro do script do atualizador. Aqui está uma função de exemplo 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 tem 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 de 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ê deve liberar todos os recursos que estiver segurando e retornar imediatamente NULL (isso propaga abortos na pilha edify). Caso contrário, você assume a propriedade do Valor retornado e é 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ê pode 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;
    }

A verificação de NULL e a liberação de argumentos avaliados anteriormente podem ser tediosos para vários argumentos. A função ReadValueArgs() pode facilitar isso. Em vez do código acima, você poderia ter escrito isso:

   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 falha. Mas ReadValueArgs() lida com a avaliação de cada argumento e libera todos os argumentos previamente avaliados (assim como define 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* ).

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 deve ser um objeto Value* ; a propriedade desse objeto passará para o chamador. O chamador se apropria de quaisquer dados apontados por este Value* — especificamente o membro de dados.

Nesse 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'd da string constante a ser retornada, já que o chamador irá free() ambos. Não se esqueça de chamar FreeValue() nos objetos que você obteve 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 ligar 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 as funções específicas do device . whatever o que device . whatever para evitar conflitos com futuras funções incorporadas 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 interface do usuário 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 a compilação de recuperação para receber sua biblioteca. Adicione sua biblioteca a TARGET_RECOVERY_UPDATER_LIBS (que pode conter várias 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 de atualização 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 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 compilado. (Se sua árvore inclui vários dispositivos, você deseja apenas que o arquivo tardis.dat seja adicionado ao construir 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 blobs 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 nomeado 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 perto do início da geração de um OTA completo. Este é um bom lugar para emitir declarações sobre o estado atual do dispositivo. Não emita comandos de script que fazem alterações no dispositivo.
FullOTA_InstallBegin()
Chamado depois que todas as declarações sobre o estado do dispositivo foram aprovadas, mas antes que qualquer alteração tenha 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 a emissão dos comandos de script para atualizar as partições de inicialização e do sistema. 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 depois que todas as declarações sobre o estado do dispositivo foram aprovadas, mas antes que qualquer alteração tenha 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 que os arquivos que vai tocar têm 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 que quaisquer alterações tenham sido 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 à sua contraparte do pacote OTA completo, isso é chamado no final da geração do script, depois que os comandos de script para atualizar as partições de inicialização e do sistema foram emitidos. 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 pode reiniciar 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 info 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 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 enviar texto para o script. Certifique-se de que o texto de saída termine com um ponto e vírgula para que não seja executado em comandos emitidos posteriormente.

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

Especifique a localização do módulo

Especifique a localização do script releasetools.py do seu dispositivo em seu 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 a localização do script releasetools.py. Ao construir o dispositivo tardis, o script releasetools.py é incluído no arquivo target-files .zip ( META/releasetools.py ).

Quando você executa as ferramentas de lançamento ( img_from_target_files ou ota_from_target_files ), o script releasetools.py nos arquivos de destino .zip, se presente, é preferível ao 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 que você corrija erros e faça alterações nas extensões releasetools e aplique essas alterações em arquivos de destino antigos.

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

Carregamento lateral

O Recovery tem um mecanismo de sideload para instalar manualmente um pacote de atualização sem baixá-lo 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 sideloading era 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 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 para sideload: 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 . Faça sideload de um pacote de atualização do armazenamento externo (diretório /sdcard ). Seu recovery.fstab deve definir o ponto de montagem /sdcard . This is not usable on devices that emulate an SD card with a symlink to /data (or some similar mechanism). /data is typically not available to recovery because it may be encrypted. The recovery UI displays a menu of .zip files in /sdcard and allows the user to select one.
  • APPLY_CACHE . Similar to loading a package from /sdcard except that the /cache directory (which is always available to recovery) is used instead. From the regular system, /cache is only writable by privileged users, and if the device isn't bootable then the /cache directory can't be written to at all (which makes this mechanism of limited utility).
  • APPLY_ADB_SIDELOAD . Allows user to send a package to the device via a USB cable and the adb development tool. When this mechanism is invoked, recovery starts up its own mini version of the adbd daemon to let adb on a connected host computer talk to it. This mini version supports only a single command: adb sideload filename . The named file is sent from the host machine to the device, which then verifies and installs it just as if it had been on local storage.

A few caveats:

  • Only USB transport is supported.
  • If your recovery runs adbd normally (usually true for userdebug and eng builds), that will be shut down while the device is in adb sideload mode and will be restarted when adb sideload has finished receiving a package. While in adb sideload mode, no adb commands other than sideload work ( logcat , reboot , push , pull , shell , etc. all fail).
  • You cannot exit adb sideload mode on the device. To abort, you can send /dev/null (or anything else that's not a valid package) as the package, and then the device will fail to verify it and stop the installation procedure. The RecoveryUI implementation's CheckKey() method will continue to be called for keypresses, so you can provide a key sequence that reboots the device and works in adb sideload mode.