Tópicos avançados

Essas seções são para referência e não é necessário que você as leia de cima para baixo.

Use APIs de framework:

Essas APIs serão agrupadas no SDK para uma plataforma de API mais consistente (por exemplo, evitando objetos UserHandle), mas, por enquanto, você pode fazer chamadas diretamente.

A implementação é simples: se você puder interagir, faça isso. Se não, mas você pode solicitar, mostre o comando/banner/tooltip/etc. Se o usuário concordar em acessar as Configurações, crie a intent de solicitação e use Context#startActivity para enviar o usuário para lá. Você pode usar a transmissão para detectar quando essa capacidade muda ou apenas verificar novamente quando o usuário voltar.

Para testar isso, abra o TestDPC no seu perfil de trabalho, vá até a parte de baixo e selecione a opção de adicionar o nome do pacote à lista de permissões de apps conectados. Isso imita a lista de permissões do administrador do app.

Glossário

Esta seção define os principais termos relacionados ao desenvolvimento de vários perfis.

Configuração de perfis diferentes

Uma configuração de perfil cruzado agrupa classes relacionadas de provedor de perfil cruzado e fornece uma configuração geral para os recursos de perfil cruzado. Normalmente, há uma única anotação @CrossProfileConfiguration por base de código, mas em alguns aplicativos complexos pode haver várias.

Conector de perfil

Um conector gerencia as conexões entre perfis. Normalmente, cada tipo de perfil cruzado aponta para um conector específico. Todos os tipos de perfil cruzado em uma única configuração precisam usar o mesmo conector.

Classe do provedor de perfil cruzado

Uma classe de provedor de perfil cruzado agrupa tipos de perfil cruzado relacionados.

Mediator

Um mediador fica entre o código de alto e baixo nível, distribuindo chamadas para os perfis corretos e mesclando resultados. Esse é o único código que precisa estar sensível ao perfil. Esse é um conceito de arquitetura, não algo integrado ao SDK.

Tipo de perfil cruzado

Um tipo de perfil cruzado é uma classe ou interface que contém métodos com a anotação @CrossProfile. O código desse tipo não precisa estar ciente do perfil e, idealmente, deve agir apenas com os dados locais.

Tipos de perfil

Tipo de perfil
AtualO perfil ativo em que estamos executando.
Outro(se existir) O perfil em que não estamos executando.
PessoalUsuário 0, o perfil no dispositivo que não pode ser desativado.
TrabalhoNormalmente, o usuário 10, mas pode ser maior, pode ser ativado e desativado, usado para conter dados e apps de trabalho.
PrincipalOpcionalmente definido pelo aplicativo. O perfil que mostra uma visualização mesclada dos dois perfis.
SecundárioSe o primário for definido, o secundário será o perfil que não é primário.
FornecedorOs fornecedores do perfil principal são os dois perfis, enquanto os fornecedores do perfil secundário são apenas o próprio perfil secundário.

Identificador do perfil

Uma classe que representa um tipo de perfil (pessoal ou de trabalho). Eles serão retornados por métodos executados em vários perfis e podem ser usados para executar mais código nesses perfis. Eles podem ser serializados em um int para armazenamento conveniente.

Este guia descreve as estruturas recomendadas para criar funcionalidades eficientes e manuteníveis entre perfis no seu app Android.

Converta o CrossProfileConnector em um singleton

Apenas uma instância deve ser usada durante o ciclo de vida do aplicativo. Caso contrário, você vai criar conexões paralelas. Isso pode ser feito usando um framework de injeção de dependência, como o Dagger, ou usando um padrão Singleton clássico, em uma nova classe ou em uma já existente.

Injete ou transmita a instância de perfil gerada para sua classe quando você fizer a chamada, em vez de criá-la no método

Isso permite transmitir a instância FakeProfile gerada automaticamente nos testes de unidade mais tarde.

Considere o padrão de mediador

Esse padrão comum é fazer com que uma das suas APIs (por exemplo, getEvents()) reconheça o perfil de todos os autores das chamadas. Nesse caso, a API atual pode se tornar um método ou uma classe "mediator" que contém a nova chamada para o código gerado em vários perfis.

Dessa forma, você não força todos os autores da chamada a saberem fazer uma chamada entre perfis. Ela passa a fazer parte da sua API.

Considere anotar um método de interface como @CrossProfile para evitar expor suas classes de implementação em um provedor

Isso funciona bem com frameworks de injeção de dependência.

Se você estiver recebendo dados de uma chamada entre perfis, considere adicionar um campo que faça referência ao perfil de origem

