Temas avanzados

Estas secciones son de referencia y no es necesario que las leas de arriba abajo.

Usa las APIs del framework:

Estas APIs se unirán en el SDK para obtener una plataforma de API más coherente (p.ej., evitar objetos UserHandle), pero por ahora, puedes llamarlas directamente.

La implementación es sencilla: si puedes interactuar, hazlo. Si no es así, pero puedes solicitarlo, muestra el mensaje, el banner, la información sobre herramientas, etcétera, al usuario. Si el usuario acepta ir a Configuración, crea el intent de solicitud y usa Context#startActivity para enviarlo allí. Puedes usar la transmisión para detectar cuándo cambia esta función o simplemente volver a verificarla cuando el usuario regrese.

Para probar esto, deberás abrir TestDPC en tu perfil de trabajo, ir al final y seleccionar agregar el nombre de tu paquete a la lista de entidades permitidas de las apps conectadas. Esto imita la acción del administrador de incluir tu app en la lista de entidades permitidas.

Glosario

En esta sección, se definen los términos clave relacionados con el desarrollo multiperfil.

Configuración de perfiles sincronizados

Una configuración de perfil sincronizado agrupa clases relacionadas de proveedores de perfiles sincronizados y proporciona una configuración general para las funciones de perfil sincronizado. Por lo general, habrá una anotación @CrossProfileConfiguration por base de código, pero en algunas aplicaciones complejas puede haber varias.

Conector de perfiles

Un conector administra las conexiones entre los perfiles. Por lo general, cada tipo de perfil cruzado apuntará a un conector específico. Cada tipo de perfil sincronizado en una sola configuración debe usar el mismo conector.

Clase del proveedor de perfiles sincronizados

Una clase de proveedor de perfiles sincronizados agrupa tipos de perfiles sincronizados relacionados.

Mediator

Un mediador se encuentra entre el código de alto nivel y el de bajo nivel, distribuye las llamadas a los perfiles correctos y combina los resultados. Este es el único código que debe ser consciente del perfil. Este es un concepto arquitectónico en lugar de algo integrado en el SDK.

Tipo de perfil sincronizado

Un tipo de perfil cruzado es una clase o interfaz que contiene métodos con anotaciones @CrossProfile. El código de este tipo no necesita conocer el perfil y, en lo posible, solo debe actuar en sus datos locales.

Tipos de perfiles

Tipo de perfil
ActualEs el perfil activo en el que estamos ejecutando.
Otro(si existe) El perfil en el que no estamos ejecutando.
PersonalUsuario 0, el perfil del dispositivo que no se puede apagar
TrabajoPor lo general, es el usuario 10, pero puede ser superior, se puede activar y desactivar, y se usa para contener apps y datos de trabajo.
PrincipalLa aplicación lo define de manera opcional. El perfil que muestra una vista combinada de ambos perfiles.
SecundariaSi se define el perfil principal, el secundario es el que no es principal.
ProveedorLos proveedores del perfil principal son ambos perfiles, mientras que los proveedores del perfil secundario son solo el perfil secundario.

Identificador de perfil

Es una clase que representa un tipo de perfil (personal o de trabajo). Estos se mostrarán con métodos que se ejecutan en varios perfiles y se pueden usar para ejecutar más código en esos perfiles. Estos se pueden serializar en un int para un almacenamiento conveniente.

En esta guía, se describen las estructuras recomendadas para crear funciones de varios perfiles eficientes y fáciles de mantener en tu app para Android.

Convierte tu CrossProfileConnector en un singleton

Solo se debe usar una instancia durante el ciclo de vida de tu aplicación, de lo contrario, crearás conexiones paralelas. Esto se puede hacer con un framework de inserción de dependencias, como Dagger, o con un patrón singleton clásico, ya sea en una clase nueva o en una existente.

Inyecta o pasa la instancia de perfil generada a tu clase para cuando realices la llamada, en lugar de crearla en el método.

Esto te permite pasar la instancia de FakeProfile generada automáticamente en tus pruebas de unidades más adelante.

