Sujets avancés

Ces sections sont destinées à servir de référence. Il n'est pas nécessaire de les lire de haut en bas.

Utiliser les API du framework :

Ces API seront encapsulées dans le SDK pour une surface d'API plus cohérente (par exemple, en évitant les objets UserHandle), mais pour l'instant, vous pouvez les appeler directement.

L'implémentation est simple : si vous pouvez interagir, faites-le. Si ce n'est pas le cas, mais que vous pouvez le demander, affichez votre invite/bannière/info-bulle/etc. Si l'utilisateur accepte d'accéder aux paramètres, créez l'intent de requête et utilisez Context#startActivity pour y envoyer l'utilisateur. Vous pouvez utiliser la diffusion pour détecter quand cette fonctionnalité change, ou simplement vérifier à nouveau lorsque l'utilisateur revient.

Pour tester cela, vous devez ouvrir TestDPC dans votre profil professionnel, accéder à en bas de l'écran, puis sélectionnez l'option permettant d'ajouter le nom de votre package à la liste d'autorisation des applications connectées. Cela imite l'ajout de votre application à la liste d'autorisation de l'administrateur.

Glossaire

Cette section définit les termes clés associés au développement dans tous les profils.

Configuration multiprofil

Une configuration multicompte regroupe un fournisseur multiprofil associé. Classe et fournit une configuration générale pour les fonctionnalités interprofils. En règle générale, il y a une annotation @CrossProfileConfiguration par codebase. , mais dans certaines applications complexes, il peut y en avoir plusieurs.

Connecteur de profil

Un connecteur gère les connexions entre les profils. En règle générale, chaque type de profil croisé pointe vers un connecteur spécifique. Chaque type de profil croisé doit utiliser le même connecteur.

Classe de fournisseur multiprofil

Une classe de fournisseur multiprofil regroupe les types de profils multiprofils connexes.

Mediator

Un médiateur se trouve entre le code de haut niveau et le code de bas niveau, distribuant les appels aux profils appropriés et fusionnant les résultats. Il s'agit du seul code qui doit être compatible avec le profil. Il s'agit d'un concept architectural plutôt que d'un élément intégré le SDK.

Type de profil croisé

Un type multiprofil est une classe ou une interface contenant des méthodes annotées @CrossProfile Le code de ce type n'a pas besoin d'être compatible avec le profil et, dans l'idéal, ne doit agir que sur ses données locales.

Types de profil

Type de profil
ActuelProfil actif que nous exécutons.
Autre(Le cas échéant) Profil sur lequel nous n'exécutons pas la requête.
PersonalUtilisateur 0, profil de l'appareil qui ne peut pas être désactivé.
ProfessionnelGénéralement, l'utilisateur 10, mais il peut être supérieur. Peut être activé ou désactivé. Utilisé pour contenir des applications et des données professionnelles.
PrincipalÉventuellement défini par l'application. Le profil auquel affiche une vue fusionnée des deux profils.
SecondaireSi le profil principal est défini, le profil secondaire n'est pas principale.
FournisseurLes fournisseurs du profil principal sont les deux profils, les fournisseurs du profil secondaire n'est que le profil secondaire lui-même.

Identifiant de profil

Classe représentant un type de profil (personnel ou professionnel). Il s'agira renvoyées par des méthodes s'exécutant sur plusieurs profils et pouvant être utilisées pour exécuter sur ces profils. Ils peuvent être sérialisés en int pour plus de commodité stockage.

Ce guide présente les structures recommandées pour créer des fonctionnalités interprofils faciles à gérer dans votre application Android.

Convertir votre CrossProfileConnector en singleton

Une seule instance doit être utilisée tout au long du cycle de vie de votre application, sinon vous créerez des connexions parallèles. Vous pouvez le faire à l'aide d'un framework d'injection de dépendances tel que Dagger, ou à l'aide d'un patron Singleton classique, dans une nouvelle classe ou dans une classe existante.

Injectez ou transmettez l'instance Profile générée à votre classe pour effectuer l'appel, au lieu de la créer dans la méthode

Vous pouvez ainsi transmettre l'instance FakeProfile générée automatiquement dans vos tests unitaires plus tard.

Tenir compte du modèle de médiateur

