Guia do desenvolvedor sobre personalização no dispositivo

A personalização no dispositivo (ODP, na sigla em inglês) foi criada para proteger as informações dos usuários finais contra aplicativos. Os aplicativos usam a ODP para personalizar produtos e serviços para usuários finais, mas não podem ver as personalizações exatas feitas para o usuário, a menos que haja interações diretas fora da ODP entre o aplicativo e o usuário final. Para aplicativos com modelos de aprendizado de máquina ou análises estatísticas, a ODP oferece um conjunto de serviços e algoritmos para garantir que eles sejam devidamente anonimizados usando os mecanismos de privacidade diferencial adequados. Para mais detalhes, consulte a explicação sobre a personalização no dispositivo.

O ODP executa o código do desenvolvedor em um IsolatedProcess que não tem acesso direto à rede, aos discos locais ou a outros serviços em execução no dispositivo, mas tem acesso às seguintes fontes de dados persistentes localmente:

  • RemoteData: dados imutáveis de chave-valor baixados de back-ends remotos operados por desenvolvedores, quando aplicável.
  • LocalData: dados de chave-valor mutáveis persistidos localmente pelo desenvolvedor, se aplicável.
  • UserData: dados do usuário fornecidos pela plataforma.

Há suporte para as seguintes saídas:

  • Saída persistente: essas saídas podem ser usadas em processamentos locais futuros, produzindo saídas exibidas, treinamento de modelos facilitado pelo aprendizado federado ou análise estatística entre dispositivos facilitada pelo Federated Analytics.
    • Os desenvolvedores podem gravar solicitações e resultados de processamento na tabela REQUESTS local.
    • Os desenvolvedores podem gravar dados adicionais associados a uma solicitação anterior na tabela EVENTS.
  • Saída mostrada:
    • Os desenvolvedores podem retornar HTML renderizado pelo ODP em um WebView dentro de um SurfaceView. O conteúdo renderizado ali não fica visível para o app de invocação.
    • Os desenvolvedores podem incorporar URLs de eventos fornecidos pelo ODP na saída HTML para acionar o registro e o processamento das interações do usuário com o HTML renderizado. O ODP intercepta solicitações para esses URLs e invoca o código para gerar dados que são gravados na tabela EVENTS.

Os apps clientes e SDKs podem invocar a ODP para mostrar conteúdo HTML em um SurfaceView usando as APIs da ODP. O conteúdo renderizado em um SurfaceView não fica visível para o app de chamada. O app cliente ou o SDK pode ser uma entidade diferente daquela que está desenvolvendo com a ODP.

O serviço ODP gerencia o app cliente que quer invocar a ODP para mostrar conteúdo personalizado na interface. Ele baixa conteúdo de endpoints fornecidos pelo desenvolvedor e invoca a lógica para pós-processamento dos dados baixados. Ele também media todas as comunicações entre o IsolatedProcess e outros serviços e apps.

Os apps clientes usam métodos na classe OnDevicePersonalizationManager para interagir com o código do desenvolvedor em execução em um IsolatedProcess. O código do desenvolvedor executado em um IsolatedProcess estende a classe IsolatedService e implementa a interface IsolatedWorker. A IsolatedService precisa criar uma instância de IsolatedWorker para cada solicitação.

O diagrama a seguir mostra a relação entre os métodos em OnDevicePersonalizationManager e IsolatedWorker.

Diagrama da relação entre OnDevicePersonalizationManager e IsolatedWorker.

Um app cliente chama a ODP usando o método execute com um IsolatedService nomeado. O serviço da ODP encaminha a chamada para o método onExecute do IsolatedWorker. O IsolatedWorker retorna registros a serem persistidos e conteúdo a ser exibido. O serviço ODP grava a saída persistente na tabela REQUESTS ou EVENTS e retorna uma referência opaca à saída exibida para o app cliente. O app cliente pode usar essa referência opaca em uma futura chamada requestSurfacePackage para mostrar qualquer conteúdo na interface.

Saída persistente