Considera el patrón de mediador

Este patrón común consiste en hacer que una de tus APIs existentes (p.ej., getEvents()) tenga en cuenta el perfil para todos sus emisores. En este caso, tu API existente puede convertirse en un método o una clase de "mediador" que contenga la nueva llamada al código generado en varios perfiles.

De esta manera, no obligas a que todos los emisores sepan cómo realizar una llamada entre perfiles, ya que solo se convierte en parte de tu API.

Considera si deseas anotar un método de interfaz como @CrossProfile en su lugar para evitar tener que exponer tus clases de implementación en un proveedor.

Esto funciona bien con frameworks de inserción de dependencias.

Si recibes datos de una llamada entre perfiles, considera agregar un campo que haga referencia al perfil del que provienen.

Esta puede ser una práctica recomendada, ya que es posible que quieras saber esto en la capa de IU (p.ej., agregar un ícono de insignia para trabajar en el contenido). También puede ser necesario si algún identificador de datos ya no es único sin él, como los nombres de paquetes.

Perfil sincronizado

En esta sección, se describe cómo crear tus propias interacciones entre perfiles.

Perfiles principales

La mayoría de las llamadas de los ejemplos de este documento contienen instrucciones explícitas sobre en qué perfiles ejecutarse, incluidos los de trabajo, los personales y ambos.

En la práctica, para las apps con una experiencia combinada en un solo perfil, es probable que desees que esta decisión dependa del perfil en el que se ejecuta, por lo que hay métodos convenientes similares que también tienen esto en cuenta para evitar que tu base de código esté llena de condicionales de perfil si-entonces.

Cuando crees la instancia del conector, puedes especificar cuál es el tipo de perfil "principal" (p.ej., "TRABAJO"). Esto habilita opciones adicionales, como las siguientes:

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 perfiles sincronizados

Las clases y las interfaces que contienen un método con la anotación @CrossProfile se conocen como tipos de perfil múltiple.

La implementación de los tipos de perfiles cruzados debe ser independiente del perfil en el que se ejecutan. Pueden realizar llamadas a otros métodos y, en general, deben funcionar como si se ejecutaran en un solo perfil. Solo tendrá acceso al estado en su propio perfil.

Ejemplo de tipo de perfil sincronizado:

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

Anotaciones de la clase

Para proporcionar la API más sólida, debes especificar el conector para cada tipo de perfil sincronizado, de la siguiente manera:

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

Esto es opcional, pero significa que la API generada será más específica en los tipos y más estricta en la verificación del tiempo de compilación.

Interfaces

Cuando anotas métodos en una interfaz como @CrossProfile, estás indicando que puede haber alguna implementación de este método a la que se pueda acceder en todos los perfiles.

Puedes mostrar cualquier implementación de una interfaz de perfil sincronizado en un proveedor de perfil sincronizado. De esta manera, indicas que se debe poder acceder a esta implementación en todos los perfiles. No es necesario que anotas las clases de implementación.

Proveedores de perfiles sincronizados

Cada tipo de perfil sincronizado debe proporcionarse mediante un método con la anotación @CrossProfileProvider. Se llamará a estos métodos cada vez que se realice una llamada entre perfiles, por lo que se recomienda que mantengas objetos singleton para cada tipo.

Constructor

Un proveedor debe tener un constructor público que no acepte argumentos o un argumento Context único.

Métodos del proveedor

Los métodos del proveedor no deben tener argumentos o tener un solo argumento Context.

Inserción de dependencias

Si usas un framework de inyección de dependencias, como Dagger, para administrar las dependencias, te recomendamos que ese framework cree tus tipos de perfil múltiple como de costumbre y, luego, inyecte esos tipos en tu clase de proveedor. Los métodos @CrossProfileProvider pueden mostrar esas instancias insertadas.

Conector de perfiles

Cada configuración de perfil sincronizado debe tener un solo conector de perfil, que es responsable de administrar la conexión con el otro perfil.

Conector de perfil predeterminado