Ce modèle courant consiste à créer l'une de vos API existantes (par exemple, getEvents()). en tenant compte du profil de tous ses appelants. Dans ce cas, votre API existante peut simplement devenir une méthode ou une classe de "médiation" contenant le nouvel appel au code multiprofil généré.

De cette façon, vous ne forcez pas chaque appelant à savoir qu’il doit effectuer un appel interprofil est intégré à votre API.

Envisagez d'annoter une méthode d'interface en tant que @CrossProfile pour éviter d'avoir à exposer vos classes d'implémentation dans un fournisseur.

Cela fonctionne bien avec les frameworks d'injection de dépendances.

Si vous recevez des données à partir d'un appel interprofil, envisagez d'ajouter un champ faisant référence au profil d'où elles proviennent.

Il peut s'agir d'une bonne pratique, car vous voudrez peut-être le savoir au niveau de la couche d'interface utilisateur (par exemple, ajouter une icône de badge aux éléments de travail). Il peut également être nécessaire si des identifiants de données ne sont plus uniques sans lui, par exemple les noms de packages.

Profils croisés

Cette section explique comment créer vos propres interactions entre les profils.

Profils principaux

La plupart des appels des exemples de ce document contiennent des instructions explicites les profils à utiliser, y compris le travail, personnel et les deux.

En pratique, pour les applications dont l'expérience est fusionnée sur un seul profil, vous souhaitez probablement que cette décision dépende du profil sur lequel vous exécutez l'application. Il existe donc des méthodes pratiques similaires qui tiennent également compte de ce point, afin d'éviter que votre base de code ne soit encombrée de conditions de profil if-else.

Lorsque vous créez votre instance de connecteur, vous pouvez spécifier le type de profil qui est votre "principal" (par exemple, "TRAVAIL"). Cela active d'autres options, telles que suivantes:

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

Types de profils croisés

Les classes et les interfaces contenant une méthode annotée @CrossProfile sont appelées types de profils croisés.

L'implémentation des types de profils croisés doit être indépendante du profil sur lequel ils s'exécutent. Ils sont autorisés à appeler d'autres méthodes et, en général, doivent fonctionner comme s'ils étaient exécutés sur un seul profil. Il n'ont accès à l'état que dans leur propre profil.

Exemple de type de profil croisé:

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

Annotation de classe

Pour fournir l'API la plus performante, vous devez spécifier le connecteur pour chaque croisement. type de profil, comme suit:

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

Cette option est facultative, mais signifie que l'API générée sera plus spécifique sur les types et plus stricte sur la vérification au moment de la compilation.

Interfaces

En annotant les méthodes sur une interface en tant que @CrossProfile, vous indiquez que il peut y avoir une implémentation de cette méthode qui doit être accessible entre les profils.

Vous pouvez renvoyer n'importe quelle implémentation d'une interface interprofils dans un Profile Provider, ce qui implique que cette implémentation doit être accessible dans tous les profils. Vous n'avez pas besoin de annoter les classes d'implémentation.

Fournisseurs multiprofils

Chaque type de profil croisé doit être fourni par une méthode @CrossProfileProvider annoté. Ces méthodes seront appelées chaque fois qu'un appel interprofil sera effectué. Il est donc recommandé de gérer des singletons pour chaque type.

Constructeur

Un fournisseur doit disposer d'un constructeur public qui n'accepte aucun argument ou un seul argument Context.

Méthodes du fournisseur

Les méthodes du fournisseur ne doivent pas accepter d'arguments ou un seul argument Context.

Injection de dépendances

Si vous utilisez un framework d'injection de dépendances tel que Dagger pour gérer les dépendances, nous vous recommandons de demander à ce framework de créer vos types multiprofils comme vous le feriez habituellement, puis d'injecter ces types dans votre classe de fournisseur. Les méthodes @CrossProfileProvider peuvent ensuite renvoyer ces instances injectées.

Connecteur de profil

Chaque configuration de profil croisé doit comporter un seul connecteur de profil, qui est chargé de gérer la connexion à l'autre profil.

Connecteur de profil par défaut