O serviço ODP persiste um registro na tabela REQUESTS depois que a implementação do desenvolvedor de onExecute é retornada. Cada registro na tabela REQUESTS contém alguns dados comuns por solicitação gerados pelo serviço da ODP e uma lista de Rows retornados. Cada Row contém uma lista de pares (key, value). Cada valor é um escalar, uma string ou um blob. Os valores numéricos podem ser informados após a agregação, e os dados de string ou blob podem ser informados após a aplicação da privacidade diferencial local ou central. Os desenvolvedores também podem gravar eventos de interação do usuário subsequentes na tabela EVENTS. Cada registro nessa tabela está associado a uma linha na tabela REQUESTS.EVENTS O serviço ODP registra de forma transparente um carimbo de data/hora e o nome do pacote do app de chamada e do APK do desenvolvedor da ODP com cada registro.

Antes de começar

Antes de começar a desenvolver com o ODP, configure o manifesto do pacote e ative o modo de desenvolvedor.

Configurações do manifesto do pacote

Para usar a ODP, é necessário:

  1. Uma tag <property> em AndroidManifest.xml que aponta para um recurso XML no pacote que contém informações de configuração do ODP.
  2. Uma tag <service> em AndroidManifest.xml que identifica a classe que estende IsolatedService, conforme mostrado no exemplo a seguir. O serviço na tag <service> precisa ter os atributos exported e isolatedProcess definidos como true.
  3. Uma tag <service> no recurso XML especificado na etapa 1 que identifica a classe de serviço da etapa 2. A tag <service> também precisa incluir outras configurações específicas da ODP dentro dela, conforme mostrado no segundo exemplo.

AndroidManifest.xml

<!-- Contents of AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.odpsample" >
    <application android:label="OdpSample">
        <!-- XML resource that contains other ODP settings. -->
        <property android:name="android.ondevicepersonalization.ON_DEVICE_PERSONALIZATION_CONFIG"
                  android:resource="@xml/OdpSettings"></property>
        <!-- The service that ODP binds to. -->
        <service android:name="com.example.odpsample.SampleService"
                android:exported="true" android:isolatedProcess="true" />
    </application>
</manifest>

Manifesto específico do ODP no recurso XML

O arquivo de recurso XML especificado na tag <property> também precisa declarar a classe de serviço em uma tag <service> e especificar o endpoint do URL de onde a ODP fará o download do conteúdo para preencher a tabela RemoteData, conforme mostrado no exemplo a seguir. Se você estiver usando os recursos de computação federada, também precisará especificar o endpoint do URL do servidor de computação federada a que o cliente de computação federada se conectará.

<!-- Contents of res/xml/OdpSettings.xml -->
<on-device-personalization>
   <!-- Name of the service subclass -->
   <service name="com.example.odpsample.SampleService">
     <!-- If this tag is present, ODP will periodically poll this URL and
          download content to populate REMOTE_DATA. Developers that do not need to
          download content from their servers can skip this tag. -->
     <download-settings url="https://example.com/get" />
     <!-- If you want to use federated compute feature to train a model, you
          need to specify this tag. -->
     <federated-compute-settings url="https://fcpserver.example.com/" />
   </service>
</on-device-personalization>

Ativar o modo de desenvolvedor

Siga as instruções na seção Ativar opções do desenvolvedor da documentação do Android Studio para ativar o modo de desenvolvedor.

Configurações de interruptor e flag

O ODP tem um conjunto de chaves e flags usadas para controlar determinadas funcionalidades:

  • _global_killswitch: a chave global para todos os recursos da ODP. Defina como "false" para usar a ODP.
  • _federated_compute_kill_switch: _a chave que controla todas as funcionalidades de treinamento (aprendizado federado) do ODP. Defina como "false" para usar o treinamento.
  • _caller_app_allowlist: controla quem pode chamar a ODP. Os apps (nome do pacote, certificado [opcional]) podem ser adicionados aqui ou definidos como * para permitir todos.
  • _isolated_service_allowlist: controla quais serviços podem ser executados no processo de serviço isolado.

É possível executar os comandos a seguir para configurar todas as chaves e flags para usar o ODP sem restrições:

# Set flags and killswitches
adb shell device_config set_sync_disabled_for_tests persistent
adb shell device_config put on_device_personalization global_kill_switch false
adb shell device_config put on_device_personalization federated_compute_kill_switch false
adb shell device_config put on_device_personalization caller_app_allow_list \"*\"
adb shell device_config put on_device_personalization isolated_service_allow_list \"*\"

APIs do lado do dispositivo

Confira a documentação de referência da API Android para ODP.

Interações com IsolatedService

A classe IsolatedService é uma classe base abstrata que todos os desenvolvedores que pretendem desenvolver para a ODP precisam estender e declarar no manifesto do pacote como em execução em um processo isolado. O serviço ODP inicia esse serviço em um processo isolado e faz solicitações a ele. O IsolatedService recebe solicitações do serviço ODP e cria um IsolatedWorker para processar a solicitação.

Os desenvolvedores precisam implementar os métodos da interface IsolatedWorker para processar solicitações de apps clientes, conclusões de downloads e eventos acionados pelo HTML renderizado. Todos esses métodos têm implementações padrão sem operação, para que os desenvolvedores possam pular a implementação dos métodos que não interessam.

A classe OnDevicePersonalizationManager fornece uma API para que apps e SDKs interajam com um IsolatedService implementado por um desenvolvedor em execução em um processo isolado. Confira alguns casos de uso pretendidos:

Gerar conteúdo HTML para mostrar em um SurfaceView

Para gerar conteúdo a ser exibido com OnDevicePersonalizationManager#execute, o app de chamada pode usar o objeto SurfacePackageToken retornado em uma chamada requestSurfacePackage subsequente para solicitar que o resultado seja renderizado em um SurfaceView .

Em caso de sucesso, o receptor é chamado com um SurfacePackage para uma visualização renderizada pelo serviço ODP. Os aplicativos cliente precisam inserir o SurfacePackage em um SurfaceView na hierarquia de visualização.

Quando um app faz uma chamada requestSurfacePackage com um SurfacePackageToken retornado por uma chamada OnDevicePersonalizationManager#execute anterior, o serviço do ODP chama IsolatedWorker#onRender para buscar o snippet HTML a ser renderizado em um frame isolado. Um desenvolvedor não tem acesso a LocalData ou UserData durante essa fase. Isso impede que o desenvolvedor incorpore UserData potencialmente sensíveis nos URLs de busca de recursos no HTML gerado. Os desenvolvedores podem usar IsolatedService#getEventUrlProvider para gerar URLs de rastreamento a serem incluídos no HTML gerado. Quando o HTML é renderizado, o serviço do ODP intercepta solicitações para esses URLs e chama IsolatedWorker#onEvent. É possível invocar getRemoteData() ao implementar onRender().

Rastrear eventos em conteúdo HTML

A classe EventUrlProvider fornece APIs para gerar URLs de acompanhamento de eventos que os desenvolvedores podem incluir na saída HTML. Quando o HTML é renderizado, o ODP invoca IsolatedWorker#onEvent com o payload do URL do evento.

O serviço do ODP intercepta solicitações para URLs de eventos gerados pelo ODP no HTML renderizado, chama IsolatedWorker#onEvent e registra o EventLogRecord retornado na tabela EVENTS.

Gravar resultados permanentes

Com o OnDevicePersonalizationManager#execute, o serviço tem a opção de gravar dados no armazenamento permanente (tabelas REQUESTS e EVENTS). Confira as entradas que podem ser gravadas nessas tabelas:

  • um RequestLogRecord a ser adicionado à tabela REQUESTS.
  • uma lista de objetos EventLogRecord a serem adicionados à tabela EVENTS, cada um contendo um ponteiro para um RequestLogRecord gravado anteriormente .

Os resultados permanentes no armazenamento do dispositivo podem ser consumidos pelo aprendizado federado para treinamento de modelos.

Gerenciar tarefas de treinamento no dispositivo

