Bibliothèque partagée sous Android

GNU/Linux Magazine HS n° 056 | septembre 2011 | Philippe Prados
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Nous allons proposer une solution technique permettant de partager un JAR d'un APK avec les autres APK installés sur le terminal Android.

1. Proposer un composant pour toutes les applications Android

La plupart des applications pour Android sont autonomes. Elles se suffisent à elles-mêmes. Il arrive parfois que l'on souhaite proposer un composant applicatif, destiné à plusieurs applications. Il y a plusieurs approches pour faire cela. La plus simple consiste à proposer un simple Jar à intégrer dans chaque application.

Nous souhaitons proposer un composant plus complexe, porté par un APK et utilisé par plusieurs applications APK simultanément.

Le framework Android est conçu comme cela. Une application spécifique system_app est lancée au boot, puis la bibliothèque android.jar s'occupe de communiquer avec, à l'aide des mécanismes d'invocation cross processus (AIDL).

Ne pouvant enrichir la bibliothèque android.jar, nous devons procéder autrement.

Imaginons un composant qui expose une API pour vérifier la licence d'une application, ou pour proposer un mécanisme d'achat pendant l'exécution de l'application. Ces services sont délégués à la place de marché intégrée.

Notre composant expose des interfaces à l'aide d'un AIDL, un fichier de description d'interface permettant l'invocation de méthodes entre deux applications (cf. Hors-Série Android).

Prenons l'exemple de l'AIDL suivant :

package org.checklicence.lib.shared;

interface ICheckLicence

{

 boolean checkLicence();

}

Eclipse va invoquer l'utilitaire aidl pour générer du code pour la partie cliente et la partie serveur. Nous obtenons, dans le répertoire /gen, l'interface org.checklicence.lib.shared.ICheckLicence, avec la classe interne Stub, elle-même, avec la classe interne Proxy.

Stub s'occupe de l'implémentation du service côté serveur. Proxy s'occupe de la vision du service côté client.

La méthode checkLicence() est implémentée dans le composant applicatif à l'aide d'une classe héritant de ICheckLicence.Stub.

package org.checklicence;

import org.checklicence.lib.shared.ICheckLicence;

import android.os.RemoteException;

public class CheckLicenceImpl extends ICheckLicence.Stub

{

  @Override

  public boolean checkLicence() throws RemoteException

  {

    // TODO Vérifier la licence de l'utilisateur

    return true;

  }

}

Les classes générées par l'AIDL sont nécessaires au client et au serveur. Chaque client devra donc avoir ces classes. Comment, dans ces conditions, faire évoluer l'interface ? Si on ajoute une nouvelle méthode à l'interface, il est nécessaire de re-générer le code et de le distribuer à toutes les applications. Ce n'est pas très évolutif !

1.1 Partage de la bibliothèque cliente

La bibliothèque cliente est fortement adhérente avec la partie serveur. Si le code du serveur évolue, la bibliothèque cliente doit également évoluer. Oui mais voilà, tous les APK qui intègrent cette bibliothèque doivent alors être mis à jour ! C'est justement ce que nous souhaitons éviter.

Notre objectif est de publier la bibliothèque cliente dans l'APK du composant et de permettre à chaque application de la récupérer dynamiquement. Ainsi, une mise à jour de l'APK permettra d'adapter la bibliothèque cliente de toutes les applications. Nous voulons, en fait, une bibliothèque partagée. Android ne propose pas cela. Qu'à cela ne tienne.

Pour que cela soit possible, nous devons organiser le code.

Nous allons découper notre projet en trois parties :

- La bibliothèque d'interface (CheckLicence-lib). C'est une bibliothèque pratiquement vide, permettant aux applications d'invoquer le service. La phase d'initialisation va récupérer dynamiquement l'implémentation des interfaces. Le code généré par l'AIDL n'est pas exposé à ce niveau.

- La bibliothèque cliente d'implémentation (CheckLicence-sharedlib). Cette bibliothèque propose une implémentation des interface de la bibliothèque d'interface. Elle encapsule le code généré par l'AIDL. Elle sera empaquetée dans l'APK du composant applicatif.