Si solo hay una configuración de perfil sincronizado en una base de código, puedes evitar crear tu propio conector de perfiles y usar com.google.android.enterprise.connectedapps.CrossProfileConnector. Este es el valor predeterminado que se usa si no se especifica ninguno.

Cuando construyas el conector de perfiles cruzados, puedes especificar algunas opciones en el compilador:

  • Servicio de ejecutor programado

    Si deseas tener control sobre los subprocesos que crea el SDK, usa #setScheduledExecutorService().

  • Binder

    Si tienes necesidades específicas relacionadas con la vinculación de perfiles, usa #setBinder. Es probable que solo los controladores de políticas de dispositivo la usen.

Conector de perfil personalizado

Necesitarás un conector de perfil personalizado para poder establecer algunos parámetros de configuración (con CustomProfileConnector) y necesitarás uno si necesitas varios conectores en una sola base de código (por ejemplo, si tienes varios procesos, recomendamos un conector por proceso).

Cuando crees un ProfileConnector, debería verse de la siguiente manera:

@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 cambiar el nombre del servicio generado (al que se debe hacer referencia en tu AndroidManifest.xml), usa serviceClassName=.

  • primaryProfile

    Para especificar el perfil principal, usa primaryProfile.

  • availabilityRestrictions

    Para cambiar las restricciones que el SDK impone a las conexiones y la disponibilidad del perfil, usa availabilityRestrictions.

Controladores de políticas de dispositivo

Si tu app es un controlador de política de dispositivo, debes especificar una instancia de DpcProfileBinder que haga referencia a tu DeviceAdminReceiver.

Si implementas tu propio conector de perfil, haz lo siguiente:

@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();
  }
}

o bien, usa el CrossProfileConnector predeterminado:

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

Configuración de perfiles sincronizados

La anotación @CrossProfileConfiguration se usa para vincular todos los tipos de perfiles sincronizados con un conector para enviar llamadas de método correctamente. Para ello, anotamos una clase con @CrossProfileConfiguration que apunta a todos los proveedores, de la siguiente manera:

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

Esto validará que, para todos los tipos de perfiles cruzados, tengan el mismo conector de perfil o que no se especifique ningún conector.

  • serviceSuperclass

    De forma predeterminada, el servicio generado usará android.app.Service como la superclase. Si necesitas que una clase diferente (que, a su vez, debe ser una subclase de android.app.Service) sea la superclase, especifica serviceSuperclass=.

  • serviceClass

    Si se especifica, no se generará ningún servicio. Debe coincidir con el serviceClassName del conector de perfil que usas. Tu servicio personalizado debe enviar llamadas con la clase _Dispatcher generada de la siguiente manera:

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;
  }
}

Puedes usar esta función si necesitas realizar acciones adicionales antes o después de una llamada entre perfiles.

  • Conector

    Si usas un conector que no sea el CrossProfileConnector predeterminado, debes especificarlo con connector=.

Visibilidad

Cada parte de tu aplicación que interactúe entre perfiles debe poder ver tu conector de perfiles.

Tu clase con anotaciones @CrossProfileConfiguration debe poder ver todos los proveedores que se usan en tu aplicación.

Llamadas síncronas

El SDK de Connected Apps admite llamadas síncronas (de bloqueo) para los casos en los que no se pueden evitar. Sin embargo, el uso de estas llamadas tiene varias desventajas (como la posibilidad de que se bloqueen durante mucho tiempo), por lo que se recomienda evitar las llamadas síncronas siempre que sea posible. Para usar llamadas asíncronas, consulta Llamadas asíncronas .

Titulares de conexión

Si usas llamadas síncronas, debes asegurarte de que haya un titular de conexión registrado antes de realizar llamadas entre perfiles. De lo contrario, se arrojará una excepción. Para obtener más información, consulta Titulares de conexión.