O serviço ODP chama IsolatedWorker#onTrainingExample quando um job de treinamento de computação federada é iniciado e quer receber exemplos de treinamento fornecidos por desenvolvedores que adotam a ODP. É possível invocar getRemoteData(), getLocalData(), getUserData() e getLogReader() ao implementar onTrainingExample().

Para programar ou cancelar jobs de computação federada, use a classe FederatedComputeScheduler, que fornece APIs para todos os IsolatedService do ODP. Cada trabalho de computação federada pode ser identificado pelo nome da população.

Antes de programar um novo job de computação federada:

  • Uma tarefa com esse nome de população já precisa ter sido criada no servidor de computação federada remota.
  • O endpoint do URL do servidor de computação federada já precisa estar especificado nas configurações do manifesto do pacote com a tag federated-compute-settings.

Interações com saída persistente

A seção a seguir descreve como interagir com a saída persistente na ODP.

Ler tabelas locais

A classe LogReader fornece APIs para ler as tabelas REQUESTS e EVENTS. Essas tabelas contêm dados gravados por IsolatedService durante chamadas de onExecute() ou onEvent(). Os dados dessas tabelas podem ser usados para treinamento de modelo facilitado pelo aprendizado federado ou análise estatística entre dispositivos facilitada pela análise federada.

Interações com conteúdo baixado

A seção a seguir descreve como interagir com o conteúdo baixado no ODP.

Baixar conteúdo de servidores

O serviço ODP baixa periodicamente o conteúdo do URL declarado no manifesto do pacote do IsolatedService e chama onDownloadCompleted após a conclusão do download. O download é um arquivo JSON que contém pares de chave-valor.

Os desenvolvedores que adotam o ODP podem escolher qual subconjunto do conteúdo baixado deve ser adicionado à tabela RemoteData e qual deve ser descartado. Os desenvolvedores não podem modificar o conteúdo baixado. Isso garante que a tabela RemoteData não contenha dados do usuário. Além disso, os desenvolvedores podem preencher a tabela LocalData como quiserem. Por exemplo, eles podem armazenar em cache alguns resultados pré-calculados.

Formato da solicitação de download

O ODP consulta periodicamente o endpoint do URL declarado no manifesto do pacote do desenvolvedor para buscar conteúdo e preencher a tabela RemoteData.

O endpoint deve retornar uma resposta JSON, conforme descrito mais adiante. A resposta JSON precisa conter um syncToken que identifique a versão dos dados enviados, além de uma lista de pares de chave-valor a serem preenchidos. O valor syncToken precisa ser um carimbo de data/hora em segundos, ajustado a um limite de hora UTC. Como parte da solicitação de download, a ODP fornece o syncToken do download concluído anteriormente e o país do dispositivo como os parâmetros syncToken e country no URL de download. O servidor pode usar o syncToken anterior para implementar downloads incrementais.

Formato do arquivo de download

O arquivo baixado é um JSON com a seguinte estrutura. O arquivo JSON precisa conter um syncToken para identificar a versão dos dados que estão sendo baixados. O syncToken precisa ser um carimbo de data/hora UTC fixado em um limite de hora e exceder o syncToken do download anterior. Se o syncToken não atender aos dois requisitos, o conteúdo baixado será descartado sem processamento.

O campo "contents" é uma lista de tuplas (chave, dados, codificação). O key precisa ser uma string UTF-8. O campo encoding é um parâmetro opcional que especifica como o campo data é codificado. Ele pode ser definido como "utf8" ou "base64" e é considerado "utf8" por padrão. O campo key é convertido em um objeto String, e o campo data é convertido em uma matriz de bytes antes de chamar onDownloadCompleted()..

{
  // syncToken must be a UTC timestamp clamped to an hour boundary, and must be
  // greater than the syncToken of the previously completed download.
  "syncToken": <timeStampInSecRoundedToUtcHour>,
  "contents": [
    // List of { key, data } pairs.
    { "key": "key1",
      "data": "data1"
    },
    { "key": "key2",
      "data": "data2",
      "encoding": "base64"
    },
    // ...
  ]
}

APIs do lado do servidor