- L'application APK offrant le service (CheckLicence.apk). Cette application propose des services accessibles à distance, par d'autres applications, et la bibliothèque cliente d'invocation.

La bibliothèque d'interface est une bibliothèque la plus petite possible, ne possédant que des interfaces ou des classes abstraites. Cette bibliothèque expose les API utilisées par les applications exploitant le composant. Comme il s'agit d'interface, il est facile de maintenir une compatibilité ascendante. L'ajout d'une méthode n'invalide pas le code existant.

Pour initialiser le composant, nous proposons alors une méthode statique primitive, permettant d'obtenir une implémentation de la bibliothèque cliente.

package org.checklicence.lib;

import android.content.Context;

public abstract class CheckLicenceForMyMarket

{

  // API publique pour les applications

  public abstract boolean checkLicenceForMyMarket();

  static CheckLicenceForMyMarket getManager(Context context)

  {

    // TODO

  }

}

Toute la difficulté consiste à implémenter la méthode getManager(). Nous allons voir comment faire cela.

Commençons par utiliser secrètement notre code généré depuis l'AIDL, dans une classe CheckLicenceForMyMarketImpl, surchargeant CheckLicenceForMyMarket. Notre implémentation va initier la communication avec l'implémentation présente dans la place de marché, via un bindService().

public class CheckLicenceForMyMarketImpl extends CheckLicenceForMyMarket

{

  private static final String ACTION_LICENCE="org.checklicence.LICENCE";

  private ICheckLicence mRemoteLicence;

  public CheckLicenceForMyMarketImpl(Context context)

  {

    final Intent intent=new Intent(ACTION_LICENCE);

    context.bindService(intent, new ServiceConnection()

    {

      @Override

      public void onServiceDisconnected(ComponentName name)

      {

        mRemoteLicence=null;

      }

      @Override

      public void onServiceConnected(ComponentName name, IBinder binder)

      {

        mRemoteLicence=ICheckLicence.Stub.asInterface(binder);

      }

    }, Context.BIND_AUTO_CREATE);

  }

  @Override

  public boolean checkLicenceForMyMarket()

  {

    try

    {

      return mRemoteLicence.checkLicence();

    }

    catch (RemoteException e)

    {

      throw new Error(e);

    }

  }

}

Cette classe est à placer dans la bibliothèque d'implémentation.

Une première implémentation de la méthode getManager() peut être celle-ci :

static CheckLicenceForMyMarket getManager(Context context)

{

  return new CheckLicenceForMyMarketImpl(context);

}

Mais dans ce cas, la classe CheckLicenceForMyMarketImpl doit être placée dans CheckLicence-lib, ce que nous voulons éviter. Nous souhaitons la placer dans CheckLicence-sharedlib car l'implémentation est fortement liée aux services publiés par CheckLicence.apk.

La prochaine chose à faire est de modifier le build du projet CheckLicence-sharedlib pour générer un Jar signé, aligné et utilisant le format DEX.

Il faut revenir un peu sur la machine virtuelle Dalvik utilisée par Android. Cette machine virtuelle utilise un bytecode différent du bytecode standard de Java. La particularité de ce dernier est qu'il prépare la liaison entre toutes les classes de l'archive. Si vous dépliez un APK (qui est un fichier ZIP), vous trouverez un fichier classes.dex à la place des nombreux fichiers .class d'une archive classique. Ce fichier possède l'union de toutes les classes de l'archive, avec un bytecode spécifique. Le format est conçu pour pouvoir être directement mappé en mémoire à l'aide d'un mmap. Pour optimiser cela, l'archive doit être alignée sur des frontières de mots assembleur. Cela fait partie du processus de build d'Android lors de la génération de la release. Le Jar doit également être signé, car les classes sont vérifiées lors de leur installation en mémoire.

Notre objectif est de réaliser une archive JAR compatible Dalvik, qui sera publiée par CheckLicence.apk.

Le mécanisme de build d'eclipse n'est pas suffisant pour cela. Nous devons convertir le projet pour utiliser ant, bien connu des développeurs Java.

Cela nous génère un fichier build.xml que nous allons modifier.

