Essas seções são para referência e não é necessário que você as leia de cima para baixo.
Solicitar consentimento do usuário
Use APIs de framework:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
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 | |
---|---|
Atual | O perfil ativo em que estamos executando. |
Outro | (se existir) O perfil em que não estamos executando. |
Pessoal | Usuário 0, o perfil no dispositivo que não pode ser desativado. |
Trabalho | Normalmente, o usuário 10, mas pode ser maior, pode ser ativado e desativado, usado para conter dados e apps de trabalho. |
Principal | Opcionalmente definido pelo aplicativo. O perfil que mostra uma visualização mesclada dos dois perfis. |
Secundário | Se o primário for definido, o secundário será o perfil que não é primário. |
Fornecedor | Os 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.
Soluções recomendadas de arquitetura
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
), useserviceClassName=
.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 deandroid.app.Service
) para ser a superclasse, especifiqueserviceSuperclass=
.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 usandoconnector=
.
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.