Nesta seção, descrevemos como interagir com as APIs do servidor de computação federada.

APIs do servidor de computação federada

Para programar um job de computação federada no lado do cliente, você precisa de uma tarefa com um nome de população criado no servidor de computação federada remota. Nesta seção, vamos abordar como criar uma tarefa desse tipo no servidor de computação federada.

Diagrama da topologia cliente-servidor de computação federada.

Ao criar uma tarefa para o Criador de tarefas, os desenvolvedores da ODP precisam fornecer dois conjuntos de arquivos:

  1. Um modelo tff.learning.models.FunctionalModel salvo ao chamar a API tff.learning.models.save_functional_model. Confira um exemplo no nosso repositório do GitHub.
  2. Um fcp_server_config.json que inclui políticas, configuração de aprendizado federado e configuração de privacidade diferencial. Confira a seguir um exemplo de fcp_server_config.json:
{
  # Task execution mode.
  mode: TRAINING_AND_EVAL
  # Identifies the set of client devices that participate.
  population_name: "mnist_cnn_task"
  policies {
    # Policy for sampling on-device examples. It is checked every
    # time a device is attempting to start a new training.
    min_separation_policy {
      # The minimum separation required between two successful
      # consective task executions. If a client successfully contributes
      # to a task at index `x`, the earliest they can contribute again
      # is at index `(x + minimum_separation)`. This is required by
      # DP.
      minimum_separation: 1
    }
    data_availability_policy {
      # The minimum number of examples on a device to be considered
      # eligible for training.
      min_example_count: 1
    }
    # Policy for releasing training results to developers adopting ODP.
    model_release_policy {
      # The maximum number of training rounds.
      num_max_training_rounds: 512
    }
  }

  # Federated learning setups. They are applied inside Task Builder.
  federated_learning {
    # Use federated averaging to build federated learning process.
    # Options you can choose:
      # * FED_AVG: Federated Averaging algorithm
      #            (https://arxiv.org/abs/2003.00295)
      # * FED_SGD: Federated SGD algorithm
      #            (https://arxiv.org/abs/1602.05629)
    type: FED_AVG
    learning_process {
      # Optimizer used at client side training. Options you can choose:
      # * ADAM
      # * SGD
      client_optimizer: SGD
      # Learning rate used at client side training.
      client_learning_rate: 0.02
      # Optimizer used at server side training. Options you can choose:
      # * ADAM
      # * SGD
      server_optimizer: SGD
      # Learning rate used at server side training.
      server_learning_rate: 1.0
      runtime_config {
        # Number of participating devices for each round of training.
      report_goal: 2
      }
      metrics {
        name: "sparse_categorical_accuracy"
      }
    }
    evaluation {
      # A checkpoint selector controls how checkpoints are chosen for
      # evaluation. One evaluation task typically runs per training
      # task, and on each round of execution, the eval task
      # randomly picks one checkpoint from the past 24 hours that has
      # been selected for evaluation by these rules.
      # Every_k_round and every_k_hour are definitions of quantization
      # buckets which each checkpoint is placed in for selection.
      checkpoint_selector: "every_1_round"
      # The percentage of a populate that should delicate to this
      # evaluation task.
      evaluation_traffic: 0.2
      # Number of participating devices for each round of evaluation.
      report_goal: 2
    }
  }

  # Differential Privacy setups. They are enforced inside the Task
  # Builder.
  differential_privacy {
    # * fixed_gaussian: DP-SGD with fixed clipping norm described in
    #                   "Learning Differentially Private Recurrent
    #                   Language Models"
    #                   (https://arxiv.org/abs/1710.06963).
    type: FIXED_GAUSSIAN
    #   The value of the clipping norm.
    clip_norm: 0.1
    # Noise multiplier for the Gaussian noise.
    noise_multiplier: 0.1
  }
}

Confira mais exemplos no nosso repositório do GitHub.

Depois de preparar essas duas entradas, invoque o Task Builder para criar artefatos e gerar novas tarefas. Instruções mais detalhadas estão disponíveis no nosso repositório do GitHub.