S'il n'y a qu'une seule configuration multiprofil dans un codebase, vous pouvez éviter de créer votre propre Profile Connector et utiliser com.google.android.enterprise.connectedapps.CrossProfileConnector. Il s'agit de la valeur par défaut utilisée si aucune n'est spécifiée.

Lorsque vous créez le connecteur entre les profils, vous pouvez spécifier certaines options dans l'outil de création :

  • Scheduled Executor Service

    Si vous souhaitez contrôler les threads créés par le SDK, utilisez #setScheduledExecutorService(),

  • Binder

    Si vous avez des besoins spécifiques concernant la liaison de profil, utilisez #setBinder. Ce n'est probablement utilisé que par les contrôleurs des règles relatives aux appareils.

Connecteur de profil personnalisé

Vous aurez besoin d'un connecteur de profil personnalisé pour pouvoir définir une configuration (à l'aide de CustomProfileConnector) et si vous avez besoin de plusieurs connecteurs dans un même codebase (par exemple, si vous avez plusieurs processus, nous vous recommandons d'utiliser un connecteur par processus).

La création d'un ProfileConnector doit se présenter comme suit:

@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

    Pour modifier le nom du service généré (qui doit être référencé dans votre AndroidManifest.xml), utilisez serviceClassName=.

  • primaryProfile

    Pour spécifier le profil principal, utilisez primaryProfile.

  • availabilityRestrictions

    Pour modifier les restrictions que le SDK applique aux connexions et à la disponibilité des profils, utilisez availabilityRestrictions.

Outils de contrôle des règles relatives aux appareils

Si votre application est un outil de contrôle des règles relatives aux appareils, vous devez spécifier une instance de DpcProfileBinder faisant référence à votre DeviceAdminReceiver.

Si vous mettez en œuvre votre propre connecteur de profil:

@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 en utilisant la valeur par défaut CrossProfileConnector :

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

Configuration interprofil

L'annotation @CrossProfileConfiguration permet de relier l'ensemble types de profils à l'aide d'un connecteur pour répartir correctement les appels de méthode. À pour ce faire, nous annotons une classe avec @CrossProfileConfiguration qui pointe vers chaque fournisseur, comme ceci:

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

Cela permet de vérifier que pour tous les types de profils croisés, ils ont le même connecteur de profil ou aucun connecteur spécifié.

  • serviceSuperclass

    Par défaut, le service généré utilisera android.app.Service comme super-classe. Si vous avez besoin d'une autre classe (qui doit elle-même être une sous-classe de android.app.Service) pour être la super-classe, spécifiez serviceSuperclass=.

  • serviceClass

    Si cette valeur est spécifiée, aucun service n'est généré. Il doit correspondre à l'serviceClassName du connecteur de profil que vous utilisez. Votre doit acheminer les appels à l'aide de la classe _Dispatcher générée en tant que tels que:

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

Vous pouvez l'utiliser si vous devez effectuer d'autres actions avant ou après appel interprofils.

  • Connecteur

    Si vous utilisez un connecteur autre que CrossProfileConnector par défaut, vous devez le spécifier à l'aide de connector=.

Visibilité

Chaque partie de votre application qui interagit dans tous les profils doit pouvoir voir votre connecteur de profil.

Votre classe annotée @CrossProfileConfiguration doit pouvoir afficher toutes les fournisseur utilisé dans votre application.

Appels synchrones

Le SDK des applications connectées prend en charge les appels synchrones (bloquants) dans les cas où elles sont inévitables. Cependant, l'utilisation de de ces appels (comme le blocage potentiel d'appels pendant une longue période). recommandé d'éviter les appels synchrones lorsque cela est possible. Pour utiliser des appels asynchrones, consultez la section Appels asynchrones.

Porte-connexions

Si vous utilisez des appels synchrones, vous devez vous assurer qu'un détenteur de connexion est enregistré avant d'effectuer des appels interprofils, sinon une exception sera générée. Pour en savoir plus, consultez la section "Supports de connexion".

Pour ajouter un détenteur de connexion, appelez ProfileConnector#addConnectionHolder(Object) avec n'importe quel objet (potentiellement l'instance d'objet qui effectue l'appel interprofil). Cela permet d'enregistrer que cet objet utilise connexion et tentera d'établir une connexion. Cette méthode doit être appelée avant tout appel synchrone. Il s'agit d'un appel non bloquant. Il est donc possible que la connexion ne soit pas prête (ou qu'elle ne soit pas possible) au moment de l'appel. Dans ce cas, le comportement habituel de gestion des erreurs s'applique.