Para agregar un contenedor de conexión, llama a ProfileConnector#addConnectionHolder(Object) con cualquier objeto (posiblemente, la instancia del objeto que realiza la llamada entre perfiles). Esto registrará que este objeto está usando la conexión y que intentará establecer una. Se debe llamar a esta función antes de realizar cualquier llamada síncrona. Esta es una llamada no bloqueante, por lo que es posible que la conexión no esté lista (o que no sea posible) cuando realices la llamada. En ese caso, se aplica el comportamiento habitual de control de errores.

Si no tienes los permisos adecuados entre perfiles cuando llames a ProfileConnector#addConnectionHolder(Object) o no hay ningún perfil disponible para conectarse, no se arrojará ningún error, pero nunca se llamará a la devolución de llamada conectada. Si el permiso se otorga más adelante o si el otro perfil está disponible, se establecerá la conexión y se llamará a la devolución de llamada.

Como alternativa, ProfileConnector#connect(Object) es un método de bloqueo que agregará el objeto como un contenedor de conexión y establecerá una conexión o arrojará un UnavailableProfileException. No se puede llamar a este método desde el subproceso de IU.

Las llamadas a ProfileConnector#connect(Object) y a ProfileConnector#connect similares muestran objetos que se cierran automáticamente y que quitarán automáticamente el contenedor de conexión una vez cerrado. Esto permite usos como los siguientes:

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

Una vez que termines de realizar llamadas síncronas, debes llamar a ProfileConnector#removeConnectionHolder(Object). Una vez que se quiten todos los sujetadores de conexión, se cerrará la conexión.

Conectividad

Se puede usar un objeto de escucha de conexión para recibir información cuando cambia el estado de la conexión, y se puede usar connector.utils().isConnected para determinar si hay una conexión. Por ejemplo:

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

Llamadas asíncronas

Cada método expuesto en la división del perfil debe designarse como bloqueado (síncrono) o no bloqueado (asíncrono). Cualquier método que devuelva un tipo de datos asíncrono (p.ej., un ListenableFuture) o acepte un parámetro de devolución de llamada se marca como no bloqueador. Todos los demás métodos se marcan como bloqueos.

Se recomiendan las llamadas asíncronas. Si debes usar llamadas síncronas, consulta Llamadas síncronas.

Devoluciones de llamada

El tipo más básico de llamada no bloqueante es un método void que acepta como uno de sus parámetros una interfaz que contiene un método al que se llamará con el resultado. Para que estas interfaces funcionen con el SDK, la interfaz debe estar annotate @CrossProfileCallback. Por ejemplo:

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

Luego, esta interfaz se puede usar como parámetro en un método annotated @CrossProfile y se puede llamar como de costumbre. Por ejemplo:

@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
});

Si esta interfaz contiene un solo método, que toma cero o uno parámetros, también se puede usar en llamadas a varios perfiles a la vez.

Se puede pasar cualquier cantidad de valores mediante una devolución de llamada, pero la conexión solo se mantendrá abierta para el primer valor. Consulta los Conectores para obtener información sobre cómo mantener la conexión abierta para recibir más valores.

Métodos síncronos con devoluciones de llamada

Una característica inusual de usar devoluciones de llamada con el SDK es que, técnicamente, puedes escribir un método síncrono que use una devolución de llamada:

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

En este caso, el método es síncrono, a pesar de la devolución de llamada. Este código se ejecutaría correctamente:

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

Sin embargo, cuando se lo llame con el SDK, no se comportará de la misma manera. No hay garantía de que se haya llamado al método de instalación antes de que se imprima “This prints third”. Cualquier uso de un método que el SDK marque como asíncrono no debe hacer suposiciones sobre cuándo se llamará al método.

Devoluciones de llamada simples

Las "devoluciones de llamada simples" son una forma más restrictiva de devolución de llamada que permite funciones adicionales cuando se realizan llamadas entre perfiles. Las interfaces simples deben contener un solo método, que puede tomar cero o uno de los parámetros.

Puedes aplicar la restricción de que una interfaz de devolución de llamada debe permanecer especificando simple=true en la anotación @CrossProfileCallback.

Las devoluciones de llamada simples se pueden usar con varios métodos, como .both(), .suppliers() y otros.

