Ces sections sont destinées à servir de référence. Il n'est pas nécessaire de les lire de haut en bas.
Demander le consentement de l'utilisateur
Utiliser les API du framework:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
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 le moment, 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, aller tout en bas et sélectionner l'option permettant d'ajouter le nom de votre package à la liste d'autorisation des applications connectées. Cela imite la "liste d'autorisation" de l'administrateur pour votre application.
Glossaire
Cette section définit les termes clés liés au développement multiprofil.
Configuration multiprofil
Une configuration multiprofil regroupe les classes de fournisseurs de profils croisés associées et fournit une configuration générale pour les fonctionnalités multiprofils.
En règle générale, il existe une annotation @CrossProfileConfiguration
par codebase, mais dans certaines applications complexes, il peut y en avoir plusieurs.
Connecteur de profils
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é dans une même configuration doit utiliser le même connecteur.
Classe de fournisseur multiprofil
Une classe de fournisseur de profils croisés regroupe des types de profils croisés associés.
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 d'architecture plutôt que d'un élément intégré au SDK.
Type de profil croisé
Un type de profil croisé 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 profils
Type de profil | |
---|---|
Actuel | Profil actif sur lequel nous exécutons l'opération. |
Autre | (le cas échéant) Profil sur lequel nous n'effectuons pas d'exécution. |
Personal | Utilisateur 0, profil de l'appareil qui ne peut pas être désactivé. |
Professionnel | Gé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 | Défini par l'application (facultatif). Profil qui affiche une vue fusionnée des deux profils. |
Secondaire | Si "primary" est défini, "secondary" correspond au profil qui n'est pas défini comme principal. |
Fournisseur | Les fournisseurs de la fiche principale sont les deux fiches, et ceux de la fiche secondaire ne sont que la fiche secondaire elle-même. |
Identifiant du profil
Classe représentant un type de profil (personnel ou professionnel). Ils seront renvoyés par des méthodes exécutées sur plusieurs profils et peuvent être utilisés pour exécuter plus de code sur ces profils. Ils peuvent être sérialisés dans un int
pour un stockage pratique.
Solutions recommandées pour l'architecture
Ce guide présente les structures recommandées pour créer des fonctionnalités multiprofils efficaces et 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 modèle Singleton classique, dans une nouvelle classe ou dans une classe existante.
Injectez ou transmettez l'instance de profil générée dans votre classe lorsque vous effectuez l'appel, plutôt que 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.
Envisager le modèle de médiateur
Ce modèle courant consiste à rendre l'une de vos API existantes (par exemple, getEvents()
) consciente du profil pour 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 n'obligez pas chaque appelant à savoir comment effectuer un appel interprofil. Il devient simplement une partie de 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 professionnels). Il peut également être nécessaire si des identifiants de données ne sont plus uniques sans lui, par exemple les noms de packages.
Profil croisé
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 sur les profils à exécuter, y compris professionnel, personnel et les deux.
En pratique, pour les applications dont l'expérience fusionnée ne concerne qu'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 permet d'activer des options supplémentaires, comme les 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. Ils n'auront accès qu'à l'état de 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 efficace, vous devez spécifier le connecteur pour chaque type de profil croisé, 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 des méthodes sur une interface en tant que @CrossProfile
, vous indiquez qu'il peut y avoir une implémentation de cette méthode qui doit être accessible dans tous les profils.
Vous pouvez renvoyer n'importe quelle implémentation d'une interface multicompte dans un fournisseur multicompte. Vous indiquez ainsi que cette implémentation doit être accessible entre les différents profils. Vous n'avez pas besoin d'annoter les classes d'implémentation.
Fournisseurs de services de profil croisé
Chaque type de profil croisé doit être fourni par une méthode annotée @CrossProfileProvider
. 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 profils
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 profils, utilisez
#setBinder
. Cette valeur n'est probablement utilisée que par les outils de contrôle 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).
Lors de la création d'un élément ProfileConnector
, il 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
), utilisezserviceClassName=
.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 contrôleur de règles relatives aux appareils, vous devez spécifier une instance de DpcProfileBinder
faisant référence à votre DeviceAdminReceiver
.
Si vous implémentez 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 multiprofil
L'annotation @CrossProfileConfiguration
permet de lier tous les types de profils croisés à l'aide d'un connecteur afin de distribuer correctement les appels de méthode. Pour ce faire, nous annotons une classe avec @CrossProfileConfiguration
, qui pointe vers chaque fournisseur, comme suit:
@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}
Cela permet de vérifier que pour tous les types de profils croisés, le même connecteur de profil est utilisé ou qu'aucun connecteur n'est 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 deandroid.app.Service
) pour être la super-classe, spécifiezserviceSuperclass=
.serviceClass
Si cette valeur est spécifiée, aucun service ne sera généré. Il doit correspondre à l'
serviceClassName
du connecteur de profil que vous utilisez. Votre service personnalisé doit distribuer les appels à l'aide de la classe_Dispatcher
générée comme suit:
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 des actions supplémentaires avant ou après un appel interprofil.
Connecteur
Si vous utilisez un connecteur autre que
CrossProfileConnector
par défaut, vous devez le spécifier à l'aide deconnector=
.
Visibilité
Chaque partie de votre application qui interagit entre les profils doit pouvoir voir votre connecteur de profil.
Votre classe annotée @CrossProfileConfiguration
doit pouvoir voir tous les fournisseurs utilisés dans votre application.
Appels synchrones
Le SDK Connected Apps accepte les appels synchrones (bloquants) dans les cas où ils sont inévitables. Toutefois, l'utilisation de ces appels présente un certain nombre d'inconvénients (par exemple, le blocage des appels pendant une longue période). Il est donc recommandé d'éviter les appels synchrones dans la mesure du 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 "Détenteurs de connexions".
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 enregistre que cet objet utilise la connexion et tente de créer 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 qu'aucun profil n'est disponible pour la connexion, aucune erreur n'est générée, mais le rappel connecté n'est 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é.
ProfileConnector#connect(Object)
est également une méthode bloquante qui ajoutera l'objet en tant que détenteur de connexion et établira une connexion ou générera une exception UnavailableProfileException
. Cette méthode ne peut pas être appelée à partir du
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 les éléments suivants:
try (ProfileConnectionHolder p = connector.connect()) {
// Use the connection
}
Une fois que vous avez terminé d'effectuer des appels synchrones, vous devez appeler ProfileConnector#removeConnectionHolder(Object)
. Une fois tous les titulaires de la 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 change, et connector.utils().isConnected
peut être utilisé pour déterminer si une connexion est présente. 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 au niveau de la division de profil doit être désignée comme bloquante (synchrone) ou non bloquante (asynchrone). Toute méthode qui renvoie un type de données asynchrone (par exemple, un ListenableFuture
) ou qui accepte un paramètre de rappel est marquée comme non bloquante. 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 la section Appels synchrones.
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 ne contient qu'une seule méthode, qui accepte zéro ou un paramètre, elle peut également être utilisée 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. Consultez la section "Porteurs de connexion" pour savoir comment maintenir la connexion ouverte afin de recevoir d'autres valeurs.
Méthodes synchrones avec rappels
Une caractéristique inhabituelle de l'utilisation de rappels avec le SDK est que vous pouvez techniquement écrire 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
Les "appels de rappel simples" sont une forme de rappel plus restrictive qui permet d'utiliser 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.
Porte-connexions
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 avec un rappel ou un futur, la connexion reste 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 futures sont également compatibles de manière native avec 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 futures, il vous suffit de déclarer un type Future compatible comme type de retour d'une méthode multiprofil, puis de l'utiliser normalement.
Cette fonctionnalité est la même que celle des rappels, où une méthode synchrone qui renvoie un événement futur (par exemple, à l'aide de immediateFuture
) se comporte différemment lorsqu'elle est exécutée sur le profil actuel que lorsqu'elle est exécutée sur un autre profil. 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.
Threads
Ne bloquez pas sur le résultat d'un futur ou d'un rappel interprofils sur le thread principal. Dans certains cas, votre code sera bloqué indéfiniment. En effet, la connexion à l'autre profil est également établie sur le thread principal, ce qui ne se produira jamais s'il est bloqué en attente d'un résultat interprofil.
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 détenteurs de connexions 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 supprimer manuellement des détenteurs de connexion pour mieux contrôler la connexion. Vous pouvez ajouter des détenteurs de connexion à l'aide de connector.addConnectionHolder
et en supprimer à l'aide de connector.removeConnectionHolder
.
Lorsqu'au moins un détenteur de connexion est ajouté, le SDK tente de 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 le supprimer lorsqu'il n'est plus pertinent.
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, tous les appels effectués à l'autre profil lorsqu'il n'est pas disponible entraînent l'émission d'une exception UnavailableProfileException
(ou la transmission à 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 quelconque d'entrées dans la liste générée (1 si l'autre profil est indisponible ou 2 s'il est disponible).
Exceptions
Toutes les exceptions non contrôlées qui se produisent après un appel au profil actuel seront propagées comme d'habitude. Cela s'applique quelle que soit la méthode utilisée pour effectuer l'appel (#current()
, #personal
, #both
, etc.).
Les exceptions non vérifiées qui se produisent après un appel à l'autre profil entraînent l'émission d'un ProfileRuntimeException
avec l'exception d'origine comme cause. Cela s'applique quelle que soit la méthode utilisée pour effectuer l'appel (#other()
, #personal
, #both
, etc.).
ifAvailable
Au lieu de capturer et de gérer des instances UnavailableProfileException
, vous pouvez utiliser la méthode .ifAvailable()
pour fournir une valeur par défaut qui sera renvoyée au lieu de générer une UnavailableProfileException
.
Exemple :
profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);
Tests
Pour rendre votre code testable, vous devez injecter des instances de votre connecteur de profil dans tout code qui l'utilise (pour vérifier la disponibilité du profil, pour vous connecter manuellement, etc.). Vous devez également injecter des instances de vos types respectueux du profil là où ils sont utilisés.
Nous fournissons des faux connecteurs et types que vous pouvez utiliser pour les 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'
Ensuite, annotez 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 génère des faux pour tous les types et connecteurs utilisés dans la configuration.
Créez des instances de ces faux éléments 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 acceptés 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 élément 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 acceptés (par exemple, java.util.Collection
) peuvent avoir n'importe quel type accepté comme paramètre de type. Exemple :
java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>>
est 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, commencez par déterminer s'il est possible de l'implémenter correctement avec android.os.Parcelable
ou java.io.Serializable
. Si elle ne peut pas voir de enveloppes parcellables, elle ne peut pas prendre en charge votre type.
Wrappers Future personnalisés
Si vous souhaitez utiliser un type futur qui ne figure pas dans la liste précédente, consultez les enveloppes futures pour l'ajouter.
Wrappers Parcelable
Les wrappers parcelables permettent au SDK d'ajouter la prise en charge des types non parcelables qui ne peuvent pas être modifiés. Le SDK inclut des wrappers pour de nombreux types, mais si le type 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 parcellable. Il suit un contrat statique défini et est enregistré auprès du SDK afin de pouvoir convertir un type donné en type parcelable, et d'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 prendre en charge le type de manière transparente.
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 peuvent être trouvées en appelant .typeArguments()
. Chaque argument de type est lui-même un 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'inscrire auprès du SDK
Une fois créé, vous devez enregistrer votre wrapper parcelable personnalisé auprès du SDK pour l'utiliser.
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 les 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 future classe de wrapper doit être annotée @CustomFutureWrapper
, en spécifiant la classe encapsulée comme originalType
. Exemple :
@CustomFutureWrapper(originalType=SettableFuture.class)
Format
Les futurs wrappers doivent étendre com.google.android.enterprise.connectedapps.FutureWrapper
.
Les futurs wrappers doivent comporter une méthode W create(Bundler, BundlerType)
statique qui crée une instance du wrapper. En même temps, cela devrait créer une instance du type de futur encapsulé. Cette valeur doit être renvoyée par une méthode getFuture()
T
non statique. Les méthodes onResult(E)
et onException(Throwable)
doivent être implémentées pour transmettre le résultat ou l'objet throwable à l'avenir encapsulé.
Les futurs wrappers doivent également disposer d'une méthode BundlerType, T, FutureResultWriter<E>)
void writeFutureResult(Bundler,
statique. Cela devrait s'enregistrer avec les résultats transmis à l'avenir, et lorsqu'un résultat est donné, appelez resultWriter.onSuccess(value)
. Si une exception est fournie, resultWriter.onFailure(exception)
doit être appelé.
Enfin, les wrappers futurs doivent également disposer d'une méthode groupResults(Map<Profile, T<E>> results)
T<Map<Profile, E>>
statique qui convertit une carte de profil en futur, en futur d'une carte de profil en résultat.
CrossProfileCallbackMultiMerger
peut être utilisé pour simplifier cette logique.
Exemple :
/** 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;
}
}
S'inscrire auprès du SDK
Une fois créé, vous devrez l'enregistrer auprès du SDK pour utiliser votre wrapper de futur personnalisé.
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 mode Démarrage direct, 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é, vous devez 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
sur le compilateur.
Avec ce changement, vous serez informé de la disponibilité et pourrez passer des appels interprofils 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.