Plugin de layout
Est-il possible d'ajouter des extensions à une application Android, et particulièrement des extensions à un layout ? C'est ce que nous allons voir.
Par Philippe PRADOS - 2013
www.prados.fr
Les entreprises utilisent de plus en plus des terminaux Androids pour offrir des applications à leurs employés. C'est particulièrement le cas des entreprises de coursiers ou de relevés de compteurs. En effet, les agents parcourent des kilomètres dans des environnements difficiles au niveau connexion, pour offrir de plus en plus de services innovants. Tous les intervenants itinérants ont vocation à utiliser les technologies mobiles, mais malheureusement, dans des conditions au niveau réseau, pas toujours idéales.
Les applications de gestion des tournées doivent offrir de plus en plus de fonctionnalités. Certaines sont spécifiques à une localisation géographique, d'autres sont partiellement en tests ou dépendent des profils des employés.
Une application Android monolithique n'est plus la solution. Il faut concevoir une application pouvant accueillir des extensions. Cela permet, par le simple choix des APK installés, d'enrichir les tournées des agents. Un Mobile Device Managment (MDM) permet alors d'installer le jeu d'extension correspondant à chaque profil. Lors de la mise à jour d'un composant, seul ce dernier est impacté.
De nombreux scénarios peuvent bénéficier de cela.
Avant toute chose, les sources sont présent ici : https://github.com/pprados/android-pluginviews
Communication avec les plugins
La première étape à franchir est de choisir une technologie pour la communication avec les plugins. Ajouter un plugin c'est ajouter un APK. Donc nous devons permettre à deux APK de communiquer.
Android nous propose plusieurs approches, mais la plus adaptée est d'exposer un composant aux autres applications via une interface. Cela s'effectue très simplement en quelques étapes.
La première chose à faire est de décrire l'interface de communication. On ne va pas utiliser Java pour cela, mais une syntaxe très proche : l'AIDL. Un fichier avec cette extension est traité par l'environnement de développement d'Android pour générer du code servant pour le client et pour le serveur.
Un fichier AIDL est composé d'une première instruction pour indiquer le nom du package où devront être généré les classes. Ensuite, on peut trouver des imports pour récupérer les structures des autres AIDL.
Enfin, une interface décrit les différentes méthodes avec les paramètres et leurs types.
package fr.prados.pluginview.provider;
import android.os.Bundle;
interface RemoteViewProvider
{
int getSize(in Bundle bundle);
}
Pour les paramètres de type Object, il faut indiquer si ce dernier sera utilisé uniquement en lecture par la méthode appelée (attribut in), sera utilisé uniquement pour que la méthode appelée y place des informations (attribut out) ou une combinaison des deux (attribut inout).
Les objets sont envoyés par « valeur », après avoir été sérialisés via les Parcel (sérialisation spécifique d'Android, bien plus efficace que la sérialisation java classique). Ils doivent alors avoir leurs propres fichiers AIDL pour déclarer cela.
package android.os.Bundle ;
parcelable Bundle;
Pratiquement toutes les structures de données d'Android possèdent leurs fichiers AIDL dans les sources. Certains sont déjà connus du compilateur AIDL. C'est le cas pour Bundle. Nous pouvons alors simplifier notre déclaration en supprimant l'import et le fichier android/os/Bundle.aidl.
package fr.prados.pluginview.provider;
interface RemoteViewProvider
{
int getSize(in Bundle bundle);
}
Un paramètre peut être une structure exposée par un autre service. C'est comme cela que l'on peut monter des callbacks entre APKs.
Avec un fichier comme celui-là dans votre projet, le compilateur va générer une classe RemoteViewProvider avec une interface RemoveViewProvider.RemoveViewProvider et deux sous-classes RemoveViewProvider.Stub et RemoteViewProvider.Stub.Proxy. Cette dernière ne nous intéresse pas, sauf à creuser le protocole. C'est l'objet d'autres articles que j'ai publiés.
La deuxième chose à faire est de proposer une implémentation du service. C'est super simple à faire. Il suffit de créer une classe qui hérite de RemoteViewProvider.Stub.
public class RemoteViewProviderImpl extends fr.prados.pluginview.provider.RemoteViewProvider.Stub
{
…
}
On a déclaré notre interface et une implémentation. Il faut maintenant permettre aux autres applications de se connecter à une instance RemoteViewProviderImpl.
Dans les autres architectures d'IPC, il faut généralement s'adresser à un annuaire. Sous Android, une utilisation particulière des services permet d'adresser cela.
Nous devons rédiger un service simple, surchargeant la méthode onBind() pour livrer une instance de RemoteViewProviderImpl.
package fr.prados.pluginview.provider1;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class ProviderService extends Service
{
@Override
public IBinder onBind(Intent intent)
{
return new RemoteViewProviderImpl(this);
}
}
Une dernière étape, déclarer le service dans le fichier AndroidManifest.xml.
<service
android:name=".ProviderService"
android:permission="fr.prados.PLUGINVIEW"
android:exported="false"
>
<intent-filter>
<action android:name="fr.prados.pluginview.providers" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
Et voilà. Avec deux classes, un fichier AIDL et la déclaration du service dans le manifest, nous sommes capables d'invoquer un traitement présent dans un autre APK.
Mais, c'est en fait plus compliqué que cela. En effet, le code généré à partir du fichier AIDL doit être utilisé à la fois par le client du service et par le producteur du service. Il faut réorganiser ces fichiers dans différents projets Android.
La recette n'est pas très compliquée. Créez trois projets Android. L'un pour le client, l'un pour le serveur, et le dernier, un projet Android Library avec uniquement le fichier aidl placé dans le bon répertoire du package.
Les deux premiers projets référencent le projet librairie avec l'AIDL. Ainsi, le code généré est disponible pour les deux parties.
Dans le projet serveur, on place la classe du service et la classe d'implémentation et on déclare le service dans le manifeste.
Ce qui est amusant, c'est que l'on peut faire plusieurs projets d'implémentations du service. Le client pourra alors tous les retrouver pour gérer une collection de plugins.
Découverte des plugins
Ok, nous avons un projet client qui souhaite découvrir tous les plugins. Pour cela, il faut utiliser
mContext.getPackageManager().queryIntentServices(mIntent, 0);
Il faut utiliser un Intent construit avec l'action déclarée dans le manifeste.
mIntent=new Intent("fr.prados.pluginview.providers")
Cela permet de récupérer la liste de toutes les applications implémentant un service répondant à l'Intent.
Nous pouvons alors itérer sur les ServiceInfos pour demander ensuite une connexion directe avec chacun.
for (ResolveInfo info : providersInfo)
{
final ServiceInfo serviceInfo = info.serviceInfo;
if (serviceInfo == null)
continue;
final Intent intent=new Intent(mIntent);
// Select specific package
intent.setClassName(serviceInfo.packageName, serviceInfo.name);
...
boolean rc = mContext.bindService(
intent, conn, Context.BIND_AUTO_CREATE);
}
J'ai fait abstraction de l'instance ServiceConnection qui propose deux call-backs. L'une est invoquée lorsque le service a réussi à se connecter, l'autre lorsque le service est déconnecté (perte du processus de l'APK du plugin par exemple).
Pour déconnecter un service, il faut réutiliser la même instance de ServiceConnection pour faire un unbindService().
Pour gérer correctement un ensemble de plugins, il faut donc garder la liste des ServiceConnection en parallèle de la liste des instances remotes.
Lorsqu'une connexion avec un plugin est effective, la méthode onServiceConnected() de la callback reçoit un paramètre IBinder qu'il suffit de convertir pour répondre à l'interface. En fait, c'est un cast particulier via une méthode générée.
@Override
public void onServiceConnected(ComponentName className, IBinder service)
{
RemoteViewProvider provider=RemoteViewProvider.Stub.asInterface(service)
...
}
Cette méthode vérifie si l'instance référencée par service est une instance locale, dans ce cas un cast classique est suffisant. Si l'instance est distante, la méthode crée une instance Proxy compatible avec l'interface. Ainsi, tous les usages de l'interface seront propagées à une instance présente dans un autre APK.
Généralisons la gestion des plugins
Bon, nous savons comment proposer des plugins et comment s'y connecter. Si nous souhaitons faire une classe générique pour gérer automatiquement cela, c'est plus compliqué.
Nous souhaitons créer un classe qui gère un type générique généré par un AIDL.
public class BindProviders<T extends android.os.IInterface>
{
...
}
Cette classe va recevoir l'Intent en paramètre pour découvrir les plugins, se connecter sur chacun et gérer le cycle de vie des connexions. Une call-back sera invoquée lorsque tous les plugins seront connectés.
/**
* A callback called when all providers are connected.
* @author Philippe Prados
*
* @param <T> AIDL interface
*/
public interface CallBack<T>
{
/**
* Call-back invoked when all providers are connected.
* @param providers
*/
void onProvidersBinded(List<T> providers);
}
Comme nous devons garder l'instance ServiceConnection et l'instance du proxy du plugin connecté, je déclare une sous-classe comme ceci.
interface ProviderServiceConnection<T> extends ServiceConnection
{
T getProvider();
}
Je peux alors garder deux listes.
// Providers and connections
private List<ProviderServiceConnection<T>> mProvidersConnections=
new ArrayList<ProviderServiceConnection<T>>();
// Only providers
private List<T> mProviders=new ArrayList<T>();
Pour le moment, tous va bien. La difficulté arrive lorsque l'on souhaite convertir le IBinder lors d'une méthode onServiceConnected() en T. On souhaite appeler :
mProvider = (T)T.Stub.asInterface(service)
Mais ce n'est pas possible. Le mécanisme de généricité en Java perd le type T à l'exécution (type Erasure). Nous devons alors procéder autrement.
Class<?> clazz=Class.forName(mParameterType.getName()+"$Stub");
Method asInterface=clazz.getMethod("asInterface", android.os.IBinder.class);
mProvider = (T)asInterface.invoke(null, service);
Cela nous impose de posséder le type de T à l'exécution. Le constructeur le demande car il ne peut le découvrir.
public BindProviders(
Context context,
Intent intent,
Class<T> parameterType,
CallBack<T> callback)
{
mContext=context;
mIntent=intent;
mParameterType=parameterType;
mCallback=callback;
}
Maintenant, c'est bon. Nous pouvons proposer une méthode open() pour connecter tous les plugins et une méthode close() pour s'y déconnecter.
La méthode getProviders() retourne la liste des providers connectés.
Pour utiliser cette classe avec l'exemple d'AIDL ci-dessus, il faut simplement rédiger
new BindProviders<RemoteViewProvider>(context,intent, RemoteViewProvider.class,this)
Dans un fragment, il faut invoquer la méthode open() dans onAttach() et la méthode close() dans onDetach().
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
// Connect to all providers
mProviderBinder=new BindProviders<T>(activity,mIntent,mParameterType,this);
mProviderBinder.open();
}
@Override
public void onDetach()
{
super.onDetach();
// Deconnect all providers
mProviderBinder.close();
mProviderBinder=null;
}
Pour éviter les fuites mémoires, il est préférable de s'appuyer sur le cycle de vie des composants graphiques pour gérer les plugins. Vouloir faire le malin en gardant la liste en cache est rarement une bonne idée.
Enrichissement d'un Layout
Parfait, nous avons une call-back qui nous donne la liste des plugins. Mais pour le moment, nous ne pouvons qu'invoquer des traitements distants. Comment enrichir notre interface utilisateur à l'aide de plugin ?
Notre objectif est de présenter une liste dont les lignes ont été alimentées par les plugins. Chacun pouvant produire plusieurs lignes.

Pour pouvoir faire cela, nous allons utiliser la technique utilisée par Android pour gérer les notifications et les Widgets du bureau. Ces derniers sont des bouts d'applications proposées à l'utilisateur, alors que le processus correspondant n'est pas présent en tâche de fond.
En fait, les notifications et les widgets sont des layouts simplifiés avec une interaction avec l'utilisateur très réduite, généralement limitée à l'appui d'un composant ou au parcours d'une liste pour les dernières versions. Le layout est récupéré par le framework en interrogeant l'application, puis gardé au chaud. L'application peut alors être tuée par le système. Le layout est toujours disponible.
Nous devons alors proposer une méthode à notre AIDL qui permet de retourner un Layout initialisé par valeur. Il existe une classe pour cela : RemoteView.
Nous ajoutons une méthode à notre AIDL.
package fr.prados.pluginview.provider;
interface RemoteViewProvider
{
int getSize(in Bundle bundle);
android.widget.RemoteViews getLoadingView(in Bundle bundle);
}
Il n'est pas nécessaire de créer un fichier android.widget.RemoteViews.aidl, car Android le connaît déjà.
Côté implémentation, nous devons proposer un layout simple, pouvant être présent dans une ligne d'une ListView.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/provider1">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginTop="16dp"
android:textColor="@color/textview_textcolor"
android:textAppearance="?android:attr/textAppearanceLarge"
/>
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button" />
</RelativeLayout>
Nous avons un simple texte est un bouton.
Dans la méthode d'implémentation de getLoadingView() exposé par l'AIDL, nous pouvons instancier cela :
final RemoteViews remoteView=
new RemoteViews(mAppPackage,
R.layout.provider1);
Le premier paramètre est le package Android de l'application du plugin. Pourquoi ce paramètre ? Nous allons le voir plus loin.
Comme notre plugin peut proposer plusieurs lignes, nous devons les identifier. Pour cela, nous proposons de recevoir cette information dans le Bundle reçu en paramètre de la méthode.
int i=1;
if (bundle!=null) i=bundle.getInt("id", 1);
Nous pouvons alors modifier le texte avec la méthode suivante de RemoteView.
remoteView.setCharSequence(R.id.text, "setText", "Provider1 #"+i);
C'est un peu étrange comme façon de modifier le layout, livrer le nom d'une méthode et ses paramètres. Pourquoi ne pas modifier directement le texte dans la vue ? Tous va devenir plus clair dans un instant.
Nous voulons également que lors de l'appui sur la ligne, un Intent spécifique vers un écran de notre plugin soit déclenché.
final Intent intent=new Intent(mContext,Provider1Activity.class);
intent.putExtra("id", i);
final PendingIntent pendingIntent=PendingIntent.getActivity(mContext,
i, //request code must be different for each
intent, 0);
remoteView.setOnClickPendingIntent(R.id.provider1, pendingIntent);
Attention, l'identifiant du PendingIntent doit être différent pour chaque ligne. Il est possible d'utiliser un simple numéro incrémenté à chaque fois. Nous avons choisi d'utiliser l'id de la ligne.
Si nous souhaitons avoir une réaction uniquement sur l'appui du bouton, la dernière ligne est différente.
remoteView.setOnClickPendingIntent(R.id.button1, pendingIntent);
Nous pouvons retourner dans notre instance RemoteView.
return remoteView ;
Il nous reste à résoudre la méthode getSize() pour indiquer combien de ligne le plugin génère.
@Override
public int getSize(Bundle bundle) throws RemoteException
{
return 3; // arbitrary number of items
}
Nous voici avec la première implémentation d'un plugin. Chaque plugin peut proposer son layout.
Utilisation des layouts
Nous allons maintenant consommer nos layouts. Pour cela, nous créons un ListFragment.
public class RemoteListFragment
extends ListFragment
{
...
}
Nous injectons la tuyauterie comme décrit précédemment pour avoir une call-back invoqué avec la liste des plugins. Merci à BindProviders<>.
Il nous faut un Adapter pour faire le lien entre la liste et les plugins.
class ComplexRemoteViewAdapter extends BaseAdapter
{
private Context mContext;
private ArrayList<RemoteViewProvider> mItems=
new ArrayList<RemoteViewProvider>();
private Bundle mBundle=new Bundle();
@Override
public void notifyDataSetChanged()
{
super.notifyDataSetChanged();
updateData();
}
public ComplexRemoteViewAdapter(Context context)
{
mContext=context;
updateData();
}
@Override
public int getCount()
{
return mItems.size();
}
…
}
Avec une liste qui associe à chaque ligne, le RemoteViewProvider, on ajoute une petite méthode pour initialiser tous cela.
private void updateData()
{
for (RemoteViewProvider provider:getProviders())
{
try
{
final int size = provider.getSize(mBundle);
for (int i=0;i<size;++i)
mItems.add(provider);
}
catch (RemoteException e)
{
// Ignore. Bug with the provider
}
}
}
La méthode la plus importante est getView().
@Override
public View getView(int pos, View old, ViewGroup parent)
{
try
{
final Bundle bundle=new Bundle();
bundle.putInt("id", pos);
final RemoteViews remoteViews = mItems.get(pos).getLoadingView(bundle);
final View view=remoteViews.apply(mContext,parent);
}
catch (RemoteException e)
{
e.printStackTrace();
TextView tv=new TextView(parent.getContext());
tv.setText("ERROR");
return tv;
}
}
La méthode apply() d'un RemoteViews permet de demander la construction de la vue décrite dans l'instance. La vue ainsi créée peut-être retournée pour être affichée dans la ListView.
Première remarque, nous devons capturer l'exception RemoteException car nous invoquons un processus distant qui peut planter. Nous verrons comment faire en sorte que cette exception ne soit jamais lancée.
Dans le corps de la méthode, nous construisons un Bundle que nous alimentons avec le numéro de la ligne. Puis nous invoquons la méthode getLoadingView() du plugin.
Dans :la call-back du Fragment, nous rafraîchissons la liste.
@Override
public void onProvidersBinded(List<RemoteViewProvider> providers)
{
if (mAdapter!=null)
mAdapter.notifyDataSetChanged();
}
Et dans pour la vue nous déclarons l'adapter :
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState)
{
mAdapter=new ComplexRemoteViewAdapter(getActivity());
setListAdapter(mAdapter);
return super.onCreateView(inflater, container, savedInstanceState);
}
Cela fonctionne parfaitement. Nous avons bien un ListFragment dont chaque ligne est alimentée par un plugin. Lors de l'appui sur une ligne, un startActivity() est déclenché vers l'activité correspondante du plugin.
Gestion de la feuille de style
C'est bien, mais dans le layout du plugin, nous avons explicitement indiqué textColor et textAppearance.
<TextView
…
android:textColor="@color/textview_textcolor"
android:textAppearance="?android:attr/textAppearanceLarge"/>
Est-il possible d'utiliser la feuille de style de l'application racine pour afficher les layouts des plugins ? Cela serait cool !
En virant ces attributs, rien ne se passe comme prévu. Le texte est affiché en blanc sur fond gris clair. Intervenir sur la feuille de style du plugin ne fonctionne pas. Il faut nous plonger dans le code d'Android pour comprendre pourquoi.
On retrousse les manches, et c'est parti. En étudiant les sources de RemoteView, on est surpris de l'approche utilisée. A priori, on imagine qu'une vue est créée dans le plugin, puis sérialisée dans une RemoteView avant d'être envoyée à l'application racine et être reconstruite.
Comme toujours avec Android, le code n'est pas comme on l'imagine.
En fait, un RemoteView possède trois informations principales : le package de l'application qui porte le layout, le numéro du layout dans cette application (R.layout....) et une liste de commandes à appliquer pour modifier la vue créée.
Cela permet à la méthode apply() de créer un Context pour le package du plugin, pour lui demander de créer la hiérarchie de vues. Avec l'arbre en poche, la méthode applique successivement les traitements enregistrés par le plugin.
Par exemple, lorsque nous avons écrit ceci dans le plugin :
remoteView.setCharSequence(R.id.text, "setText", "Provider1 #"+i);
Cela n'a pas modifié la vue, mais ajouté une commande au RemoteView pour que cela soit appliqué localement par l'application racine. Il n'est donc pas possible d'utiliser un RemoteView sans layout associé.
Et comme le LayoutInflater est associé au Context de l'application du plugin, il ne peut utiliser la feuille de style locale.
Regardons plus précisément le code de la méthode apply()
public View apply(Context context, ViewGroup parent, OnClickHandler handler)
{
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
Context c = prepareContext(context);
LayoutInflater inflater = (LayoutInflater)
c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater = inflater.cloneInContext(c);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}
Une première remarque. Le code ajoute un Filter à l'inflater. Cela va permettre d'invoquer la méthode onLoadClass() pour chaque vue du layout.
public boolean onLoadClass(Class clazz)
{
return clazz.isAnnotationPresent(RemoteView.class);
}
En fait, cela permet de filtrer les vues compatibles avec les RemoteViews. Les classes correspondantes sont identifiées dans les sources d'Android avec l'annotation @RemoteView. Voilà une technique élégante pour gérer cela.
Ensuite, nous avons l'invocation de prepareContext() pour obtenir un Context spécifique. Regardons le détail de cette méthode.
private Context prepareContext(Context context)
{
Context c;
String packageName = mPackage;
if (packageName != null)
{
try
{
c = context.createPackageContext(packageName, Context.CONTEXT_RESTRICTED);
}
catch (NameNotFoundException e)
{
Log.e(LOG_TAG, "Package name " + packageName + " not found");
c = context;
}
}
else
{
c = context;
}
return c;
}
Cool. C'est ce à quoi on s'attendait. Le code construit un Context sur l'application du plugin, pour pouvoir y récupérer les ressources.
Pour forcer la feuille de style, il faudrait pouvoir adapter ce Context, pour que la méthode getTheme() retourne notre thème. Nous devons alors manipuler à distance, la méthode prepareContext().
Comme elle s'appuie sur le Context qu'on livre à la méthode apply(), essayons de modifier la méthode createPackageContext() pour wrapper la méthode getTheme(). La classe ContextWrapper() va nous aider.
final View view=remoteViews.apply(new ContextWrapper(mContext)
{
public Context createPackageContext(String packageName, int flags)
throws NameNotFoundException
{
Context context=
new ContextWrapper(getBaseContext().createPackageContext(packageName, flags))
{
// Delegate the theme to the context
@Override
public Theme getTheme()
{
return mContext.getTheme();
}
};
return context;
}
}, parent);
Super ! Cela fonctionne avec un Android 4.2 !
Jusqu'au jour ou je reçois la mise à jour 4.3 de l'OS. Vous savez, celle qui a fortement renforcée la sécurité. Et là, cela ne fonctionne plus. La raison est à trouver dans la nouvelle implémentation de la méthode prepareContext().
c = context.createPackageContextAsUser(
packageName, Context.CONTEXT_RESTRICTED, mUser);
Il n'est plus possible de gérer le thème car impossible de surcharger la méthode createPackageContextAsUser().
Conclusion, ce n'est pas parce que les sources d'Android sont disponibles qu'il faut faire le malin :-)
Les layouts des plugins doivent indiquer tous les paramètres et ne pas s'appuyer sur la feuille de style.
Optimisation
Utiliser des plugins dans des applications différentes est une bonne idée, mais au niveau consommation de ressources mémoires, ce n'est pas l'idéal. En effet, chaque plugin est porté par un processus qui porte le framework Android. (Je sais, il y a des astuces pour partager l'essentiel du framework en mémoire par les différentes applications).
Lors de l'utilisation de plugin, il est plus judicieux de partager le processus entre l'application racine et les plugins. C'est très facile à faire sous Android. Juste quelques paramètres dans les manifests.
Dans le manifest de l'application racine, il faut indiquer un sharedUserId dans le marqueur <manifest/>.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.prados.pluginview.client"
android:versionCode="1"
android:versionName="1.0"
android:sharedUserId="fr.prados.pluginview"
>
Puis dans marqueur <application/> il faut donner un nom au processus via l'attribut android:process.
<application
android:process="fr.prados.pluginview"
Il faut procéder de même pour les applications portant les plugins.
Cela présente plusieurs avantages. La communication avec les plugins s'effectue dans le même processus, sans passer par le noyau Linux. Les méthodes asInterface() présentée plus haut utilisent le cast et non la création d'un proxy. Ainsi, les méthodes ne peuvent plus retourner des RemoteExceptions. La découverte des plugins et leurs connections est plus rapide. Il n'est plus nécessaire d'exposer les services aux autres applications. Un attribut export="false" pour les services des plugins permet de garantir la sécurité.
Pour finir
Nous voici arrivé au terme de ce voyage dans une fonctionnalité peu connue d'Android. Tous les sources sont disponibles. Nous avons proposé une petite classe pour découvrir et gérer les connexions avec les plugins ; nous avons créé des layouts dans les plugins pour les utiliser dans une autre application ; puis nous avons optimisé et sécurisé tous cela.
Cela semble compliqué, mais en fait, avec une classe et quelques classes génériques présentes dans les sources pour simplifier encore la vie, en dix minutes vous pouvez avoir des plugins graphiques.