Si vous ne disposez pas des autorisations interprofils appropriées lorsque vous appelez ProfileConnector#addConnectionHolder(Object) ou aucun profil n'est disponible pour de connexion, aucune erreur ne sera générée, mais le rappel connecté ne sera jamais appelé. Si l'autorisation est accordée ultérieurement ou si l'autre profil devient disponible, la connexion sera établie et le rappel appelé.

Sinon, ProfileConnector#connect(Object) est une méthode de blocage qui ajoute l'objet en tant que conteneur de connexion et établit une connexion ou pour générer une UnavailableProfileException. Cette méthode ne peut pas être appelée depuis Thread UI.

Les appels à ProfileConnector#connect(Object) et à ProfileConnector#connect similaires renvoient des objets à fermeture automatique qui supprimeront automatiquement le détenteur de la connexion une fois fermés. Cela permet d'utiliser, par exemple, les éléments suivants:

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

Après avoir effectué des appels synchrones, vous devez appeler ProfileConnector#removeConnectionHolder(Object) Une fois tous les détenteurs de connexion supprimés, la connexion sera fermée.

Connectivité

Un écouteur de connexion peut être utilisé pour être informé lorsque l'état de la connexion les modifications, et connector.utils().isConnected peut être utilisé pour déterminer si un si une connexion est établie. Exemple :

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

Appels asynchrones

Chaque méthode exposée dans la division du profil doit être désignée comme bloquante. (synchrone) ou non bloquante (asynchrone). Toute méthode renvoyant une type de données asynchrone (par exemple, ListenableFuture) ou accepte un rappel est marqué comme non bloquant. Toutes les autres méthodes sont marquées comme bloquantes.

Nous vous recommandons d'utiliser des appels asynchrones. Si vous devez utiliser des appels synchrones, consultez Synchrone Appels.

Rappels

Le type d'appel non bloquant le plus élémentaire est une méthode void qui accepte comme l'un de ses paramètres une interface contenant une méthode à appeler avec le résultat. Pour que ces interfaces fonctionnent avec le SDK, elles doivent être annotées @CrossProfileCallback. Exemple :

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

Cette interface peut ensuite être utilisée comme paramètre dans une méthode annotée @CrossProfile et être appelée comme d'habitude. Exemple :

@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 cette interface contient une seule méthode, qui accepte zéro ou un , il peut également être utilisé dans les appels de plusieurs profils à la fois.

Vous pouvez transmettre un nombre illimité de valeurs à l'aide d'un rappel, mais la connexion ne sera maintenue ouverte que pour la première valeur. Pour en savoir plus sur les conteneurs de connexion, consultez la section "Supports de connexion". maintenir la connexion ouverte pour recevoir plus de valeurs.

Méthodes synchrones avec rappels

Une fonctionnalité inhabituelle de l'utilisation de rappels avec le SDK est que vous pouvez Écrivez techniquement une méthode synchrone qui utilise un rappel:

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

Dans ce cas, la méthode est en fait synchrone, malgré le rappel. Ce code s'exécute correctement :

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

Toutefois, lorsqu'il est appelé à l'aide du SDK, son comportement n'est pas le même. Il n'est pas garanti que la méthode d'installation ait été appelée avant l'impression de "This prints third" (Ceci imprime le troisième). Toute utilisation d'une méthode marquée comme asynchrone par le SDK ne doit faire aucune hypothèse sur le moment où la méthode sera appelée.

Rappels simples

"Rappels simples" sont une forme de rappel plus restrictive qui permet des fonctionnalités supplémentaires lors des appels interprofils. Les interfaces simples doivent contenir une seule méthode, qui peut prendre zéro ou un paramètre.

Vous pouvez exiger qu'une interface de rappel doit rester en spécifiant simple=true dans l'annotation @CrossProfileCallback.

Les rappels simples sont utilisables avec diverses méthodes telles que .both(), .suppliers(), etc.

Supports de connexion