Essa pode ser uma boa prática, já que você pode querer saber isso na camada da interface (por exemplo, adicionar um ícone de selo a itens de trabalho). Ele também pode ser necessário se os identificadores de dados não forem mais exclusivos sem ele, como nomes de pacotes.

Perfil cruzado

Esta seção descreve como criar suas próprias interações entre perfis.

Perfis principais

A maioria das chamadas nos exemplos deste documento contém instruções explícitas sobre em quais perfis executar, incluindo trabalho, pessoal e ambos.

Na prática, para apps com uma experiência combinada em apenas um perfil, é provável que você queira que essa decisão dependa do perfil em que você está executando. Portanto, há métodos convenientes semelhantes que também levam isso em consideração para evitar que o conjunto de códigos seja preenchido com condicionais de perfil if-else.

Ao criar a instância do conector, você pode especificar qual tipo de perfil é "principal" (por exemplo, "TRABALHO"). Isso permite outras opções, como as seguintes:

profileCalendarDatabase.primary().getEvents();

profileCalendarDatabase.secondary().getEvents();

// Runs on all profiles if running on the primary, or just
// on the current profile if running on the secondary.
profileCalendarDatabase.suppliers().getEvents();

Tipos de perfil cruzado

As classes e interfaces que contêm um método com anotação @CrossProfile são chamadas de tipos de perfil cruzado.

A implementação de tipos de perfil cruzado precisa ser independente do perfil, o perfil em que eles estão sendo executados. Eles podem fazer chamadas para outros métodos e, de modo geral, precisam funcionar como se estivessem sendo executados em um único perfil. Eles só terão acesso ao estado no próprio perfil.

Exemplo de tipo de perfil unificado:

public class Calculator {
  @CrossProfile
  public int add(int a, int b) {
    return a + b;
  }
}

Anotação de classe

Para fornecer a API mais forte, especifique o conector para cada tipo de perfil cruzado, conforme mostrado abaixo:

@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
  @CrossProfile
  public int add(int a, int b) {
    return a + b;
  }
}

Isso é opcional, mas significa que a API gerada será mais específica em tipos e mais rigorosa na verificação no tempo de compilação.

Interfaces

Ao anotar métodos em uma interface como @CrossProfile, você declara que pode haver alguma implementação desse método que precisa ser acessível em todos os perfis.

Você pode retornar qualquer implementação de uma interface de perfil cruzado em um provedor de perfil cruzado. Ao fazer isso, você está dizendo que essa implementação precisa ser acessível em todos os perfis. Não é necessário anotar as classes de implementação.

Provedores de perfil cruzado

Cada Cross Profile Type precisa ser fornecido por um método anotado @CrossProfileProvider. Esses métodos serão chamados sempre que uma chamada entre perfis for feita. Portanto, é recomendável manter singletons para cada tipo.

Construtor

Um provedor precisa ter um construtor público que não aceite argumentos ou um único argumento Context.

Métodos do provedor

Os métodos do provedor não podem ter argumentos ou apenas um argumento Context.

Injeção de dependência

Se você estiver usando um framework de injeção de dependência, como o Dagger, para gerenciar dependências, recomendamos que esse framework crie seus tipos de perfil cruzado como faria normalmente e injete esses tipos na classe do provedor. Os métodos @CrossProfileProvider podem retornar essas instâncias injetadas.

Conector de perfil

Cada configuração de perfil cruzado precisa ter um único conector de perfil, que é responsável por gerenciar a conexão com o outro perfil.

Conector de perfil padrão

Se houver apenas uma configuração de perfil cruzado em uma base de código, você poderá evitar a criação do seu próprio conector de perfil e usar com.google.android.enterprise.connectedapps.CrossProfileConnector. Esse é o padrão usado se nenhum for especificado.

Ao criar o conector de perfis diferentes, é possível especificar algumas opções no builder:

  • Serviço de executor programado

    Se você quiser ter controle sobre as linhas de execução criadas pelo SDK, use #setScheduledExecutorService().

  • Binder

    Se você tiver necessidades específicas relacionadas à vinculação de perfil, use #setBinder. Essa provavelmente é usada apenas por controladores de política do dispositivo.

Conector de perfil personalizado

Você vai precisar de um conector de perfil personalizado para definir algumas configurações (usando CustomProfileConnector) e vai precisar de um se precisar de vários conectores em um único código-base. Por exemplo, se você tiver vários processos, recomendamos um conector por processo.

Ao criar um ProfileConnector, ele vai ficar assim:

@GeneratedProfileConnector
public interface MyProfileConnector extends ProfileConnector {
  public static MyProfileConnector create(Context context) {
    // Configuration can be specified on the builder
    return GeneratedMyProfileConnector.builder(context).build();
  }
}
  • serviceClassName

    Para mudar o nome do serviço gerado (que precisa ser referenciado no AndroidManifest.xml), use serviceClassName=.

  • primaryProfile

    Para especificar o perfil principal, use primaryProfile.

  • availabilityRestrictions

    Para mudar as restrições que o SDK impõe às conexões e à disponibilidade do perfil, use availabilityRestrictions.

Controladores de política de dispositivo

Se o app for um controlador de política de dispositivo, especifique uma instância de DpcProfileBinder que faça referência ao DeviceAdminReceiver.

Se você estiver implementando seu próprio conector de perfil:

@GeneratedProfileConnector
public interface DpcProfileConnector extends ProfileConnector {
  public static DpcProfileConnector get(Context context) {
    return GeneratedDpcProfileConnector.builder(context).setBinder(new
DpcProfileBinder(new ComponentName("com.google.testdpc",
"AdminReceiver"))).build();
  }
}

ou usando o CrossProfileConnector padrão:

CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();

Configuração de perfis diferentes

A anotação @CrossProfileConfiguration é usada para vincular todos os tipos de perfil cruzado usando um conector para despachar chamadas de método corretamente. Para fazer isso, anotamos uma classe com @CrossProfileConfiguration, que aponta para todos os provedores, assim:

@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}

Isso vai validar que, para todos os tipos de perfil cruzado, eles têm o mesmo conector de perfil ou nenhum conector especificado.

  • serviceSuperclass

    Por padrão, o serviço gerado vai usar android.app.Service como a superclasse. Se você precisar de uma classe diferente (que precisa ser uma subclasse de android.app.Service) para ser a superclasse, especifique serviceSuperclass=.

  • serviceClass

    Se especificado, nenhum serviço será gerado. Ele precisa corresponder ao serviceClassName no conector de perfil que você está usando. Seu serviço personalizado precisa enviar chamadas usando a classe _Dispatcher gerada, como esta:

public final class TestProfileConnector_Service extends Service {
  private Stub binder = new Stub() {
    private final TestProfileConnector_Service_Dispatcher dispatcher = new
TestProfileConnector_Service_Dispatcher();

    @Override
    public void prepareCall(long callId, int blockId, int numBytes, byte[] params)
{
      dispatcher.prepareCall(callId, blockId, numBytes, params);
    }

    @Override
    public byte[] call(long callId, int blockId, long crossProfileTypeIdentifier,
int methodIdentifier, byte[] params,
    ICrossProfileCallback callback) {
      return dispatcher.call(callId, blockId, crossProfileTypeIdentifier,
methodIdentifier, params, callback);
    }

    @Override
    public byte[] fetchResponse(long callId, int blockId) {
      return dispatcher.fetchResponse(callId, blockId);
  };

  @Override
  public Binder onBind(Intent intent) {
    return binder;
  }
}

Isso pode ser usado se você precisar realizar outras ações antes ou depois de uma chamada entre perfis.

  • Conector

    Se você estiver usando um conector diferente do CrossProfileConnector padrão, especifique-o usando connector=.

Visibilidade

Todas as partes do aplicativo que interagem entre perfis precisam ter acesso ao conector de perfil.

Sua classe @CrossProfileConfiguration com anotação precisa acessar todos os provedores usados no aplicativo.

Chamadas síncronas

O SDK de apps conectados oferece suporte a chamadas síncronas (de bloqueio) para casos em que elas são inevitáveis. No entanto, há várias desvantagens em usar essas chamadas, como o potencial de bloqueio por um longo período. Por isso, recomendamos que você evite chamadas síncronas sempre que possível. Para usar chamadas assíncronas, consulte Chamadas assíncronas .

Detentores de conexão

Se você estiver usando chamadas síncronas, verifique se há um detenha de conexão registrado antes de fazer chamadas entre perfis. Caso contrário, uma exceção será gerada. Para mais informações, consulte "Detentor de conexão".

Para adicionar um detentor de conexão, chame ProfileConnector#addConnectionHolder(Object) com qualquer objeto (possivelmente, a instância de objeto que está fazendo a chamada entre perfis). Isso vai registrar que esse objeto está usando a conexão e tentará fazer uma conexão. Ele precisa ser chamado antes de qualquer chamada síncrona. Essa é uma chamada não bloqueante, então é possível que a conexão não esteja pronta (ou não seja possível) no momento em que você faz a chamada. Nesse caso, o comportamento normal de processamento de erros se aplica.

Se você não tiver as permissões adequadas entre perfis ao chamar ProfileConnector#addConnectionHolder(Object) ou se nenhum perfil estiver disponível para conexão, nenhum erro será gerado, mas o callback conectado nunca será chamado. Se a permissão for concedida mais tarde ou se o outro perfil ficar disponível, a conexão será feita e o callback será chamado.

Como alternativa, ProfileConnector#connect(Object) é um método de bloqueio que adiciona o objeto como um detentor de conexão e estabelece uma conexão ou gera uma UnavailableProfileException. Esse método não pode ser chamado da linha de execução da interface.

As chamadas para ProfileConnector#connect(Object) e o ProfileConnector#connect semelhante retornam objetos de fechamento automático que removem automaticamente o detentor de conexão quando fechados. Isso permite usos como:

try (ProfileConnectionHolder p = connector.connect()) {
  // Use the connection
}

Quando terminar de fazer chamadas síncronas, chame ProfileConnector#removeConnectionHolder(Object). Quando todos os detentores de conexão forem removidos, a conexão será encerrada.

Conectividade

Um listener de conexão pode ser usado para receber informações quando o estado da conexão muda, e connector.utils().isConnected pode ser usado para determinar se uma conexão está presente. Exemplo:

// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
  if (crossProfileConnector.utils().isConnected()) {
    // Make cross-profile calls.
  }
});