Titulares de conexión

Cuando se realiza una llamada asíncrona (con devoluciones de llamada o futuros), se agrega un contenedor de conexión cuando se realiza la llamada y se quita cuando se pasa una excepción o un valor.

Si esperas que se pase más de un resultado con una devolución de llamada, debes agregarla manualmente como un contenedor de conexión:

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

  profileMyClass.other().registerListener(b);

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

Esto también se puede usar con un bloque try-with-resources:

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

  // Other things running while we expect results
}

Si hacemos una llamada con una devolución de llamada o un futuro, la conexión permanecerá abierta hasta que se pase un resultado. Si determinamos que no se pasará un resultado, debemos quitar la devolución de llamada o el objeto Future como contenedor de conexión:

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

Para obtener más información, consulta Titulares de conexión.

Futures

El SDK también admite de forma nativa los futuros. El único tipo de futuro compatible de forma nativa es ListenableFuture, aunque se pueden usar tipos de futuro personalizados. Para usar futuros, solo debes declarar un tipo de Future compatible como el tipo de datos que se muestra de un método de perfil cruzado y, luego, usarlo como de costumbre.

Tiene la misma "función inusual" que las devoluciones de llamada, en las que un método síncrono que muestra un objeto Future (p.ej., con immediateFuture) se comportará de manera diferente cuando se ejecute en el perfil actual en comparación con otro perfil. Cualquier uso de un método que el SDK marque como asíncrono no debe suponer cuándo se llamará al método.

Subprocesos

No bloquees el resultado de un futuro o una devolución de llamada multiperfil en el subproceso principal. Si lo haces, en algunas situaciones, tu código se bloqueará de forma indefinida. Esto se debe a que la conexión con el otro perfil también se establece en el subproceso principal, lo que nunca ocurrirá si está bloqueado a la espera de un resultado de varios perfiles.

Disponibilidad

El objeto de escucha de disponibilidad se puede usar para recibir información cuando cambia el estado de disponibilidad, y connector.utils().isAvailable se puede usar para determinar si hay otro perfil disponible para su uso. Por ejemplo:

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

Titulares de conexión

Los titulares de conexión son objetos arbitrarios que se registran como interesados en que se establezca y mantenga la conexión entre perfiles.

De forma predeterminada, cuando se realizan llamadas asíncronas, se agrega un contenedor de conexión cuando comienza la llamada y se quita cuando se produce un resultado o un error.

Los titulares de conexión también se pueden agregar y quitar de forma manual para ejercer un mayor control sobre la conexión. Los titulares de conexión se pueden agregar con connector.addConnectionHolder y quitar con connector.removeConnectionHolder.

Cuando se agregue al menos un contenedor de conexión, el SDK intentará mantener una conexión. Cuando no se agregan tenedores de conexión, se puede cerrar la conexión.

Debes mantener una referencia a cualquier contenedor de conexión que agregues y quitarlo cuando ya no sea relevante.

Llamadas síncronas

Antes de realizar llamadas síncronas, se debe agregar un contenedor de conexión. Esto se puede hacer con cualquier objeto, aunque debes hacer un seguimiento de ese objeto para que se pueda quitar cuando ya no necesites realizar llamadas síncronas.

Llamadas asíncronas

Cuando realices llamadas asíncronas, los titulares de conexión se administrarán automáticamente para que la conexión esté abierta entre la llamada y la primera respuesta o error. Si necesitas que la conexión sobreviva más allá de esto (p.ej., para recibir varias respuestas con una sola devolución de llamada), debes agregar la devolución de llamada como un contenedor de conexión y quitarla una vez que ya no necesites recibir más datos.

Manejo de errores

De forma predeterminada, cualquier llamada que se realice al otro perfil cuando este no esté disponible hará que se arroje un UnavailableProfileException (o se pase al Future, o la devolución de llamada de error para una llamada asíncrona).

Para evitar esto, los desarrolladores pueden usar #both() o #suppliers() y escribir su código para controlar cualquier cantidad de entradas en la lista resultante (será 1 si el otro perfil no está disponible o 2 si está disponible).