Lorsque vous effectuez un appel asynchrone (à l'aide de rappels ou de futures), un détenteur de connexion est ajouté lors de l'appel et supprimé lorsqu'une exception ou une valeur est transmise.

Si vous prévoyez de transmettre plusieurs résultats à l'aide d'un rappel, vous devez ajouter manuellement le rappel en tant que détenteur de connexion :

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

  profileMyClass.other().registerListener(b);

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

Vous pouvez également l'utiliser avec un bloc try-with-resources :

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

  // Other things running while we expect results
}

Si nous effectuons un appel pour un rappel ou un futur, la connexion restera ouverte jusqu'à ce qu'un résultat soit transmis. Si nous déterminons qu'un résultat ne sera pas transmis, nous devons supprimer le rappel ou le futur en tant que détenteur de la connexion :

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

Pour en savoir plus, consultez la section "Détenteurs de connexions".

Contrats à terme

Les objets Future sont également pris en charge de manière native par le SDK. Le seul type de future compatible en mode natif est ListenableFuture, bien que des types de future personnalisés puissent être utilisés. Pour utiliser des objets Future, il vous suffit de déclarer un type Future compatible comme valeur d'une méthode multiprofil, puis l'utiliser normalement.

Elle présente la même « fonctionnalité inhabituelle » en tant que rappels, où une méthode synchrone qui renvoie un avenir (par exemple, en utilisant immediateFuture) se comporte différemment lorsqu'il est exécuté sur le profil actuel plutôt que sur un autre. Toute utilisation d'un marquée comme asynchrone par le SDK ne doit formuler aucune hypothèse concernant la date est appelée.

Threads

Ne pas bloquer le résultat d'un futur interprofil ou d'un rappel sur la page principale thread. Si vous procédez ainsi, votre code bloquera dans certains cas indéfiniment. En effet, la connexion à l'autre profil est également établi sur le thread principal, ce qui ne se produira jamais s'il est bloqué en attente un résultat dans tous les profils.

Disponibilité

L'écouteur de disponibilité peut être utilisé pour être informé lorsque l'état de disponibilité change, et connector.utils().isAvailable peut être utilisé pour déterminer si un autre profil est disponible. Exemple :

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

Porte-connexions

Les titulaires de connexion sont des objets arbitraires qui sont enregistrés comme ayant un intérêt pour la connexion interprofils établie et maintenue.

Par défaut, lorsque vous effectuez des appels asynchrones, un détenteur de connexion est ajouté au début de l'appel et supprimé en cas de résultat ou d'erreur.

Vous pouvez également ajouter et retirer manuellement des titulaires de connexion pour vous offrir plus de contrôle. via la connexion. Vous pouvez ajouter des conteneurs de connexion à l'aide de connector.addConnectionHolder, et supprimé à l'aide de connector.removeConnectionHolder

Lorsqu'au moins un conteneur de connexion est ajouté, le SDK tente maintenir une connexion. Si aucun titulaire de connexion n'est ajouté, la connexion peut être fermée.

Vous devez conserver une référence à tout titulaire de connexion que vous ajoutez et la supprimer. lorsqu'elles ne sont plus pertinentes.

Appels synchrones

Avant d'effectuer des appels synchrones, vous devez ajouter un détenteur de connexion. Vous pouvez le faire avec n'importe quel objet, mais vous devez le suivre afin qu'il puisse être supprimé lorsque vous n'avez plus besoin d'effectuer d'appels synchrones.

Appels asynchrones

Lorsque vous effectuez des appels asynchrones, les détenteurs de connexion sont gérés automatiquement afin que la connexion soit ouverte entre l'appel et la première réponse ou erreur. Si vous avez besoin que la connexion survive au-delà de ce point (par exemple, pour recevoir plusieurs réponses à l'aide d'un seul rappel), vous devez ajouter le rappel lui-même en tant que détenteur de la connexion et le supprimer une fois que vous n'avez plus besoin de recevoir d'autres données.

Gestion des erreurs

Par défaut, tout appel passé à l'autre profil lorsque l'autre profil n'est pas disponible entraînera la génération d'une erreur UnavailableProfileException (ou transmis à l'objet Future ou le rappel d'erreur pour un appel asynchrone).