Nous modifions le marqueur <setup/> par ceci :

   <setup import="false"/>

  <property name="verbose" value="true" />

    <import file="${sdk.dir}/tools/ant/main_rules.xml" />

  

    <path id="project.libraries.src">

    <pathelement location="${android.library.reference.1}/src"/>

    </path>

  <target name="-post-compile">

    <delete>

        <fileset dir="${out.dir}/classes/org/checklicence/lib" includes="*.class"/>

      </delete>

  </target>

  <property name="out.unsigned.file" value="${out.release.file}" />

  <target name="package" depends="-dex">

    <property name="out.unsigned.file" value="${out.release.file}" />

    <jar destfile="${out.unsigned.file}"

         basedir="${out.dir}/"

         includes="classes.dex"

      />

    <echo>Shared librairy done</echo>

  </target>

  

  <target name="release" depends="package">

      <echo>Signing final jar...</echo>

      <signjar

              jar="${out.unsigned.file}"

              signedjar="../CheckLicence/assets/sharedlib.jar"

              keystore="${key.store}"

              storepass="${key.store.password}"

              alias="${key.alias}"

              keypass="${key.alias.password}"

              verbose="${verbose}" />

  </target>

Cette modification va générer un fichier sharedlib.jar dans le répertoire asset du projet CheckLicence. Ce fichier possède les classes à partager. N'oubliez pas de créer un fichier local.properties avec les informations personnelles, comme la localisation du SDK, le fichier des clés et les mots de passe.

Le fichier classes.dex possède toutes les classes de l'archive. Lors du chargement par un classloader, il ne va pas vérifier s'il existe déjà une version de la classe dans un classloader supérieur. Contrairement au Java classique, nous devons supprimer toutes les classes de ${out.dir}/classes/org/checklicence/lib pour ne pas créer d’ambiguïté par la suite.

Nous devons ensuite modifier le build d'eclipse pour lui demander d'utiliser le fichier ant.

Nous avons créé une bibliothèque Android que nous plaçons dans le répertoire ../CheckLicence/assets/sharedlib.jar.

Notre application CheckLicence.apk peut alors l'exposer à toutes les autres applications. Mais comme les assets ne sont pas réellement des fichiers, juste un accès à l'APK de l'application, il faut auparavant en faire une copie accessible par toutes les applications. Nous faisons cela dans la méthode onCreate() d'une instance Application, bien entendu en tâche de fond, car il est interdit d'avoir des IO dans le main thread.

public class MyApplication extends Application

{

  private static final String TAG="app";

  private static final boolean USE_SHAREDLIB=true;

  private static final String SHARED_LIB="sharedlib.jar";

  @Override

  public void onCreate()

  {

    super.onCreate();

    if (USE_SHAREDLIB)

    {

      // Copy a public version of shared library

      new Thread("Copy shared library")

      {

        public void run()

        {

          final SharedPreferences prefs=getSharedPreferences("sharedlib",Context.MODE_PRIVATE);

          final long lastCopied=prefs.getLong("copy", -1);

          final long packageLastModified=new File(getApplicationInfo().publicSourceDir).lastModified();

          if (packageLastModified>lastCopied)

          {

            InputStream in=null;

            OutputStream out=null;

            try

            {

              in=getAssets().open(SHARED_LIB);

              out=openFileOutput(SHARED_LIB, Context.MODE_WORLD_READABLE);

              byte[] buf=new byte[1024*4];

              for (;;)

              {

                int s=in.read(buf);

                if (s<1) break;

                out.write(buf,0,s);

              }

              prefs.edit().putLong("copy",packageLastModified).commit();

            }

            catch (IOException e)

            {

              Log.e(TAG,"Impossible to copy shared library",e);

            }

            finally

            {

              if (in!=null)

              {

                try

                {

                  in.close();

                }

                catch (IOException e)

                {

                  Log.e(TAG,"Impossible to close input stream",e);

                }

                try

                {

                  out.close();

                }

                catch (IOException e)

                {

                  Log.e(TAG,"Impossible to close input stream",e);

                }

              }

            }

          }

        }

      }.start();

      

    }

  }

}

Enfin, nous avons un sharedlib.jar de type Android, disponible en lecture par toutes les applications. Nous pouvons maintenant utiliser un ClassLoader pour charger la bibliothèque et implémenter notre méthode getManager().

