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 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 | |
---|---|
Actuel | Profil actif que nous exécutons. |
Autre | (Le cas échéant) Profil sur lequel nous n'exécutons pas la requête. |
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 | Éventuellement défini par l'application. Le profil auquel affiche une vue fusionnée des deux profils. |
Secondaire | Si le profil principal est défini, le profil secondaire n'est pas principale. |
Fournisseur | Les 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.
Solutions architecturales recommandées
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
), 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 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 deandroid.app.Service
) pour être la super-classe, spécifiezserviceSuperclass=
.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 deconnector=
.
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.