Excepciones

Todas las excepciones sin verificar que se produzcan después de una llamada al perfil actual se propagarán como de costumbre. Esto se aplica independientemente del método que se use para realizar la llamada (#current(), #personal, #both, etcétera).

Las excepciones sin verificar que ocurren después de una llamada al otro perfil provocarán que se muestre una ProfileRuntimeException con la excepción original como causa. Esto se aplica independientemente del método que se use para realizar la llamada (#other(), #personal, #both, etc.).

ifAvailable

Como alternativa para capturar y controlar instancias de UnavailableProfileException, puedes usar el método .ifAvailable() para proporcionar un valor predeterminado que se mostrará en lugar de arrojar una UnavailableProfileException.

Por ejemplo:

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

Prueba

Para que tu código sea testable, debes insertar instancias de tu conector de perfil en cualquier código que lo use (para verificar la disponibilidad del perfil, para conectarte de forma manual, etcétera). También debes insertar instancias de tus tipos conscientes del perfil donde se usan.

Proporcionamos simulaciones de tu conector y tipos que se pueden usar en las pruebas.

Primero, agrega las dependencias de prueba:

  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'

Luego, anota tu clase de prueba con @CrossProfileTest y, así, identifica la clase @CrossProfileConfiguration que se probará:

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

}

Esto provocará la generación de falsificaciones para todos los tipos y conectores que se usen en la configuración.

Crea instancias de esas falsificaciones en tu prueba:

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();

Configura el estado del perfil:

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

Pasa el conector falso y la clase de perfil cruzado a tu código en prueba y, luego, realiza llamadas.

Las llamadas se enrutarán al destino correcto y se arrojarán excepciones cuando se realicen llamadas a perfiles desconectados o no disponibles.

Tipos admitidos

Los siguientes tipos son compatibles sin esfuerzo adicional de tu parte. Se pueden usar como argumentos o tipos de devolución para todas las llamadas entre perfiles.

  • Primitivos (byte, short, int, long, float, double, char, boolean)
  • Primitivos en cuadros (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,
  • Cualquier elemento que implemente android.os.Parcelable
  • Cualquier elemento que implemente java.io.Serializable
  • Arrays no primitivos de una sola dimensión
  • java.util.Optional,
  • java.util.Collection
  • java.util.List
  • java.util.Map
  • java.util.Set
  • android.util.Pair
  • com.google.common.collect.ImmutableMap.

Cualquier tipo genérico admitido (por ejemplo, java.util.Collection) puede tener cualquier tipo admitido como su parámetro de tipo. Por ejemplo:

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

Futures

Los siguientes tipos solo se admiten como tipos de datos que se muestran:

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

Wrappers de Parcelable personalizados

Si tu tipo no está en la lista anterior, primero considera si se puede implementar correctamente android.os.Parcelable o java.io.Serializable. Si así es, no podrá ver los wrappers parcelables para agregar compatibilidad con tu tipo.

Wrappers de Future personalizados

Si deseas usar un tipo futuro que no está en la lista anterior, consulta wrappers futuros para agregar compatibilidad.

Wrappers de Parcelable

Los wrappers parcelables son la forma en que el SDK agrega compatibilidad con tipos no parcelables que no se pueden modificar. El SDK incluye wrappers para muchos tipos, pero si no se incluye el tipo que necesitas usar, debes escribir el tuyo.

Un wrapper de Parcelable es una clase diseñada para unir otra clase y hacerla parcelable. Sigue un contrato estático definido y se registra en el SDK para que se pueda usar para convertir un tipo determinado en un tipo parcelable y también extraer ese tipo del tipo parcelable.

Anotación

La clase del wrapper parcelable debe estar annotate @CustomParcelableWrapper, especificando la clase unida como originalType. Por ejemplo:

@CustomParcelableWrapper(originalType=ImmutableList.class)

Formato

Los wrappers parcelables deben implementar Parcelable correctamente y deben tener un método W of(Bundler, BundlerType, T) estático que una el tipo unido y un método T get() no estático que devuelva el tipo unido.

El SDK usará estos métodos para proporcionar una compatibilidad perfecta con el tipo.

Bundler

Para permitir el enlace de tipos genéricos (como listas y mapas), al método of se le pasa un Bundler que puede leer (con #readFromParcel) y escribir (con #writeToParcel) todos los tipos admitidos en un Parcel y un BundlerType que representa el tipo declarado que se escribirá.

Las instancias de Bundler y BundlerType también son parcelables y deben escribirse como parte del parcelamiento del wrapper parcelable para que se pueda usar cuando se reconstruya el wrapper parcelable.

Si BundlerType representa un tipo genérico, las variables de tipo se pueden encontrar llamando a .typeArguments(). Cada argumento de tipo es un BundlerType.

Por ejemplo, consulta 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];
      }
    };
}