Chamadas assíncronas

Todos os métodos expostos na divisão de perfil precisam ser designados como bloqueadores (síncronos) ou não bloqueadores (assíncronos). Qualquer método que retorne um tipo de dados assíncrono (por exemplo, um ListenableFuture) ou aceite um parâmetro de callback é marcado como não bloqueado. Todos os outros métodos são marcados como bloqueadores.

Recomendamos o uso de chamadas assíncronas. Se você precisar usar chamadas síncronas, consulte Chamadas síncronas.

Callbacks

O tipo mais básico de chamada não bloqueante é um método vazio que aceita como um dos parâmetros uma interface que contém um método a ser chamado com o resultado. Para que essas interfaces funcionem com o SDK, elas precisam ser anotadas com @CrossProfileCallback. Exemplo:

@CrossProfileCallback
public interface InstallationCompleteListener {
  void installationComplete(int state);
}

Essa interface pode ser usada como um parâmetro em um método com anotação @CrossProfile e ser chamado normalmente. Exemplo:

@CrossProfile
public void install(String filename, InstallationCompleteListener callback) {
  // Do something on a separate thread and then:
  callback.installationComplete(1);
}

// In the mediator
profileInstaller.work().install(filename, (status) -> {
  // Deal with callback
}, (exception) -> {
  // Deal with possibility of profile unavailability
});

Se essa interface tiver um único método, que recebe zero ou um parâmetro, ela também poderá ser usada em chamadas para vários perfis de uma só vez.

Qualquer número de valores pode ser transmitido usando um callback, mas a conexão só será mantida aberta para o primeiro valor. Consulte "Detentor de conexão" para informações sobre como manter a conexão aberta para receber mais valores.

Métodos síncronos com callbacks

Um recurso incomum do uso de callbacks com o SDK é que você pode escrever tecnicamente um método síncrono que usa um callback:

public void install(InstallationCompleteListener callback) {
  callback.installationComplete(1);
}

Nesse caso, o método é realmente síncrono, apesar do callback. Esse código seria executado corretamente:

System.out.println("This prints first");
installer.install(() -> {
        System.out.println("This prints second");
});
System.out.println("This prints third");

No entanto, quando chamado usando o SDK, o comportamento não será o mesmo. Não há garantia de que o método de instalação será chamado antes que "This prints third" seja impresso. Qualquer uso de um método marcado como assíncrono pelo SDK não pode fazer suposições sobre quando o método será chamado.

Callbacks simples

Os "callbacks simples" são uma forma mais restritiva de callback que permite recursos adicionais ao fazer chamadas entre perfis. Interfaces simples precisam conter um único método, que pode receber zero ou um parâmetro.

É possível forçar que uma interface de callback precise permanecer especificando simple=true na anotação @CrossProfileCallback.

Callbacks simples podem ser usados com vários métodos, como .both(), .suppliers() e outros.

Detentores de conexão

Ao fazer uma chamada assíncrona (usando callbacks ou futuros), um detenha de conexão é adicionado ao fazer a chamada e removido quando uma exceção ou um valor é transmitido.

Se você espera que mais de um resultado seja transmitido usando um callback, adicione manualmente o callback como um detentor de conexão:

MyCallback b = //...
connector.addConnectionHolder(b);

  profileMyClass.other().registerListener(b);

  // Now the connection will be held open indefinitely, once finished:
  connector.removeConnectionHolder(b);

Isso também pode ser usado com um bloco try-with-resources:

MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
  profileMyClass.other().registerListener(b);

  // Other things running while we expect results
}

Se fizermos uma chamada com um callback ou futuro, a conexão será mantida aberta até que um resultado seja transmitido. Se determinarmos que um resultado não será transmitido, removeremos o callback ou o futuro como detentor da conexão:

connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);

Para mais informações, consulte "Detentor de conexão".

Futures

O SDK também oferece suporte nativo a futuros. O único tipo Future com suporte nativo é ListenableFuture, embora tipos Future personalizados possam ser usados. Para usar futuros, basta declarar um tipo Future compatível como o tipo de retorno de um método de perfil cruzado e usá-lo normalmente.

Esse é o mesmo "recurso incomum" dos callbacks, em que um método síncrono que retorna um futuro (por exemplo, usando immediateFuture) se comporta de maneira diferente quando executado no perfil atual em comparação com outro perfil. Qualquer uso de um método marcado como assíncrono pelo SDK não pode fazer suposições sobre quando o método será chamado.

Linhas de execução

Não bloquear o resultado de um futuro entre perfis ou callback na linha de execução principal. Se você fizer isso, em algumas situações, seu código será bloqueado indefinidamente. Isso ocorre porque a conexão com o outro perfil também é estabelecida na linha de execução principal, o que nunca vai ocorrer se ela estiver bloqueada aguardando um resultado entre perfis.

Disponibilidade

O listener de disponibilidade pode ser usado para receber informações quando o estado de disponibilidade mudar, e connector.utils().isAvailable pode ser usado para determinar se outro perfil está disponível para uso. Exemplo:

crossProfileConnector.registerAvailabilityListener(() -> {
  if (crossProfileConnector.utils().isAvailable()) {
    // Show cross-profile content
  } else {
    // Hide cross-profile content
  }
});

Detentores de conexão

Os detentores de conexão são objetos arbitrários registrados como tendo interesse na conexão entre perfis estabelecida e mantida.

Por padrão, ao fazer chamadas assíncronas, um detentor de conexão é adicionado quando a chamada começa e removido quando ocorre algum resultado ou erro.

Os detentores de conexão também podem ser adicionados e removidos manualmente para exercer mais controle sobre a conexão. Os detentores de conexão podem ser adicionados usando connector.addConnectionHolder e removidos usando connector.removeConnectionHolder.

Quando pelo menos um detentor de conexão é adicionado, o SDK tenta manter uma conexão. Quando não há nenhum detentor de conexão adicionado, a conexão pode ser encerrada.

Você precisa manter uma referência a qualquer detentor de conexão adicionado e removê-lo quando ele não for mais relevante.

Chamadas síncronas

Antes de fazer chamadas síncronas, é necessário adicionar um detentor de conexão. Isso pode ser feito usando qualquer objeto, mas você precisa acompanhar esse objeto para que ele possa ser removido quando não precisar mais fazer chamadas síncronas.

Chamadas assíncronas

Ao fazer chamadas assíncronas, os detentores de conexão são gerenciados automaticamente para que a conexão seja aberta entre a chamada e a primeira resposta ou erro. Se você precisar que a conexão sobreviva além disso (por exemplo, para receber várias respostas usando um único callback), adicione o callback como um detenha a conexão e remova-o quando não precisar mais receber dados.

Tratamento de erros

Por padrão, todas as chamadas feitas para o outro perfil quando ele não está disponível resultam em uma UnavailableProfileException sendo gerada (ou transmitida para o futuro ou callback de erro para uma chamada assíncrona).

Para evitar isso, os desenvolvedores podem usar #both() ou #suppliers() e escrever o código para lidar com qualquer número de entradas na lista resultante (1 se o outro perfil estiver indisponível ou 2 se estiver disponível).

Exceções