public static synchronized CheckLicenceForMyMarket getManager(Context context)

{

    

    if (mSingleton==null)

    {

      try

      {

          ClassLoader classLoader=CheckLicenceForMyMarket.class.getClassLoader();

          if (USE_SHAREDLIB)

          {

          File dir=context.getApplicationContext().getDir("dexopt", Context.MODE_PRIVATE);

          final String packageName="org.checklicence";

          PackageInfo info=context.getPackageManager().getPackageInfo(packageName, 0);

          String jar=info.applicationInfo.dataDir+"/files/"+SHARED_LIB;

          InputStream in=new FileInputStream(jar); in.read(); in.close(); // Check if is readable

          classLoader=

            new DexClassLoader(jar,

                dir.getAbsoluteFile().getAbsolutePath(),null,

                classLoader

                );

          }

        mSingleton=(CheckLicenceForMyMarket)classLoader.loadClass(BOOTSTRAP_CLASS)

          .getConstructor(Context.class).newInstance(context);

      }

      catch (Exception e)

      {

        throw new Error("Install the Remote Android package",e);

      }

    }

    return mSingleton;

  }

  

   "org.checklicence.lib.shared.CheckLicenceForMyMarketImpl";

  static final boolean USE_SHAREDLIB=true;

  static final String SHARED_LIB="sharedlib.jar";

}

Nous utilisons un ClassLoader spécifique pour les fichiers DEX. Ce dernier a besoin d'un nom de fichier source et d'un répertoire valide en écriture, qui va permettre de stocker une version optimisée de notre archive. Comme nous avons bien supprimé les classes partagées entre notre archive et l'archive pré-installée dans chaque application, il n'y a aucun problème de confusion dans les classes. Sinon, une erreur absconse va trahir la présence d'une Dalvik par rapport à une JVM classique.

Nous pouvons enfin proposer un exemple d'application Android qui utilise notre bibliothèque partagée. L'application doit utiliser la bibliothèque légère CheckLicence-lib, cela permet d'utiliser en réalité CheckLicence-sharedlib dont l'archive vient de l'application CheckLicence.apk.

public class TestCheckLicenceActivity extends Activity

{

  private CheckLicenceForMyMarket mLicenceManager;

  

    @Override

    

{

        super.onCreate(savedInstanceState);

        

        mLicenceManager=CheckLicenceForMyMarket.getManager(this);

        

        Button button=new Button(this);

        button.setText("Check licence");

        button.setOnClickListener(new OnClickListener()

    {

      

      @Override

      public void onClick(View v)

      {

        checkLicence();

      }

    });

        setContentView(button);

    }

    private void checkLicence()

    {

      boolean licence=mLicenceManager.checkLicenceForMyMarket();

      new AlertDialog.Builder(this)

          .setMessage("Licence ="+licence)

          .setOnCancelListener(

              new AlertDialog.OnCancelListener()

              {

                public void onCancel(DialogInterface dialog)

                {

                  finish();

                }

              })

          .show();

    }

}

Avec les sources, vous trouverez quatre packages :

- CheckLicence, qui est l'application portant la bibliothèque partagée ainsi que l'implémentation du service de vérification de la licence.

- CheckLicence-lib, qui est la bibliothèque la plus légère possible, ne proposant que des interfaces et la méthode getManager().

- CheckLicence-sharedlib, qui est la bibliothèque partagée par toutes les applications, via le classloader.

- CheckLicence-test, l'application de test de tout cela.

Tout est disponible sur www.prados.fr.

Conclusion

Voici un nouveau composant pour notre place de marché. Nous pouvons maintenant proposer des API spécifiques pour permettre de vérifier dynamiquement la licence du programme, ou pour permettre l'achat d'extension pendant l'exécution de l'application. Si une vulnérabilité est découverte dans l'API, une simple mise à jour du market suffit. Il n'est pas nécessaire de modifier toutes les applications qui proposent un achat dans l'application !

Avec beaucoup d'efforts, on arrive à dépasser les limites du framework proposé. Rien n'est simple dans le petit monde d'Android ;-)

Tags : Android, BIND, diff, SSL