Regístrate con el SDK

Una vez creado, para usar tu wrapper parcelable personalizado, deberás registrarlo con el SDK.

Para ello, especifica parcelableWrappers={YourParcelableWrapper.class} en una anotación CustomProfileConnector o CrossProfile en una clase.

Wrappers futuros

Los wrappers futuros son la forma en que el SDK agrega compatibilidad con futuros en todos los perfiles. El SDK incluye compatibilidad con ListenableFuture de forma predeterminada, pero para otros tipos de Future puedes agregar compatibilidad por tu cuenta.

Un wrapper de Future es una clase diseñada para unir un tipo de Future específico y ponerlo a disposición del SDK. Sigue un contrato estático definido y se debe registrar con el SDK.

Anotación

La clase del wrapper futuro debe estar annotate @CustomFutureWrapper, especificando la clase unida como originalType. Por ejemplo:

@CustomFutureWrapper(originalType=SettableFuture.class)

Formato

Los wrappers futuros deben extender com.google.android.enterprise.connectedapps.FutureWrapper.

Los wrappers futuros deben tener un método W create(Bundler, BundlerType) estático que cree una instancia del wrapper. Al mismo tiempo, esto debería crear una instancia del tipo futuro unido. Esto debe mostrarse mediante un método getFuture() T no estático. Los métodos onResult(E) y onException(Throwable) deben implementarse para pasar el resultado o el objeto throwable al futuro unido.

Los wrappers futuros también deben tener un método BundlerType, T, FutureResultWriter<E>) void writeFutureResult(Bundler, estático. Esto debería registrarse con el valor pasado en el futuro para obtener resultados y, cuando se proporcione un resultado, llamar a resultWriter.onSuccess(value). Si se proporciona una excepción, se debe llamar a resultWriter.onFailure(exception).

Por último, los wrappers futuros también deben tener un método T<Map<Profile, E>> groupResults(Map<Profile, T<E>> results) estático que convierta un mapa de perfil a futuro en un futuro de un mapa de perfil a resultado. Se puede usar CrossProfileCallbackMultiMerger para facilitar esta lógica.

Por ejemplo:

/** 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;
  }
}

Regístrate con el SDK

Una vez creado, para usar tu wrapper futuro personalizado, deberás registrarlo con el SDK.

Para ello, especifica futureWrappers={YourFutureWrapper.class} en una anotación CustomProfileConnector o CrossProfile en una clase.

Modo de inicio directo

Si tu app admite el modo de inicio directo, es posible que debas realizar llamadas entre perfiles antes de que se desbloquee el perfil. De forma predeterminada, el SDK solo permite conexiones cuando el otro perfil está desbloqueado.

Para cambiar este comportamiento, si usas un conector de perfil personalizado, debes especificar 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();
  }
}

Si usas CrossProfileConnector, usa .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT _AWARE en el compilador.

Con este cambio, se te informará sobre la disponibilidad y podrás realizar llamadas entre perfiles cuando el otro perfil no esté desbloqueado. Es tu responsabilidad asegurarte de que tus llamadas solo accedan al almacenamiento encriptado del dispositivo.