Pour éviter cela, les développeurs peuvent utiliser #both() ou #suppliers() et écrire leur code pour gérer un nombre illimité d'entrées dans la liste générée (1 si l'autre profil est indisponible ou 2 s'il est disponible).

Exceptions

Toute exception décochée qui se produit après un appel au profil actuel se propager comme d'habitude. Cela s'applique quelle que soit la méthode utilisée (#current(), #personal, #both, etc.).

Les exceptions décochées qui se produisent après un appel à l'autre profil se produiront dans une exception ProfileRuntimeException générée avec l'exception d'origine en tant que la cause. Cela s'applique quelle que soit la méthode utilisée pour effectuer l'appel (#other(), #personal, #both, etc.).

ifAvailable

Au lieu d'intercepter et de gérer UnavailableProfileException vous pouvez utiliser la méthode .ifAvailable() pour fournir une valeur par défaut qui sera renvoyé au lieu de générer une UnavailableProfileException.

Exemple :

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

Tests

Pour que votre code puisse être testé, vous devez injecter des instances de votre profil à tout code qui l'utilise (pour vérifier la disponibilité du profil, se connecter manuellement, etc.). Vous devez également injecter des instances de vos types respectueux du profil là où ils sont utilisés.

Nous fournissons des copies de vos connecteurs et types qui peuvent être utilisés lors des tests.

Commencez par ajouter les dépendances de test :

  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'

Annotez ensuite votre classe de test avec @CrossProfileTest, en identifiant la Classe annotée @CrossProfileConfiguration à tester:

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

}

Cela entraînera la génération de faux pour tous les types et connecteurs utilisés dans le configuration.

Créez des instances de ces instances fictives dans votre test:

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

Configurez l'état du profil:

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

Transmettez le faux connecteur et la classe de profil croisé dans votre code en cours de test, puis effectuez des appels.

Les appels seront acheminés vers la bonne cible, et des exceptions seront générées lors des appels vers des profils déconnectés ou indisponibles.

Types de pollens

Les types suivants sont pris en charge sans effort supplémentaire de votre part. Ils peuvent être utilisés comme arguments ou types de retour pour tous les appels interprofils.

  • Primitifs (byte, short, int, long, float, double, char, boolean)
  • Primitifs en boîte (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,
  • Tout élément qui implémente android.os.Parcelable
  • Tout ce qui implémente java.io.Serializable,
  • Tableaux non primitifs à dimension unique,
  • java.util.Optional,
  • java.util.Collection
  • java.util.List
  • java.util.Map
  • java.util.Set
  • android.util.Pair
  • com.google.common.collect.ImmutableMap.

Tous les types génériques compatibles (par exemple, java.util.Collection) peuvent être associés à type pris en charge comme paramètre de type. Exemple :

java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>> correspond à un type valide.

Contrats à terme

Les types suivants ne sont acceptés qu'en tant que types de retour :

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

Enveloppes Parcelable personnalisées

Si votre type ne figure pas dans la liste précédente, déterminez d’abord s’il peut être fait pour implémentez correctement android.os.Parcelable ou java.io.Serializable. Si il ne peut pas voir les valeurs parcelables des wrappers ajouter la prise en charge de votre type.

Wrappers Future personnalisés

Si vous souhaitez utiliser un type "futur" qui ne figure pas dans la liste précédente, reportez-vous à la section Futures et des wrappers pour la rendre compatible.

Enveloppes Parcelable

Les wrappers parcellables permettent au SDK d'ajouter la prise en charge des types non parcellables qui ne peuvent pas être modifiés. Le SDK inclut des wrappers pour de nombreux types, mais si le que vous devez utiliser n'est pas inclus, vous devez écrire le vôtre.

Un wrapper Parcelable est une classe conçue pour encapsuler une autre classe et la rendre parcelable. Il suit un contrat statique défini et est enregistré auprès du SDK Il peut donc être utilisé pour convertir un type donné en un type parcelable, et aussi extraire ce type du type parcelable.

Annotation

La classe de wrapper parcelable doit être annotée @CustomParcelableWrapper, en spécifiant la classe encapsulée en tant que originalType. Exemple :

@CustomParcelableWrapper(originalType=ImmutableList.class)

Format

Les wrappers Parcelable doivent implémenter Parcelable correctement et doivent disposer d'une méthode W of(Bundler, BundlerType, T) statique qui encapsule le type encapsulé et d'une méthode T get() non statique qui renvoie le type encapsulé.

Le SDK utilisera ces méthodes pour fournir une prise en charge fluide du type.

Bundler

Pour permettre l'encapsulation de types génériques (tels que les listes et les cartes), la méthode of reçoit un Bundler capable de lire (à l'aide de #readFromParcel) et d'écrire (à l'aide de #writeToParcel) tous les types compatibles dans un Parcel, ainsi qu'un BundlerType représentant le type déclaré à écrire.

Les instances Bundler et BundlerType sont elles-mêmes parcellables et doivent être écrites dans le cadre du parcellage du wrapper parcellable afin qu'il puisse être utilisé lors de la reconstruction du wrapper parcellable.

Si BundlerType représente un type générique, les variables de type sont disponibles en appelant .typeArguments(). Chaque argument de type est lui-même une BundlerType.

Par exemple, consultez 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];
      }
    };
}