Todas as exceções não verificadas que ocorrerem após uma chamada para o perfil atual serão propagadas normalmente. Isso se aplica independentemente do método usado para fazer a chamada (#current(), #personal, #both etc.).

Exceções não verificadas que ocorrem após uma chamada para o outro perfil resultam em uma ProfileRuntimeException gerada com a exceção original como causa. Isso se aplica independentemente do método usado para fazer a chamada (#other(), #personal, #both etc.).

ifAvailable

Como alternativa para capturar e lidar com instâncias UnavailableProfileException, use o método .ifAvailable() para fornecer um valor padrão que será retornado em vez de gerar uma UnavailableProfileException.

Exemplo:

profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);

Teste

Para tornar o código testável, injete instâncias do conector de perfil em qualquer código que o use (para verificar a disponibilidade do perfil, conectar manualmente etc.). Você também precisa injetar instâncias dos tipos do seu perfil onde eles são usados.

Fornecemos simulações do conector e dos tipos que podem ser usados em testes.

Primeiro, adicione as dependências de teste:

  testAnnotationProcessor
'com.google.android.enterprise.connectedapps:connectedapps-processor:1.1.2'
  testCompileOnly
'com.google.android.enterprise.connectedapps:connectedapps-testing-annotations:1.1.2'
  testImplementation
'com.google.android.enterprise.connectedapps:connectedapps-testing:1.1.2'

Em seguida, anote a classe de teste com @CrossProfileTest, identificando a classe @CrossProfileConfiguration anotada a ser testada:

@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {

}

Isso vai gerar fakes para todos os tipos e conectores usados na configuração.

Crie instâncias desses dados falsos no teste:

private final FakeCrossProfileConnector connector = new
FakeCrossProfileConnector();
private final NotesManager personalNotesManager = new NotesManager(); //
real/mock/fake
private final NotesManager workNotesManager = new NotesManager(); // real/mock/fake

private final FakeProfileNotesManager profileNotesManager =
  FakeProfileNotesManager.builder()
    .personal(personalNotesManager)
    .work(workNotesManager)
    .connector(connector)
    .build();

Configure o estado do perfil:

connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();

Transmita o conector falso e a classe de perfil cruzado para o código em teste e faça chamadas.

As chamadas serão roteadas para o destino correto, e exceções serão geradas ao fazer chamadas para perfis desconectados ou indisponíveis.

Tipos monitorados

Os tipos a seguir são compatíveis sem nenhum esforço extra da sua parte. Eles podem ser usados como argumentos ou tipos de retorno para todas as chamadas entre perfis.

  • Primitivas (byte, short, int, long, float, double, char, boolean).
  • Primitivas encapsuladas (java.lang.Byte, java.lang.Short, java.lang.Integer, java.lang.Long, java.lang.Float, java.lang.Double, java.lang.Character, java.lang.Boolean, java.lang.Void).
  • java.lang.String,
  • Qualquer coisa que implemente android.os.Parcelable,
  • Qualquer coisa que implemente java.io.Serializable,
  • Matrizes não primitivas de uma dimensão,
  • java.util.Optional,
  • java.util.Collection,
  • java.util.List,
  • java.util.Map,
  • java.util.Set,
  • android.util.Pair,
  • com.google.common.collect.ImmutableMap.

Qualquer tipo genérico com suporte (por exemplo, java.util.Collection) pode ter qualquer tipo com suporte como parâmetro de tipo. Exemplo:

java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>> é um tipo válido.

Futures

Os seguintes tipos são aceitos apenas como tipos de retorno:

  • com.google.common.util.concurrent.ListenableFuture

Wrappers parceláveis personalizados

Se o tipo não estiver na lista anterior, primeiro considere se ele pode ser implementado corretamente como android.os.Parcelable ou java.io.Serializable. Se ele não encontrar wrappers parceláveis para adicionar suporte ao seu tipo.

Wrappers futuros personalizados

Se você quiser usar um tipo futuro que não está na lista anterior, consulte wrappers futuros para adicionar suporte.

Wrappers parceláveis

Os wrappers parceláveis são a maneira como o SDK adiciona suporte a tipos não parceláveis que não podem ser modificados. O SDK inclui wrappers para muitos tipos, mas se o tipo que você precisa usar não estiver incluído, será necessário escrever o próprio.

Um wrapper parcelável é uma classe projetada para agrupar outra e torná-la parcelável. Ele segue um contrato estático definido e é registrado no SDK para que possa ser usado para converter um determinado tipo em um tipo parcelável e também extrair esse tipo do tipo parcelável.

Nota

A classe de wrapper parcelável precisa ser anotada como @CustomParcelableWrapper, especificando a classe embrulhada como originalType. Exemplo:

@CustomParcelableWrapper(originalType=ImmutableList.class)

Formato

Os wrappers parceláveis precisam implementar Parcelable corretamente e ter um método W of(Bundler, BundlerType, T) estático que encapsule o tipo encapsulado e um método T get() não estático que retorne o tipo encapsulado.

O SDK vai usar esses métodos para oferecer suporte perfeito ao tipo.

Bundler

Para permitir o agrupamento de tipos genéricos (como listas e mapas), o método of é transmitido a um Bundler que é capaz de ler (usando #readFromParcel) e gravar (usando #writeToParcel) todos os tipos com suporte em um Parcel e um BundlerType que representa o tipo declarado a ser gravado.

As instâncias Bundler e BundlerType são parceláveis e precisam ser escritas como parte do empacotamento do wrapper parcelável para que ele possa ser usado ao reconstruir o wrapper parcelável.

Se o BundlerType representar um tipo genérico, as variáveis de tipo poderão ser encontradas chamando .typeArguments(). Cada argumento de tipo é um BundlerType.

Para conferir um exemplo, consulte ParcelableCustomWrapper:

public class CustomWrapper<F> {
  private final F value;

  public CustomWrapper(F value) {
    this.value = value;
  }
  public F value() {
    return value;
  }
}

@CustomParcelableWrapper(originalType = CustomWrapper.class)
public class ParcelableCustomWrapper<E> implements Parcelable {

  private static final int NULL = -1;
  private static final int NOT_NULL = 1;

  private final Bundler bundler;
  private final BundlerType type;
  private final CustomWrapper<E> customWrapper;

  /**
  *   Create a wrapper for a given {@link CustomWrapper}.
  *
  *   <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
  */
  public static <F> ParcelableCustomWrapper<F> of(
      Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {
    return new ParcelableCustomWrapper<>(bundler, type, customWrapper);
  }

  public CustomWrapper<E> get() {
    return customWrapper;
  }

  private ParcelableCustomWrapper(
      Bundler bundler, BundlerType type, CustomWrapper<E> customWrapper) {
    if (bundler == null || type == null) {
      throw new NullPointerException();
    }
    this.bundler = bundler;
    this.type = type;
    this.customWrapper = customWrapper;
  }

  private ParcelableCustomWrapper(Parcel in) {
    bundler = in.readParcelable(Bundler.class.getClassLoader());

    int presentValue = in.readInt();

    if (presentValue == NULL) {
      type = null;
      customWrapper = null;
      return;
    }

    type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
    BundlerType valueType = type.typeArguments().get(0);

    @SuppressWarnings("unchecked")
    E value = (E) bundler.readFromParcel(in, valueType);

    customWrapper = new CustomWrapper<>(value);
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeParcelable(bundler, flags);

    if (customWrapper == null) {
      dest.writeInt(NULL);
      return;
    }

    dest.writeInt(NOT_NULL);
    dest.writeParcelable(type, flags);
    BundlerType valueType = type.typeArguments().get(0);
    bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);
  }

  @Override
  public int describeContents() {
    return 0;
  }

  @SuppressWarnings("rawtypes")
  public static final Creator<ParcelableCustomWrapper> CREATOR =
    new Creator<ParcelableCustomWrapper>() {
      @Override
      public ParcelableCustomWrapper createFromParcel(Parcel in) {
        return new ParcelableCustomWrapper(in);
      }

      @Override
      public ParcelableCustomWrapper[] newArray(int size) {
        return new ParcelableCustomWrapper[size];
      }
    };
}

Fazer o registro no SDK

Depois de criado, para usar o wrapper parcelável personalizado, você precisará registrá-lo com o SDK.

Para fazer isso, especifique parcelableWrappers={YourParcelableWrapper.class} em uma anotação CustomProfileConnector ou CrossProfile em uma classe.

Wrappers futuros

Os wrappers futuros são a forma como o SDK adiciona suporte a futuros em vários perfis. O SDK inclui suporte para ListenableFuture por padrão, mas para outros tipos Future, você pode adicionar suporte por conta própria.

Um wrapper de futuro é uma classe projetada para agrupar um tipo de futuro específico e torná-lo disponível para o SDK. Ele segue um contrato estático definido e precisa ser registrado no SDK.

Nota

A classe de wrapper futura precisa ser anotada como @CustomFutureWrapper, especificando a classe envolta como originalType. Exemplo:

@CustomFutureWrapper(originalType=SettableFuture.class)

Formato

Os wrappers futuros precisam estender com.google.android.enterprise.connectedapps.FutureWrapper.

Os wrappers futuros precisam ter um método W create(Bundler, BundlerType) estático que crie uma instância do wrapper. Ao mesmo tempo, isso precisa criar uma instância do tipo futuro encapsulado. Isso precisa ser retornado por um método getFuture() T não estático. Os métodos onResult(E) e onException(Throwable) precisam ser implementados para transmitir o resultado ou o throwable para o futuro envolvido.

Os wrappers futuros também precisam ter um método void writeFutureResult(Bundler, BundlerType, T, FutureResultWriter<E>) estático. Isso precisa ser registrado com o elemento transmitido no futuro para resultados. Quando um resultado for fornecido, chame resultWriter.onSuccess(value). Se uma exceção for fornecida, resultWriter.onFailure(exception) precisará ser chamada.

Por fim, os wrappers futuros também precisam ter um método T<Map<Profile, E>> groupResults(Map<Profile, T<E>> results) estático que converta um mapa de perfil para futuro, em um futuro de um mapa de perfil para resultado. O CrossProfileCallbackMultiMerger pode ser usado para facilitar essa lógica.

Exemplo:

/** A basic implementation of the future pattern used to test custom future
wrappers. */
public class SimpleFuture<E> {
  public static interface Consumer<E> {
    void accept(E value);
  }
  private E value;
  private Throwable thrown;
  private final CountDownLatch countDownLatch = new CountDownLatch(1);
  private Consumer<E> callback;
  private Consumer<Throwable> exceptionCallback;

  public void set(E value) {
    this.value = value;
    countDownLatch.countDown();
    if (callback != null) {
      callback.accept(value);
    }
  }

  public void setException(Throwable t) {
    this.thrown = t;
    countDownLatch.countDown();
    if (exceptionCallback != null) {
      exceptionCallback.accept(thrown);
    }
  }

  public E get() {
    try {
      countDownLatch.await();
    } catch (InterruptedException e) {
      eturn null;
    }
    if (thrown != null) {
      throw new RuntimeException(thrown);
    }
    return value;
  }

  public void setCallback(Consumer<E> callback, Consumer<Throwable>
exceptionCallback) {
    if (value != null) {
      callback.accept(value);
    } else if (thrown != null) {
      exceptionCallback.accept(thrown);
    } else {
      this.callback = callback;
      this.exceptionCallback = exceptionCallback;
    }
  }
}
/** Wrapper for adding support for {@link SimpleFuture} to the Connected Apps SDK.
*/
@CustomFutureWrapper(originalType = SimpleFuture.class)
public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {

  private final SimpleFuture<E> future = new SimpleFuture<>();

  public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType
bundlerType) {
    return new SimpleFutureWrapper<>(bundler, bundlerType);
  }

  private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {
    super(bundler, bundlerType);
  }

  public SimpleFuture<E> getFuture() {
    return future;
  }

  @Override
  public void onResult(E result) {
    future.set(result);
  }

  @Override
  public void onException(Throwable throwable) {
    future.setException(throwable);
  }

  public static <E> void writeFutureResult(
      SimpleFuture<E> future, FutureResultWriter<E> resultWriter) {

    future.setCallback(resultWriter::onSuccess, resultWriter::onFailure);
  }

  public static <E> SimpleFuture<Map<Profile, E>> groupResults(
      Map<Profile, SimpleFuture<E>> results) {
    SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();

    CrossProfileCallbackMultiMerger<E> merger =
        new CrossProfileCallbackMultiMerger<>(results.size(), m::set);
    for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {
      result
        .getValue()
        .setCallback(
          (value) -> merger.onResult(result.getKey(), value),
          (throwable) -> merger.missingResult(result.getKey()));
    }
    return m;
  }
}

Fazer o registro no SDK

Depois de criado, para usar o wrapper futuro personalizado, você precisará registrá-lo no SDK.

Para fazer isso, especifique futureWrappers={YourFutureWrapper.class} em uma anotação CustomProfileConnector ou CrossProfile em uma classe.

Modo de inicialização direta

Se o app oferecer suporte ao modo de inicialização direta, talvez seja necessário fazer chamadas entre perfis antes que o perfil seja desbloqueado. Por padrão, o SDK só permite conexões quando o outro perfil está desbloqueado.

Para mudar esse comportamento, se você estiver usando um conector de perfil personalizado, especifique availabilityRestrictions=AvailabilityRestrictions.DIRECT_BOOT_AWARE:

@GeneratedProfileConnector
@CustomProfileConnector(availabilityRestrictions=AvailabilityRestrictions.DIRECT_BO
OT_AWARE)
public interface MyProfileConnector extends ProfileConnector {
  public static MyProfileConnector create(Context context) {
    return GeneratedMyProfileConnector.builder(context).build();
  }
}

Se você estiver usando CrossProfileConnector, use .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT _AWARE no builder.

Com essa mudança, você vai receber informações sobre a disponibilidade e poderá fazer chamadas entre perfis quando o outro perfil não estiver desbloqueado. É sua responsabilidade garantir que as chamadas acessem apenas o armazenamento criptografado do dispositivo.