S'enregistrer auprès du SDK

Une fois votre enveloppe parcelable personnalisée créée, vous devez l'enregistrer pour pouvoir l'utiliser. avec le SDK.

Pour ce faire, spécifiez parcelableWrappers={YourParcelableWrapper.class} dans une annotation CustomProfileConnector ou CrossProfile sur une classe.

Future Wrappers

Les wrappers de futures permettent au SDK d'ajouter la prise en charge des futures dans les profils. Le SDK est compatible avec ListenableFuture par défaut, mais vous pouvez ajouter la compatibilité vous-même pour d'autres types de Future.

Un wrapper Future est une classe conçue pour encapsuler un type Future spécifique et le mettre à la disposition du SDK. Il suit un contrat statique défini et doit être enregistré auprès du SDK.

Annotation

La classe wrapper future doit être annotée @CustomFutureWrapper, en spécifiant la classe encapsulée en tant que originalType. Exemple :

@CustomFutureWrapper(originalType=SettableFuture.class)
``` ### Format

Future wrappers must extend
`com.google.android.enterprise.connectedapps.FutureWrapper`.

Future wrappers must have a static `W create(Bundler, BundlerType)` method which
creates an instance of the wrapper. At the same time this should create an
instance of the wrapped future type. This should be returned by a non-static `T`
`getFuture()` method. The `onResult(E)` and `onException(Throwable)` methods
must be implemented to pass the result or throwable to the wrapped future.

Future wrappers must also have a static `void writeFutureResult(Bundler,`
`BundlerType, T, FutureResultWriter<E>)` method. This should register with the
passed in future for results, and when a result is given, call
`resultWriter.onSuccess(value)`. If an exception is given,
`resultWriter.onFailure(exception)` should be called.

Finally, future wrappers must also have a static `T<Map<Profile, E>>`
`groupResults(Map<Profile, T<E>> results)` method which converts a map from
profile to future, into a future of a map from profile to result.
`CrossProfileCallbackMultiMerger` can be used to make this logic easier.

For example:

```java
/** A very simple 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;
  }
}

S'enregistrer auprès du SDK

Une fois le wrapper créé, vous devez l'enregistrer auprès de le SDK.

Pour ce faire, spécifiez futureWrappers={YourFutureWrapper.class} dans une annotation CustomProfileConnector ou CrossProfile sur une classe.

Mode Démarrage direct

Si votre application est compatible avec le démarrage direct mode , vous devrez peut-être effectuer des appels interprofils avant que le profil ne soit déverrouillé. Par défaut, le SDK n'autorise les connexions que lorsque l'autre profil est déverrouillé.

Pour modifier ce comportement, si vous utilisez un connecteur de profil personnalisé, doit spécifier 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 vous utilisez CrossProfileConnector, utilisez .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT _AWARE activé le compilateur.

Grâce à ce changement, vous serez informé de leur disponibilité et pourrez effectuer des les appels de profil, lorsque l'autre profil n'est pas déverrouillé. Il est de votre responsabilité de vous assurer que vos appels n'accèdent qu'au stockage chiffré de l'appareil.