Développement sous Android

Chapitres traités   

Pour cette dernière étude de l'environnement Android, nous allons en profiter pour découvrir plein de fonctionnalités intéressantes comme le partage des données, les services, la communication réseau, la prise en compte des capteurs intégrés, la géolocalisation, le graphisme, le multimédia, etc.

 


Choix du chapitre Les fournisseurs de contenu (Content Provider)

Vous pouvez décider d'étendre votre application en proposant des extensions utilisant les mécanismes de partage de données mis à disposition par un service déjà installé sur le téléphone de l'utilisateur. Le partage des données Android via les fournisseurs de contenu est un excellent moyen de diffuser de l'information selon une interface standard d'échange.

L'utilisation des bases de données vous permet de stocker des données complexes et structurées. L'accès à une base de données n'est possible que depuis l'application à partir de laquelle elle a été créée. Si vous souhaitez exposer les données d'une application à d'autres applications, par exemple des photos prises par votre application, Android prévoit un mécanisme plutôt élégant pour parvenir à cette fin : la notion de fournisseur de contenu, sous la forme d'une interface générique permettant d'exposer les données d'une application en lecture et/ou en écriture.

Ce mécanisme permet de faire une scission claire entre votre application et les données exposées. Ainsi, en rendant vos données disponibles au travers d'une telle interface, vous rendez votre application accessible et extensible à d'autres applications en tant que fournisseur de contenu, que ces applications soient créées par vous-même ou des tiers.

Les URIs

Avec Android, toute URI de schéma content:// représente une ressource servie par un fournisseur de contenu. Les fournisseurs de contenu encapsulent les données en utilisant des instances d'URI comme descripteur.

Nous ne savons jamais d'où viennent les données représentées par l'URI et nous n'avons pas besoin de le savoir : la seule chose qui compte est qu'elles soient disponibles lorsque nous en avons besoin.

Ces données pourraient être stockées dans une base de données SQLite ou dans des fichiers plats, voire récupérées à partir d'un terminal ou stockées sur un serveur situé loin d'ici, sur Internet.

A partir d'une URI, vous pouvez réaliser les opérations CRUD de base (Create, Read, Update, Delete) en utilisant un fournisseur de contenu. Les instances d'URI peuvent représenter des collections ou des éléments individuels. Grâce aux premières, vous pouvez créer de nouveaux contenus via des opérations d'insertion. Avec les secondes, vous pouvez lire les données qu'elles représentent, les modifier ou les supprimer.

Composantes d'une URI

Un fournisseur de contenu fonctionne un peu à la manière d'un service web accessible en REST (que nous verrons lors de cette étude) : vous exposez un ensemble d'URI capable de vous retourner différents ensembles d'éléments (tous les éléments, un seul ou un sous-ensemble) en fonction de l'URI et des paramètres.

Quand une requête cible un fournisseur de contenu, c'est le système qui gère l'instanciation de celui-ci. Un développeur n'aura jamais à instancier les objets d'un fournisseur de contenu lui-même.

Le modèle simplifié de construction d'une URI est constitué du schéma, de l'espace de noms des données et, éventuellement, de l'identifiant de l'instance. Ces différents composants sont séparés par des barres de fraction, comme dans une URL.

content://constants/5. Cette URI représente une instance constants d'identifiant 5.

La combinaison du schéma et de l'espace de noms est appelé URI de base d'un fournisseur de contenu ou d'un ensemble de données supporté par un fournisseur de contenu.

Dans l'exemple précédent, content://contants est l'URI de base d'un fournisseur de contenu qui sert des informations sur "contants" (en l'occurence, des constantes physiques).

L'URI de base peut être plus compliquée. Celle des contacts est, par exemple, content://com.android.contacts/contacts, car le fournisseur de contenu des contacts peut fournir d'autres données en utilisant d'autres valeurs pour l'URI de base. L'URI de base représente une collection d'instances. Combinée avec un identifiant d'instance (5, par exemple), elle représente une instance unique.

La plupart des API d'Android s'attendent à ce que les URI soient des objets de type Uri, bien qu'il soit plus naturel de les considérer comme des chaînes. La méthode statique Uri.parse() permet ainsi de créer une instance d'URI à partir de sa représentation textuelle.

D'où viennent ces instances d'URI ?

Le point de départ le plus courant, lorsque nous connaissons le type de données avec lequel nous souhaitons travailler, consiste à obtenir l'URI de base du fournisseur de contenu lui-même. CONTENT_URI, par exemple, est l'URI de base des contacts représentés par des personnes, elle correspond à content://com.android.contacts/contacts.

Si vous avez simplement besoin de la collection, cette URI fonctionne telle quelle ; si vous avez besoin d'une instance dont vous connaissez l'identifiant, vous pouvez l'ajouter à la fin de cette dernière, afin d'obtenir une URI pour cette instance précise.

Accéder à un fournisseur de contenu

Pour accéder à un fournisseur de contenu, vous devrez utiliser la classe android.content.ContentResolver. Cette classe est un véritable utilitaire qui sera votre principal point d'accès vers les fournisseurs de contenu Android.

Portez également une attention particulière à l'espace de noms android.provider dans lequel vous trouverez un ensemble de classes facilitant l'accès aux fournisseurs de contenu natifs de la plate-forme Android (appels, contacts, etc.).

Vous pouvez récupérer une instance - unique pour l'application - de la classe ContentResolver eu utilisant la méthode getContentResolver().

ContentResolver fournisseur = getContentResolver();

Chaque fournisseur de contenu expose publiquement une propriété CONTENT_URI, qui stocke l'URI de requête du contenu, comme le montre les fournisseurs de contenu natifs d'Android suivants :

android.provider.CallLog.Calls.CONTENT_URI
android.provider.Calendar.CONTENT_URI android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
Effectuer une requête

Tout comme les bases de données abordées dans l'étude précédente, les fournisseurs de contenu retournent leurs résultats sous la forme d'un Cursor. Du coup, vous pouvez effectuer les mêmes opérations que lorsque vous manipuliez des bases de données, l'utilisation dans le cadre d'un fournisseur de contenu ne limite en rien ses fonctionnalités.

Une requête s'effectuera en utilisant la méthode query() du ContentResolver en passant les paramètres listés dans le tableau suivant :

  1. uri : L'URI du fournisseur de contenu.
  2. projection : La projection, soit les colonnes que vous souhaitez voir retournées. Cette valeur peut aussi être null.
  3. where : Une clause pour filtrer les éléments retournés. Dans cette chaîne de caractères, l'utilisation du '?' est possible ; chacun de ces caractères sera remplacé par les valeurs du paramètre de sélection (le paramètre suivant). Cette valeur peut aussi être égale à null.
  4. selection : Un tableau de sélections qui remplaceront les caractères '?' placés dans la clause where. Ceci permet de rendre dynamiques les requêtes de façon à ne pas avoir à manipuler une chaîne de caractères pour construire la requête mais de le faire via l'ensemble de valeurs d'un tableau rempli dynamiquement à l'exécution. Cette valeur peut aussi être null.
  5. order : le nom de la colonne associée à une chaîne de tri ('DESC' ou 'ASC'). Cette valeur peut aussi être égale à null.
// Requêter toutes les données du fournisseur de contenu
ContentResolver fournisseur = getContentResolver();
Cursor données = fournisseur.query(MonFournisseur.CONTENT_URI, null, null, null, null);

// Filtrer les données retournées et trier par ordre croissant sur le nom
ContentResolver fournisseur = getContentResolver();
String[] projection = new String[] {"nom", "prénom", "âge"};
String filtre = "prénom = Julien";
String ordre = "nom ASC";
Cursor données = fournisseur.query(MonFournisseur.CONTENT_URI, projection, filtre, null, ordre);  
  1. Une fois le curseur récupéré, vous opérez de la même façon qu'avec une base de données.
  2. La partie concernant la création d'un fournisseur de contenu expliquera plus en détail comment fonctionne la paramètre URI d'une requête.
  3. Pour restreindre la requête à un élément en particulier, vous pouvez ajouter la valeur de son identifiant (id) correspondant. par exemple, si celui-ci est 10, l'URI sera content://fr.btsiris.db/10.

Il existe plusieurs méthodes d'aide, telles que ContentUris.withAppendedId() et Uri.withAppendPath(), qui vous faciliteront la vie. pour continuer notre exemple, le code correspondant pour construire la requête adéquate permettant de ne cibler qu'un élément en particulier s'écrit ainsi :

Uri requête = Uri.parse("content://fr.btsiris.bd");
Uri requêteParticulière = ContentUris.withAppendedId(requête, 10);
Uri requêteParticulière = Uri.withAppendPath(requête, "10");
Cursor données = managedQuery(requêteParticulière, null, null, null, null);  
Les fournisseurs de contenu natifs

Android possède quelques fournisseurs de contenu disponibles nativement permettant d'exposer certaines informations contenues dans les bases de données du système.

  1. Contacts : Retrouvez, ajoutez ou modifiez les informations des contacts.
  2. Magasin multimédia : Le magasin multimédia stocke l'ensemble des fichiers multimédias de votre appareil. Ajouter ou supprimez les fichiers multimédias depuis ce fournisseur de contenu.
  3. Navigateur : Accédez à l'historique, aux marque-pages ou aux recherches.
  4. Appels : Accédez à l'historique des appels entrants, sortants et manqués.
  5. Paramètres : Accédez aux paramètres système - sonnerie, etc. - pour lire ou modifier certaines préférences.

Les concepteurs de la plate-forme Android l'ont bien compris, pouvoir accéder librement aux contacts d'un appareil de téléphonie est une aubaine pour bon nombre d'applications. En accordant les permissions adéquates, un développeur peut effectuer une requête, modifier, supprimer ou encore ajouter un contact.

L'exemple suivant permet de lire l'ensemble des contacts stockés dans l'appareil :

fr.btsiris.fournisseur.FournisseurContenu.java
package fr.btsiris.fournisseur;

import android.app.*;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.*;

public class FournisseurContenu extends ListActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Uri requête = ContactsContract.Contacts.CONTENT_URI;
        String ordre = ContactsContract.Contacts.DISPLAY_NAME +" COLLATE LOCALIZED ASC";
        Cursor lignes = getContentResolver().query(requête, null, null, null, ordre);
        startManagingCursor(lignes);
        ListAdapter liste = new SimpleCursorAdapter(this, 
                android.R.layout.two_line_list_item, lignes, 
                new String[] {ContactsContract.Data.DISPLAY_NAME}, 
                new int[] {android.R.id.text1});
        setListAdapter(liste);
    }
}
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<ListView  xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@android:id/list"
      android:layout_width="fill_parent" 
      android:layout_height="fill_parent"
/>

La plupart des fournisseurs de contenu ne donnent accès à leurs contenus qu'avec des permissions. Par conséquent, n'oubliez pas d'ajouter la permission adéquate à l'application lors de la lecture de ce fournisseur de contenu, comme dans l'exemple ci-dessous :

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.fournisseur"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="@string/app_name" >
        <activity android:name="FournisseurContenu"  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>      
    </application>
    <uses-permission android:name="android.permission.READ_CONTACTS" />
</manifest>
Ajouter, supprimer ou mettre à jour des données via le fournisseur de contenu

Les fournisseurs ne sont pas seulement des points d'exposition pour permettre aux applications d'effectuer des requêtes sur les bases de données d'autres applications. Ils permettent aussi de manipuler les données exposées en termes d'ajout, de modification et de suppression. Toutes ces actions s'effectuent toujours à l'aide de la classe ContentResolver.

  1. Ajouter des données : Pour insérer des données, la classe ContentResolver propose deux méthodes : insert() et bulkInsert(). La première permet d'insérer une seule donnée et la seconde d'insérer un jeu de données en un seul appel.
    // Créer un objet contenant les valeurs de la donnée à ajouter
    ContentResolver fournisseur = getContentResolver();
    ContentValues valeur = new ContentValues();
    valeur.put("nom", "REMY");
    valeur.put("prénom", "Emmanuel");
    Uri nouvelleValeur = fournisseur.insert(MonFournisseur.CONTENT_URI, valeur);
    
    // Insérer des données en masse dans un fournisseur de contenu
    ContentResolver fournisseur = getContentResolver();
    ContentValues[ ] valeurs = new ContentValues[5];
    valeurs[0] = new ContentValues();
    valeurs[0].put("nom", "REMY");
    valeurs[0].put("prénom", "Emmanuel");
    valeurs[1] = new ContentValues();
    valeurs[1].put("nom", "ALBIN");
    valeurs[1].put("prénom", "Michel");
    ...
    int nombreValeurs = fournisseur.bulkInsert(MonFournisseur.CONTENT_URI, valeurs);  
  2. Mettre à jour les données : Vous avez trouvé les éléments qui vous intéressent depuis le fournisseur de contenu et vous souhaitez les mettre à jour ? Pour cela, la classe ContentResolver met à votre disposition la méthode update(). Celle-ci prend en paramètres une valeur ContentValues spécifiant les valeurs de chaque colonne de l'entrée ainsi qu'un paramètre de filtre pour spécifier quel ensemble de données est concernée par cette mise à jour. Tous les éléments répondant à la condition du filtre sont mis à jour et la valeur que retourne la méthode update() est le nombre d'éléments ayant été mis à jour avec succès.
    // Mise à jour des éléments du fournisseur de contenu
    ContentResolver fournisseur = getContentResolver();
    ContentValues valeur = new ContentValues();
    valeur.put("nom", "REMY");
    valeur.put("prénom", "Emmanuel");
    String filtre = "nom = REMY" ;
    int nombreValeurs = fournisseur.update(MonFournisseur.CONTENT_URI, valeur, filtre, null);
  3. Supprimer des données : Tout comme le curseur de base de données, vous avez la possibilité de supprimer des éléments directement depuis le fournisseur de contenu avec la méthode delete(). Vous pouvez supprimer une seule valeur, celle ciblée par l'URI, ou plusieurs valeurs en spécifiant un paramètre de filtre.
    // Suppression de l'élément possédant un id de 10
    ContentResolver fournisseur = getContentResolver();
    Uri requête = Uri.parse("content://fr.btsiris.bd");
    Uri recherche = ContentUris.withAppendedId(requête, 10);
    fournisseur.delete(recherche, null, null);  
    // Suppression de tous les éléments du fournisseurs de contenu dont le prénom est 'Julien'
    String filtre = "prénom = Julien" ;      
    fournisseur.delete(requête, filtre, null);  

Créer un fournisseur de contenu

La création d'un fournisseur de contenu est d'une importance capitale lorsqu'une application souhaite mettre ses données à disposition d'autres applications. Si elle ne les garde que pour elle-même, vous pouvez éviter la création d'un fournisseur de contenu en vous contentant d'accéder directement aux données depuis vos activités.

Un fournisseur de contenu est une classe Java, tout comme une activité est une intention, la principale étape de la création d'un fournisseur consiste à produire sa classe Java, qui doit hériter de ContentProvider.

Cette sous-classe doit implémenter six méthodes qui, ensemble, assurent le service qu'un fournisseur de contenu est censé offrir aux activités qui veulent créer, lire, modifier ou supprimer du contenu.

La création d'un fournisseur de contenu s'effectue en dérivant une classe de ContentProvider et en redéfinissant les méthodes du tableau ci-dessous en fonction de vos besoin :

  1. onCreate() : Obligatoire. Appelée lors de la création du fournisseur de contenu. Renvoie un booléen pour spécifier que le fournisseur de contenu a été chargé avec succès.
  2. query() : Requête sur le fournisseur de contenu. Retourne un curseur permettant au programme local l'ayant appelé de naviguer au sein des résultats retournés.
  3. delete() : Suppression d'une ou plusieurs lignes de données. L'entier retourné représente le nombre de lignes supprimées.
  4. insert() : Appelée lorsqu'une insertion de données doit être réalisée sur le fournisseur de contenu. L'URI renvoyée correspond à la ligne nouvellement insérée.
  5. update() : Mise à jour d'une URI. Toutes les lignes correspondant à la sélection seront mises à jour avec les valeurs fournies dans l'objet de type ContentValues des paramètres.
  6. getType() : Retourne le type de contenu MIME à partir de l'URI spécifiée en paramètre.

La méthode query() retourne un objet Cursor vous permettant de manipuler et de naviguer dans les données de la même façon qu'avec un curseur de base de données SQLite (Cursor n'est qu'une interface). Par conséquent, vous pouvez utiliser n'importe quel curseur disponible dans le SDK Android : SQLiteCursor (base de données SQLite), MatrixCursor (pour les données non stockées dans une base de données), MergeCursor, etc.

  1. De façon à identifier sans ambiguïté et pouvoir manipuler un fournisseur de conetnu, vous devez spécifier une URI unique, en général basé sur l'espace de nom de la classe d'implémentation du fournisseur. De façon à être accessible par tous les développeurs et applications, l'URI doit être exposé sous la variable publique statique nommée CONTENT_URI : "content://fr.btsiris.bd/personnes".
  2. Avec cette URI, vous demanderez au fournisseur de contenu que nous allons élaborer de renvoyer toutes les personnes enregistrées actuellement dans la base de données. Si vous complétez la requête en la prefixant avec un numéro, vous demanderez uniquement la personne souhaitée à l'aide de son identifiant : "content://fr.btsiris.bd/personnes/10".
  3. Le SDK d'Android possède une classe UriMatcher permettant de faciliter la gestion des URI. En utilisant et configurant celle-ci, vous n'aurez pas à scinder les URI vous-même pour essayer d'en extraire le contenu, la classe UriMatcher le fera pour vous.

Ce chapitre n'est pas erminé et sera traité dans un avenir proche.
.

 

Choix du chapitre Création d'un service

Les services d'Android sont destinés aux processus de longue haleine, qui peuvent devoir continuer de s'exécuter même lorsqu'ils sont séparés de toute activité. A titre d'exemple, citons la musique, qui continue d'être jouée même lorsque l'activité de "lecture" a été supprimée, la récupération des mises à jour des flux RSS sur Internet et la persistance d'une session de messagerie instantanée, même lorsque le client a perdu le focus à cause de la réception d'un appel téléphonique.

Un service est créé lorsqu'il est lancé manuellement (via un appel à l'API) ou quand une activité tente de s'y connecter via une communication interprocessus (IPC). Il perdure jusqu'à ce qu'il ne soit plus nécessaire et que le système ait besoin de récupérer de la mémoire. Cette longue exécution ayant un coût, les services doivent prendre garde à ne pas consommer trop de CPU sous peine d'user prématurément la batterie du terminal.

A la différence d'autres plates-formes du marché, Android offre un environnement pour ces applications sans interface utilisateur. Ainsi, les services sont des applications dérivant de la classe android.app.Service, qui s'exécutent en arrière-plan et qui sont capables de signaler à l'utilisateur qu'un événement ou une information nouvelle requiert son attention.

  1. Contrairement aux activités qui offrent une interface utilisateur, les services en sont complètement dépourvus. Ils peuvent cependant réagir à des événements, mettre à jour des fournisseurs de contenu, effectuer n'importe quel autre traitement..., et enfin notifier leur état à l'utilisateur via des toasts et des notifications.
  2. Ces services, complètement invisibles pour l'utilisateur, possèdent un cycle de vie similaire à une activité et peuvent être contrôlés depuis d'autres applications (activité, service et Broadcast Receiver). Il est ainsi commun de réaliser une application composée d'activités et d'un service ; un lecteur de musique utilisera une interface de sélection des musiques alors que le service jouera celles-ci en arrière plan.
  3. Les applications qui s'exécutent fréquemment ou continuellement sans nécessiter constamment une interaction avec l'utilisateur peuvent être implémentées sous la forme d'un service. Les applications qui récupèrent des informations (informations, météo, résultats sportifs, etc.) via des flux RSS/Atom représentent de bons exemples de services.

Création d'un service

Les services sont conçus pour s'exécuter en arrière plan et nécessitent de pouvoir être démarrés, contrôlés et stoppés par d'autres applications. Les services partagent un certain nombre de comportements avec les activités. C'est pourquoi vous retrouverez des méthodes similaires à celles abordées précédemment pour les activités. Pour créer un service, dérivez votre classe de android.app.Service puis, à l'instar d'une activité, redéfinissez les méthodes du cycle de vie avant de déclarer le service dans le fichier de configuration de l'application.

Les services peuvent redéfinir trois méthodes : onCreate(), onStart() et onDestroy(). Les méthodes onCreate() et onDestroy() sont respectivement appelées lors de la création et de la destruction du service, à l'instar d'une activité.

La méthode onBind() est la seule méthode que vous devez obligatoirement implémenter dans un service. Cette méthode retourne un objet IBender qui permet au mécanisme IPC - communication interprocessus permettant d'appeler des méthodes distantes - de fonctionner.

Une fois votre service créé, il vous faudra, comme pour les activités d'une application, déclarer celui-ci dans le fichier de configuration de l'application au moyen de la balise <service android:name=".MonService" />. Voici les paramètres supplémentaires que vous pouvez spécifier sous forme d'attributs dans l'élément <service> :

  1. process : Spécifie un nom de processus dans lequel le code sera exécuté.
  2. enabled : Indique si le service est actif ou non (peut-être instancié par le système).
  3. exported : Booléen indiquant si le service est utilisable par d'autres applications.
  4. permission : Spécifie une permission nécessaire pour exécuter ce service.

En plus des attributions dans l'élément <service>, vous pouvez utiliser l'élément <meta-data> pour spécifier des données supplémentaires et une section <intent-filter> à l'instar des activités.

Démarrer et arrêter un service

Les services peuvent être démarrés manuellement (via l'appel de la méthode startService() ou via un objet Intent) ou quand une activité essaie de se connecter au service via une communication interprocessus (IPC).

  1. Pour démarrer un service manuellement, utilisez la méthode startService(), laquelle accepte en paramètre un objet Intent. Ce dernier permet de démarrer un service en spécifiant son type de classe ou une action spécifiée par le filtre d'intention du service.
    // Démarre le service explicitement
    startService(new Intent(this, MonService.class));
    
    // Démarre le service en utilisant une action (enregistré dans Intent Filter).
    startService(new Intent(MonService.MON_ACTION_SPECIALE));

    Le moyen le plus simple de démarrer un service est de spécifier le nom de sa classe en utilisant le paramètre class. Cette option ne fonctionne que si le service est un composant de votre application et non un service tiers. Si le service à démarrer n'a pas été créé par vous, la seule alternative sera d'utiliser l'Intent associé à ce service.

  2. La méthode startService() peut être appelée plusieurs fois, ce qui aura pour conséquence d'appeler plusieurs fois la méthode onStart() du service. Sachez donc gérer cette situation, de façon à ne pas allouer de ressources inutiles ou réaliser des traitements, qui rendraient caduque la bonne exécution de votre service.
  3. Pour arrêter un service, utilisez la méthode stopService() avec l'action que vous avez utilisée lors de l'appel à la méthode startService() :
    stopService(new Intent(this, monService.getClass()));

Contrôler un service depuis une activité

L'exécution d'un service requiert le plus souvent d'avoir pu configuré ce dernier, souvent à l'aide d'une interface que l'utilisateur pourra utiliser pour spécifier ou changer les paramètres. Afin de pouvoir communiquer avec un service, vous avez plusieurs possibilités à votre disposition.

  1. La première consiste à exploiter le plus possible le mécanisme des intentions au sein du service.
  2. La seconde (dans le cas de deux applications distinctes) consiste à effectuer un couplage fort entre les processus de l'application appelante et le processus du service en utilisant le langage AIDL (Android Interface Definition Language), qui permet la définition de l'interface du service au niveau du système - ce thème ne sera pas abordé dans cette étude.
  3. Il existe aussi une autre alternative qui combine ces deux possibilités en contrôlant un service à l'aide d'une activité et en les liant, à condition que le service et l'activité fassent partie de la même application. C'est cette dernière façon que nous allons détailler ci-après.

Lorsqu'une activité est liée à un service, celle-ci possède une référence permanente vers le service, qui vous permet de communiquer avec le service comme si ce dernier n'était qu'une simple référence à un objet de votre activité.

Pour permettre à une activité de s'associer à un service, la méthode onBind() doit être implémentée au niveau du service. Cette méthode retourne un objet qui sera utilisé plus tard par l'activité pour récupérer la référence vers le service. La méthode onBind() doit renvoyer un type IBinder propre à votre service, ce qui signifie que vous devez créer votre propre implémentation de l'interface IBinder :

public class ServiceConversion extends Service {
...
   @Override
   public IBinder onBind(Intent intention) { return new ConversionBinder();  }
   
   public class ConversionBinder extends Binder {
      ServiceConversion getService() { return ServiceConversion.this; }
   }
}

Une fois la méthode onBind() implémentée au niveau de votre service, vous devez maintenant connecter l'activité au service. Pour ce faire, vous devez utiliser une connexion de type ServiceConnection : ce sont les méthodes de cette dernière qui seront appelées par l'activité pour se connecter ou se déconnecter du service.

Créez la connexion au niveau de l'activité, en redéfinissant les méthodes onServiceConnected() et onServiceDisconnected() de la classe ServiceConnection. La première méthode sera appelée lors de la connexion de l'activité au service et la seconde lors de la déconnexion.

public class Conversion extends Activity {
   private ServiceConversion service;
   
   private ServiceConnection connexion = new ServiceConnection() {
      public void onServiceConnected(ComponentName nom, IBinder binder) {
         service = ((ServiceConversion.ConversionBinder)binder).getService();
      }

      public void onServiceDisconnected(ComponentName nom) {
         service = null;
      }
   };
}

Une fois que la méthode onBind() du service est implémentée et qu'un objet ServiceConnection est prêt à faire office de médiateur, l'activité doit encore demander de façon explicite l'association au service, à l'aide de la méthode bindService(). Cette méthode prend trois paramètres :

  1. L'intention du service que vous souhaitez invoquer, souvent de façon explicite directement en utilisant la classe du service ;
  2. Une instance de la connexion créée vers le service ;
  3. Un drapeau spécifiant les options d'association, souvent 0 ou la constante BIND_AUTO_CREATE. Une autre valeur BIND_DEBUG_UNBIND permet d'afficher des informations de débogage. Vous pouvez combiner les deux valeurs à l'aide de l'opérateur binaire &.
public class Conversion extends Activity {

   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState); 
       Intent soumettre = new Intent(this, ServiceConversion.class);
       bindService(soumettre, connexion, Context.BIND_AUTO_CREATE);
   } 
}

Une fois l'association effectuée à l'aide de la méthode bindService(), la méthode onServiceConnected() sera appelée et la référence vers le service associé sera récupérée. Vous pourrez ainsi appeler toutes les méthodes et propriétés publiques du service au travers de cette référence comme s'il s'agissait d'un simple objet de l'activité, et de cette façon contrôler le service au travers de l'interface utilisateur proposée par l'activité. Pour vous déconnecter, utilisez la méthode unbindService() de l'instance de l'activité.

A titre d'exemple, nous allons modifier un projet que nous avons déjà mis en oeuvre lors de l'étude précédente. Il s'agit de la conversion monétaire. Bien que cela ne soit pas très utile, je vous propose, comme un cas d'école, d'intégrer à ce projet un service qui s'occupera de la conversion elle-même.

Au niveau du visuel, nous ne verrons aucune modification apparente si ce n'est la présence d'affichage de messages qui nous montrent le démarrage et l'arrêt du service.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.conversion"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Service en action" >
        <activity android:name="Conversion" android:label="Conversion">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name="ServiceConversion" />
    </application>
</manifest>   
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:stretchColumns="1">
    <TableRow>
       <fr.btsiris.conversion.Monnaie 
          android:id="@+id/euro"
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content"
          android:layout_span="3"/>
       <Button     
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content" 
          android:text="Franc"
          android:onClick="calculFranc"  />
    </TableRow>
    <TableRow>
       <fr.btsiris.conversion.Monnaie 
          android:id="@+id/franc"
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content"
          android:layout_span="3"/>
       <Button     
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content" 
          android:text="€uro"
          android:onClick="calculEuro"  />
    </TableRow>    
</TableLayout>
fr.btsiris.conversion.ServiceConversion.java
package fr.btsiris.conversion;

import android.app.*;
import android.content.Intent;
import android.os.*;
import android.widget.Toast;

public class ServiceConversion extends Service {
   private final double TAUX = 6.55957;

   @Override
   public void onCreate() {
      super.onCreate();
      Toast.makeText(this, "Service en action", Toast.LENGTH_LONG).show();
   }

   @Override
   public void onDestroy() {
      super.onDestroy();
      Toast.makeText(this, "Service désactivé", Toast.LENGTH_LONG).show();
   }
   
   public double euroFranc(double euro) {
      return euro * TAUX;
   }
   
   public double francEuro(double franc) {
      return franc / TAUX;
   }   
   
   @Override
   public IBinder onBind(Intent intention) { return new ConversionBinder();  }
   
   public class ConversionBinder extends Binder {
      ServiceConversion getService() { return ServiceConversion.this; }
   }
} 

Dans ce cas de figure, il n'est absolument pas nécessaire de redéfinir les méthodes de rappel onStart() et onDestroy(). Je les ai placés juste pour vérifier le moment de l'activation et de la désactivation du service, comme un système de débogage.

fr.btsiris.conversion.Monnaie.java
package fr.btsiris.conversion;

import android.content.Context;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.EditText;
import java.text.*;

public class Monnaie extends EditText {
   private NumberFormat formatMonnaie;

   public Monnaie(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      init();
   }

   public Monnaie(Context context, AttributeSet attrs) {
      super(context, attrs);
      init();
   }

   public Monnaie(Context context) {
      super(context);
      init();
   }
   
   private void init() {
      setSymbole('€');
      setValeur(0.0);
      setGravity(Gravity.RIGHT);
      setInputType(InputType.TYPE_CLASS_NUMBER);
   }

   public void setSymbole(char symbole) {
      formatMonnaie = new DecimalFormat("#,##0.00 "+symbole);
   }

   public double getValeur() {
      try {
         Number valeur = (Number) formatMonnaie.parse(getText().toString());
         setText(formatMonnaie.format(valeur));
         return valeur.doubleValue();
      } 
      catch (ParseException ex) { return 0.0; }
   }

   public void setValeur(double valeur) {
      setText(formatMonnaie.format(valeur));
   }
}  
fr.btsiris.conversion.Conversion.java
package fr.btsiris.conversion;

import android.app.Activity;
import android.content.*;
import android.os.*;
import android.view.View;

public class Conversion extends Activity {
   private Monnaie euro, franc;
   private ServiceConversion service;
   
   private ServiceConnection connexion = new ServiceConnection() {
      public void onServiceConnected(ComponentName nom, IBinder binder) {
         service = ((ServiceConversion.ConversionBinder)binder).getService();
      }

      public void onServiceDisconnected(ComponentName nom) {
         service = null;
      }
   };
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState); 
       setContentView(R.layout.main);
       euro = (Monnaie) findViewById(R.id.euro);
       franc = (Monnaie) findViewById(R.id.franc);
       franc.setSymbole('F');     
       franc.setValeur(0.0);
       Intent soumettre = new Intent(this, ServiceConversion.class);
       bindService(soumettre, connexion, Context.BIND_AUTO_CREATE);
   }

   @Override
   protected void onDestroy() {
      super.onDestroy();
      unbindService(connexion);
   }   
    
   public void calculFranc(View vue) {
      franc.setValeur(service.euroFranc(euro.getValeur()));
   }
   
   public void calculEuro(View vue) {
      euro.setValeur(service.francEuro(franc.getValeur()));
   }   
}

 

Choix du chapitre Géolocalisation, Google Maps et GPS

L'une des caractéristiques essentielles des téléphones mobiles est leur portabilité, et il n'est donc pas surprenant que quelques-unes des fonctionnalités les plus séduisantes d'Android soient les services permettant de déterminer, de contextualiser et de cartographier des positions géographiques.

Vous pouvez créer des activités fondées sur des cartes utilisant Google Maps comme un élément d'interface utilisateur. Vous aurez un accès complet aux cartes, ce qui vous permettra de contrôler les paramètres d'affichage, de changer le niveau de zoom et de faire un panoramique. Vous pourrez annoter les cartes et gérer la saisie de l'utilisateur pour produire des informations et des fonctionnalités contextuelles.

Nous couvrirons également dans ce chapitre les services de géolocalisation, qui permettent de déterminer la position courante de l'appareil. Ils incluent le GPS et la technologie Google de localisation cellulaire. Vous pourrez spécifier quelle technologie utiliser en la nommant explicitement ou implicitement, en définissant un ensemble de critères en termes de précision, coût ou autres.

Les cartes et les services de géolocalisation utilisent la latitude et la longitude pour identifier les positions géographiques, mais la plupart des utilisateurs raisonnent plutôt en terme d'adresses. Android fournit un Geocoder qui supporte les géocodages avant et inverse. Grâce à lui, vous pourrez convertir latitudes et longitudes en adresses et inversement.

Utilisés conjointement, la cartographie, le géocodage et les services de géolocalisation fournissent une puissante boîte à outils pour incorporer la mobilité native de votre téléphone à vos applications. Les services de géolocalisation d'Android sont donc divisés en deux grandes parties :

  1. Les API qui gèrent les plans (dans l'espace de noms com.google.android.maps) ;
  2. Les API qui gèrent la localisation à proprement parler (dans l'espace de noms android.location).

Nous allons d'abord voir comment déterminer la position d'un appareil sur Android. Ensuite, nous verrons en deux temps comment utiliser l'émulateur pour simuler des positions en étudiant d'abord la génération des fichiers de points et ensuite leur emploi. La suite du chapitre sera consacrée aux capacités de la plate-forme et notamment à l'affichage de cartes Google Maps ainsi qu'aux différents dessins ou affichage réalisables grâce à ces mêmes cartes.

Utiliser les services de géolocalisation

Les services de géolocalisation sont un terme générique désignant les différentes technologies utilisées pour déterminer la position courante d'un appareil. Les deux principaux éléments sont les suivants :

  1. Location Manager : Fournit des points d'entrée vers les services de géolocalisation.
  2. Location Providers : (fournisseurs de position) : Chacun d'eux représente une technologie de localisation de la position de l'appareil.

A l'aide du Location Manager vous pouvez :

  1. Obtenir une position courante.
  2. Suivre des déplacements.
  3. Déclencher des alertes de proximité en détectant les mouvements dans une zone spécifique.
  4. Trouver les Location Providers disponibles.

Lorsqu'il s'agit de déterminer la position courante de l'appareil, plusieurs problèmes peuvent être rencontrés :

  1. Tous les appareils ne disposent pas tous du même matériel de géolocalisation (certains n'ont pas de récepteur GPS) ;
  2. Les conditions d'utilisation du téléphone peuvent rendre inutilisable une méthode de localisation (cas de fonctionnalités GPS dans un tunnel, par exemple).
Sélectionner un fournisseur de position - Location Provider

En fonction de l'appareil, Android peut utiliser plusieurs technologies pour déterminer la position courante. Chacune d'elles, appelée Location Provider, offrira diverses caractéristiques en matière de consommation d'énergie, de coût, de précision et de capacité à déterminer l'altitude, la vitesse ou le cap.

C'est pourquoi Android offre plusieurs moyens de localisation au travers d'une liste de fournisseurs de positions : selon les conditions du moment ou vos propres critères, Android se chargera de sélectionner le plus apte à donner la position de l'appareil.

Il faudra donc récupérer cette liste de fournisseurs et déterminer celui qui est le plus adapté à l'utilisation souhaitée. De manière générale, nous distinguons deux types de fournisseurs naturels :

  1. Le fournisseur basé sur la technologie GPS, de type LocationManager.GPS_PROVIDER. C'est le plus précis des deux, mais c'est également le plus consommateur en terme de batterie.
  2. Le fournisseur qui se repère grâce aux antennes des opérateurs mobiles et aux points d'accès WI-FI, de type LocationManager.NETWORK_PROVIDER.
// Ainsi, pour obtenir une instance d'un provider spécifique, appelez la méthode getProvider() en lui passant son nom :
String fournisseur = LocationManager.GPS_PROVIDER;
LocationProvider gpsProvider = sysLocalisation.getProvider(fournisseur);

Ceci n'est en général utile que pour déterminer les capacités d'un provider particulier. La plupart des méthodes d'un Location Manager ne requièrent que le nom du provider pour exécuter les services de géolocalisation.

Obtenir la liste des fournisseurs de position

Le but des services de géolocalisation est de déterminer la position de l'appareil. L'accès à ces services est géré par le système au travers de la méthode getSystemService(). La classe des fournisseurs de position, LocationProvider, est une classe abstraite : chacune des différentes implémentations (correspondant aux différents moyens de localisation à notre disposition) fournit l'accès aux informations de localisation d'une manière qui lui est propre.

//  Le code suivant permet d'obtenir la liste de tous les fournisseurs :
LocationManager sysLocalisation = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
List<String> fournisseurs = sysLocalisation.getAllProviders();

A un instant donné, tous les outils de géolocalisation de l'appareil ne sont peut-être pas disponibles : l'utilisateur peut en effet décider de désactiver certains moyens (par exemple, le GPS peut être désactivé pour économiser les batteries).

Une autre alternative permet ainsi d'obtenir la liste de tous les providers disponibles sur l'appareil en appelant cette fois-ci la méthode getProviders() en spécifiant un booléen pour indiquer si vous désirez toute la liste des fournisseurs ou uniquement ceux qui sont activés :

//  Le code suivant permet d'obtenir la liste de tous les fournisseurs activés :
LocationManager sysLocalisation = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
boolean actifsSeulement = true;
List<String> fournisseurs = sysLocalisation.getProviders(actifsSeulement);

Comme nous l'avons vu plus haut, si vous souhaitez obtenir un fournisseur particulier, appelez plutôt la méthode getProvider() en spécifiant le type de fournisseur à l'aide des constantes prédéfinies LocationManager.GPS_PROVIDER ou LocationManager.NETWORK_PROVIDER :

LocationManager sysLocalisation = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
String fournisseur = LocationManager.GPS_PROVIDER;
LocationProvider gpsProvider = sysLocalisation.getProvider(fournisseur);

Comme la plupart des fonctionnalités sous Android, pour utiliser ces quelques lignes de code, il ne faut pas oublier de déclarer les permissions appropriées dans la section <manifest> du fichier de configuration de l'application.

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
    <application android:label="@string/app_name" >
		....    
    </application>
    //  L'utilisation du fournisseur de type GPS est lié à la déclaration de permission suivante :
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    //  L'utilisation du fournisseur réseau dépend quant à lui de la permission :
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
</manifest>

Nous retrouvons ici la notion de localisation grossière ou fine suivant la méthode utilisée par l'implémentation.
.

Je rappelle encore une fois que le but des services de géolocalisation est de déterminer la position de l'appareil. Une seule ligne de code suffit pour cela. Il faut cependant choisir auparavant un fournisseur de position. Une fois cette étape réalisée, nous utilisons la méthode getLastKnownLocation() issue de la classe LocationManager, avec comme paramètre le fournisseur de position choisi. Le retour de cette méthode est une instance d'un objet Location si une position a été calculée, sinon son résultat vaut null :

LocationManager sysLocalisation = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
String fournisseur = LocationManager.GPS_PROVIDER;
Location localisation = sysLocalisation.getLastKnownLocation(fournisseur);

L'objet de type Location renvoyé par la méthode getLastKnownLocation() inclut toutes les informations de position disponibles données par le fournisseur : latitude, longitude, cap, altitude, vitesse et heure à laquelle la position a été déterminée. Toutes ces propriétés sont accessibles via les méthodes getXxx() de l'objet Location. Dans certaines instances, des détails additionnels sont fournis dans le Bundle des compléments.

Choix d'un fournisseur de position en fonctions de critères

Il est peu probable dans la plupart des scenarii que vous souhaitiez explicitement choisir le fournisseur de position à utiliser. Plus communément, vous spécifierez les exigences qu'un fournisseur devra respecter et laisserez Android déterminer la meilleure technologie à utiliser.

Une méthode existe dans Android pour choisir un fournisseur à partir de besoins que vous formulez, ce qui permet de déterminer le fournisseur le plus adapté à vos attentes.

La classe centrale pour la définition des critères se nomme Criteria. Grâce à elle vous pouvez spécifier vos exigences en terme de précision (fine ou approximative), de consommation d'énergie (faible, moyenne, élevée), de coût et de capacité à renvoyer l'altitude, la vittesse et le cap.

//  Spécifie des critères de précision approximative, de faible consommation d'énergie et pas d'altitude, cap et vitesse.
Criteria critères = new Criteria(); 
critères.setAccuracy(Criteria.ACCURACY_COARSE);
critères.setPowerRequirement(Criteria.POWER_LOW);
critères.setAltitudeRequired(false);
critères.setBearingRequired(false);
critères.setSpeedRequired(false);
critères.setCostAllowed(true);
//  Spécifie des critères de localisation la plus fine possible avec l'altitude fournie obligatoirement.
Criteria critères = new Criteria(); 
critères.setAccuracy(Criteria.ACCURACY_FINE);
critères.setAltitudeRequired(true);

Une fois les critères définis, nous utilisons la méthode getBestProvider() de l'instance de la classe LocationManager récupérée un peu plus tôt. Le booléen en paramètre donne la possibilité de se restreindre aux fournisseurs activés uniquement.

LocationManager sysLocalisation = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
Criteria critères = new Criteria(); 
critères.setAccuracy(Criteria.ACCURACY_FINE);
critères.setAltitudeRequired(true);
String fournisseur = sysLocalisation.getBestProvider(critères, true);

Si plus d'un fournisseur de position correspond à vos critères, celui possédant la précision la plus élevée est renvoyé. Si aucun fournisseur ne répond à vos exigences, les critères sont assouplis dans l'ordre suivant jusqu'à ce qu'un fournisseur soit trouvé :

  1. Consommation d'énergie.
  2. Précision.
  3. Capacité à déterminer le cap, la vitesse et l'altitude.

Le critère de coût n'est jamais modifié. Si aucun fournisseur n'est trouvé, la valeur null est renvoyée.
.

Avant d'aller plus loin, je vous propose de prendre tout de suite un premier exemple de géolocalisation qui permet de récupérer tout simplement la latitude et la longitude de votre mobile juste au démarrage de votre mobile.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.localisation"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="GPS" >
        <activity android:name="Geolocalisation" android:label="Où suis-je ?">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView  
        android:id="@+id/latitude"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content"  />
    <TextView  
        android:id="@+id/longitude"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content"  />
</LinearLayout>
fr.btsiris.localisation.Geolocalisation.java
package fr.btsiris.localisation;

import android.app.Activity;
import android.content.Context;
import android.location.*;
import android.os.Bundle;
import android.widget.TextView;

public class Geolocalisation extends Activity {
   private TextView latitude, longitude;
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        latitude = (TextView) findViewById(R.id.latitude);
        longitude = (TextView) findViewById(R.id.longitude);
        
        LocationManager localisations = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        Location position = localisations.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        if (position!=null) {
            latitude.setText("Latitude : "+position.getLatitude());
            longitude.setText("Longitude : "+position.getLongitude());           
        }
        else latitude.setText("Position  non déterminée ");
    }
}

Si vous ne possédez pas de capteur GPS dans votre mobile, prévoyez le type de fournisseur LocationManager.NETWORK_PROVIDER. Je vous propose une autre alternative, qui aboutit au même résultat, qui prend en compte le choix du fournisseur en fonctions de différents critères.

fr.btsiris.localisation.Geolocalisation.java
package fr.btsiris.localisation;

import android.app.Activity;
import android.content.Context;
import android.location.*;
import android.os.Bundle;
import android.widget.TextView;

public class Geolocalisation extends Activity {
   private TextView latitude, longitude;
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        latitude = (TextView) findViewById(R.id.latitude);
        longitude = (TextView) findViewById(R.id.longitude);
        
        LocationManager localisations = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        Criteria critères = new Criteria(); 
        critères.setAccuracy(Criteria.ACCURACY_COARSE);
        critères.setPowerRequirement(Criteria.POWER_LOW);
        critères.setAltitudeRequired(false);
        critères.setBearingRequired(false);
        critères.setSpeedRequired(false);
        critères.setCostAllowed(true);
        
        String fournisseur = localisations.getBestProvider(critères, true);        
        Location position = localisations.getLastKnownLocation(fournisseur);
        if (position!=null) {
            latitude.setText("Latitude : "+position.getLatitude());
            longitude.setText("Longitude : "+position.getLongitude());  
        }
        else latitude.setText("Position  non déterminée ");
    }
}
Suivre les déplacements

La plupart des applications de localisation doivent réagir aux déplacements de l'utilisateur. Se contenter de sonder le LocationManager ne suffira pas à forcer l'obtention des nouvelles mises à jour des Location Providers. Détecter le changement de position relève de deux problématiques : recevoir des mises à jour de sa position et détecter le mouvement.

Ces deux problématiques de changement se résolvent par l'utilisation de la même méthode du LocationManager. Il s'agit de la méthode requestLocationUpdates() dont voici les paramètres utiles :

LocationManager sysLocalisation = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
String fournisseur = LocationManager.GPS_PROVIDER;
sysLocalisation.requestLocationUpdates(fournisseur, temps, distance, écouteurLocalisation);
  1. Le premier paramètre est le fournisseur de position souhaité.
  2. Ensuite, le deuxième paramètre indique le temps entre deux mises à jour exprimé en millisecondes. Attention à ne pas mettre de valeur trop faibles, qui accaparerait les ressources du téléphone et viderait votre batterie. La documentation du SDK recommande une valeur minimale de 60 000 ms.
  3. Le troisième paramètre précise la distance correspond au nombre de mètres qui doivent être parcourus avant de recevoir une nouvelle position. Si elle est supérieure à zéro, la mise à jour s'effectuera uniquement une fois la distance parcourue.
  4. Enfin, le dernier paramètre est l'écouteur (une implémentation de l'interface LocationListener) qui recevra les diverses notifications. Cet écouteur propose quatre méthodes que vous devez redéfinir suivant les circonstances :
    class ChangementPosition implements LocationListener {
           
        public void onLocationChanged(Location position) {
            // Met à jour l'application en fonction de la nouvelle position.
        }
    
        public void onStatusChanged(String fournisseur, int statut, Bundle extras) {
            // Met à jour l'application si le status du matériel a changé.
        }
    
        public void onProviderEnabled(String fournisseur) {
            // Met à jour l'application lorsque le fournisseur est activé.
        }
    
        public void onProviderDisabled(String fournisseur) {
            // Met à jour l'application lorsque le fournisseur est désactivé.
        }       
    }
  5. Pour arrêter les mises à jour, il faut fournir au gestionnaire de localisation l'instance de l'écouteur à retirer au travers de la méthode removeUpdates().

Les GPS ont pour la plupart une consommation électrique significative. Pour la minimiser, vous devez désactiver les mises à jour à chaque fois que cela est possible, particulièrement lorsqu'elles servent à mettre à jour l'interface utilisateur mais que votre application n'est pas visible. Vous pouvez également améliorer les performances en augmentant le temps minimal entre les mises à jour.

Dans ce deuxième exemple nous prévoyons une mise à jour de la position de votre mobile qui permet de suivre votre déplacement :
fr.btsiris.localisation.Geolocalisation.java
package fr.btsiris.localisation;

import android.app.Activity;
import android.content.Context;
import android.location.*;
import android.os.Bundle;
import android.widget.*;

public class Geolocalisation extends Activity {
   private TextView latitude, longitude;
   private LocationManager localisations;
   private String fournisseur;
   
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        latitude = (TextView) findViewById(R.id.latitude);
        longitude = (TextView) findViewById(R.id.longitude);     
        
        Criteria critères = new Criteria(); 
        critères.setAccuracy(Criteria.ACCURACY_COARSE);
        critères.setPowerRequirement(Criteria.POWER_LOW);
        critères.setAltitudeRequired(false);
        critères.setBearingRequired(false);
        critères.setSpeedRequired(false);
        critères.setCostAllowed(true);
        
        localisations = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        fournisseur = localisations.getBestProvider(critères, true);   
        localisations.requestLocationUpdates(fournisseur, 5000, 5, new ChangementPosition());
    }

    class ChangementPosition implements LocationListener {
         public void onLocationChanged(Location position) {
            if (position!=null) {
                  latitude.setText("Latitude : "+position.getLatitude());
                  longitude.setText("Longitude : "+position.getLongitude());  
            }
            else latitude.setText("Position  non déterminée ");
            Toast.makeText(Geolocalisation.this, "Nouvelle position", Toast.LENGTH_SHORT).show();
         }
         public void onStatusChanged(String fournisseur, int statut, Bundle extras) {  }
         public void onProviderEnabled(String fournisseur) {  }
         public void onProviderDisabled(String fournisseur) {  } 
    }
}  

Voici une autre alternative qui prend en compte soit le GPS intégré soit le réseau téléphonique. Par ailleurs, je fais en sorte d'arrêter le service de géolocalisation lorsque l'activité n'est plus en action. Le service se remettra automatiquement en place lorsque nous réactiverons l'activité.

fr.btsiris.localisation.Geolocalisation.java
package fr.btsiris.localisation;

import android.app.Activity;
import android.content.Context;
import android.location.*;
import android.os.Bundle;
import android.widget.*;

public class Geolocalisation extends Activity {
   private TextView latitude, longitude;
   private LocationManager localisations;
   private String fournisseur;
   private ChangementPosition changements = new ChangementPosition();
   
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        latitude = (TextView) findViewById(R.id.latitude);
        longitude = (TextView) findViewById(R.id.longitude);     
        localisations = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        miseAJourDeLaPosition(localisations.getLastKnownLocation(LocationManager.GPS_PROVIDER));
    }

    @Override
    protected void onStart() {
        super.onStart();
        localisations.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2, changements);   
        localisations.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 5000, 5, changements); 
    }

    @Override
    protected void onStop() {
       super.onStop();
       localisations.removeUpdates(changements);
    } 

    private void miseAJourDeLaPosition(Location position) {
        if (position!=null) {
            latitude.setText("Latitude : "+position.getLatitude());
            longitude.setText("Longitude : "+position.getLongitude());  
        }
        else latitude.setText("Position  non déterminée ");
        Toast.makeText(Geolocalisation.this, "Nouvelle position", Toast.LENGTH_SHORT).show();       
    }
    
    class ChangementPosition implements LocationListener {
         public void onLocationChanged(Location position) { miseAJourDeLaPosition(position);  }
         public void onStatusChanged(String fournisseur, int statut, Bundle extras) { }
         public void onProviderEnabled(String fournisseur) { }
         public void onProviderDisabled(String fournisseur) { } 
    }
}
Alertes de proximité

Il est souvent utile de voir vos applications réagir lorsque l'utilisateur se déplace en direction d'un lieu particulier ou s'en éloigne. Les alertes de proximité permettent de mettre en place des déclencheurs exécutés dans ces cas-là.

Pour mettre en place une alerte de proximité pour une zone de donnée, sélectionnez le centre de celle-ci (en l'exprimant en longitude et latitude), le rayon et un délai d'expiration de l'alerte. Elle sera déclenchée si l'appareil entre ou sort du rayon défini.

Lorsqu'elles sont déclenchées, les alertes de proximité exécutent des intentions, le plus souvent des Broadcast Intents. Pour spécifier l'intention, utilisez un PendingIntent, une classe qui wrappe l'intention en une sorte de pointeur de méthode au moyen de la méthode getBroadcast() :

Intent intention = new Intent(MON_ACTION);
PendingIntent proximité = PendingIntent.getBroadcast(this, -1, intention, 0);

La méthode addProximityAlert() de la classe LocationManager permet, quant à elle, d'enregistrer un PendingIntent qui sera déclenché quand l'appareil se trouvera à une certaine distance du point fixé. La méthode possède la signature suivante :

public void addProximityAlert(double latitude, double longitude, float rayon, long expiration, PendingIntent intention);
  1. latitude : correspond à la latitude du point d'alerte ;
  2. longitude : correspond à la longitude du point d'alerte ;
  3. rayon : correspond au rayon autour du point d'alerte qui déterminera la sensibilité de la détection ;
  4. expiration : définit une période en millisecondes à la fin de laquelle l'élément LocationManager retirera le point d'alerte des alertes surveillées. La valeur -1 signifie que l'enregistrement est valide jusqu'à qu'il soit retiré manuellement par la méthode removeProximityAlert().
  5. intention : est utilisé pour générer l'intention déclenchée quand l'appareil rentre ou sort de la zone désignée par le point et le rayon.

L'intention PendingIntent est étendue avec un extra dont la clé se nomme KEY_PROXIMITY_ENTERING. Nous obtenons alors un booléen dont la valeur est true si nous sommes dans la zone définie et false si nous la quittons.

La gestion de l'alerte se réalise au travers d'un BroadcastReceiver, dont vous devez redéfinir la méthode de rappel onReceiver() qui permet de spécifier quelles actions vous souhaitez réaliser par rapport à la proximité du lieu, qu'il est ensuite nécessaire d'enregistrer au travers de la méthode registerReceiver(). Si l'activité n'est plus active, il est souhaitable d'enlever l'alerte au moyen de la méthode removeProximityAlert().

Afin de factoriser toutes ces petites connaissances que nous venons d'acquérir, je vous propose de reprendre l'exemple précédent, et de l'étoffer avec une alerte qui se produit lorsque nous sortons d'une zone de 7 mètres de rayon. Quand l'activité démarre, elle mesure la postion actuelle qui sert alors de référence pour l'alerte.
fr.btsiris.localisation.Geolocalisation.java
package fr.btsiris.localisation;

import android.app.*;
import android.content.*;
import android.location.*;
import android.os.Bundle;
import android.widget.*;

public class Geolocalisation extends Activity {
   private TextView latitude, longitude;
   private LocationManager localisations;
   private ChangementPosition changements = new ChangementPosition();
   private static final String ALERTE_PROXIMITE = "fr.btsiris.localisation";
   private PendingIntent proximite;
   private Location position;
   private boolean premiereFois = true;
   
    @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        latitude = (TextView) findViewById(R.id.latitude);
        longitude = (TextView) findViewById(R.id.longitude);     
        localisations = (LocationManager) getSystemService(Context.LOCATION_SERVICE);     
    }

    @Override
    protected void onStart() {
        super.onStart();
        position = localisations.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        localisations.requestLocationUpdates(LocationManager.GPS_PROVIDER, 3000, 2, changements);   
        localisations.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 3000, 2, changements); 
    }

    @Override
    protected void onStop() {
       super.onStop();
       localisations.removeUpdates(changements);
       localisations.removeProximityAlert(proximite);
    } 

    private void miseAJourDeLaPosition(Location position) {
       if (position!=null) {
          latitude.setText("Latitude : "+position.getLatitude());
          longitude.setText("Longitude : "+position.getLongitude());  
       }
       else latitude.setText("Position  non déterminée ");      
     }
    
    private void ajouterAlerte() {
       Intent intention = new Intent(ALERTE_PROXIMITE);
       proximite = PendingIntent.getBroadcast(this, -1, intention, 0);
       double lat = position.getLatitude();
       double lng = position.getLongitude();
       localisations.addProximityAlert(lat, lng, 7.0F, -1, proximite);
       IntentFilter filtre = new IntentFilter(ALERTE_PROXIMITE);
       registerReceiver(new Zone(), filtre);
    }
    
    class ChangementPosition implements LocationListener {
       public void onLocationChanged(Location position) { 
          if (premiereFois) { premiereFois = false; ajouterAlerte(); }
          miseAJourDeLaPosition(position);  
       }
       public void onStatusChanged(String fournisseur, int statut, Bundle extras) { }
       public void onProviderEnabled(String fournisseur) { }
       public void onProviderDisabled(String fournisseur) { } 
    }
    
    class Zone extends BroadcastReceiver {
       @Override
       public void onReceive(Context context, Intent intention) {
          String cle = LocationManager.KEY_PROXIMITY_ENTERING;
          boolean dans = intention.getBooleanExtra(cle, false);
          String alerte = dans ? "Rentre dans la zone" : "Sort de la zone";
          Toast.makeText(Geolocalisation.this, alerte, Toast.LENGTH_LONG).show();           
       }       
    }
}

Conversion d'adresses et d'endroits - Géocodage

Le géocodage permet de déterminer des coordonnées en latitude et longitude à partir d'une adresse ou d'une description d'un endroit. A l'inverse, le géocodage permet de retrouver un lieu en fonction de ses coordonnées. Ces fonctionnalités sont liées à l'API Google.

Ainsi, le géocodage, au travers de la classe Geocoder, permet de traduire automatiquement des adresses en coordonnées géographiques et inversement. Cela permet de connaître les positions utilisées par les services de géolocalisation et les activités géographiques. Les recherches de géocodage ont lieu sur le serveur et vos applications devront donc avoir la permission de se connecter à Internet :

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
    <application android:label="@string/app_name" >
		....    
    </application>
    //  L'utilisation du fournisseur de type GPS est lié à la déclaration de permission suivante :
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    //  L'utilisation du géocodage dépend quant à lui de la permission :
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>

La classe Geocoder donne accès à deux fonctions de géocodage :

  1. Géocodage avant : Détermine la latitude et la longitude d'une adresse.
  2. Géocodage inverse : Détermine l'adresse en fonction de la latitude et de la longitude.

Le résultat de ces appels sont contextualisés par le biais d'une locale (utilisée pour définir votre emplacement et votre langue habituels). L'extrait qui suit montre comment la mettre en oeuvre lors de la création du Geocoder. Si vous ne la spécifiez pas, celle de votre appareil sera utilisée par défaut.

Geocoder geocodage = new Geocoder(this, Locale.FRANCE);
Geocoder geocodage = new Geocoder(this, Locale.getDefault());

Les deux fonctions de géocodage renvoient une liste d'objets Address. Chacun peut contenir plusieurs résultats en fonction d'une limite que vous spécifiez lors de l'appel. Chaque objet Address est renseigné avec tous les détails que le géocoder a pu déterminer. Cela peut inclure la latitude, la longitude, un numéro de téléphone et des détails d'adresse de granularité de plus en plus fine allant du pays au numéro de la voie.

Les recherches du Geocoder sont effectuées de façon synchrone et bloquent donc le thread appelant. Dans le cas de connexions réseau lentes, cela peut se traduire par un écran figé. dans la plupart des cas, il est conseillé de déplacer ces recherches vers un Service ou un Thread en arrière-plan.

Géocodage inverse

Le géocodage inverse renvoie des adresses en fonctions de lieux spécifiés par leur latitude et leur longitude. Il permet de reconnaître les positions renvoyées par les services de géolocalisation.

Pour effectuer une recherche inversée, passez la latitude et la longitude à la méthode getFromLocation() du Geocoder. Elle renverra une liste des correspondances possibles d'adresses. Si le Geocoder ne peut déterminer aucune adresse pour les coordonnées spécifiées, il renverra la valeur null.

L'exactitude et la granularité des recherches inversées dépendent entièrement de la qualité des données de la base de géocodage. La conséquence en est que la qualité des résultats peut varier de façon importante selon les pays.

Voici un exemple ci-dessous que permet de spécifier l'adresse complète de la localité où nous trouvons en plus des coordonnées géographiques :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.localisation"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="GPS" >
        <activity android:name="Geolocalisation" android:label="Où suis-je ?">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>   
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
   <TextView  
      android:id="@+id/latitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"  />
   <TextView  
      android:id="@+id/longitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"   />
   <TextView  
      android:id="@+id/adresse"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"   />      
</LinearLayout>
fr.btsiris.localisation.Geolocalisation.java
package fr.btsiris.localisation;

import android.app.Activity;
import android.content.Context;
import android.location.*;
import android.os.Bundle;
import android.widget.*;
import java.io.IOException;
import java.util.*;

public class Geolocalisation extends Activity {
   private TextView latitude, longitude, adresse;
   private LocationManager localisations;
   private ChangementPosition changements = new ChangementPosition();
   private Geocoder geocodage;
   
   @Override
    public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        latitude = (TextView) findViewById(R.id.latitude);
        longitude = (TextView) findViewById(R.id.longitude);   
        adresse =  (TextView) findViewById(R.id.adresse);  
        geocodage = new Geocoder(this, Locale.FRANCE);
        localisations = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        miseAJourDeLaPosition(localisations.getLastKnownLocation(LocationManager.GPS_PROVIDER));
    }

    @Override
    protected void onStart() {
        super.onStart();
        localisations.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2, changements);   
        localisations.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 5000, 5, changements); 
    }

    @Override
    protected void onStop() {
       super.onStop();
       localisations.removeUpdates(changements);
    } 

    private void miseAJourDeLaPosition(Location position) {
       if (position!=null) {
          double lat = position.getLatitude();
          double lng = position.getLongitude();
          latitude.setText("Latitude : "+lat);
          longitude.setText("Longitude : "+lng);  
          try {
             List<Address> adresses = geocodage.getFromLocation(lat, lng, 1);
             StringBuilder texte = new StringBuilder();
             if (adresses.size() > 0) {
                Address une = adresses.get(0);
                texte.append(une.getAddressLine(0)).append("\n");
                texte.append(une.getPostalCode()).append(" ").append(une.getLocality()).append("\n");
                texte.append(une.getCountryName()).append("\n");
                adresse.setText(texte.toString());
             }
          } 
          catch (IOException ex) { }
       }
       else latitude.setText("Position  non déterminée ");
       Toast.makeText(Geolocalisation.this, "Nouvelle position", Toast.LENGTH_SHORT).show();    
    }    
     
    class ChangementPosition implements LocationListener {
         public void onLocationChanged(Location position) { miseAJourDeLaPosition(position);   }
         public void onStatusChanged(String fournisseur, int statut, Bundle extras) {  }
         public void onProviderEnabled(String fournisseur) {  }
         public void onProviderDisabled(String fournisseur) {  } 
    }
} 
Géocodage avant

Le géocodage avant (ou tout simplement géocodage) détermine cette fois-ci les coordonnées géographiques d'un lieu à partir d'un texte décrivant un endroit. Le format des adresses, noms de stations ou codes postaux dépendent évidemment de la zone géographique concernée.

Ce que nous appelons lieu valide varie effectivement en fonction de la zone géographique. Il s'agira en général d'une adresse normale de granularité variable (depuis le pays jusqu'au numéro dans la voie), d'un code postal, d'une gare, d'un monument, d'un hôpital ou, de façon générale, de tout ce qui peut être utilisé dans une recherche sur Google Maps.

Pour effectuer un géocodage, appelez la méthode getFromLocationName() sur une instance de Geocoder. Passez-lui le lieu dont vous souhaitez obtenir les coordonnées ainsi que le nombre maximal de résultats :

Geocoder geocodage = new Geocoder(this, Locale.FRANCE);
List<Address> positions = geocodage.getFromLocationName(adresse, 1);

La liste renvoyée peut inclure plusieurs correspondances possibles pour le lieu indiqué. Chaque résultat inclura une latitude et une longitude ainsi que toutes les informations d'adresse disponibles. Ceci est utile pour confirmer que la position a été correctement résolue ainsi que pour fournir des informations spécifiques lors de recherches de monuments.

Comme pour le géocodage inverse, null est renvoyé si aucune correspondance n'est trouvée. De même, la disponibilité, l'exactitude et la granularité des résultats dépendent entièrement des données disponibles dans la base.

Lorsque vous effectuez des recherches inversées, l'objet Locale spécifié lors de la création du Geocoder est particulièrement important. La Locale fournit le contexte géographique pour l'interprétation des résultats de recherche car un nom de lieu peut exister à plusieurs endroits. Lorsque c'est possible, sélectionnez une Locale régionale pour éviter les ambiguïtés.

Voici un autre exemple qui permet de retrouver la latitude et la longitude à partir d'une adresse :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.localisation"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Localisation" >
        <activity android:name="Localisation" android:label="Latitude et Longitude ?">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="3px">
   <EditText 
      android:id="@+id/rue"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Rue" />   
   <EditText 
      android:id="@+id/ville"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Ville" />         
   <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Rechercher" 
      android:onClick="localiser"/>         
   <TextView  
      android:id="@+id/latitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:textStyle="bold"
      android:textColor="#00FF00" 
      android:textSize="18sp" />
   <TextView  
      android:id="@+id/longitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"  
      android:textStyle="bold"
      android:textColor="#00FF00"
      android:textSize="18sp" />
</LinearLayout>  
fr.btsiris.localisation.Localisation.java
package fr.btsiris.localisation;

import android.app.Activity;
import android.location.*;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.io.IOException;
import java.util.*;

public class Localisation extends Activity {
   private TextView latitude, longitude;
   private EditText rue, ville;
   private Geocoder geocodage;
    
   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.main);
       latitude = (TextView) findViewById(R.id.latitude);
       longitude = (TextView) findViewById(R.id.longitude);   
       rue =  (EditText) findViewById(R.id.rue);
       ville =  (EditText) findViewById(R.id.ville);
       geocodage = new Geocoder(this, Locale.FRANCE);       
   }
   
   public void localiser(View vue) {
      String adresse = rue.getText().toString() + ", " + ville.getText().toString() + ", France";
      try {
         List<Address> positions = geocodage.getFromLocationName(adresse, 1);
         if (positions.size() > 0) {
            Toast.makeText(this, adresse, Toast.LENGTH_LONG).show();
            Address position = positions.get(0);
            latitude.setText("Latitude = "+position.getLatitude());
            longitude.setText("Longitude = "+position.getLongitude());
         }
      } 
      catch (IOException ex) { Toast.makeText(this, "Non localisée", Toast.LENGTH_LONG).show(); }      
   }          
}

Création d'activités géographiques utilisant Google Maps

La manipulation d'adresses ou de lieux, que ce soit pour un affichage ou une saisie, peut se faire via des affichages textes ou des formulaires mais l'utilisateur est habitué à tout visualiser sur des cartes. Afin de réaliser des applications utilisant ces vues à base de cartes, nous allons employer, entre autres, les différentes classes suivantes :

  1. MapActivity : la classe de base à étendre pour créer une activité qui contiendra une carte (une MapView). Cette classe prend en charge le cycle de vie de l'application et la gestion du service d'arrière-plan requis pour afficher les cartes. En conséquence, vous ne pouvez utiliser les MapView que dans des activités dérivées de la classe MapActivity.
  2. MapView : une vue affichant une carte sous forme de tuiles (parties de carte téléchargées indépendamment). L'une des façon les plus intuitive de présenter le contexte géographique est de l'afficher sur une carte. A l'aide d'une MapView, vous pouvez créer des activités présentant des cartes interactives. Les MapView supportent les annotations par le biais des Overlays et par l'épinglage de Views sur des positions géographiques.
  3. MapControler : permet de contrôler la carte (centrage, niveau de zoom, etc.). Ce système apporte le contrôle total par programmation sur l'affichage de la carte, vous permettant de contrôler le zoom, la position et les modes d'affichage, y compris l'option d'afficher des vues satellite, de la rue ou du trafic.
  4. Overlay : sorte de calque pour rajouter des annotations sur les cartes. Avec elle, vous pourrez utiliser un Canvas pour dessiner autant de couches que vous voudrez, qui seront affichées au dessus de la MapView.
  5. MyLocationOverlay : est un Overlay particulier qui peut être utilisé pour afficher la position courante et l'orientation de l'appareil.
  6. ItemizedOverlays et OverlayItems : sont utilisés pour vous permettre de créer une couche de marqueurs de cartes, affichés à l'aide de Drawable et de texte associé.

Les activités géographiques sont exécutées nativement sur l'appareil, vous permettant de vous appyer sur son matériel et sa mobilité pour apporter une expérience personnalisée à l'utilisateur.

Authentification - obtention de votre clé d'API

Pour utiliser une MapView dans votre application, vous devrez d'abord obtenir une clé d'API sur le site des développeurs Android http://code.google.com/android/maps-api-signup.html (voir plus loin). Sans elle, la MapView ne pourra pas télécharger les images utilisées pour afficher la carte.

Pour obtenir une clé, vous devrez spécifier l'empreinte MD5 du certificat utilisé pour signer votre application. En général, vous signerez votre application à l'aide de deux certificats, l'un de débogage, l'autre de production.

Pour voir les images des cartes pendant le débogage, vous devrez obtenir une clé d'API enregistrée via l'empreint MD5 du certificat de débogage. L'emplacement du magasin de clés (keystore) de débogage est stocké aux emplacement suivants :

  1. Windows Vista & 7 : \Utilisateurs\<utilisateur>\.android\debug.
  2. Windows XP : \Documents and Settings\<utilisateur>\.android\debug.
  3. Linux ou Mac : ~/.android/debug.keystore.

ATTENTION : Chaque ordinateur que vous utilisez lors du développement aura son propre certificat de débogage et sa propre valeur MD5. Si vous désirez déboguer et développer des applications géographiques sur plusieurs machines, vous devez générer et utiliser plusieurs clés d'API.

Récupération de votre clé Google Map en mode débogage

Comme nous venons de le signaler, afin de pouvoir utiliser Google Map dans votre application, il vous faut une clé API. Voici les étapes pour obtenir cette dernière :

  1. Génération du MD5 : La première étape pour l’obtention de la clé API, c’est de créer votre MD5 Checksum. Il est créé grâce au debug certificate. Nous avons besoin de ce MD5, car il faut que la Google Map de l’application soit signée et la clé API sert à ça. Pour créer votre MD5, il faut trouver où se situe votre fichier debug.keystore. Une fois le chemin connu, il faut taper la commande keytool en mode console et respecter les options suivantes :

  2. Génération de votre clé API : Il suffit maintenant de vous rendre sur : http://code.google.com/intl/fr/android/maps-api-signup.html. Vous devez par contre disposer d’un compte Google. Pour pouvoir générer votre clé, il vous suffit d’accepter les termes et saisir le MD5 obtenu au dessus.

  3. Obtention de la clé API : La clé obtenue correspond à l’instance de votre ordinateur, si vous changer d’ordinateur, il faudra générer une autre clé. Gardez la clé que vous avez obtenu dans un coin ou un fichier, nous verrons son utilisation lors de l'élaboration d'une application affichant une carte Google Map.


Récupération de votre clé Google Map en mode production définitive pour le déploiement sur votre smartphone

Avant de compiler et de signer votre application pour la diffuser, vous devrez obtenir une clé d'API en utilisant l'empreinte MD5 de votre certificat de production. Déterminez-la avec la commande keytool et spécifiez le paramètre -liste et les keystore et alias que vous utiliserez pour signer votre application. Vous devrez entrer vos mots de passe de keystore et d'alias avant que l'empreinte MD5 ne vous soit fournie.

keytool -list -alias mon-android-alias -keystore mon-android-keystore

Créer une activité géographique

La classe MapActivity est étroitement liée à la classe MapView. Elle permet, grâce à ses différentes méthodes, de prendre en charge les tâches - notamment de connexion réseau - pour simplifier l'utilisation d'une vue orientée carte.

Pour utiliser des cartes dans vos applications, vous devez étendre MapActivity. Le layout de la nouvelle classe doit inclure une MapView pour afficher l'élément d'interface Google Maps. En pratique, l'emploi de la classe MapActivity dans une application nécessite plusieurs étapes :

  1. La bibliothèque cartographique d'Android n'est pas un paquetage standard Android. En tant qu'API optionnelle, elle doit être explicitement incluse dans le manifeste de l'application afin de pouvoir être utilisée.
    <uses-library android:name="com.google.android.maps" />

    Ce paquetage ne fait pas partie du projet open-source Android standard. Il est fourni par Google dans le SDK Android et est disponible sur la plupart des appareils. Mais, n'étant pas standard, il se peut qu'un appareil ne le supporte pas.

  2. Les images des cartes sont téléchargées à la demande et votre application doit donc avoir la permission d'utiliser Internet. Effectivement, puisque les tuiles du plan ainsi que toutes les informations comme le trafic routier sont téléchargées au fur et à mesure (vous devez avoir remarqué qu'à chaque déplacement de la carte, les zones sont affichées progressivement), il faut également prévoir la permission d'accéder aux données Internet comme suit :
    <uses-permission android:name="android.permission.INTERNET" />  
    AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="fr.btsiris.localisation"  android:versionCode="1"  android:versionName="1.0">
        <application android:label="Cartographie" >    
            <activity android:name="Cartographie" android:label="Où suis-je ?">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <uses-library android:name="com.google.android.maps" />
        </application>
        <uses-permission android:name="android.permission.INTERNET" />
    </manifest>
  3. Une fois la bibliothèque ajoutée et la permission configurée, vous êtes prêt à créer votre nouvelle activité géographique. Cette activité contiendra une vue de type plan et un contrôleur de carte. Les vues de type plan (MapView ne peuvent être utilisés que dans une activité qui étend la classe MapActivity. Redéfinissez la méthode onCreate() pour disposer l'écran contenant la MapView et la méthode isRouteDisplayed() pour envoyer true si l'activité doit afficher des informations de directions.

    Android ne supporte actuellement qu'une seule MapActivity et une seule MapView par application.
    .

    public class Cartographie extends MapActivity {
       private MapView carte;
       private MapController controleur;
       
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            carte = (MapView) findViewById(R.id.carte);
            controleur = carte.getController();
        }
    
       @Override
       protected boolean isRouteDisplayed() {
          return false;
       }
    }
Manipuler la classe MapView

La classe MapView gère l'affichage de la carte (proposée par Google). Par défaut, MapView montre la carte standard. Vous pouvez en plus choisir d'afficher une vue satellite, les vues plus précises des bâtiments au niveau de la rue ainsi que le trafic estimé :

MapView carte = (MapView) findViewById(R.id.carte);
carte.setSatellite(true);
carte.setSteetView(true);
carte.setTraffic(true);

Vous pouvez également interroger la carte (MapView) pour déterminer les zooms courant et maximal ainsi que le centre de la carte et les longitudes et latitudes visibles (en degrés). Ce dernier (vois extrait ci-après) est particulièrement utile pour effectuer des recherches de géocodage géographiquement bornées. Vous pouvez également de façon optionnelle afficher des contrôles de zoom standard en utilisant la méthode setBuiltInZoomControls().

MapView carte = (MapView) findViewById(R.id.carte);
int zoomMaxi = carte.getMaxZoomLevel();
GeoPoint point = carte.getMapCenter();
int latitude = carte.getLatitudeSpan();
int longitude = carte.getLongitudeSpan();
carte.setBuiltInZoomControls(true);

Pour employer correctement la classe MapView dans votre application, n'oubliez pas, bien entendu, de vous occuper de sa présentation au moyen du layout associé qui complète le squelette de l'activité que nous venons de découvrir :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"   >
...
   <com.google.android.maps.MapView 
      android:id="@+id/carte"   
      android:layout_width="fill_parent" 
      android:layout_height="fill_parent" 
      android:enabled="true"
      android:clickable="true"
      android:apiKey="0kHT1LZrcfazDGalQpFJThJQVO55qyxFQ15LbZA" />       
</LinearLayout>

Nous retrouvons sans surprise la vue avec son identifiant. Nous constatons la présence d'un paramètre supplémentaire essentiel : la clé Google Maps. Elle peut soit être saisie directement dans la description de la vue, comme le montre le code précédent, soit donné sous forme de ressource annexe.

Manipuler la classe MapControler

Nous utilisons la classe MapControler pour manipuler la carte en termes de position (recentrer) et de zoom. La récupération de l'instance de MapControler associée à une vue de type carte se fait à l'aide d'un accesseur getController() de la classe MapView.

MapController controleur = carte.getController();

Dans les classes géographiques Android, les positions sur une carte sont représentées par des objets GeoPoint qui contiennent la latitude et la longitude mesurées en micro-degrés, à savoir degrés*1E6 (10 puissance 6). Avant de pouvoir utiliser les valeurs de latitude et de longitude stockées dans les objets Location renvoyés par les services de géolocalisation, vous devez les convertir en micro-degrés et les stocker en GeoPoint.

Double lat = Double.parseDouble(latitude.getText().toString()) * 1E6;
Double lng = Double.parseDouble(longitude.getText().toString()) * 1E6;
GeoPoint point = new GeoPoint(lat.intValue(), lng.intValue());
controleur.animateTo(point); ou controleur.setCenter(point); 
controleur.setZoom(1);
  1. Recentrez et zoomez dans la carte en utilisant respectivement les setCenter() et setZoom() disponibles dans le MapController de la classe MapView. La méthode setCenter() permet de "sauter" vers une nouvelle position. Pour une transition sans à-coups, utilisez la méthode animateTo() de MapView.
  2. Lorsque vous utilisez la méthode setZoom(), 1 représente le zoom le plus large (le plus lointain) et 21, le plus proche. Le niveau de zoom réel disponible pour une position donnée dépend de la résolution des cartes Google et de l'imagerie disponible pour la zone. Vous pouvez également utiliser les méthodes zoomIn() et zoomOut() pour modifier le niveau par incrément de 1.
  3. Lorsque vous utilisez Google Maps, vous remarquez que le fond cartographique se charge selon un découpage (dit tuilage) comprenant un ensemble de morceaux (tuiles) visibles à l'écran. Quand vous vous déplacez sur la carte, les tuiles sont chargées au fur et à mesure. Ce tuilage est stocké, pour chaque niveau de précision (et donc de zoom pour nous), sur les serveurs de Google.
  4. Quand nous changeons le niveau de précision (de zoom), nous chargeons un ensemble de tuiles différent. Suivant la zone ciblée, les images sont disponibles à des précisions variées. Le niveau de zoom est réglable de 1 (plus petit niveau) à 21 inclus, sachant qu'il n'existe pas toujours de tuiles du lieu choisi aux niveaux de zoom les plus élevés (la zone n'a pas été photographiée avec cette précision ou elle fait partie d'un ensemble sensible qui doit être masqué par exemple). Pour donner un ordre d'idée, au niveau de zoom 1, l'Equateur terrestre mesure 256 pixels de long et que chaque niveau de zoom successifs grossit d'un facteur 2.
A titre d'exemple, je vous propose de faire une application simple, sans tenir compte pour l'instant de la géolocalisation, qui affiche la carte du lieu suivant la latitude et la longitude proposée manuellement avec visualisation des fonctions Zoom :
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="3px">
   <EditText 
      android:id="@+id/latitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Latitude" />
   <EditText 
      android:id="@+id/longitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Longitude" />     
   <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Localiser" 
      android:onClick="localiser"/>    
   <com.google.android.maps.MapView 
      android:id="@+id/carte"   
      android:layout_width="fill_parent" 
      android:layout_height="fill_parent" 
      android:enabled="true"
      android:clickable="true"
      android:apiKey="0kHT1LZrcfazDGalQpFJThJQVO55qyxFQ15LbZA" />       
</LinearLayout>
  
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.localisation"  android:versionCode="1"  android:versionName="1.0">
    <application android:label="Cartographie" >    
        <activity android:name="Cartographie" android:label="Où suis-je ?">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <uses-library android:name="com.google.android.maps" />
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>
fr.btsiris.localisation.Cartographie.java
package fr.btsiris.localisation;

import android.os.Bundle;
import android.view.View;
import android.widget.*;
import com.google.android.maps.*;

public class Cartographie extends MapActivity {
   private EditText latitude, longitude;
   private MapView carte;
   private MapController controleur;
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        latitude = (EditText) findViewById(R.id.latitude);
        longitude = (EditText) findViewById(R.id.longitude);
        carte = (MapView) findViewById(R.id.carte);
        carte.setBuiltInZoomControls(true);
        controleur = carte.getController();
    }

   @Override
   protected boolean isRouteDisplayed() {
      return false;
   }
   
   public void localiser(View vue) {
      Double lat = Double.parseDouble(latitude.getText().toString()) * 1E6;
      Double lng = Double.parseDouble(longitude.getText().toString()) * 1E6;
      GeoPoint point = new GeoPoint(lat.intValue(), lng.intValue());
      controleur.animateTo(point);
   }
}
Placer des données sur une carte

Les calques (Overlays) permettent d'ajouter des formes, des images ou du texte sur une vue de type carte (MapView). Il est possible de superposer plusieurs calques qui masquerons potentiellement des zones correspondant à des couches plus anciennes.

Les Overlays vous permettent d'ajouter des annotations et une gestion des clics dans les MapViews. Chaque Overlay permet de tracer des primitives 2D, comme du texte, des lignes des images et des formes, sur un Canvas qui recouvrira ensuite une MapView.

Vous pouvez ainsi ajouter plusieurs Overlays sur une carte. Tous les Overlays assignés à une MapView sont ajoutés comme des couches, chaque nouvelle couche masquant potentiellement les précédentes. Les clics des utilisateurs sont transmis au travers de la pile jusqu'à ce qu'ils soient pris en charge par un Overlay ou enregistrés comme des clics sur la MapView elle-même.

Créer et utiliser des calques

Pour créer un calque, vous devez définir une nouvelle classe qui hérite de la classe Overlay. Nous disposons alors d'une sorte de canevas pour dessiner. Les instructions de dessins personnalisés doivent alors être placées dans la méthode draw() qui est à redéfinir. Pour réagir aux actions utilisateur sur ces annotations, il faut également redéfinir la méthode onTap().

Chaque Overlay est un Canvas possédant un fond transparent, recouvrant une MapView et utilisé pour prendre en charge les clics sur la carte (qui ont lieu en général lorsque l'utilisateur tape sur une annotation ajoutée par l'Overlay).

import android.graphics.Canvas;
import com.google.android.maps.*;

public class Calque extends Overlay {

   @Override
   public void draw(Canvas canevas, MapView carte, boolean ombres) {
      if (!ombres) {
         // Dessiner les annotations sur la couche principale
      }
      else {
         // Dessiner les annotations sur la couche cachée
      }      
   }

   @Override
   public boolean onTap(GeoPoint centre, MapView carte) {
      return false;
   }   
}
Ajouter et supprimer des calques de la vue

Chaque MapView contient la liste de ses calques en cours d'affichage. Nous pouvons obtenir une référence à cette liste en appelant la méthode getOverlays() issue de la classe MapView :

List<Overlay> calques = carte.getOverlays();
Calque nouveau = new Calque();
calques.add(nouveau);
carte.postInvalidate();

L'ajout et la suppression de calques dans la liste sont automatiquement sécurisés et synchronisés. L'ajout et la suppression se fait ainsi de façon totalement transparente, sans préoccupation de gestion multi-tâche étant donné que la liste a été passé à la méthode Collection.synchronized() avant de nous être retournée.

Ainsi, pour ajouter un Overlay sur une MapView, créez simplement une instance de l'Overlay et ajoutez-la à la liste sans autre écriture supplémentaire. Tous les calques de la liste seront dessinés par ordre croissant et recevront des événements progressivement en ordre inverse tant qu'un calque ne les traitera pas.

L'Overlay ajouté sera affiché la prochaine fois que la MapView sera redessinée et il est donc conseillé d'appeler la méthode postInvalidate() après une modification de la liste afin de mettre à jour l'affichage de la carte.

Dessiner sur un calque

Le dessin sur un calque est possible en redéfinissant la méthode draw() du calque, un peu comme si nous redéfinissions la méthode paintComponent() d'un composant Swing de l'API de Java SE.

Comme nous l'avons découvert, cette méthode draw() possède trois paramètres que nous allons maintenant détailler :

  1. Le premier est de type Canvas et constitue la surface sur laquelle nous allons dessiner. L'objet Canvas contient des méthodes pour tracer des primitives 2D sur vos cartes (lignes, texte, formes, ellipses, images, etc.). Utilisez des objets Paint pour définir le style et la couleur.
  2. Le deuxième est la référence de type MapView qui correspond à la vue qui affiche la carte concernée par notre dessin.
  3. Le dernier est un booléen qui indique dans quel appel nous nous trouvons. En effet, cette méthode est appelée deux fois pour permettre, quand ombres vaut true, de dessiner des ombres aux formes créées.

ATTENTION : Le Canvas utilisé pour tracer les annotations des Overlays est un Canvas standard représentant la surface affichée visible. Pour ajouter des annotations basées sur des positions, vous devrez effectuer une conversion entre coordonnées géographiques et coordonnées d'écran.

Introduction aux projections
La classe Projection vous permet de traduire des coordonnées de latitude et longitude en coordonnées d'écran. Un objet de type Projection est obtenu grâce à la méthode getProjection() de la classe MapView. Dès lors, à l'aide de cet objet, nous pouvons réaliser la conversion dans les deux sens, d'un GeoPoint vers un Point et vice versa, respectivement avec les méthodes toPixels() et fromPixels().
Projection transformation = carte.getProjection();
Double lat = Double.parseDouble(latitude.getText().toString()) * 1E6;
Double lng = Double.parseDouble(longitude.getText().toString()) * 1E6;
GeoPoint pointCarte = new GeoPoint(lat.intValue(), lng.intValue());
Point pointEcran = new Point();
transformation.toPixels(pointCarte, pointEcran);
Les styles
Nous disposons à présent de coordonnées pour dessiner. Il reste à choisir le style du pinceau que nous allons utiliser pour réaliser une forme, et c'est là qu'intervient la classe Paint. Elle va nous permettre de déterminer le style et la couleur des formes et des textes que nous voulons dessiner sur le calque grâce aux méthodes setARGB(), setAlpha(), setColor(), setAntiAlias(), etc.
Paint pinceau = new Paint();
pinceau.setARGB(255, 255, 255, 0);
La boîte à outils du dessinateur
Une fois les coordonnées de la forme et le style choisi, nous utilisons toutes les méthodes de l'instance de Canvas pour réaliser des tracés personnalisés 2D sur vos cartes (lignes, texte, formes, ellipse, images, etc.) à l'aide des méthodes tel que drawLine(), drawCircle(), drawRect(), drawText(), drawBitmap(), drawPicture(), etc.
canevas.drawText("C'est ici !", pointEcran.x, pointEcran.y, pinceau);
Réagir à une sélection

Le tapotement sur l'écran sont l'équivalent des clics de souris. Vous les prendrez en charge en redéfinissant le gestionnaire onTap() dans la classe d'extension de l'Overlay. Il reçoit deux paramètres :

  1. Un GeoPoint contenant la latitude et la longitude de la position cliquée.
  2. La MapView sur laquelle l'événement a été déclenché.

Lorsque vous décidez de redéfinir onTap(), la méthode devra renvoyer true si elle a pris en charge un clic particulier et false pour laisser un autre Overlay le gérer.

Nous modifions le projet précédent pour intégrer un calque qui affiche une simple annotation qui montre un point bleu et le texte "Je suis là" également en bleu systématiquement au centre de la carte :
fr.btsiris.localisation.Calque.java
package fr.btsiris.localisation;

import android.graphics.*;
import com.google.android.maps.*;

public class Calque extends Overlay {

   @Override
   public void draw(Canvas canevas, MapView carte, boolean ombres) {
      if (!ombres) {         
         Point pointEcran = new Point();
         Projection transformation = carte.getProjection();
         transformation.toPixels(carte.getMapCenter(), pointEcran);
         Paint pinceau = new Paint();
         pinceau.setARGB(200, 0, 0, 255);
         pinceau.setAntiAlias(true);
         pinceau.setFakeBoldText(true);
         int rayon = 5;
         canevas.drawCircle(pointEcran.x, pointEcran.y, rayon, pinceau);
         canevas.drawText("Je suis là", pointEcran.x+10, pointEcran.y-10, pinceau);
      }
   }

   @Override
   public boolean onTap(GeoPoint centre, MapView carte) {
      return false;
   }   
}
fr.btsiris.localisation.Cartographie.java
package fr.btsiris.localisation;

import android.os.Bundle;
import android.view.View;
import android.widget.*;
import com.google.android.maps.*;
import java.util.List;

public class Cartographie extends MapActivity {
   private EditText latitude, longitude;
   private MapView carte;
   private MapController controleur;
   private Calque calque = new Calque();
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        latitude = (EditText) findViewById(R.id.latitude);
        longitude = (EditText) findViewById(R.id.longitude);
        carte = (MapView) findViewById(R.id.carte);
        carte.setBuiltInZoomControls(true);
        controleur = carte.getController();
        List<Overlay> calques = carte.getOverlays();
        calques.add(calque);
    }

   @Override
   protected boolean isRouteDisplayed() {
      return false;
   }
   
   public void localiser(View vue) {
      Double lat = Double.parseDouble(latitude.getText().toString()) * 1E6;
      Double lng = Double.parseDouble(longitude.getText().toString()) * 1E6;
      GeoPoint point = new GeoPoint(lat.intValue(), lng.intValue());
      controleur.animateTo(point);
   }
}
Calque MyLocationOverlay

L'API Android propose également un calque prédéfini spécifique qui peut répondre à deux problématiques usuelles :

  1. Montrer où vous êtes sur la carte à partir de coordonnées GPS ou d'un autre fournisseur de position.
  2. Montrer votre orientation grâce à la boussole électronique si elle est disponible sur votre matériel.

La classe MyLocationOverlay est un Overlay particulier pour afficher votre position courante et votre orientation sur une MapView. Pour l'utiliser, vous devez l'instancier, lui passer le contexte de l'application et la carte cible puis l'ajouter à la liste des Overlays.

MapView carte = (MapView) findViewById(R.id.carte);
List<Overlay> calques = carte.getOverlays();
MyLocationOverlay calque = new MyLocationOverlay(this, carte);
calques.add(calque);
calque.enableCompass();
calque.enableMyLocation();

Vous pouvez maintenant l'utiliser pour afficher à la fois votre position courante (représentée par un marqueur bleu clignotant) et votre direction (représentée sous la forme d'une boussole sur la carte). Pour activer ces fonctionnalités spécifiques, utilisez pour cela les méthodes enabledCompass() et enableMyLocation() sur l'instance que vous souhaitez impacter.

Afin d'optimiser la durée de vie de la batterie, il serait judicieux d'activer et de désactiver ces fonctionnalités au moment opportun, au travers des méthodes de rappel, respectivement onResume() ou onStart() et onPause() ou onStop().

Je vous propose encore un autre exemple qui fusionne cette fois-ci la géolocalisation issue du GPS et qui visualise cette localisation à l'aide de Google Map en affichant en plus la boussole et le point clignotant représentant le position trouvée. (La capture d'écran, issue de l'émulateur, ne montre malheureusement pas ce calque particulier).
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.localisation"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Cartographie">    
        <activity android:name="Cartographie" android:label="Où suis-je ?">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <uses-library android:name="com.google.android.maps" />  
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.maps.MapView  xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@+id/carte"   
      android:layout_width="fill_parent" 
      android:layout_height="fill_parent" 
      android:enabled="true"
      android:clickable="true"
      android:apiKey="0kHT1LZrcfaxP_vuzbfx3ScZMpYTT-8wmSDX2RA" 
/>
fr.btsiris.localisation.Cartographie.java
package fr.btsiris.localisation;

import android.content.Context;
import android.location.*;
import android.os.Bundle;
import com.google.android.maps.*;
import java.util.List;

public class Cartographie extends MapActivity {
   private MapView carte;
   private MapController controleur;
   private MyLocationOverlay calque;
   private LocationManager localisations;
   private ChangementPosition changements = new ChangementPosition();  
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);       
        carte = (MapView) findViewById(R.id.carte);
        carte.setBuiltInZoomControls(true);
        controleur = carte.getController();
        List<Overlay> calques = carte.getOverlays();
        calque = new MyLocationOverlay(this, carte);
        calques.add(calque);
        localisations = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        miseAJourDeLaPosition(localisations.getLastKnownLocation(LocationManager.GPS_PROVIDER));
    }

    @Override
    protected boolean isRouteDisplayed() { return false; }
   
    @Override
    protected void onStart() {
        super.onStart();
        localisations.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2, changements);   
        localisations.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 5000, 5, changements); 
        calque.enableCompass();
        calque.enableMyLocation();       
    }

    @Override
    protected void onStop() {
       super.onStop();
       localisations.removeUpdates(changements);
       calque.disableCompass();
       calque.disableMyLocation();
    } 
    
    private void miseAJourDeLaPosition(Location position) {
       if (position!=null) {
          Double lat = position.getLatitude() * 1E6;
          Double lng = position.getLongitude() * 1E6;
          GeoPoint point = new GeoPoint(lat.intValue(), lng.intValue());
          controleur.animateTo(point);
       }
    }        
    
    class ChangementPosition implements LocationListener {
       public void onLocationChanged(Location position) { miseAJourDeLaPosition(position);   }
       public void onStatusChanged(String fournisseur, int statut, Bundle extras) {  }
       public void onProviderEnabled(String fournisseur) {  }
       public void onProviderDisabled(String fournisseur) {  } 
    }   
}
Classes ItemizedOverlay et OverlayItem

Comme son nom l'indique, la classe ItemizedOverlay affiche sur une carte une liste de points d'intérêts. Ces points sont des instances de la classe OverlayItem.

La classe ItemizedOverlay fourni un raccourci pour ajouter des marqueurs à une carte, vous permettant d'assigner une image et son texte associé à une position géographique. L'instance ItemizedOverlay prend en charge le dessin, le placement, la gestion des clics, le contrôle du focus et l'optimisation du layout de chaque OverlayItem.

Pour ajouter un marqueur ItemizedOverlay à votre carte, il est nécessaire de créer une nouvelle classe étendant ItemizedOverlay<OverlayItem>. En réalité, ItemizedOverlay est une classe générique qui vous permet de créer des extensions fondées sur n'importe quelle sous-classe dérivée d'OverlayItem.

Voici les différentes étapes de la manipulation de ce type de calque :

  1. Comme nous venons de le dire, en premier lieu, une classe doit hériter de Itemized<OverlayItem>. Nous créons également un constructeur qui prendra en paramètre un objet de type Drawable. Cet objet est un marqueur qui mettra en valeur les points que nous souhaitons montrer (comme les bulles rouges de Google Maps).
  2. Dans le constructeur, préparez tous les éléments OverlayItem (la localisation, un titre, un texte plus complet) et appeler la méthode populate() une fois qu'ils sont prêts.
  3. Implémenter la méthode size() qui renvoie le nombre d'éléments du calque.
  4. Redéfinir la méthode createItem() pour renvoyer les éléments OverlayItem un par un en fonction de l'index donné.
  5. Instancier la classe ItemizedOverlay et l'ajouter aux calques de la vue.
Afin de bien comprendre ces différentes notions, je vous propose de rajouter des fonctionnalités supplémentaires sur l'application précédente. Nous pouvons placer un certain nombre de marqueurs, visualisés sous forme de pastilles, qui seront créés à partir de la saisie d'une adresse. Une fois que le marqueur est placé sur la carte, il est possible de visualiser l'adresse exacte par une simple clic sur la pastille représentant ce marqueur.

Dans cette application, nous disposons maintenant de deux vues, donc de deux activités. Il est donc nécessaire de prévoir deux classes séparées avec leurs layouts associés. La gestion des marqueurs se fera également dans une classe à part.

Pour générer ces pastilles qui poitent les différents lieux sur la carte, nous avons besoin d'une simple image créée avec n'importe quel éditeur graphique ou téléchargée sur Internet.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="2px">
   <Button 
      android:id="@+id/recherche"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Nouvelle position" 
      android:layout_alignParentBottom="true"
      android:onClick="localiser"/>    
   <com.google.android.maps.MapView 
      android:id="@+id/carte"   
      android:layout_width="fill_parent" 
      android:layout_height="fill_parent" 
      android:enabled="true"
      android:clickable="true"
      android:layout_above="@id/recherche"
      android:apiKey="0kHT1LZrcfaxP_vuzbfx3ScZMpYTT-8wmSDX2RA" />       
</RelativeLayout>
res/layout/adresses.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="2px">
   <EditText 
      android:id="@+id/rue"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Rue" />   
   <EditText 
      android:id="@+id/ville"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Ville" />                
   <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Rechercher" 
      android:onClick="valider"/>         
</LinearLayout>
fr.btsiris.localisation.Cartographie.java
package fr.btsiris.localisation;

import android.content.*;
import android.graphics.drawable.Drawable;
import android.location.*;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import com.google.android.maps.*;
import java.io.IOException;
import java.util.*;

public class Cartographie extends MapActivity {
   private MapView carte;
   private MapController controleur;
   private MyLocationOverlay position;
   private Marqueur marqueurs;
   private LocationManager localisations;
   private ChangementPosition changements = new ChangementPosition();  
   private Intent intention;
   private Geocoder geocodage;
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);    
        intention = new Intent(this, NouvelleAdresse.class);
        carte = (MapView) findViewById(R.id.carte);
        carte.setBuiltInZoomControls(true);
        controleur = carte.getController();
        List<Overlay> calques = carte.getOverlays();
        position = new MyLocationOverlay(this, carte);
        calques.add(position);
        Drawable pastille = getResources().getDrawable(R.drawable.marqueur);
        marqueurs = new Marqueur(pastille, this);
        calques.add(marqueurs);
        geocodage = new Geocoder(this, Locale.FRANCE);
        localisations = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        miseAJourDeLaPosition(localisations.getLastKnownLocation(LocationManager.GPS_PROVIDER));
    }

    @Override
    protected boolean isRouteDisplayed() { return false; }
   
    @Override
    protected void onStart() {
        super.onStart();
        localisations.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2, changements);   
        localisations.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 5000, 5, changements); 
        position.enableCompass();
        position.enableMyLocation();       
    }

    @Override
    protected void onStop() {
       super.onStop();
       localisations.removeUpdates(changements);
       position.disableCompass();
       position.disableMyLocation();
    } 
    
    private void miseAJourDeLaPosition(Location position) {
       if (position!=null) {
          Double lat = position.getLatitude() * 1E6;
          Double lng = position.getLongitude() * 1E6;
          GeoPoint point = new GeoPoint(lat.intValue(), lng.intValue());
          controleur.animateTo(point);
       }
    }        
    
    class ChangementPosition implements LocationListener {
       public void onLocationChanged(Location position) { miseAJourDeLaPosition(position);   }
       public void onStatusChanged(String fournisseur, int statut, Bundle extras) {  }
       public void onProviderEnabled(String fournisseur) {  }
       public void onProviderDisabled(String fournisseur) {  } 
    }   
    
    public void localiser(View bouton) {
       startActivityForResult(intention, 1);
    }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      if (resultCode==RESULT_OK) {
         String endroit = data.getExtras().getString("localisation");
         try {
            List<Address> positions = geocodage.getFromLocationName(endroit, 1);
            if (positions.size() > 0) {
               Address localisation = positions.get(0);
               marqueurs.ajout(localisation);
            }
         } 
         catch (IOException ex) { Toast.makeText(this, "Non localisée", Toast.LENGTH_LONG).show(); }   
      }
   }  
}
  1. La classe représentant le calque de gestion des différents marqueurs se nomme Marqueur dont le code est présenté plus bas. Il s'agit finalement d'un calque comme un autre qui est rajouté à l'ensemble des calques.
  2. L'objet pastille récupère l'image proposée dans les ressources.
fr.btsiris.localisation.NouvelleAdresse.java
package fr.btsiris.localisation;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;

public class NouvelleAdresse extends Activity {
   private EditText rue, ville;

   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);     
      setContentView(R.layout.adresses);
      rue = (EditText) findViewById(R.id.rue);
      ville = (EditText) findViewById(R.id.ville);
   }
   
   public void valider(View vue) {
      String adresse = rue.getText().toString() + ", " + ville.getText().toString() + ", France";
      getIntent().putExtra("localisation", adresse);
      setResult(RESULT_OK, getIntent());     
      finish();
   }
} 
fr.btsiris.localisation.Marqueur.java
package fr.btsiris.localisation;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.location.Address;
import android.widget.Toast;
import com.google.android.maps.*;
import java.util.ArrayList;

public class Marqueur extends ItemizedOverlay<OverlayItem> {
   private ArrayList<OverlayItem> liste = new ArrayList<OverlayItem>();
   private Drawable image;
   private Context contexte;

   public Marqueur(Drawable image, Context contexte) {
      super(image);
      this.image = image;
      this.contexte = contexte;
      populate();
   }
   
   @Override
   protected OverlayItem createItem(int index) {
      return liste.get(index);
   }

   @Override
   public int size() {
      return liste.size();
   }  
   
   public void ajout(Address adresse) {
      Double latitude = adresse.getLatitude() * 1E6;
      Double longitude = adresse.getLongitude() * 1E6;
      GeoPoint point = new GeoPoint(latitude.intValue(), longitude.intValue());
      liste.add(new OverlayItem(point, adresse.getLocality(), adresse.getAddressLine(0)));
      populate();
   }

   @Override
   public void draw(Canvas canevas, MapView carte, boolean ombre) {
      super.draw(canevas, carte, ombre);
      boundCenterBottom(image);
   }

   @Override
   protected boolean onTap(int index) {
      OverlayItem marque = liste.get(index);
      String texte = marque.getTitle() +'\n'+ marque.getSnippet() + '\n'+ marque.routableAddress();
      Toast.makeText(contexte, texte, Toast.LENGTH_LONG).show();
      return true;
   }
}

Vous avez juste ci-dessus le code du calque à proprement parler. Il est chargé d'afficher les différents lieux importants. Si l'uilisateur sélectionne l'un des marqueurs, un descriptif s'affichera.

  1. En suivant la démarche proposée, nous devons d'abord utiliser le mécanisme d'héritage.
  2. Ensuite, nous créons le constructeur qui prend en paramètre le marqueur (stocké pour être réutilisé) et le contexte (également stocké, notamment pour l'affichage de messages après sélection des éléments).
  3. Nous créons une collection d'OverlayItem qui va contenir tous les marqueurs à afficher avec une pastille sur la carte.
  4. Avec la méthode ajout(), nous ajoutons un par un les marqueurs à cette collection en précisant, à chaque fois, la localisation exacte, le nom de la ville suivi de l'adresse complète.
  5. Parallèlement, nous redéfinissons les méthodes obligatoires createItem() et size(). La première renvoie un marqueur situé à un index donné dans la collection, la seconde donne la taille de la collection. Ces deux méthodes permettent au calque de parcourir les marqueurs et de les afficher avec l'appel de la méthode populate().
  6. En redéfinissant la méthode draw(), nous délégons une grosse partie du travail de dessin du marqueur au calque, mais nous l'aidons tout de même à déterminer l'emplacement de la pastille et surtout le bas afin que l'ombre soit correctement dessinée.
  7. L'affichage des informations complémentaires sur chaque pastille est réalisé via la méthode onTap(). Nous affichons sous forme de toast le marqueur concerné en rapprochant l'index passé en paramètre de l'élément de rang équivalent dans la collection globale.
Epingler des Views à une carte et à des positions

Vous avez la possibilité d'épingler n'importe quel objet dérivé d'une View à une MapView (y compris les layouts et autres ViewGroup) en l'attachant à une position à l'écran ou géographique.

Dans le dernier cas, la view se déplacera pour suivre sa position épinglée sur la carte, agissant comme un véritable marqueur interactif. Cette solution consommant plus de ressources, elle est habituellement réservée à l'affichage de détails dans une bulle affichée pour fournir des informations complémentaires lorsque nous cliquons sur un marqueur.

  1. Vous pouvez implémenter les deux mécanismes en appelant addView() sur la carte, en général depuis les méthodes onCreate() ou onRestore() dans l'activité la contenant. Passer la vue que vous désirez épingler ainsi que les paramètres de layout à utiliser.
  2. Les paramètres MapView.LayoutParams que vous passerez à la méthode addView() déterminent comment la vue est ajoutée à la carte.
  3. Pour ajouter une nouvelle View relativement à l'écran, spécifiez un nouveau MapView.LayoutParams contenant les arguments correspondant à la hauteur et à la largeur de la View, les coordonnées cibles (x, y) de l'écran et l'alignement à utiliser pour le positionnement.
  4. Pour épingler une View relativement à une position géographique, passez quatre paramètres dans MapView.LayoutParams, représentant la hauteur, la largeur, le GeoPoint cible et l'alignement du layout.
  5. Un panoramique de la carte laissera la première TextView en place dans le coin supérieur gauche alors que la seconde se déplacera pour rester épinglée à sa position.
  6. Pour supprimer une View d'une MapView, appelez la méthode removeView() en passant l'instance de View que vous voulez supprimer.

 

Choix du chapitre Capteurs, accéléromètre (Dessins 2D - Drawables)

Les dernières générations de téléphones tentent d'embarquer de plus en plus de fonctionnalités matérielles : GPS, boussole, gyroscope, Wi-Fi, Bluetooth, capteur magnétique, capteur de température, etc. Bien évidemment, en tant que dévelopeur, vous comprendrez immédiatement que ce sont autant d'opportunités de créer des applications toujours plus immersives, communicantes et interactives !

Nous allons découvrir dans ce chapitre les capteurs existant sous Android et comment utiliser le Sensor Manager pour les monitorer. Nous examinerons de près l'accéléromètre et les capteurs d'orientation et les utiliserons pour déterminer les changements dans l'orientation de l'appareil ainsi que l'accélération. Ceci sera particulièrement utile pour créer des interfaces utilisateur basées sur le mouvement et vous permettra de donnéer une nouvelle dimension à vos applications basées sur la géolocalisation.

Les ressources disponibles sur un appareil Android

La plate-forme Android gère de façon native bon nombre de ressources matérielles telles que le clavier et le trackball qui ne nécessitent pas vraiment d'adaptation lorsque ceux-ci ne sont pas disponibles sur le terminal. Par exemple, le clavier physique sera remplacé par un clavier virtuel sans avoir besoin de reprogrammer l'interface utilisateur.

Liste des principales ressources utilisables depuis Android
Affichage
- Appareil photo
- 3D
Entrées
- Clavier
- Matrice tactile
- Boule de commande (trackball)
Son
- Haut parleur / Microphone
- Reconnaissance vocale
- Vibration
Réseau
- 3G
- Wi-Fi
- Bluetooth
- GPS
Capteurs
- Accéléromètre
- Orientation
- Champ magnétique
- Température

Les capteurs

Une bonne partie du succès des terminaux modernes est la gestion du mouvement. Cela ne serait pas possible sans l'extraordinaire démocratisation des capteurs embarqués. Désormais, votre appareil détecte le mouvement et peut se situer dans l'espace. Voyons comment tout cela fonctionne sur Android, en créant un projet qui utilise les capteurs.

Comme pour les services de géolocalisation, Android rend abstraites les implémentations des capteurs de chaque appareil. La classe Sensor est utilisée pour décrire les propriétés de chaque capteur matériel, comme son type, son nom, son fabricant et ses détails de précision et de portée.

Identification des capteurs

Le panel des capteurs s'est étoffé avec les versions successives d'Android : accéléromètre, capteur de pression, capteur de proximité... Ceci dit, il ne faut pas croire que tous les capteurs sont disponibles sur tous les terminaux, loin de là !

L'API d'Android 1.6 a donc introduit une nouvelle classe Sensor responsable de la récupération des données, sous la houlette d'un autre classe nommée SensorManager qui comme son nom l'indique chapeaute le groupe de capteurs au travers d'un service commun à tout le système. SensorManager est ainsi utilisé pour gérer l'ensemble des capteurs disponibles sur les appareils Android.

Utilisez la méthode getSystemService() pour renvoyer une référence vers le service SensorManager :

SensorManager gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);

Ensuite, la classe Sensor contient un ensemble de constantes utilisées pour décrire quel type de capteur matériel est représenté par un objet de type Sensor. Ces constantes sont de la forme Sensor.TYPE_<Type>. La section suivante décrit chaque type de capteur supporté. Vous apprendrez ensuite comment trouver et utiliser chacun d'eux.

Liste des différents types de capteurs disponibles
Sensor.TYPE_ACCELEROMETER
Permet d'évaluer les mouvements du terminal ou tout simplement la gravité. Accéléromètre à trois axes renvoyant l'accélération courante en m/s2.
Sensor.TYPE_GYROSCOPE
Permet de connaître la position angulaire en degré du terminal en fonction des trois axes x, y et z.
Sensor.TYPE_LIGHT
Le capteur de lumière permet d'évaluer l'exposition lumineuse subie par le terminal. Capteur de lumière ambiante renvoyant une valeur exprimée en lux. Le capteur de lumière est couramment utilisé pour contrôler la luminosité de l'écran.
Sensor.TYPE_MAGNETIC_FIELD
Permet de détecter les modifications des champs magnétiques environnants. Capteur de champ magnétique renvoyant le champ en microteslas suivant les trois axes.
Sensor.TYPE_ORIENTATION
Permet de déterminer la position du terminal en fonction des trois axes x, y et z.
Sensor.TYPE_PRESSURE
Indique la pression atmosphérique. Capteur de pression renvoyant une valeur exprimée en kilopascals.
Sensor.TYPE_PROXIMITY
Indique la distance du terminal par rapport à un objet. Capteur de proximité indiquant la distance entre l'appareil et un objet cible en mètres. La façon dont l'objet est sélectionné et la distance supportée dépendront de l'implémentation matérielle du détecteur de proximité. Un usage typique est la détection de l'approche de l'appareil de l'oreille de l'utilisateur, l'ajustement automatique de la luminosité de l'écran ou le lancement d'une commande vocale.
Sensor.TYPE_TEMPERATURE
Indique la température à proximité du capteur. Thermomètre renvoyant la température en degré Celsius. Il peut s'agir de la température ambiante, de celle de la batterie ou d'un capteur distant en fonction de l'implémentation matérielle.
Trouver les capteurs

Un appareil Android peut contenir plusieurs implémentations d'un type de capteur particulier. Pour déterminer l'implémentation par défaut, utilisez la méthode getDefaultSensor() issue de la classe SensorManager en lui passant le type de capteur requis sous la forme d'une des constantes décrites dans la section précédente.

Si le capteur n'est pas disponible sur le terminal, la méthode getDefaultSensor() renverra une valeur null nous permettant d'agir en conséquence.

SensorManager gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor gyroscope = gestionCapteurs.getDefaultSensor(Sensor.TYPE_GYROSCOPE);

De façon alternative, utilisez plutôt la méthode getSensorList() pour renvoyer la liste de tous les capteurs d'un type donné comme dans le code suivant, qui renvoie tous les capteurs de pression :

SensorManager gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);
List<Sensor> capteursPression = gestionCapteurs.getSensorList(Sensor.TYPE_PRESSURE);

Enfin, pour déterminer tous les capteurs disponibles sur une plate-forme hôte, utilisez de nouveau la méthode getSensorList(), en lui passant cette fois-ci la constante Sensor.TYPE_ALL. Cette technique vous permet de déterminer quels capteurs et types de capteurs sont disponibles sur l'appareil.

SensorManager gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);
List<Sensor> capteurs = gestionCapteurs.getSensorList(Sensor.TYPE_ALL);
Je vous propose de réaliser un premier projet qui permet de recenser tous les capteurs que vous avez à votre disposition sur votre terminal.
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"  
    android:id="@android:id/list"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent" 
/>
fr.btsiris.capteurs.Capteur.java
package fr.btsiris.capteurs;

import android.app.ListActivity;
import android.content.Context;
import android.hardware.*;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import java.util.List;

public class Capteur extends ListActivity {
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      SensorManager gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);
      List<Sensor> capteurs = gestionCapteurs.getSensorList(Sensor.TYPE_ALL);
      String[] listeCapteurs = new String[capteurs.size()];
      for (int i=0; i<capteurs.size(); i++) listeCapteurs[i] = capteurs.get(i).getName();
      setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listeCapteurs));
   }
}
Comment utiliser les capteurs

Pour accéder aux capteurs, après avoir récupérer l'instance du service, vous devez vous abonner aux événements du capteur qui vous intéressent.

Pour cela, vous devez implémenter l'interface SensorEventListener et redéfinir les méthodes adpatées à ce type d'événements, d'une part la méthode onSensorChanged() pour monitorer les valeurs du capteur et d'autre part la méthode onAccuracyChanged() pour réagir à ses changements de précision.

class Scrutation implements SensorEventListener {
    public void onSensorChanged(SensorEvent evt) {
       // A faire : Monitorer les changements dans le capteur
    }

    public void onAccuracyChanged(Sensor capteur, int accuracy) {
       // A faire : Réagir aux demandes de changement de précision du capteur
    }
}

Le paramètre de type SensorEvent dans la méthode onSensorChanged() contient quatre propriétés utilisées pour décrire un événement particulier du capteur.

Paramètres de SensorEvent
sensor
L'objet de type Sensor ayant déclanché l'événement.
accuracy
La précision du capteur lorsque l'événement est survenu (faible, moyenne, haute ou non fiable).
values
Tableau de flottants contenant la ou les nouvelles valeurs détectées.
timestamp
L'horodatage (en nanosecondes) auquel l'événement est survenu.

Vous pouvez monitorer séparément les changements dans la précision d'un capteur en utilisant la méthode onAccuracyChanged(). Dans les deux gestionnaires, la valeur accuracy représente la précision du capteur monitoré sous la forme de constantes prédéfinies dont la liste est proposée ci-dessous.

Précision du capteur
SensorManager.SENSOR_STATUS_ACCURACY_LOW
Indique que le capteur possède une faible précision et doit être recalibré.
SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM
Indique que les données du capteur sont de précision intermédiaire et qu'une recalibration améliorerait la lecture.
SensorManager.SENSOR_STATUS_ACCURACY_HIGH
Indique que le capteur a la précision la plus élevée possible.
SensorManager.SENSOR_STATUS_UNRELIABLE
Indique que les données du capteur ne sont pas fiable et qu'une calibration est nécessaire ou que la lecture n'est pas possible.

Pour obtenir les données, nous devons nous enregistrer auprès du service comme écouteur de type SensorEventListener à l'aide de la méthode registerListener() de la classe SensorManager. Spécifiez alors le capteur à observer et la fréquence à laquelle vous voulez recevoir les mises à jour.

SensorManager gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor gyroscope = gestionCapteurs.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
Scrutation scrutation = new Scrutation();
gestionCapteurs.registerListener(scrutation, gyroscope, SensorManager.SENSOR_DELAY_NORMAL);

Comme nous le constatons, la récupération d'une instance de capteur s'associe à un taux de rafraîchissement des données qui peut être plus ou moins précis. Ce taux influence directement les performances de l'application ainsi que l'usage de la batterie. Plus le taux est élévé, plus les données sont rafraîchies et précises, et plus la consommation d'énergie est élevée. Le SensorManager contient les constantes suivantes (dans l'ordre décroissant de réactivité) pour sélectionner une fréquence appropriée :

Taux de rafraîchissement des mesures effectuées
SensorManager.SENSOR_DELAY_FASTER
Le taux de rafraîchissement le plus élevé. Les données sont récupérées aussi vite que possible par le système.
SensorManager.SENSOR_DELAY_GAME
Taux utilisable pour des applications nécessitant des données précises telles que les jeux.
SensorManager.SENSOR_DELAY_NORMAL
Taux normal utilisé par exemple par le système pour détecter les changements d'orientation.
SensorManager.SENSOR_DELAY_UI
Le taux le plus bas utilisé dans les applications ne nécessitant qu'une faible précision ou ne sollicitant que très peu de données, comme les composants graphiques d'une interface utilisateur.

La fréquence que vous sélectionnez n'est pas obligatoire. Le SensorManager peut répondre plus ou moins vite que ce que vous avez spécifié, mais il a tendance à être plus rapide. Pour minimiser le coût en ressources d'utilisation d'un capteur dans votre application, sélectionnez la fréquence la plus faible.

Il est également important de désenregistrer votre SensorEventListener lorsque votre application n'a plus besoin de recevoir de mise à jour, en utilisant la méthode opposée unregisterListener(). Il est d'ailleurs conseillé d'enregistrer et de désenregistrer votre SensorEventListener dans les méthodes de rappel onResume() et onPause() de vos activités pour garantir qu'ils ne sont utilisés que lorsque votre activité est active.

Interpréter les valeurs des capteurs

La longueur et la composition des valeurs renvoyées par l'événement onSensorChanged() varient selon le capteur monitoré. Les détails sont donnés ci-dessous. Des détails complémentaires , du capteur d'orientation et du capteur de champ magnétique seront donnés dans les sections suivantes.

Type de capteur Nombre Contenu des valeurs Commentaire
TYPE_ACCELEROMETER 3 value[0] : Latéral
value[1] : Longitudinal
value[2] : Vertical
Accélération suivant trois axes en m/s2. Le SensorManager contient un ensemble de constantes de gravité de la forme SensorManager.GRAVITY_*.
TYPE_GYROSCOPE 3 value[0] : Azimut
value[1] : Tangage
value[2] : Roulis
Orientation de l'appareil en degrés suivant trois axes.
TYPE_LIGHT 1 value[0] : Luminosité Mesurée en lux. Le SensorManger contient un ensemble de constantes représentant différentes luminosités de la forme SensorManager.LIGNT_*.
TYPE_MAGNETIC_FIELD 3 value[0] : Latéral
value[1] : Longitudinal
value[2] : Vertical
Champ magnétique local mesuré en microteslas (µT).
TYPE_ORIENTATION 3 value[0] : Azimut
value[1] : Tangage
value[2] : Roulis
Orientation de l'appareil en degrés suivant trois axes.
TYPE_PRESSURE 1 value[0] : Pression Mesurée en kilopascals (KP).
TYPE_PROXIMITY 1 value[0] : Distance Mesurée en mètres.
TYPE_TEMPERATURE 1 value[0] : Température Mesurée en degré Celsius.
Utiliser le compas, l'accéléromètre et les capteurs d'orientation

La prise en compte du mouvement au sein des applications est possible grâce à la présence du capteur d'orientation et de l'accéléromètre dans la plupart des appareils récents. Ces dernières années, ces capteurs sont devenus de plus en plus répandus et ont trouvé leur place dans les manettes de contrôle de jeux et les smartphones.

Les accéléromètres et les compas sont utilisés pour fournir des fonctionnalités fondées sur la direction, l'orientation et le mouvement de l'appareil. La disponibilté d'un compas et d'un accéléromètre dépend du matériel sur lequel votre application est exécutée. Lorsqu'ils sont disponibles, ils sont exposés via le SensorManager, vous permettant de :

  1. Déterminer l'orientation courante de l'appareil.
  2. Monitorer et tracer les changements d'orientation.
  3. Savoir à quelle direction l'utilisateur fait face.
  4. Monitorer l'accélération dans toutes les directions : verticale, latérale ou longitudinale.

Cela ouvre quelques perspectives inhabituelles à vos applications. En monitorant l'orientation, la direction et le mouvement, vous pouvez :

  1. Utiliser le compas et l'accéléromètre pour déterminer votre vitesse et votre direction. En conjonction avec une carte, un appareil photo et des services de géolocalisation, vous pouvez créer des interfaces en réalité augmentée qui rajoutent des données locales à un flux temps réel de l'appareil photo.
  2. Créer des interfaces utilisateur s'ajustant dynamiquement à l'orientation de l'appareil. Android modifie déjà l'orientation de l'écran natif lorsque l'appareil passe du mode portrait au mode paysage et inversement.
  3. Monitorer une accélération rapide pour déterminer si l'appareil tombe ou est lancé.
  4. Mesurer le mouvement ou la vibration. Vous pourrez par exemple créer une application qui vous permet de vérouiller l'appareil. Si un mouvement est détecté pendant qu'il est vérouillé, une alerte SMS contenant la position courante peut être envoyée.
  5. Créer des contrôles d'interface utilisateur qui utilisent les gestes et les mouvements comme mode d'entrée.

Vous devrez toujours vérifier la disponibilité des capteurs requis et vous assurer que votre application se terminera proprement s'ils sont absents.

Introduction aux accéléromètres

Les accéléromètres, comme leur nom l'indique, sont utilisés pour mesurer l'accélération. Ils sont aussi parfois nommés capteurs de gravité.

Les accéléromètres sont appelés capteurs de gravité en raison de leur incapacité à distinguer une accélération due à un mouvement d'une accélération due à la gravité. Par conséquent, une accélération détectant une accélération sur l'axe z (vertical) lira -9.81 m/s2 lorsque l'appareil sera immobile (cette valeur est disponible dans la constante SensorManager.STANDARD_GRAVITY).

L'accélération est définie comme la modification de la vitesse d'un mouvement et les accéléromètres mesureront donc comment la vitesse de l'appareil change dans une direction donnée. En utilisant un accéléromètre, vous pourrez détecter les mouvements et, plus utilement, la variation de leur vitesse.

Il est important de noter que les accéléromètres ne mesurent pas la vélocité, et vous ne pourrez donc pas mesurer directement la vitesse par une seule lecture de l'accéléromètre. A la place, vous devrez mesurer les changements d'accélération dans le temps.

Généralement, vous serez intéressé par les changements d'accélération par rapport à un état repos ou par des mouvements rapides (repérables par des changements rapides dans l'accélération) comme des gestes de l'utilisateur. Dans le premier cas, vous devrez souvent calibrer l'appareil pour calculer l'orientation et l'accélération initiales afin de les prendre en compte dans les résultats futurs.

Détecter les changements d'accélération

Comme nous l'avons découvert dans le tableau un peu plus haut, le SensorManager renvoie les modifications selon trois axes. Les valeurs de la propriété values du paramètre de type SensorEvent issu de l'écouteur SensorEventListener représente latérale, longitudinale et verticale dans cet ordre.

Le SensorManager considère qu'un appareil est "au repos" lorsqu'il est posé face vers le haut en mode portrait sur une surface plane. L'accélération peut donc être mesurée selon trois directions :

  1. latéral : gauche-droite : Accélération vers la gauche ou la droite, les valeurs positives représentent la droite et les négatives, la gauche. Une accélération latérale positive sera détectée sur un appareil posé à plat sur son dos en mode portrait et déplacé vers la droite.
  2. longitudinal : avant-arriere : Accélération vers l'avant ou vers l'arrrière, une valeur positive représentant l'avant. Dans la même configuration que ci-dessus, une accélération longitudinale positive sera créée en déplaçant l'appareil vers l'avant.
  3. verticale : haut-bas : Accélération vers le haut ou vers le bas, une valeur positive représentant le haut. Au repos, l'accéléromètre enregistre une valeur de 9.81 m/s2 du fait de la gravité.

Comme nous l'avons décrit plus haut, vous pouvez monitorer les changements dans l'accélération en utilisant un SensorEventListener. Enregistrer une implémentation de ce type d'écouteur avec le SensorManager en utilisant un objet Sensor de type Sensor.TYPE_ACCELEROMETER pour interroger l'accéléromètre.

Dans ce deuxième petit projet, nous allons tester simplement l'accéléromètre intégré du terminal en affichant sous forme de texte la valeur des trois axes représentatifs. Nous prenons en compte l'accéléromètre par défaut en utilisant une fréquence normale.

Encore une fois, l'émulateur d'Android, nous le comprenons bien, ne permet pas d'obtenir les données de capteurs virtuels : il est donc nécessaire de travailler sur un vrai mobile.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="2px"
    android:background="#FFFF00" >
   <TextView 
      android:id="@+id/lateral"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:textSize="23dp"
android:textColor="#FF4400" /> <TextView android:id="@+id/longitudinal" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="23dp"
android:textColor="#FF4400"
/> <TextView android:id="@+id/vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="23dp"
android:textColor="#FF4400" /> </LinearLayout>
fr.btsiris.capteurs.Accelerometre.java
package fr.btsiris.capteurs;

import android.app.Activity;
import android.content.Context;
import android.hardware.*;
import android.os.Bundle;
import android.widget.TextView;

public class Accelerometre extends Activity implements SensorEventListener {
   private SensorManager gestionCapteurs;
   private Sensor accelerometre;
   private TextView lateral, longitudinal, vertical;
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      lateral = (TextView) findViewById(R.id.lateral);
      longitudinal = (TextView) findViewById(R.id.longitudinal);
      vertical = (TextView) findViewById(R.id.vertical);
      gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);     
      accelerometre = gestionCapteurs.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
   }

   @Override
   protected void onStart() {
      super.onStart();
      gestionCapteurs.registerListener(this, accelerometre, SensorManager.SENSOR_DELAY_NORMAL);
   }

   @Override
   protected void onStop() {
      super.onStop();
      gestionCapteurs.unregisterListener(this);
   }   
   
   public void onSensorChanged(SensorEvent evt) {
      if (evt.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
         lateral.setText("Latéral ........... : "+evt.values[0]);
         longitudinal.setText("Longitudinal ... : "+evt.values[1]);
         vertical.setText("Vertical .......... : "+evt.values[2]);
      }
   }

   public void onAccuracyChanged(Sensor capteur, int precision) {   }
}
Mesurer la force gravitationnelle

La force gravitationnelle est l'accélération comparée à la chute libre. Vous pouvez la mesurer en faisant la somme des accélérations dans les trois directions et en la comparant à la valeur de la chute libre.

Dans l'exemple qui suit, nous allons créer un dispositif simple pour mesurer la force gravitationnelle en utilisant l'accéléromètre pour déterminer la force exercée sur l'appareil. Du fait de la gravité, la force exercée sur l'appareil au repos est de 9,81 m/s2 vers le centre de la Terre. Dans cet exemple, vous allez l'annuler en la prenant en compte avec la constante SensorManager.STANDARD_GRAVITY.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView  
      android:id="@+id/acceleration"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:gravity="center"
      android:textStyle="bold"
      android:textSize="32sp"
      android:text="CENTRE"
      android:editable="false"
      android:singleLine="true"
      android:layout_margin="10px" />
    <TextView  
      android:id="@+id/accelerationMax"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:gravity="center"
      android:textStyle="bold"
      android:textSize="40sp"
      android:text="CENTRE"
      android:editable="false"
      android:singleLine="true"
      android:layout_margin="10px"  />     
</LinearLayout>
fr.btsiris.capteurs.Gravitation.java
package fr.btsiris.capteurs;

import android.app.Activity;
import android.content.Context;
import android.hardware.*;
import android.os.Bundle;
import android.widget.TextView;
import java.util.Timer;
import java.util.TimerTask;

public class Gravitation extends Activity implements SensorEventListener {
   private SensorManager gestionCapteurs;
   private Sensor accelerometre;
   private TextView acceleration, accelerationMax;
   private float courante, maxi;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      acceleration = (TextView) findViewById(R.id.acceleration);
      accelerationMax = (TextView) findViewById(R.id.accelerationMax);
      gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);     
      accelerometre = gestionCapteurs.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
      Timer update = new Timer("update");
      update.scheduleAtFixedRate(new TimerTask() {
         @Override
         public void run() {
            miseAJour();
         }
      }, 0, 300);
   }

   @Override
   protected void onStart() {
      super.onStart();
      gestionCapteurs.registerListener(this, accelerometre, SensorManager.SENSOR_DELAY_FASTEST);
   }

   @Override
   protected void onStop() {
      super.onStop();
      gestionCapteurs.unregisterListener(this);
   }   
   
   public void onSensorChanged(SensorEvent evt) {
      float x = evt.values[0];
      float y = evt.values[1];
      float z = evt.values[2];
      float resultante = (float) Math.sqrt(x*x + y*y + z*z);
      courante = Math.abs(resultante-SensorManager.STANDARD_GRAVITY);
      if (courante > maxi) maxi = courante;
   }

   public void onAccuracyChanged(Sensor capteur, int precision) { }
   
   private void miseAJour() {
      runOnUiThread(new Runnable() {
         public void run() {
            String Gs = ""+courante / SensorManager.STANDARD_GRAVITY ;
            acceleration.setText(Gs);
            acceleration.invalidate();            
            String Gmax = "Max = "+ maxi / SensorManager.STANDARD_GRAVITY;
            accelerationMax.setText(Gmax);
            accelerationMax.invalidate();      
         }
      });
   }
}  

Les accéléromètres peuvent être très sensibles, et mettre à jour les TextView pour chaque changement d'accélération détecté peut être très coûteux. Il est plus judicieux de créer une méthode miseAJour() qui se synchronise avec le thread de l'interface utilisateur en fonction d'un Timer.

Déterminer votre orientation

Le capteur d'orientation est une combinaison de capteurs de champ magnétique qui fonctionne comme un compas électronique et un accéléromètre pour déterminer le tangage et le roulis.

En réalité, Android fournit deux alternatives pour déterminer l'orientation de l'appareil. Vous pouvez directement interroger le capteur d'orientation ou dériver celle-ci de l'accéléromètre et du capteur de champ magnétique. La dernière option prend plus de temps mais offre l'avantage d'une plus grande précision et de pouvoir modifier la trame de référence lorsque vous déterminer votre orientation.

En utilisant la trame de référence standard, l'orientation de l'appareil est donnée selon trois dimensions comme l'illustre la liste ci-dessous :

  1. Azimut : Axe qui pointe vers le haut : L'azimut (ou lacet) est la direction à laquelle l'appareil fait face suivant l'axe des x0/360 degrés est le nord, 90 degré l'est, 180 degré le sud et 270 degré l'ouest.
  2. Tangage : Axe qui pointe vers l'avant : Le tangage représente l'ange de l'appareil autour de l'axe des y. Lorsque l'appareil est à plat sur son dos, la valeur est 0. Lorsqu'il est "debout" (sommet de l'appareil vers le haut), la valeur est -90. Lorsqu'il est vers le bas, la valeur est 90 et 180/-180 lorsqu'il est retourné.
  3. Roulis : Axe qui pointe vers le côté droit : Le roulis représente le mouvement de l'appareil autour de l'axe des z. Lorsque l'appareil est posé à plat sur son dos, la valeur est 0, -90 lorsque l'écran fait face à la gauche et 90 lorsque l'écran fait face à la droite.
Déterminer l'orientation avec le capteur d'orientation

La façon la plus simple de monitorer l'orientation de l'appareil est d'utiliser le capteur d'orientation dédié. Créer et enregistrer un SensorEventListener avec le SensorManager en utilisant le capteur d'orientation par défaut.

SensorManager gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor orientation = gestionCapteurs.getDefaultSensor(Sensor.TYPE_ORIENTATION);
gestionCapteurs.registerListener(this, orientation, SensorManager.SENSOR_DELAY_NORMAL);

Lorsque l'orientation change, la méthode onSensorChanged() de votre SensorEventListener est déclenchée. Le paramètre SensorEvent contient un tableau de float qui fournit l'orientation de l'appareil suivant les trois axes, respectivement l'azimut, le tangage et le roulis.

Voici un petit projet qui permet d'évaluer l'orientation du mobile suivant les trois axes.

Encore une fois, l'émulateur d'Android, nous le comprenons bien, ne permet pas d'obtenir les données de capteurs virtuels : il est donc nécessaire de travailler sur un vrai mobile.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="10dp"
    android:background="#FFFF00">
   <TextView 
      android:id="@+id/azimut"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"      
      android:textSize="23dp"
      android:textColor="#FF4400"/>   
   <TextView 
      android:id="@+id/tangage"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:textSize="23dp"
      android:textColor="#FF4400" /> 
   <TextView 
      android:id="@+id/roulis"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:textSize="23dp"
      android:textColor="#FF4400"/>       
</LinearLayout>
fr.btsiris.capteurs.Orientation.java
package fr.btsiris.orientation;

import android.app.Activity;
import android.content.Context;
import android.hardware.*;
import android.os.Bundle;
import android.widget.TextView;

public class Orientation extends Activity implements SensorEventListener {
   private SensorManager gestionCapteurs;
   private Sensor orientation;
   private TextView azimut, tangage, roulis;
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      azimut = (TextView) findViewById(R.id.azimut);
      tangage = (TextView) findViewById(R.id.tangage);
      roulis = (TextView) findViewById(R.id.roulis);
      gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);     
      orientation = gestionCapteurs.getDefaultSensor(Sensor.TYPE_ORIENTATION);
   }

   @Override
   protected void onStart() {
      super.onStart();
      gestionCapteurs.registerListener(this, orientation, SensorManager.SENSOR_DELAY_NORMAL);
   }

   @Override
   protected void onStop() {
      super.onStop();
      gestionCapteurs.unregisterListener(this);
   }   
   
   public void onSensorChanged(SensorEvent evt) {
      if (evt.sensor.getType() == Sensor.TYPE_ORIENTATION) {
         azimut.setText("Azimut ........... : "+evt.values[0]);
         tangage.setText("Tangage ... : "+evt.values[1]);
         roulis.setText("Roulis .......... : "+evt.values[2]);
      }
   }

   public void onAccuracyChanged(Sensor capteur, int precision) {   }
}
Donner le cap

Je vais modifier le projet précédent pour prendre en compte uniquement l'azimut et de donner ainsi le cap suivant les points cardinaux avec une rose des vents complète.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#FFFF00">
   <TextView  
      android:id="@+id/azimut"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:textSize="28sp"
      android:textStyle="italic|bold"
      android:textColor="#000000"
      android:gravity="center" />       
   <ImageView  
      android:layout_width="wrap_content" 
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:src="@drawable/rosedesvents" />
</LinearLayout>

fr.btsiris.capteurs.PointsCardinaux.java
package fr.btsiris.cardinaux;

import android.app.Activity;
import android.content.Context;
import android.hardware.*;
import android.os.Bundle;
import android.widget.TextView;

public class PointsCardinaux extends Activity implements SensorEventListener {
   private SensorManager gestionCapteurs;
   private Sensor orientation;
   private TextView azimut;
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      azimut = (TextView) findViewById(R.id.azimut);
      gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);     
      orientation = gestionCapteurs.getDefaultSensor(Sensor.TYPE_ORIENTATION);
   }

   @Override
   protected void onStart() {
      super.onStart();
      gestionCapteurs.registerListener(this, orientation, SensorManager.SENSOR_DELAY_FASTEST);
   }

   @Override
   protected void onStop() {
      super.onStop();
      gestionCapteurs.unregisterListener(this);
   }   
   
   public void onSensorChanged(SensorEvent evt) {
      if (evt.sensor.getType() == Sensor.TYPE_ORIENTATION) {
         String cap = "";
         float x = evt.values[0];
         if (x<11.25 || x>=348.75) cap ="Nord";
         else if (x>11.25 && x<33.75) cap ="Nord-Nord-Est";
         else if (x>33.75 && x<56.25) cap ="Nord-Est";
         else if (x>56.25 && x<78.75) cap ="Est-Nord-Est";
         else if (x>78.75 && x<101.25) cap ="Est";
         else if (x>101.25 && x<123.75) cap ="Est-Sud-Est";
         else if (x>123.75 && x<146.25) cap ="Sud-Est";
         else if (x>146.25 && x<168.75) cap ="Sud-Sud-Est";
         else if (x>168.75 && x<191.25) cap ="Sud";
         else if (x>191.25 && x<213.75) cap ="Sud-Sud-Ouest";
         else if (x>213.75 && x<236.25) cap ="Sud-Ouest";
         else if (x>236.25 && x<258.75) cap ="Ouest-Sud-Ouest";
         else if (x>258.75 && x<281.25) cap ="Ouest";
         else if (x>281.25 && x<303.75) cap ="Ouest-Nord-Ouest";
         else if (x>303.75 && x<326.25) cap ="Nord-Ouest";
         else if (x>326.25 && x<348.75) cap ="Nord-Nord-Ouest";
         azimut.setText(cap);
      }
   }

   public void onAccuracyChanged(Sensor capteur, int precision) {   }
}
Création d'une boussole

Pour valider l'étude de ce capteur d'orientation, je vous propose de réaliser une boussole qui nous donne le cap général

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.compas"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Boussole" >
        <activity android:name="Compas"  android:label="Boussole" android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="5dp"
android:background="#FFBB00" > <fr.btsiris.compas.VueCompas android:id="@+id/boussole" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
fr.btsiris.compas.Compas.java
package fr.btsiris.compas;

import android.app.Activity;
import android.content.Context;
import android.hardware.*;
import android.os.Bundle;

public class Compas extends Activity implements SensorEventListener {
   private VueCompas boussole;
   private SensorManager gestionCapteurs;
   private Sensor orientation;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      boussole = (VueCompas) findViewById(R.id.boussole);
      gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);     
      orientation = gestionCapteurs.getDefaultSensor(Sensor.TYPE_ORIENTATION);
   }

   @Override
   protected void onStart() {
      super.onStart();
      gestionCapteurs.registerListener(this, orientation, SensorManager.SENSOR_DELAY_FASTEST);
   }

   @Override
   protected void onStop() {
      super.onStop();
      gestionCapteurs.unregisterListener(this);
   }   
      
   public void onSensorChanged(SensorEvent evt) {
      if (evt.sensor.getType() == Sensor.TYPE_ORIENTATION) {
         boussole.setAzimut(evt.values[0]);
         boussole.invalidate();
      }
   }

   public void onAccuracyChanged(Sensor capteur, int precision) { }
}

Cette fois-ci, nous avons besoin d'une vue personnalisée qui s'occupe de réaliser les différents tracés pour la boussole elle-même.

fr.btsiris.compas.VueCompas.java
package fr.btsiris.compas;

import android.content.Context;
import android.graphics.*;
import android.util.AttributeSet;
import android.view.View;

public class VueCompas extends View {
   private Paint texte, cercle, marque;
   private int hauteurTexte;
   private float azimut;

   public void setAzimut(float azimut) { this.azimut = azimut;  }

   public VueCompas(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      init();
   }

   public VueCompas(Context context, AttributeSet attrs) {
      super(context, attrs);
      init();
   }

   public VueCompas(Context context) {
      super(context);
      init();
   }

   private void init() {
      cercle = new Paint(Paint.ANTI_ALIAS_FLAG); 
      cercle.setColor(Color.RED);
      texte =  new Paint(Paint.ANTI_ALIAS_FLAG);
      texte.setColor(Color.YELLOW);
      texte.setTextSize(20);
      hauteurTexte = (int) texte.measureText("yY");
      marque =  new Paint(Paint.ANTI_ALIAS_FLAG);
      marque.setColor(Color.YELLOW);
      marque.setStrokeWidth(3);
   }

   @Override
   protected void onMeasure(int largeur, int hauteur) {
      int dimension = Math.min(mesure(largeur), mesure(hauteur));
      setMeasuredDimension(dimension, dimension);
   }

   private int mesure(int dimension) {
      int modeMesure = MeasureSpec.getMode(dimension);
      int tailleMesure = MeasureSpec.getSize(dimension);
      if (modeMesure == MeasureSpec.UNSPECIFIED) return 200;
      else return tailleMesure;
   }
   
   @Override
   protected void onDraw(Canvas canvas) {
      int px = getMeasuredWidth() / 2;
      int py = getMeasuredHeight() / 2;
      int rayon = Math.min(py, py);
      canvas.drawCircle(px, py, rayon, cercle);
      canvas.save();
      canvas.rotate(-azimut, px, py);
      int largeurTexte = (int) texte.measureText("O");
      int cardinalX = px - largeurTexte / 2;
      int cardinalY = py - rayon+hauteurTexte;
      for (int i=0; i<8; i++) {
         canvas.drawLine(px, py-rayon, px, py-rayon+15, marque);
         canvas.save();
         canvas.translate(0, hauteurTexte);
         String[] cap = {"N", "NE", "E", "SE", "S", "SO", "O", "NO"};       
         if (i==0) {
               int flecheY = 2*hauteurTexte;
               canvas.drawLine(px, flecheY, px-5, 3*hauteurTexte, marque);
               canvas.drawLine(px, flecheY, px+5, 3*hauteurTexte, marque);
               canvas.drawLine(px, flecheY, px, 6*hauteurTexte, marque);
         }
         canvas.drawText(cap[i], cardinalX, cardinalY, texte);
         canvas.restore();
         canvas.rotate(45, px, py);
      }
      canvas.restore();
   }     
} 
Création d'un compas avec un horizon artificiel

Nous allons modifier maintenant la classe VueCompas afin d'intégrer un horizon artificiel qui nous précise à la fois la valeur du tangage et du roulis.

fr.btsiris.compas.VueCompas.java
package fr.btsiris.compas;

import android.content.Context;
import android.graphics.*;
import android.util.AttributeSet;
import android.view.View;

public class VueCompas extends View {
   private Paint texte, cercle, marque;
   private int hauteurTexte;
   private float azimut;
   private float tangage;
   private float roulis;

   public float getAzimut() { return azimut;  }
   public void setAzimut(float azimut) { this.azimut = azimut;  }

   public float getRoulis() { return roulis;  }
   public void setRoulis(float roulis) { this.roulis = roulis; }

   public float getTangage() { return tangage;  }
   public void setTangage(float tangage) { this.tangage = tangage;  }

   public VueCompas(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      init();
   }

   public VueCompas(Context context, AttributeSet attrs) {
      super(context, attrs);
      init();
   }

   public VueCompas(Context context) {
      super(context);
      init();
   }

   private void init() {
      setFocusable(true);
      cercle = new Paint(Paint.ANTI_ALIAS_FLAG); // Mise en place de l'anti-aliasing pout ne pas avoir d'effet d'escalier dans les tracés
      cercle.setColor(Color.RED);
      cercle.setStrokeWidth(1); // dimension du trait (STROKE)
      cercle.setStyle(Paint.Style.FILL_AND_STROKE);  // le bord et le contenu va être pris en compte
      texte =  new Paint(Paint.ANTI_ALIAS_FLAG);
      texte.setColor(Color.YELLOW);
      hauteurTexte = (int) texte.measureText("yY");
      marque =  new Paint(Paint.ANTI_ALIAS_FLAG);
      marque.setColor(Color.YELLOW);
   }

   @Override
   protected void onMeasure(int largeur, int hauteur) {
      int dimension = Math.min(mesure(largeur), mesure(hauteur));
      setMeasuredDimension(dimension, dimension);
   }

   private int mesure(int dimension) {
      int modeMesure = MeasureSpec.getMode(dimension);
      int tailleMesure = MeasureSpec.getSize(dimension);
      if (modeMesure == MeasureSpec.UNSPECIFIED) return 200;
      else return tailleMesure;
   }
   
   @Override
   protected void onDraw(Canvas canvas) {
      int px = getMeasuredWidth() / 2;
      int py = getMeasuredHeight() / 2;
      int rayon = Math.min(py, py);
      canvas.drawCircle(px, py, rayon, cercle);
      canvas.save();
      canvas.rotate(-azimut, px, py);
      int largeurTexte = (int) texte.measureText("O");
      int cardinalX = px - largeurTexte / 2;
      int cardinalY = py - rayon+hauteurTexte;
      for (int i=0; i<24; i++) {
         canvas.drawLine(px, py-rayon, px, py-rayon+10, marque);
         canvas.save();
         canvas.translate(0, hauteurTexte);
         if (i % 6 == 0) {
            String cap = "";
            switch (i) {
               case 0 : 
                  cap = "N";
                  int flecheY = 2*hauteurTexte;
                  canvas.drawLine(px, flecheY, px-5, 3*hauteurTexte, marque);
                  canvas.drawLine(px, flecheY, px+5, 3*hauteurTexte, marque);
                  break;

               case 6 : cap = "E"; break;
               case 12 : cap = "S"; break;
               case 18 : cap = "O"; break;
            }
            canvas.drawText(cap, cardinalX, cardinalY, texte);
         }
         else if (i % 3 == 0) {
            String angle = String.valueOf(i*15);
            float largeurAngle = texte.measureText(angle);
            int angleTexteX = (int) (px - largeurAngle / 2);
            int angleTexteY = py - rayon + hauteurTexte;
            canvas.drawText(angle, angleTexteX, angleTexteY, texte);
         }
         canvas.restore();
         canvas.rotate(15, px, py);
      }
      canvas.restore();
      
      float d = 2*rayon;
      RectF cercleRoulis = new RectF(d/3-d/7, d/2-d/7, d/3+d/7, d/2+d/7);
      marque.setStyle(Paint.Style.STROKE); // Tracé du coutour uniquement
      canvas.drawOval(cercleRoulis, marque);
      marque.setStyle(Paint.Style.FILL); // Tracé de l'intérieur uniquement
      canvas.save();
      canvas.rotate(roulis, d/3, d/2);
      canvas.drawArc(cercleRoulis, 0, 180, false, marque);
      canvas.restore();
      
      RectF cercleTangage = new RectF(2*d/3-d/7, d/2-d/7, 2*d/3+d/7, d/2+d/7);
      marque.setStyle(Paint.Style.STROKE); // Tracé du coutour uniquement
      canvas.drawOval(cercleTangage, marque);
      marque.setStyle(Paint.Style.FILL); // Tracé de l'intérieur uniquement
      canvas.drawArc(cercleTangage, 0-tangage/2, 180+tangage, false, marque);      
   }     
}
Déplacement d'une boule suivant l'horizontalité du mobile

Dernier exercice concernant l'accéléromètre en vous proposant une boule que se déplace suivant l'horizontalité de votre smartphone. Il faut par contre récupérer une image de fond et une image de la boule que vous placerez dans la ressource drawable.

fr.btsiris.niveau.Niveau.java
package fr.btsiris.niveau;

import android.app.Activity;
import android.content.Context;
import android.graphics.*;
import android.hardware.*;
import android.os.Bundle;
import android.view.*;

public class Niveau extends Activity implements SensorEventListener {
   private SensorManager gestionCapteurs;
   private Sensor accelerometre;
   private SurfaceView surface;
   private SurfaceHolder holder;
   private Bitmap boule, fond;
   private float bx, by, vx, vy, tx, ty;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      boule = BitmapFactory.decodeResource(getResources(), R.drawable.boule);
      fond = BitmapFactory.decodeResource(getResources(), R.drawable.fond);
      surface = new SurfaceView(this);
      holder = surface.getHolder();
      gestionCapteurs =  (SensorManager) getSystemService(Context.SENSOR_SERVICE);     
      accelerometre = gestionCapteurs.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
      setContentView(surface);
   }

   @Override
   protected void onStart() {
      super.onStart();
      gestionCapteurs.registerListener(this, accelerometre, SensorManager.SENSOR_DELAY_GAME);
   }

   @Override
   protected void onStop() {
      super.onStop();
      gestionCapteurs.unregisterListener(this, accelerometre);
   }
   
   public void onSensorChanged(SensorEvent evt) {
      if (evt.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
         
         // Ajout les valeurs du capteur aux variables 
         vx -= evt.values[0];
         vy += evt.values[1];
         
         // Déplacement de la boule
         bx += vx;
         by += vy;
         
         // Récupération de la taille de l'écran
         tx = surface.getWidth() - boule.getWidth();
         ty = surface.getHeight() - boule.getHeight();     
         
         // Empêcher la boule de sortir
         if (bx<0)  { bx = 0; vx = 0; }
         if (by<0)  { by = 0; vy = 0; }
         if (bx>tx) { bx = tx; vx = 0; }
         if (by>ty) { by = ty; vy = 0; }
         
         // Redessiner
         Canvas dessin = holder.lockCanvas();
         if (dessin==null) return;
         dessin.drawBitmap(fond, 0, 0, null);
         dessin.drawBitmap(boule, bx, by, null);
         holder.unlockCanvasAndPost(dessin);
      }
   }

   public void onAccuracyChanged(Sensor capteur, int precision) {   }
}
Tracés personnalisés - Graphismes 2D

J'aimerais, au travers de cet exemple, puisque je ne l'ai pas encore fait, vous parler des tracés personnalisés.

  1. Lorsque vous devez utiliser un composant dont l'apparence est à votre libre initiative, la première chose à faire consiste à créer un nouveau composant qui hérite de la classe View et de redéfinir un certain nombre de méthode dont vous avez la description ci-dessous :
  2. onDraw(Canvas) : Méthode automatiquement appelée par le système lorsque la vue doit être affichée à l'écran. La méthode fournit un Canvas en paramètre qui est utilisé comme support des dessins. Je rappelle qu'il est possible d'imposer le réaffichage au travers de la méthode invalidate() du composant à réafficher.
  3. La méthode onDraw() contient toute la magie. Vous créez un nouveau widget à partir de zéro car vous voulez créer une interface visuelle entièrement nouvelle. La paramètre Canvas de la méthode onDraw() va vous permettre de donner vie à votre imagination.
  4. Android fournit une palette d'outils pour vous y aider, à l'aide de divers objets Paint. La classe Canvas comprend toutes les méthodes drawXXX() pour le dessin d'objet 2D primitifs comme les cercles, lignes, rectangles, textes et Drawable. Elle supporte également les transformations qui permettent la rotation, le déplacement et le redimmensionnement du Canvas pendant que nous dessinons.
  5. Lorsque vous utilisez ces outils en les combinant à des Drawable et à la classe Paint (qui offre divers stylos et outils de remplissage personnalisables), la complexité et les détails que vos contrôles pourront afficher ne seront limités que par la taille de l'écran et la puissance du processeur.
  6. onSizeChanged(int, int, int, int) : Cette méthode peut être redéfinie pour être notifié d'un changement de taille de la vue : les deux premiers paramètres définissent les anciennes largeur et hauteur, alors que les deux derniers sont les nouvelles dimensions. Elle est généralement utilisée pour initialiser certianes propriétés de la vue.
  7. onMeasure(int, int) : Cette méthode est automatiquement exécutée par le système lorsque ce dernier souhaite obtenir les dimensions choisies par la vue. Le fonctionnement de cette méthode est un peu particulier car il est impératif d'appeler la méthode setMeasuredDimension(int, int) lors de son exécution. Cette commande informe le système des dimensions (largeur et hauteur) choisies.
  8. La méthode onMeasure() est appelée lorsque le contrôle parent dispose ses contrôles enfants. Il leur demande quel est l'espace dont ils ont besoin et passe deux paramètres la largeur et la hauteur. Ils spécifient l'espace disponible pour le contrôle ainsi que quelques métadonnées décrivant cet espace. Plutôt que renvoyer un résultat, vous passez la hauteur et la largeur de la vue à la méthode setMeasureDimension().
  9. Les paramètres correspondant à la largeur et à la hauteur sont passés sous forme d'entiers pour des raisons d'efficacité. Avant de pouvoir être utilisés, il sdoivent être décodés à l'aide des méthodes statiques getMode() et getSize() de la classe MeasureSpec.
  10. Selon la valeur de mode, size représente soit l'espace maximal (fill_parent) disponible pour le contrôle (si AT_MOST), soit la taille exacte (wrap_content) de votre contrôle (si EXACTLY). Si vous indiquez UNSPECIFIED, votre contrôle ne saura pas ce que la taille représente.
  11. En indiquant EXACTLY, le parent force la View à se placer dans un espace à la taille spécifiée. Dans le mode AT_MOST, il demande à la View quelle est la taille de l'espace qu'elle veut occuper, la limite haute étant spécifiée. Dans la plupart des cas, la valeur que vous renverrez sera la même.
  12. Dans les deux cas, vous devrez considérer ces limites comme absolues. Dans certaines circonstances, il peut être néanmoins approprié de renvoyer une dimension allant au-delà. Vous laisserez dans ce cas la parent choisir comment gérer le dépassement en utilisant des techniques comme le clipping (bornage du résultat) ou le scrolling (défilement).
  13. Android fournit diverses classes permettant de dessiner "en bas niveau". Les trois principales sont décrites ci-dessous :
  14. Canvas : En effectuant une analogie avec un peintre, nous pourrions comparer le Canvas à la toile (c'est d'ailleurs l'expression anglaise pour "toile") sur laquelle l'artiste dessine. Il autorise des opérations basiques, comme les translations, les rotations, etc. permettant de mimer les gestes du peintre sur la toile. Utilisé seul, un Canvas est clairement inutile. Pour tracer et dessiner sur cette feuille blanche (ou plutôt transparente), il est nécessaire d'utiliser un pinceau. Un Canvas n'a pas de taille ni d'existence réelle en tant que tel. Il doit être employé en conjonction d'un Paint et d'un Bitmap décrit ci-dessous.
  15. Paint : Traduit de façon littérale, le pinceau (ou Paintbrush) est un objet permettant de définir la forme et le style du dessin qui sera généré. Paint est donc porteur d'informations comme la couleur ou la largeur du trait, l'état de l'anti-aliasing, le style du texte, la façon dont est remplie la forme, etc.
  16. Bitmap : Il s'agit en quelque sorte de l'objet affiché. Un Bitmap est une image brute composée de pixels sur laquelle un Canvas est nécessaire lorsque nous souhaitons y dessiner.
  17. Pour se déplacer sur le Canvas, il est possible d'utiliser des méthodes telles que translate(float, float), rotate(float), scale(float, float), etc. Dès lors qu'une de ces méthodes est appelée, l'état du Canvas est impacté. Ainsi, si nous passons d'un état A à B en utilisant la transformation x, puis de B à C par la transformation y, il est nécessaire de calculer les transformations inverses y' et x' pour revenir à l'état A. Cette opération est parfois difficile, il est alors possible de sauvegarder l'état actuel d'un Canvas grâce à la méthode save(). L'état du Canvas peut ensuite être restitué par un simple appel à restore().
  18. invalidate() : Cette méthode permet d'armer un bit système sur une vue décrivant son état "impropre". Cette méthode est donc extrêmement importante car elle informe le système de la nécessité de redessiner la vue en question. Lorsqu'un appel à invalidate() est effectué, Android mémorise la demande et exécutera une passe "dessin" dès que possible (au prochain tour de boucle événementielle).
Ressources Drawable

Développer des vues personnalisées, comme l'exemple que nous venons de traiter, permet d'avoir un contrôle extrêmement précis sur les objets dessinés. C'est en réalité la façon la plus "bas-niveau" qui s'offre au développeur lorsqu'il s'agit de dessiner à l'écran. Malheureusement, dessiner "bas-niveau" est une opération longue, sujette à erreur et souvent difficile à réaliser. En effet, le développement "bas-niveau" d'interfaces graphiques oblige le programmeur à gérer manuellement un grand nombre de paramètres habituellement gérés par le système au niveau "vue", tels que la densité, la résolution, l'orientation de l'écran, etc.

Afin de faciliter la création de graphisme, Android offre une fonctionnalité extrêmement pratique : les Drawables. La notion de Drawable sous Android est assez large puisqu'elle définit tout ce qui peut être dessiné. Ainsi une couleur, une forme, un dégradé, une image, etc., sont considérés comme des Drawables.

Android utilise largement les Drawable, il en fait donc un composant essentiel à son fonctionnement. L'utilité des Drawables réside dans les points donnés ci-après :

  1. Un Drawable est une abstraction de ce qui est "dessinable". Cette généralisation facilite grandement l'utilisation des graphismes tout en donnant facilement accès à des fonctionnalités plus évoluées que de simples Bitmap et Canvas.
  2. Les Drawables gèrent de façon intrinsèque leur taille et s'adaptent automatiquement à un changement de dimensions. Cette particularité permet de s'accomoder facilement des résolutions variées de l'écosystème des terminaux Android.
  3. Dans l'exemple précédent, il a fallu étendre View pour développer nos propres graphismes. Le concept de Drawable évite cet héritage en autorisant la modification d'une vue prédéfinie de façon native. La classe View autorise, par exemple, le développeur à modifier son fond par la méthode setBackgroundDrawable(Drawable). Ainsi, vous pouvez facilement imaginer une View disposant d'un fond dégradé, coloré ou même représentant une image.
Architecture des Drawables

L'architecture des Drawables est largement inspirée de celle des View. En effet, un Drawable peut contenir d'autres Drawables "feuille". Nous retrouvons donc le modèle de conception "Composite".

La principale différence entre ces deux architectures réside dans l'impossibilité d'instancier un Drawable. En effet, cette classe est abstraite et oblige le développeur à implémenter un certain nombre de méthodes lorqu'il souhaite créer sa propre instance de Drawable.

Principales fonctionnalités des Drawables

L'obtention d'une référence sur un Drawable s'effectue par l'intermédiaire d'un objet de type android.content.res.Resources :

Drawable drawable = context.getResources().getDrawable(R.drawable.balle);

Dès lors, il est possible de modifier la plupart des propriétés du Drawable. Parmi les fonctionnalités gérées par les Drawables, nous retrouvons :

  1. La taille et l'emplacement à l'aide des méthodes suivantes:
    setBounds(Rect)
    Définit le rectangle dans lequel le Drawable sera affiché. Cette méthode permet donc de définir la taille du Drawable, mais également son origine.
    getIntrinsicWidth(), getIntrinsicHeight()
    Retourne, si possible, la taille intrinsèque du Drawable. Ainsi, les dimensions intrinsèques d'une image sont les dimensions de l'image elle-même, alors qu'un Drawable de type "couleur" n'a pas de taille intrinsèque (la valeur retournée par défaut est alors égale à -1).
    getMinimumWidth(), getMinimumHeight()
    Retourne les dimensions minimales suggérées par le Drawable.
  2. L'état : setState(int) : Un Drawable peut changer son apparence en fonction de l'état dans lequel il se trouve. A titre d'exemple, un bouton n'a pas la même apparence lorsqu'il est dans l'état pressé et lorsqu'aucune action n'est exécutée.
  3. Le niveau : Certains Drawables changent d'apparence en fonction du niveau courant. Ainsi, il est possible de définir des Drawables qui changent d'apparence facilement. Ces Drawables sont principalement utilisés pour représenter des barres de progression ou des icônes exprimant le niveau courant d'une entité graduée (qualité de la réception, état de la batterie, etc.).
  4. Les propriétés d'affichage : La classe Drawable autorise la modification de l'apparence générale du graphique sous-jacent. Des méthodes permettant de modifier l'opacité - setAlapha(int) - ou le filtre de couleur - setColorFilter(ColorFilter) - sont ainsi disponibles.
Tour d'horizon des Drawables

A l'instar d'une View, un Drawable peut être instancié via XML ou Java. Dans la suite de cette rubrique, nous allons principalement nous attarder sur le code XML exactement pour les mêmes raisons que celles évoquées pour la mise en oeuvre des View.

Il est tout de même important de conserver à l'esprit que l'instanciation via XML n'est en réalité qu'une surcouche de l'instanciation via du code Java. A ce titre, les fonctionnalités accessibles dans un fichier XML sont un sous-ensemble de celles offertes par Java. Il est donc probable que certaines fonctionnalités ne soient pas réalisables via XML, mais le soient avec Java.

Android contient de nombreux types de ressources Drawable simples qui peuvent être entièrement définies en XML. Ces ressources comprennent les classes ColorDrawable, ShapeDrawable et GradientDrawable. Elles sont stockées dans le dossier res/drawable et peuvent ainsi être référencées dans du code en utilisant leurs noms de fichiers XML en minuscules.

Si ces Drawables sont définis en XML et que vous spécifiiez leurs attributs en pixels à densité indépendante, le moteur d'exécution les mettra à l'échelle.Tout comme les graphiques vectoriels, ces Drawables peuvent être dynamiquement mis à l'échelle afin d'être affichés correctement et sans artifices, et ce quelles que soient la taille de l'écran, sa résolution ou sa densité en pixels. Les Drawables gradient constituent une exception à cette règle puisque le rayon du gradient doit être défini en pixels.

Comme nous le verrons, vous pouvez utiliser ces Drawables en les combinant à des Drawables transformables ou composites. Ensemble, ils peuvent constituer des éléments d'interface utilisateur dynamiques et évolutifs, requérant moins de ressources et agréables à afficher sur n'importe quel écran.

ColorDrawable

Le ColorDrawable est une objet n'affichant qu'une unique couleur. De façon assez logique, le ColorDrawable ne respecte pas le ColorFilter et les dimensions données par setBounds(Rect). Il s'affiche en réalité dans l'intégralité de la zone de clip.

Le ColorDrawable n'est en fait qu'une encapsulation d'une couleur dans la notion de Drawable. Puisqu'Android gère indépendamment les ressources de type "couleur", nous utiliserons rarement ce type de Drawable. Il est néanmoins à noter qu'un setBackgroundColor(int) génère en réalité un ColorDrawable à partir de la couleur donnée.

Le ColorDrawable est le plus simple des Drawables définis en XML. Il vous permet d'afficher une image basée sur une couleur unie. Les ColorDrawables sont définis dans des fichiers XML situés dans le dossier de ressources drawable, en utilisant la balise <color>.

A titre d'exemple, je vous propose de reprendre le projet sur la localisation afin de prévoir un fond de couleur unie personnalisée dans l'activité principale de l'application.

res/drawable/fond.xml
<?xml version="1.0" encoding="utf-8"?>
<color 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#FFCC00" 
/>
res/values/couleurs.xml
<?xml version="1.0" encoding="UTF-8"?>
<resources>
   <color name="texte">#0000FF</color>
</resources>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/fond"
    android:padding="10px">
   <EditText 
      android:id="@+id/rue"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Rue" />   
   <EditText 
      android:id="@+id/ville"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Ville" />         
   <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Rechercher" 
      android:onClick="localiser"/>         
   <TextView  
      android:id="@+id/latitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:textStyle="bold"
      android:textColor="@color/texte" 
      android:textSize="18sp" />
   <TextView  
      android:id="@+id/longitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"  
      android:textStyle="bold"
      android:textColor="@color/texte"
      android:textSize="18sp" />
</LinearLayout>
fr.btsiris.localisation.R.java
/* AUTO-GENERATED FILE.  DO NOT MODIFY. */

package fr.btsiris.localisation;

public final class R {
    public static final class attr {
    }
    public static final class color {
        public static final int texte=0x7f040000;
    }
    public static final class drawable {
        public static final int fond=0x7f020000;
    }
    public static final class id {
        public static final int latitude=0x7f050002;
        public static final int longitude=0x7f050003;
        public static final int rue=0x7f050000;
        public static final int ville=0x7f050001;
    }
    public static final class layout {
        public static final int main=0x7f030000;
    }
}  
ShapeDrawable

Les ShapeDrawables (forme) vous permettent de définir des formes primitives simples en spécifiant leurs dimensions, fond et coutour, à l'aide de la balise <shape>.

Chaque shape est constitué d'un type (spécifié par l'attribut shape), d'attributs de dimension et de sous-noeuds spécifiant l'épaisseur du conteour et la couleur de fond.

Android supporte actuellement les formes suivantes comme valeur de l'attribut shape
oval
Simple ovale.
rectangle
Supporte le sous-noeud <corners> avec l'attribut radius pour créer un rectangle aux angles arrondis. Il est même possible de régler chaque coin particulier au moyen des attributs spécifiques suivants : topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius.
ring
Supporte les attributs innerRadius et thickness qui vous permettent de spécifier respectivement le rayon intérieur de la forme anneau ainsi que son épaisseur. Vous pouvez également utiliser innerRadiusRatio et/ou thicknessRatio pour définir le rayon intérieur de l'anneau et son épaisseur proportionnellement à sa largeur (un rayon intérieur d'un quart de la largeur aura la valeur 4).
line
Ligne horizontale dont la largeur traverse la totalité du composant View. Cette forme particulière requiert le sous-noeud <stroke> pour définir la largeur et la couleur de la ligne.
  1. Le sous-noeud <stroke> permet de spécifier la bordure des formes à l'aide des attributs width et color. Il existe deux autres attributs dashGap et dashWidth qui permettent d'avoir des lignes discontinues en spécifiant respectivement la distance entre les traits et la longueur des traits.
  2. Vous pouvez également inclure un noeud <padding> pour positionner votre forme sur le Canvas de manière relative.
  3. De façon plus utile, vous pouvez inclure un sous-noeud spécifiant la couleur de fond. Le cas le plus simple consiste à utiliser le noeud <solid> avec l'attribut color pour définir la couleur de fond unie.
  4. Le dégradé de couleur, qui sera étudié spécifiquement dans la prochaine section, se traduit au moyen du sous-noeud <gradient>.
  5. Le sous-noeud <size> permet de préciser une dimension spécifique à l'aide des attributs width et height.
  6. useLevel : Cet attribut de la balise <shape>, normalement associé à LevelListDrawable, doit être spécifié à false si vous voulez qu'il soit visible

Je reprend l'exemple précédent, mais cette fois-ci, ce sont les TextView qui profitent du fond créer dans le drawable.

res/drawable/fond.xml
<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
       <solid android:color="#FFFF00" />
       <stroke android:width="5dp" android:color="#FFCC00" />
       <corners android:radius="15dp" />
       <padding android:left="15dp" android:top="10dp" android:right="15dp" android:bottom="10dp" />
</shape>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"android:padding="5px">
   <EditText 
      android:id="@+id/rue"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Rue" />   
   <EditText 
      android:id="@+id/ville"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Ville" />         
   <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Rechercher" 
      android:onClick="localiser"/>         
   <TextView  
      android:id="@+id/latitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:textStyle="bold"
      android:textColor="@color/texte" 
      android:textSize="18sp" 
      android:background="@drawable/fond" />
   <TextView  
      android:id="@+id/longitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"  
      android:textStyle="bold"
      android:textColor="@color/texte"
      android:textSize="18sp"
      android:background="@drawable/fond" />
</LinearLayout>
GradientDrawable

Comme son nom le laisse à penser, le GradientDrawable permet de reproduire des dégradés de couleur. Chaque gradient définit une transition harmonieuse entre deux ou trois couleurs de façon linéaire, radiale ou circulaire.

Les GradientDrawables sont définis par la balise <gradient> comme un sous-noeud de la définition d'un ShapeDrawable, au travers de la balise principale <shape>.

Chaque GradientDrawable requiert au moins un attribut startColor et un attribut endColor et supporte l'attribut optionnel middleColor., ainsi que l'attribut type.

L'attribut type vous permet de choisir votre gradient parmi les suivants
linear
Le type par défaut. Il affiche une transition de couleurs directe de startColor à endColor suivant un angle défini par l'attribut angle.
radial
Dessine un gradient radial de startColor à endColor depuis le bord externe de la forme jusqu'à son centre. Il requiert un attribut gradientRadius qui spécifie le rayon de la transition du gradient en pixels. Il supporte également optionnellement les attributs centerX et centerY pour décaler le centre du gradient. Le rayon étant défini en pixels, il ne sera pas mis à l'échelle dynamiquement pour différentes densités de pixels. Pour minimiser l'effet d'escalier, vous pourrez spécifier différentes valeurs de rayon pour différentes résolutions d'écrans.
sweep
Dessine un gradient circulaire de startColor à endColor le long du bord externe de la forme parent (un anneau, typiquement).

Voici ce que nous pouvons obtenir respectivement avec un dégradé linéaire, un radial et radial dont le centre est choisi sur le bord gauche en bas :

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/fond"
    android:padding="15dp">
   <EditText 
      android:id="@+id/rue"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Rue" />   
   <EditText 
      android:id="@+id/ville"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:hint="Ville" />         
   <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Rechercher" 
      android:onClick="localiser"/>         
   <TextView  
      android:id="@+id/latitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:textStyle="bold"
      android:textColor="@color/texte" 
      android:textSize="18sp" />
   <TextView  
      android:id="@+id/longitude"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"  
      android:textStyle="bold"
      android:textColor="@color/texte"
      android:textSize="18sp" />
</LinearLayout>
(linéaire) res/drawable/fond.xml
<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
       <stroke android:width="3dp" android:color="#FFFF00" />
       <corners android:radius="15dp" />
       <gradient 
            android:startColor="#FFFF00"
            android:endColor="#FFFF00"
            android:centerColor="#FF0000"
            android:type="linear"
            android:angle="45" />
</shape>
(radial) res/drawable/fond.xml
<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
       <stroke android:width="3dp" android:color="#FFFF00" />
       <corners android:radius="15dp" />
       <gradient 
            android:startColor="#FF0000"
            android:endColor="#FFFF00"
            android:type="radial"
            android:gradientRadius="300"  />
</shape>
(radial avec une valeur de position) res/drawable/fond.xml
<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
       <stroke android:width="3dp" android:color="#FFFF00" />
       <corners android:radius="15dp" />
       <gradient 
            android:startColor="#FF0000"
            android:endColor="#FFFF00"
            android:type="radial"
            android:gradientRadius="500"
            android:centerX="0"
            android:centerY="1"
       />
</shape>
BitmapDrawable

Le BitmapDrawable est probablement le plus utilisé des Drawables. Cet objet est la représentation sous forme de Drawable d'une image de format jpg ou png. Nous récupérons donc souvent ce dernier par un simple getDrawable() sur la ressource "brute". Il est néanmoins possible de créer un BitmapDrawable via XML à l'aide de la balise <bitmap /> et d'utiliser les paramètres avancés.

Attributs disponibles
android:antialias
Lors de l'affichage de l'image, il est possible de proposer un anti-aliasing en positionnant cet attribut à true. Cet attribut est utile dans le cas d'un rééchantillonage automatique de votre image à visualiser sur la totalité de l'écran, si cette dernière est plus petite que ce que propose l'écran.
android:filter
Dans le même ordre d'idée, il est possible de valider l'attribut filter (true) lorsque vous avez besoin que votre image soit plus douce (flou glaussien).
android:gravity
A l'instar du FrameLayout, le BitmapDrawable gère la notion de gravité. Il est ainsi possible de positionner une image à l'intérieur de la zone d'affichage du Drawable (les bounds). Par défaut, la gravité est à fill ce qui signifie que l'image est étirée pour remplir intégralement les bounds. Voici l'ensemble des valeurs possibles pour cet attribut : top, bottom, left, right, center_vertical, fill_vertical, center_horizontal, fill_horizontal, center, fill, clip_vertical, clip_horizontal, start et end.
android:src
Resource de l'image, votre fichier original.
android:tileMode
Le BitmapDrawable gère également la notion de répétition d'un motif. Cette possibilité est en quelque sorte l'équivalent de la propriété CSS background-repeat. En réalité, voici les valeurs possibles pour cet attribut : disabled (mode par défaut), clamp (Réplication de la couleur de la bordure), repeat (répétition en x et en y) et mirror(répétition également, mais avec un effet mirroir - symétrie).

Toujours par rapport au projet précédent, je propose de placer une trame de fond prédéfinie dans l'activité principale, que nous pouvons retrouver dans les ressources android dans les contantes entières qui sont à votre disposition, ici android.R.drawable.dialog_frame :

res/drawable/fond.xml
<?xml version="1.0" encoding="utf-8"?>
<bitmap  xmlns:android="http://schemas.android.com/apk/res/android" 
    android:src="@android:drawable/dialog_frame"
    android:tileMode="repeat"
/>
Drawables composites

Les Drawables composites vous permettent de combiner et de manipuler d'autres ressources Drawable. Tout Drawable peut être utilisé dans les ressources composites qui suivent, y compris les bitmaps, les shapes et les colors. De même, ces nouveaux Drawables peuvent être utilisés les uns dans les autres et assignés à des Views tout comme les autres.

Drawables transformables

Vous pouvez mettre à l'échelle et faire tourner des Drawables à l'aide des classes ScaleDrawable et RotateDrawable. Ces Drawables transformables sont particulièrement utiles pour créer des barres de progression ou des Views animées.

ScaleDrawable
Utilisez les attributs scaleHeight et scaleWidth dans la balise <scale> pour définir la hauteur et la largeur voulues relativement aux limites du Drawable d'origine. Utilisez l'attribut scaleGravity pour contrôler le point d'ancrage de l'image mise à l'échelle.
RotateDrawable
Utilisez fromDegrees et toDegrees dans l'attribut <rotate> pour définir le point de départ et la fin de l'angle de rotation autour d'un point pivot. Définissez le pivot à l'aide des attributs pivotX et pivotY, en spécifiant respectivement un pourcentage de la largeur et de la hauteur du Drawable à l'aide de la notation nn%.

Pour appliquer une mise à l'échelle et une rotation au moment de l'exécution, utilisez la méthode setLevel() sur l'objet View portant le Drawable afin d'effectuer un mouvement entre les valeurs de départ et de fin (0 à 10 000).

Lors d'un mouvement à travers plusieurs niveaux, le niveau 0 représente l'angle de départ (ou le plus petit résultat de la mise à l'échelle). Le niveau 10 000 représente la fin de la transformation (l'angle de fin ou la plus haute mise à l'échelle).

Les listing ci-dessous montrent les définitions XML de deux Drawables transformables et le code Java correspondant qui permet de les manipuler après les avoir assignés à une View image.

<?xml version="1.0" encoding="utf-8"?>
<rotate 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/icon"
    android:fromDegrees="0"
    android:toDegrees="90"
    android:pivotX="50%"
    android:pivotY="50%"
/>
<?xml version="1.0" encoding="utf-8"?>
<scale 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/icon"
    android:scaleHeight="100%"
    android:scaleWidth="100%"
/>  
ImageView  rotationImage = (ImageView) findViewById(R.id.rotationImage);
ImageView  echelleImage = (ImageView) findViewById(R.id.echelleImage);
rotationImage.setImageLevel(5000); // Fait pivoter l'image de 50%.
echelleImage.setImageLevel(5000); // Met l'image à une échelle de 50% de sa taille finale.  
Drawable layer

Le LayerDrawable vous permet d'assembler plusieurs ressources Drawable les unes sur les autres. Si vous définissez un tableau de Drawables partiellement transparents, vous pourrez les empiler les uns sur les autres afin de créer des combinaisons de formes dynamiques et de transformations.

De façon similaire, vous pouvez utiliser les LayerDrawables comme source des Drawables transformables décrits dans la section précédente ou des Drawables state list et level list que nous présenterons plus loin.

Le listing suivant montre un LayerDrawable. Il est défini via la balise <layer-list>. Dans cette balise, l'attribut drawable de chaque sous-noeud <item> définit des Drawables comme des composites. Chaque Drawable sera empilé dans l'ordre de l'index, du coup, le premier du tableau sera finalement placé au bas de la pile.

A titre d'exemple, je vous propose de prévoir le fond du projet de localisation avec deux couches, d'une part notre trame de fond sur laquelle se place le dégradé circulaire que nous avons déjà mis en oeuvre précédemment :

res/drawable/fond.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list  xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:drawable="@drawable/trame" />
   <item android:drawable="@drawable/gradient" />
</layer-list>
res/drawable/trame.xml
<?xml version="1.0" encoding="utf-8"?>
<bitmap  xmlns:android="http://schemas.android.com/apk/res/android" 
    android:src="@android:drawable/dialog_frame"
    android:tileMode="repeat"
/>
res/drawable/gradient.xml
<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
       <gradient 
            android:startColor="#AAFF0000" // Il faut une couleur transparente pour voir la trame de fond
            android:endColor="#AAFFFF00" // Il faut une couleur transparente pour voir la trame de fond
            android:type="radial"
            android:gradientRadius="500"
            android:centerX="0"
            android:centerY="1" />
</shape>
Drawables state list

Un Drawable state list est une ressource composite qui vous permet de spécifier un Drawable à afficher en fonction de l'état de la View à laquelle il a été assigné. La plupart des Views Android natives utilisent des Drawables state list, comme l'image utilisée pour les boutons et le fond des items de la ListView standard.

Pour définir un Drawable state list, créez un fichier XML spécifiant une ressource Drawable alternative pour chaque état.
.

<?xml version="1.0" encoding="utf-8"?>
<selector  xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:state_window_focused="false" android:drawable="@drawable/boutonNormal" />
   <item android:state_pressed="true"  android:drawable="@drawable/boutonPresse" />
   <item android:state_focused="true"   android:drawable="@drawable/boutonSelectionne" />
   <item android:drawable="@drawable/boutonNormal" />
</selector>

 

Choix du chapitre Prendre des photos

Les appareils photo - qui peuvent également prendre des vidéos selon les modèles - sont maintenant présents sur la plupart des téléphones. Bien qu'ils soient nettement moins performants que des appareils classiques pour la plupart des utilisations, ils offrent néanmoins de très nombreuses fonctionnalités et représentent une source quasi inépuisable d'applications.

Utiliser les intentions pour prendre des photos

La façon la plus simple de prendre une photo est d'utiliser la constante statique ACTION_IMAGE_CAPTURE du MediaStore dans un Intent passé à la méthode startActivityForResult().

Ceci lancera l'activité de l'APN (Appareil Photo Numérique), permettant aux utilisateurs de modifier manuellement les réglages, et vous évitera de redévelopper entièrement une application. L'action de capture d'image supporte deux méthodes, Thumbnail (vignette) et Full Image (taille réelle) :

  1. Thumbnail : Par défaut, la photo prise par l'action de capture d'image renverra une vignette en format bitmap dans l'extra "data" du paramètre Intent renvoyé par la méthode onActivityResult(). Appelez la méthode getParcelableExtra() sur le paramètre Intent en spécifiant le nom de l'extra "data" pour renvoyer cette vignette.
  2. Full Image : Si vous spécifiez une URI de sortie en utilisant l'extra MediaStore.EXTRA_OUTPUT dans l'Intent de lancement, l'image en taille réelle sera sauvegardée à l'emplacement spécifié. Dans ce cas, aucune vignette ne sera renvoyée dans le rappel de l'activité et le "data" de l'Intent sera null.
L'activité suivante montre comment utiliser l'action de capture d'image pour récupérer une vignette ou une photo en taille réelle à l'aide du mécanisme des intentions :
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <Button 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:text="Photo mignature" 
       android:onClick="vignette" />
    <Button 
       android:layout_width="fill_parent" 
       android:layout_height="wrap_content" 
       android:text="Photo normale" 
       android:onClick="sauvegarde" />       
    <ImageView
       android:id="@+id/image"
       android:layout_width="fill_parent" 
       android:layout_height="fill_parent"  />
</LinearLayout>
fr.btsiris.photo.Photo.java
package fr.btsiris.photo;

import android.app.Activity;
import android.content.Intent;
import android.graphics.*;
import android.net.Uri;
import android.os.*;
import android.provider.MediaStore;
import android.view.View;
import android.widget.*;
import java.io.File;

public class Photo extends Activity {
   private ImageView image;
   private Uri fichierUri;
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        image = (ImageView) findViewById(R.id.image);
   }
   
   public void vignette(View vue) {
      Intent intention = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
      startActivityForResult(intention, 1);
   }
   
   public void sauvegarde(View vue) {
      Intent intention = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
      File fichier = new File(Environment.getExternalStorageDirectory(), "test.jpg");
      fichierUri = Uri.fromFile(fichier);
      intention.putExtra(MediaStore.EXTRA_OUTPUT, fichierUri);
      startActivityForResult(intention, 1);
   }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      if (requestCode == 1) {
         Uri imageUri = null;
         if (data != null) {
            if (data.hasExtra("data")) {
               Bitmap vignette = data.getParcelableExtra("data");
               image.setImageBitmap(vignette);
            }
         }
         else {
            File fichier = new File(Environment.getExternalStorageDirectory(), "test.jpg");
            Bitmap photo = BitmapFactory.decodeFile(fichier.getPath());
            image.setImageBitmap(photo);
         }
      }
   }
}

Contrôler l'APN et prendre des photos

Que ce soit pour prendre un cliché ou pour régler des paramètres, nous utiliserons principalement la classe Camera dans nos diverses manipulations. Cette classe Camera permet d'ajuster les différents réglages de prise de vue, de spécifier les préférences pour les images et bien entendu de prendre les photos.

Pour accéder au Service Camera, utilisez la méthode statique open() de la classe Camera. Lorsque votre application a fini de l'utiliser, n'oubliez pas de la libérer en appelant la méthode release().

La méthode open() ouvrira et initialisera l'appareil photo. Vous pourrez alors modifier les réglages, configurer la surface de prévisualisation et prendre des photos comme nous allons le montrer dans les sections suivantes.

N'oubliez pas également que pour utiliser l'appareil photo sous Android, il faut ajouter la permission CAMERA dans le manifeste de l'application :

<uses-permission android:name="android.permission.CAMERA" />

Contrôler et surveiller des réglages de l'APN et les options d'image

Les réglages de l'APN sont stockés dans l'objet Camera.Parameters, accessible en appelant la méthode getParameters() de l'objet Camera.

Tous les paramètres sont alors consultables grâce à leurs accesseurs (méthodes getXxx() et setXxx() associées). Pour consulter le format photo, par exemple, nous utiliserons la méthode getPictureFormat() et pour paramétrer ce format en JPEG, nous invoquerons la méthode setPictureFormat(PixelFormat.JPEG).

Android 2.0 (API level 5) a introduit une large gamme de paramètres d'APN, chacun possédant un getter et un setter :

  1. [get / set] SceneMode : Reçoit ou renvoie une constante chaîne statique SCENE_MODE_* de la classe Camera.Parameters. Chaque mode décrit un type de scène particulier (fête, plage, soleil couchant, etc.).
  2. [get / set] FashMode : Reçoit ou renvoie une constante chaîne statique FLASH_MODE_*. Permet de spécifier le mode du flash à on, off, atténuation des yeux rouges ou flashlight.
  3. [get / set] WhiteBalance : Reçoit ou renvoie une constante chaîne statique WHITE_BALANCE_* décrivant la balance des blancs de la scène photographiée.
  4. [get / set] ColorEffect : Reçoit ou renvoie une constante chaîne statique EFFECT_* modifiant la représentation de l'image. Les effets disponibles incluent le sepia ou le noir et blanc.
  5. [get / set] FocusMode : Reçoit ou renvoie une constante chaîne statique FOCUS_MODE_* spécifiant comment l'autofocus doit tenter de se régler.

La plupart des paramètres ci-dessus ne sont vraiment utiles que si vous remplacez l'application native. Cela dit, ils peuvent également servir à personnaliser la façon dont la prévisualisation est affichée et vous permettre de la personnaliser pour des applications de réalité augmentée.

Les paramètres peuvent également servir à lire ou spécifier la taille, la qualité et le format des images, des vignettes et des prévisualisations. La liste qui suit explique comment fixer certaines de ces valeurs :

  1. Qualité des JPEG et des vignettes : Utilisez les méthodes setJpegQuality() et setJpegThumbnailQuality() en leur passant une valeur entière entre 0 et 100, cette dernière représentant la qualité la plus élevée.
  2. Taille des images, prévisualisations et vignettes : Utilisez setPictureSize(), setPreviewSize(), setJpegThumbnailSize() pour spécifier une hauteur et une largeur.
  3. Format de pixel des images et des prévisualisations : Utilisez setPictureFormat(), setPreviewFormat() pour fixer le format des images à l'aide d'une constante statique de la classe PixelFormat.
  4. Taux de trame des prévisualisations : Utilisez setPreviewFrameRate() pour spécifier ce taux en fps (frames per second).

Chaque appareil peut potentiellement supporter un sous-ensemble différent de ces paramètres. La classe Camera.Parameters inclut également une série de méthodes getSupportedXxx() permettant de déterminer les options valides à afficher à l'utilisateur ou de vérifier qu'un paramètre particulier est supporté avant de lui assigner une valeur.

Cette vérification est particulièrement importante lorsque nous sélectionnons une prévisualisation ou des tailles d'images. Voici d'ailleur un exemple qui permet de proposer des photos en sépia.

Camera appareil = Camera.open();
Camera.Parameters parametres =  appareil.getParameters();
List<String> effetsCouleur = parametres.getSupportedColorEffects();
if (effetsCouleur.contains(Camera.Parameters.EFFECT_SEPIA)) parametres.setColorEffect(Camera.Parameters.EFFECT_SEPIA);
appareil.setParameters(parametres);
Surveiller l'autofocus

Si l'APN supporte l'autofocus et qu'il soit activé, vous pouvez surveiller le succès des opérations de focus en ajoutant un AutoFocusCallback à l'objet Camera. Le gestionnaire d'événement onAutoFocus() reçoit un paramètre Camera lorsque le statut de l'autofocus a changé et un booléen pour indiquer le succès ou l'échec.

Camera appareil = Camera.open();
appareil.autoFocus(new AutoFocusCallback() {
   public void onAutoFocus(boolean success, Camera camera) {
       // A faire si l'autofocus réussit
   }
});
Utiliser les prévisualisations

L'accès aux flux vidéo temps réel de la caméra signifie que vous pouvez incorporer ce flux dans vos applications. Certaines des applications Android les plus excitantes utilisent cette fonctionnalité pour implémenter la réalité augmentée (processus d'ajout de données dynamiques contextuelles, comme des détails sur les monuments historiques ou des points d'intérêt, par dessus un flux vidéo en temps réel).

La prévisualisation du champ filmé par la caméra peut être affichée sur une surface spécifique. Ainsi, pour voir ce flux temps réel de votre caméra au sein de votre application, vous devez inclure une balise <SurfaceView> dans votre interface utilisateur.

A noter que sur un émulateur, le rendu affichera un damier sur lequel un carré bouge de façon aléatoire. Cette surface d'affichage est donc décrite dans un fichier de mise en page XML de façon assez habituelle en spécifiant sa taille.

La surface que nous allons détenir en la manipulant sera contrôlable grâce à une instance d'un objet de type SurfaceHolder. L'interface SurfaceHolder implémentée par cette objet permet :

  1. De contrôler le taille de la surface.
  2. De changer son format.
  3. D'éditer les pixels de la surface.
  4. De surveiller les changements inhérents à la surface.

Un client peut surveiller les événements liés à la surface en implémentant l'interface du callback SurfaceHolder.Callback. Une fois enregistré, ce client sera notifié des événements de création, suppression et modification de la surface. Implémenter donc un SurfaceHolder.Callback pour vérifier la construction d'une surface valide avant de la passer à la méthode setPreviewDisplay().

Rappel sur la gestion événementielle : Un callback, terme qui pourrait se traduire en français par "rappel", est en pratique matérialisé par un objet implémentant les méthodes d'une interface. Cette dernière invoquera les méthodes de l'interface que l'objet implémente pour signaler un fait donné lors d'un événement précis ou à la fin d'un traitement, par exemple.

Je vous propose de réaliser une toute petite application qui permet de faire uniquement la prévisualisation et donc d'afficher sur l'ensemble de l'écran ce que voit directement l'appareil photo :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.appareil"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Appareil photo" >
        <activity android:name="AppareilPhoto" android:label="Appareil photo" android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.CAMERA" />
</manifest>

Pensez-bien à demander l'autorisation d'utiliser l'appareil photo intégré avec la permission adaptée. Un autre détail qui a son importance, vous devez spécifier l'orientation de l'activité principale en mode paysage "landscape", sinon votre prévisualisation sera toujours montrée suivant un angle de 90°.

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <SurfaceView
         android:id="@+id/surface"
         android:layout_width="wrap_content" 
         android:layout_height="wrap_content" 
         android:layout_weight="1"/>
</LinearLayout>
fr.btsiris.appareil.AppareilPhoto.java
package fr.btsiris.appareil;

import android.app.Activity;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import java.io.IOException;

public class AppareilPhoto extends Activity implements SurfaceHolder.Callback {
   private Camera appareil;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      SurfaceView surface = (SurfaceView) findViewById(R.id.surface);
      SurfaceHolder holder = surface.getHolder();
      holder.addCallback(this);
      holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
   }

   public void surfaceCreated(SurfaceHolder holder) {
      appareil = Camera.open();
   }

   public void surfaceChanged(SurfaceHolder holder, int format, int largeur, int hauteur) {
      if (appareil!=null) {
         try {
            appareil.setPreviewDisplay(holder);
            appareil.startPreview();
         }
         catch (IOException ex) { Log.d("Prévisualisation : ", ex.getMessage()); } 
      }
   }

   public void surfaceDestroyed(SurfaceHolder holder) {
      if (appareil!=null) {
         appareil.stopPreview();
         appareil.release();
      }
   }
}
  1. Dans ce code source, nous voyons que l'activité principale implémentante l'interface SurfaceHolder.Callback. Il est nécessaire de s'enregistrer auprès de cette activité pour être notifié des événements relatifs à la surface.
  2. C'est d'ailleurs l'objet de type SurfaceHolder obtenu au près de la surface déclarée dasn le fichier de mise en page qui nous notifiera des différentes étapes de création, suppression et modification de la surface.
  3. A la création de la surface, nous récupérons l'objet permettant de manipuler la caméra.
  4. Quand la surface est modifiée, nous diffusons sur la surface les images captées par l'objectif. La documentation précise bien que cette méthode est appelée au moins une fois après la création de la surafce, donc pas de souci.
  5. Enfin, quand la surface est détruite, nous libérons les ressources liées à la caméra.

Il est également tout à fait possible d'assigner un PreviewCallback qui sera déclanché pour chaque trame de prévisualisation, vous permettant ainsi de les manipuler individuellement. Dans ce cas là, appelez la méthode setPreviewCallback() sur l'objet Camera en lui passant une nouvelle implémentation de PreviewCallback redéfinissant la méthode onPreviewFrame(). Ainsi, chaque trame sera reçue par l'événement onPreviewFrame() et l'image sera passée via le tableau d'octets.

Prendre une photo (capture d'une image)

Pour prendre une image (une photo), nous utilisons la méthode takePicture() sur une instance de l'objet Camera. Cette méthode prend en paramètre un certain nombre de callbacks. Elle prend effectivement en compte un ShutterCallback et deux implémentations de PictureCallback (l'une pour l'image Raw et l'autre pour l'image JPEG).

Chaque callback d'image recevra un tableau d'octets représentant l'image dans le format approprié, et le callback de l'obturateur (shutter) sera déclenché immédiatement après la fermeture de ce dernier.

appareil.takePicture(shutterCallback, rawCallback, jpegCallback);

Comme d'habitude, ces callback sont des interfaces que vous devez implémenter afin de répondre de façon circonstanciée à l'événement de la prise de photo, de la capture de votre image.

Prenons l'exemple de la méthode onShutter() de l'objet qui implémente l'interface ShutterCallback. Elle sera appelée au moment où l'obturateur se refermera et que la photo sera prise. Le code obtenu dans cette méthode sera alors exécutée. Nous pourrions, par exemple, jouer un son pour signaler la prise de photo.

Voici les différents callbacks qui peuvent être utilisés :

  1. Camera.AutoFocusCallback : donne des informations sur le succès ou l'échec d'une opération d'autofocus.
  2. Camera.ErrorCallback : rappel sur erreur du service de prise de vue.
  3. Camera.PictureCallback : prise d'une photographie.
  4. Camera.PreviewCallback : notification des images disponibles en prévisualisation.
  5. Camera.ShutterCallback : rappel quand l'obturateur est fermé, mais les données ne sont pas encore enregistrées.

L'objet ShutterCallback est un paramètre indispensable à chaque appel de la méthode permettant de prendre une photo. Vous pouvez ensuite choisir, grâce au PictureCallback, une capture au format brut, au format JPEG ou les deux.

Reprenons le projet précédent qui permet d'afficher une prévisualisation de la caméra et ajoutons un bouton qui, actionné, déclenchera la prise d'une image au format JPEG. Cette image sera alors enregistrée directement sur la carte SD :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.appareil"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Appareil photo" >
        <activity android:name="AppareilPhoto" android:label="Appareil photo">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.CAMERA" />
</manifest>
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <Button
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content"
         android:text="Clic clac"
         android:onClick="clicClac"/>       
    <SurfaceView
         android:id="@+id/surface"
         android:layout_width="fill_parent" 
         android:layout_height="fill_parent"/>
</LinearLayout>
fr.btsiris.appareil.AppareilPhoto.java
package fr.btsiris.appareil;

import android.app.Activity;
import android.hardware.Camera;
import android.hardware.Camera.*;
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import java.io.*;

public class AppareilPhoto extends Activity implements SurfaceHolder.Callback, PictureCallback, ShutterCallback {
   private Camera appareil;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      SurfaceView surface = (SurfaceView) findViewById(R.id.surface);
      SurfaceHolder holder = surface.getHolder();
      holder.addCallback(this);
      holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
   }
   
   public void clicClac(View vue) {
      appareil.takePicture(this, null, this);
   }

   public void surfaceCreated(SurfaceHolder holder) {
      appareil = Camera.open();
      appareil.setDisplayOrientation(90);  // A partir de la version 2.2, permet d'avoir la prévisualisation dans le bon sens
   }

   public void surfaceChanged(SurfaceHolder holder, int format, int largeur, int hauteur) {
      if (appareil!=null) {
         try {
            appareil.setPreviewDisplay(holder);
            appareil.startPreview();
         }
         catch (IOException ex) { Log.d("Prévisualisation : ", ex.getMessage()); } 
      }
   }

   public void surfaceDestroyed(SurfaceHolder holder) {
      if (appareil!=null) {
         appareil.stopPreview();
         appareil.release();
      }
   }

   public void onPictureTaken(byte[] image, Camera appareil) {
      try {
         FileOutputStream os = new FileOutputStream("/sdcard/test.jpg");
         os.write(image);
         os.close();
      }
      catch (IOException ex) { Log.e("Erreur", "Impossible de stocker la photo"); }
      appareil.startPreview();
   }

   public void onShutter() {
      Log.d(getClass().getSimpleName(), "Clic clac !");
   }
}

Vous avez ci-dessous une alternative qui permet d'archiver votre photo dans un emplacement spécifique au stockage de médias.
.

fr.btsiris.appareil.AppareilPhoto.java
package fr.btsiris.appareil;

import android.app.Activity;
import android.content.ContentValues;
import android.hardware.Camera;
import android.hardware.Camera.*;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore.Images.Media;
import android.util.Log;
import android.view.*;
import java.io.*;

public class AppareilPhoto extends Activity implements SurfaceHolder.Callback, PictureCallback, ShutterCallback {
   private Camera appareil;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      SurfaceView surface = (SurfaceView) findViewById(R.id.surface);
      SurfaceHolder holder = surface.getHolder();
      holder.addCallback(this);
      holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
   }
   
   public void clicClac(View vue) {
      appareil.takePicture(this, null, this);
   }

   public void surfaceCreated(SurfaceHolder holder) {
      appareil = Camera.open();
      appareil.setDisplayOrientation(90);
   }

   public void surfaceChanged(SurfaceHolder holder, int format, int largeur, int hauteur) {
      if (appareil!=null) {
         try {
            appareil.setPreviewDisplay(holder);
            appareil.startPreview();
         }
         catch (IOException ex) { Log.d("Prévisualisation : ", ex.getMessage()); } 
      }
   }

   public void surfaceDestroyed(SurfaceHolder holder) {
      if (appareil!=null) {
         appareil.stopPreview();
         appareil.release();
      }
   }

   public void onPictureTaken(byte[] image, Camera appareil) {
      ContentValues valeurs = new ContentValues();
      valeurs.put(Media.TITLE, "Ma photo");
      valeurs.put(Media.DESCRIPTION, "Photo prise par le téléphone");
      Uri uri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, valeurs);           
      try {
         OutputStream os = getContentResolver().openOutputStream(uri);
         os.write(image);
         os.close();
      }
      catch (IOException ex) { Log.e("Erreur", "Impossible de stocker la photo"); }
      appareil.startPreview();
   }

   public void onShutter() {
      Log.d(getClass().getSimpleName(), "Clic clac !");
   }
}

Observez la syntaxe Image.Media.EXTERNAL_CONTENT_URI. Ce morceau de code permet de récupérer l'emplacement où sont stockées les images enregistrées sur un média externe et d'y placer ses propres images. Le stockage vidéo et audio fonctionne exactement de la même façon.

Lire et écrire des images au format JPEG EXIF

La classe ExifInterface fournit les mécanismes nécessaires à la lecture et à la modification des données au format EXIF (Exchangeable Image File Format) stockées dans un fichier JPEG. Créez une instance d'ExifInterface en passant en paramètre le nom du fichier complet.

ExifInterface exif = new ExifInterface(nomFichier);

Les données EXIF sont utilisées pour stocker une large gamme de métadonnées relatives à des photographies comme la date et l'heure, les paramètres de l'APN (marque et modèle) et les réglages de l'image (ouverture et vitesse d'obturation) ainsi que des description et des emplacements.

Pour lire un attribut EXIF, appelez la méthode getAttribute() sur l'objet ExifInterface en lui passant le nom de l'attribut à lire. La classe ExifInterface contient plusieurs constantes statiques TAG_* qui peuvent être utilisées pour accéder aux métadonnées EXIF courantes. Pour modifier un attribut EXIF, utilisez la méthode setAttribute() en lui passant le nom de l'attribut et sa nouvelle valeur.

  1. TAG_APERTURE : Ouverture de l'objectif.
  2. TAG_DATETIME : L'heure et la date de la prise de vue.
  3. TAG_EXPOSURE_TIME : Temps d'exposition.
  4. TAG_FLASH : Flash actif ou pas.
  5. TAG_FOCAL_LENGTH : Longueur focale de l'objectif.
  6. TAG_GPS_ALTITUDE : Altitude relative en référence à la constante TAG_GPS_ALTITUDE_REF.
  7. TAG_GPS_ALTITUDE_REF : Altitude de référence, normalement à 0 lorsque nous sommes au niveau de la mer.
  8. TAG_GPS_DATESTAMP : Date donnée par le GPS.
  9. TAG_GPS_LATITUDE : Latitude relative en référence à la constante TAG_GPS_LATITUDE_REF.
  10. TAG_GPS_LATITUDE_REF: Latitude de référence.
  11. TAG_GPS_LONGITUDE : Longitude relative en référence à la constante TAG_GPS_LONGITUDE_REF.
  12. TAG_GPS_LONGITUDE_REF : Longitude de référence.
  13. TAG_GPS_TIMESTAMP : Heure donnée par le GPS.
  14. TAG_IMAGE_LENGTH : Poids de l'image.
  15. TAG_IMAGE_WIDTH : Largeur de l'image.
  16. TAG_ISO : Sensibilité utilisée lors de la prise de vue.
  17. TAG_MAKE : Nom du constructeur.
  18. TAG_MODEL : Modèle de l'appareil photo.
  19. TAG_ORIENTATION : Orientation de la prise de vue.
  20. TAG_WHITE_BALANCE : Balance des blancs choisie avant de prendre la photo.
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
   <TextView
         android:id="@+id/poids"
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content"
         android:text="Poids : "/>   
    <TextView
         android:id="@+id/largeur"
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content"
         android:text="Largeur : "/>   
    <Button
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content"
         android:text="Clic clac"
         android:onClick="clicClac"/>             
    <SurfaceView
         android:id="@+id/surface"
         android:layout_width="fill_parent" 
         android:layout_height="fill_parent"/>       
</LinearLayout>
fr.btsiris.appareil.AppareilPhoto.java
package fr.btsiris.appareil;

import android.app.Activity;
import android.hardware.Camera;
import android.hardware.Camera.*;
import android.media.ExifInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import android.widget.TextView;
import java.io.*;

public class AppareilPhoto extends Activity implements SurfaceHolder.Callback, PictureCallback, ShutterCallback {
   private Camera appareil;
   private TextView poids, largeur;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      SurfaceView surface = (SurfaceView) findViewById(R.id.surface);
      poids = (TextView) findViewById(R.id.poids);
      largeur = (TextView) findViewById(R.id.largeur);
      SurfaceHolder holder = surface.getHolder();
      holder.addCallback(this);
      holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
   }
   
   public void clicClac(View vue) {
      appareil.takePicture(this, null, this);
   }

   public void surfaceCreated(SurfaceHolder holder) {
      appareil = Camera.open();
      appareil.setDisplayOrientation(90);
   }

   public void surfaceChanged(SurfaceHolder holder, int format, int largeur, int hauteur) {
      if (appareil!=null) {
         try {
            appareil.setPreviewDisplay(holder);
            appareil.startPreview();
         }
         catch (IOException ex) { Log.d("Prévisualisation : ", ex.getMessage()); } 
      }
   }

   public void surfaceDestroyed(SurfaceHolder holder) {
      if (appareil!=null) {
         appareil.stopPreview();
         appareil.release();
      }
   }

   public void onPictureTaken(byte[] image, Camera appareil) {
      try {
         FileOutputStream os = new FileOutputStream("/sdcard/test.jpg");
         os.write(image);
         os.close();
         ExifInterface exif = new ExifInterface("/sdcard/test.jpg");
         poids.setText("Poids : "+exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH));
         largeur.setText("Largeur : "+exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH));
      }
      catch (IOException ex) { Log.e("Erreur", "Impossible de stocker la photo"); }
      appareil.startPreview();
   }

   public void onShutter() {
      Log.d(getClass().getSimpleName(), "Clic clac !");
   }
}

 

Choix du chapitre Bluetooth

Le Bluetooth est une technologie de communication radio de courte distance, créée pour simplifier les connexions entre appareils. Ce système a été conçu dans le but de remplacer les câbles des imprimantes, des kits "main libre", des souris/claviers, téléphones portables/PDA, etc.

La plate-forme Android supporte le Bluetooth afin de permettre à des appareils d'échanger des données entre elles. Afin de pouvoir profiter de ce système de connexion, Android apporte un ensemble d'API que nous allons détailler dans ce chapitre, et qui vont nous permettre d'interagir avec l'adaptateur Bluetooth local et à communiquer avec des périphériques distants.

Avec Bluetooth, vous pouvez rechercher des périphériques à portée de l'appareil et vous y connecter. En initiant une communication avec les Sockets Bluetooth, vous pouvez transmettre et recevoir des flux de données depuis et vers des périphériques, à partir de votre application.

Les bibliothèques Bluetooth ne sont disponibles que depuis Android 2.0 (SDK API Level 5). Il est également important de noter que tous les appareils Android n'incluent pas nécessairement un matériel Bluetooth.

Bluetooth est un protocole de communication conçu pour des communications peer-to-peer (appareil à appareil) de courtes distantes et à faible bande passante. Sous Android 2.1, seules les communications cryptées sont supportées, ce qui signifie que vous ne pouvez connecter que des périphériques préalablement appariés. Les périphériques et connexions Bluetooth sont gérés par les classes suivantes :

  1. BluetoothAdapter : Représente l'adaptateur local, c'est-à-dire l'appareil Android sur lequel votre application est exécuté. Il s'agit de la classe centrale pour les interactions avec le Bluetooth, elle est utilisée pour récupérer les appareils environnants, effectuer toutes les requêtes et communiquer avec les autres appareils.
  2. BluetoothDevice : Représente chaque périphérique distant avec lequel vous voulez communiquer. Elle est utilisée pour récupérer les informations et créer une connexion avec l'appareil distant.
  3. BluetoothSocket : Représente un point de connexion client en relation avec un service Bluetooth identifié par la classe ci-dessous. Appelez la méthode createRfcommSocketToServiceRecord() sur un objet BluetoothDevice pour créer un BluetoothSocket qui vous permettra d'effectuer une demande de connxion vers le périphérique distant puis d'initier la communication.
  4. BluetoothServerSocket : Mise en oeuvre d'un service de connexion Bluetooth qui sera en attente de l'établissement d'une communication avec un client, soumettant ensuite la requête désirée. En créant un BluetoothServerSocket, en utilisant la méthode listenUsingRfcommWithServiceRecord() sur votre BluetoothAdapter local, vous pouvez écouter les demandes de connexions provenant des BluetoothSocket des périphériques distants.

Ces API supportent la connexion à un appareil dans une configuration point à point (l'appareil est connecté à un seul autre appareil) ou alors en multi-points (l'appareil peut être connecté à plusieurs appareils simultanément.

Les permissions

A l'instar de toutes les fonctionnalités de communication, pour qu'une application puisse utiliser les capacités Bluetooth, vous devrez définir une ou deux permissions adaptées dans le manifeste :

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  1. BLUETOOTH : Cette permission autorise votre application à accepter une connexion entrante, à demander une connexion distante et à transférer des données. Elle permet également d'initier la découverte des périphériques asservis.
  2. BLUETOOTH_ADMIN : Cette permission permet d'activer la recherche des autres appareils ou de manipuler et modifier les paramètres Bluetooth du téléphone.

Accéder à l'adaptateur Bluetooth local

Avant de pouvoir utiliser le Bluetooth et de pouvoir échanger avec d'autres appareils, vous devez vérifier que l'appareil de l'utilisateur possède bien la fonctionnalité du Bluetooth et si celle-ci est activée.

Obtention de l'adaptateur Bluetooth local
L'objet BluetoothAdapter est incontournable pour l'utilisation du Bluetooth sur la plate-forme Android. La première chose que vous aurez donc à réaliser dans votre code sera de récupérer une instance de cette classe. Vous réaliserez cette opération via la méthode statique getDefaultAdapter() qui vous renverra l'objet en question et qui vous servira à vérifier l'existence de l'activation du Bluetooth sur le téléphone de l'utilisateur. Si la méthode renvoie un objet null alors le téléphone de l'utilisateur ne possède pas le Bluetooth et vous pourrez désactiver toutes les fonctionnalités utilisant ce service de communication.
Vérification de l'activation du Bluetooth sur le mobile de l'utilisateur
Si le Bluetooth est disponible sur l'appareil, vous devez également vérifier que celui-ci est activé avant d'essayer de l'utiliser. Pour cela, vous bénéficiez de la méthode isEnabled() de la classe BluetoothAdapter. Si la méthode retourne false, alors le Bluetooth n'est pas activé.
Lecture des propriétés de l'adaptateur local

Si l'adaptateur est activé et que vous ayez ajouté la permission BLUETOOTH à votre manifest, vous pouvez accéder à son nom (une chaîne arbitraire que les utilisateurs peuvent modifier puis utiliser pour identifier un périphérique particulier) et à son adresse matérielle (adresse MAC).

Réglage des propriétés de l'adaptateur local
Si vous avez également la permision BLUETOOTH_ADMIN, vous pouvez modifier le nom de l'adaptateur eu utilisant la méthode setName(). Pour obtenir une description plus détaillée de l'état courant de l'adaptateur, utilisez la méthode getState() qui renvoie l'une des constantes de BluetoothAdapter suivantes : STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, STATE_OFF.

Activation de l'adaptateur local

Par défaut, l'adaptateur Bluetooth est désactivé. Pour économiser leur batterie et optimiser la sécurité, beaucoup d'utilisateurs le laisseront ainsi.

Vous pouvez demander à l'utilisateur de l'activer en démarrant une sous-activité du système, spécialement conçue à cet effet, en utilisant la constante statique ACTION_REQUEST_ENABLE de la classe BluetoothAdapter et en spécifiant un Intent à l'aide de cette constante comme action de startActivityForResult().

Elle informe l'utilisateur que son Bluetooth va être et lui demande confirmation. Si l'utilisateur accepte, la sous-activité sera fermée et reviendra à l'activité appelante une fois l'adaptateur activé (ou si une erreur survient). Si l'utilisateur refuse, la sous-activité sera simplement fermée. Utilisez le code de retour du gestionnaire onActivityResult() pour déterminer le succès de l'opération.

Afin de valider cette première partie, je vous propose de réaliser une petite expérience qui nous permettra de connaître l'identification de votre adaptateur local en demandant l'activation du Bluetooth si ce dernier n'est pas encore actif :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.bluetooth"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Test Bluetooth" >
        <activity android:name="Bluetooth"  android:label="Identification Bluetooth !">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.BLUETOOTH" />
</manifest>
fr.btsiris.bluetooth.Bluetooth.java
package fr.btsiris.bluetooth;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;

public class Bluetooth extends Activity {
   private BluetoothAdapter adaptateurLocal;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
      if (adaptateurLocal != null) 
         if (adaptateurLocal.isEnabled()) {
            String texte = adaptateurLocal.getName() + " : " + adaptateurLocal.getAddress();  
            Toast.makeText(this, texte, Toast.LENGTH_LONG).show();
         }
         else {
            Intent intention = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(intention, 0);
         }   
      else finish();
    }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      String texte="";
      switch (resultCode) {
        case RESULT_OK :  texte = adaptateurLocal.getName() + " : " + adaptateurLocal.getAddress(); break; 
        case RESULT_CANCELED :  texte = "Bluetooth inactif : sortie de l'activité"; finish(); break;
      }
      Toast.makeText(this, texte, Toast.LENGTH_LONG).show(); 
   }
}

Il est également possible d'activer ou de désactiver directement l'adaptateur en utilisant les méthodes enabled() et disable() si vous ajouter la permission BLUETOOTH_ADMIN dans votre manifeste.

Notez que ceci ne doit être fait que lorsque c'est absolument nécessaire et que l'utilisateur doit toujours être averti si vous modifiez le statut de l'adaptateur. Dans la plupart des cas, vous devrez utiliser le mécanisme d'intention décrit plus haut.

Activer et désactiver l'adaptateur Bluetooth sont des opérations asynchrones parfois longues. Plutôt que sonder l'adaptateur, votre application devra enregistrer un Broacast Receiver écoutant l'action ACTION_STATE_CHANGED. Le Broadcast Intent inclura deux extras, EXTRA_STATE et EXTRA_PREVIOUS_STATE, qui indiquerons l'état courant et précédent de l'adaptateur.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.bluetooth"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Test Bluetooth" >
        <activity android:name="Bluetooth"  android:label="Identification Bluetooth !">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
</manifest>
fr.btsiris.bluetooth.Bluetooth.java
package fr.btsiris.bluetooth;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.content.*;
import android.os.Bundle;
import android.widget.Toast;

public class Bluetooth extends Activity  {
   private BluetoothAdapter adaptateurLocal;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
      registerReceiver(new ReceptionBluetooth(), new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
      if (adaptateurLocal != null) {
         if (!adaptateurLocal.isEnabled()) adaptateurLocal.enable();     
      }
      else finish();      
   }
   
   class ReceptionBluetooth extends BroadcastReceiver {
      @Override
      public void onReceive(Context cntxt, Intent intent) {
         int etat = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
         String texte="";
         switch (etat) {
            case BluetoothAdapter.STATE_TURNING_ON : 
               texte = "Bluetooth en cours d'activation"; break;
            case BluetoothAdapter.STATE_ON : 
               texte = adaptateurLocal.getName()+" : "+adaptateurLocal.getAddress(); break;
            case BluetoothAdapter.STATE_TURNING_OFF : 
               texte = "Bluetooth en cours de désactivation"; break;
            case BluetoothAdapter.STATE_OFF : 
               texte = "Bluetooth actuellement inactif"; break;
         }
         Toast.makeText(Bluetooth.this, texte, Toast.LENGTH_LONG).show();
      }
   }
}

Rechercher d'autres appareils Bluetooth - Découverte de périphériques

Le processus permettant à deux périphériques de se trouver pour se connecter est appelé découverte. Avant que vous ne puissiez établir un Bluetooth Socket pour communiquer, l'adaptateur local doit être apparié au périphérique distant. Et avant que cette liaison ne puisse être effectuée les deux périphériques doivent d'abord se découvrir.

Bien que le protocole Bluetooth supporte les connexion ad hoc pour le transfert de données, ce mécanisme n'est pas actuellement disponible sous Android. Les communications Bluetooth sous Android ne sont supportées qu'entre deux périphériques appariés.

Vous pouvez rechercher les appareils Bluetooth disponibles autour de l'utilisateur. Cependant, pour qu'un appareil puisse être découvert, il faut que celui-ci autorise sa découverte. Si un appareil autorise sa découverte, une association sera alors établie et les appareils échangeront leurs informations d'identification (nom de l'appareil, son adresse MAC, etc).

Une association signifie que les deux appareils sont conscients de leur existence respective, qu'ils sont capables de s'authentifier l'un l'autre et d'initier une connexion sécurisée entre eux.

Adresse Mac : En réseau informatique, une adresse MAC (Media Access Control) est un identifiant physique stocké dans une carte réseau, attribué de façon unique à chaque carte à l'échelle mondiale : cet identifiant est en partie constitué par un champs identifiant le fabriquant de la carte et l'autre partie attribuée par le constructeur lui-même.

Une fois l'association établie avec un appareil distant, vous pouvez vous connecter à celui-ci en utilisant son adresse MAC pour transférer des données via un flux RFCOMM. Android obligeant un appareil à être associé avant de pouvoir s'y connecter, tous les paramètres des appareils qui ont été associés sont enregistrés pour un usage ultérieur et ainsi éviter de réaliser systématiquement une détection des appareils.

Protocole RFCOMM : Le protocole FRCOMM émule les paramètres de la ligne série câblée ainsi que le statut d'un port série RS-232. Il est utilisé pour permettre le transfert des données série. Ce protocole est notamment utilisé par les modems, les imprimantes et les ordinateurs. Le Bluetooth utilise ce protocole pour échanger des données.

Gérer la visibilté de l'adaptateur

Pour que des appareils distants puissent trouver votre adaptateur local durant un scan, celui-ci doit être visible. Pour des raisons de confidentialité, les appareils Android ne sont pas visibles par défaut.

La visibilté de l'adaptateur Android est indiquée par son mode de scan. Vous pouvez trouver celui-ci en appelant la méthode getScanMode() de l'objet BluetoothAdapter. Elle renverra l'une des contantes suivantes :

  1. SCAN_MODE_CONNECTABLE_DISCOVERABLE : L'appareil est visible par tout périphérique Bluetooth effectuant un scan de découverte.
  2. SCAN_MODE_CONNECTABLE : Les périphériques Bluetooth ayant déjà découvert l'appareil pourront s'y connecter, mais pas les nouveaux.
  3. SCAN_MODE_NONE : La découverte est désactivée.

Si vous souhaitez rendre accessible l'appareil de l'utilisateur lors d'une recherche d'appareils Bluetooth, vous devez demander l'autorisation à l'utilisateur. Pour cela, lancez l'activité adéquate en spécifiant une intention associé à l'action ACTION_REQUEST_DISCOVERABLE.

Par défaut, la visibilité est activée durant 2 minutes. Vous pouvez modifier ce réglage en ajoutant un extra EXTRA_DISCOVERABLE_DURATION au lancement de l'intention et en spécifiant le nombre de secondes de visibilité souhaitées.

Lorsque l'intention est diffusée, l'utilisateur se voit demander l'autorisation de rendre l'appareil visible pour la durée spécifiée. Pour savoir si l'utilisateur a accepté ou rejeté la demande, redéfinissez le gestionnaire onActivityResult(). Le paramètre resultCode renvoyé indique la durée de visibilité ou un nombre négatif si l'utilisateur à refusé.

De façon alternative, vous pouvez monitorer les changements de visibilité en recevant une action ACTION_SCAN_MODE_CHANGED. Le Broadcast Intent inclut les modes de scan en cours et précédents en extras.

Je propose la même démarche que précédemment en demandant à l'utilisateur l'autorisation de rendre son mobile disponible pour la communication Bluetooth. Si l'adaptateur n'est pas actif, le système le mettra en fonction au moment de la validation :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.bluetooth"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Test Bluetooth" >
        <activity android:name="Bluetooth"  android:label="Identification Bluetooth !">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.BLUETOOTH" />
</manifest>
fr.btsiris.bluetooth.Bluetooth.java
package fr.btsiris.bluetooth;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.content.*;
import android.os.Bundle;
import android.widget.Toast;

public class Bluetooth extends Activity  {
   private BluetoothAdapter adaptateurLocal;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
      registerReceiver(new ReceptionBluetooth(), new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
      startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), 0);    
   }
   
   class ReceptionBluetooth extends BroadcastReceiver {
      @Override
      public void onReceive(Context cntxt, Intent intent) {
         String texte="";
         switch(intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) {
            case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE : texte = "Appareil visible"; break;
            case BluetoothAdapter.SCAN_MODE_CONNECTABLE : texte = "Appareil déjà visible"; break;
            case BluetoothAdapter.SCAN_MODE_NONE : texte = "Appareil non visible"; break;
         }
         Toast.makeText(Bluetooth.this, texte, Toast.LENGTH_LONG).show();
      }
   }
}  
Récupérer les appareils associés

Au lieu de lancer immédiatement une nouvelle recherche d'appareils Bluetooth, il convient dans un premier temps de rechercher si l'appareil de l'utilisateur ne connaît pas déjà l'appareil ciblé. Pour cela, vous allez demander à la méthode getBondedDevices() d'effectuer une requête sur les appareils déjà connus.

BluetoothAdapter adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
Set<BluetoothDevice> appareils = adaptateurLocal.getBondedDevices();
if (appareils.size() > 0) {
   for (BluetoothDevice appareil : appareils) {
      String msg = appareil.getName() + " - MAC : " + appareil.getAddress();
       ...
   } 
}

Avant de pouvoir communiquer avec un appareil, vous devez obtenir l'adresse de l'appareil distant en récupérant les informations des appareils associés.

Rechercher les appareils environnant

Nous allons voir maintenant comment initier une découverte depuis votre adaptateur local pour trouver des périphériques à proximité. Vous pouvez vérifier si l'adaptateur local est déjà en train d'effectuer une découverte en utilisant la méthode isDiscovering() de la classe BluetoothAdapter.

Pour lancer une découverte des appareils, utilisez la méthode startDiscovery() de la classe BluetoothAdapter. Cette méthode retourne immédiatement une valeur booléenne indiquant si la découverte s'est correctement lancée. Pour récupérer le résultat de la recherche (qui peut prendre une dizaine de secondes), vous utiliserez un récepteur de diffusion. Pour annuler une découverte en cours, appelez la méthode cancelDiscovery().

Le processus est asynchrone. Android utilise des Broadcast Intents pour vous notifier de son démarrage et de sa fin ainsi que des périphériques distants découvert durant le scan. Vous pouvez monitorer les changements dans le processus en créant des Broadcast Receivers écoutant les intentions ACTION_DISCOVERY_STARTED et ACTION_DISCOVERY_FINISHED.

Les périphériques découverts sont renvoyés par le Broadcast Intents par le biais de l'action ACTION_FOUND. Chaque Broadcast Intent inclut le nom du périphérique distant dans un extra BluetoothDevice.EXTRA_NAME ainsi qu'une représentation inchangeable du périphérique sous forme d'un objet BluetoothDevice parcelable dans l'extra BluetoothDevice.EXTRA_DEVICE.

L'objet BluetoothDevice renvoyé par la découverte représente le périphérique distant découvert. Il sera ensuite utilisé pour créer une connexion avec l'apatateur local, lier les appareils et transférer des données.

Nous allons reprendre notre petite application de test Bluetooth dans laquelle nous allons rechercher les appareils distants. Pour cela au préalable, nous rendons notre appareil visible afin de permettre la communication ultérieure entre les deux éléments. Nous en profitons pour visualier toutes les différentes phases de mise en place :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.bluetooth"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Test Bluetooth" >
        <activity android:name="Bluetooth"  android:label="Identification Bluetooth !">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
</manifest>
fr.btsiris.bluetooth.Bluetooth.java
package fr.btsiris.bluetooth;

import android.app.Activity;
import android.bluetooth.*;
import android.content.*;
import android.os.Bundle;
import android.widget.Toast;

public class Bluetooth extends Activity  {
   private BluetoothAdapter adaptateurLocal;
   private BluetoothDevice appareilDistant;
   private String depart = BluetoothAdapter.ACTION_DISCOVERY_STARTED;
   private String fin = BluetoothAdapter.ACTION_DISCOVERY_FINISHED;
   private String trouve = BluetoothDevice.ACTION_FOUND;
   private String visibilite = BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
   
   private BroadcastReceiver monitorerDecouverte = new BroadcastReceiver() {
      @Override
      public void onReceive(Context cntxt, Intent intent) {
         String action = intent.getAction();
         if (visibilite.equals(action)) {
            switch(intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) {
               case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE : 
               case BluetoothAdapter.SCAN_MODE_CONNECTABLE : 
                  if (!adaptateurLocal.isDiscovering()) adaptateurLocal.startDiscovery();  
                  break;
               case BluetoothAdapter.SCAN_MODE_NONE : 
                  Toast.makeText(Bluetooth.this, "Appareil non visible...", Toast.LENGTH_LONG).show();
                  break;
            }        
         }
         else if (depart.equals(action))
            Toast.makeText(Bluetooth.this, "Découverte en cours ...", Toast.LENGTH_LONG).show();
         else if (fin.equals(action))
            Toast.makeText(Bluetooth.this, "Découverte terminée ...", Toast.LENGTH_LONG).show();
         else if (trouve.equals(action)) {
            appareilDistant = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            String nomAppareil = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
            Toast.makeText(Bluetooth.this, "Trouvé : "+nomAppareil, Toast.LENGTH_LONG).show();
         }
      }
   };               

   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
      registerReceiver(monitorerDecouverte, new IntentFilter(depart));
      registerReceiver(monitorerDecouverte, new IntentFilter(fin));
      registerReceiver(monitorerDecouverte, new IntentFilter(trouve));   
      registerReceiver(monitorerDecouverte, new IntentFilter(visibilite));
   }

   @Override
   protected void onStart() {
      super.onStart();
      startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), 0);     
   }

   @Override
   protected void onStop() {
      super.onStop();
      adaptateurLocal.disable();
   } 
}

Je vous propose une version plus réduite qui permet de lancer la visibilité de l'appareil de l'utilisateur et la recherche de l'appareil distant en récupérant en plus son adresse MAC.

fr.btsiris.bluetooth.Bluetooth.java
package fr.btsiris.bluetooth;

import android.app.Activity;
import android.bluetooth.*;
import android.content.*;
import android.os.Bundle;
import android.widget.Toast;

public class Bluetooth extends Activity  {
   private BluetoothAdapter adaptateurLocal;
   private BluetoothDevice appareilDistant;
   private String trouve = BluetoothDevice.ACTION_FOUND;
   private String visibilite = BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
   
   private BroadcastReceiver monitorerDecouverte = new BroadcastReceiver() {
      @Override
      public void onReceive(Context cntxt, Intent intent) {
         String action = intent.getAction();
         if (visibilite.equals(action)) {
            if (!adaptateurLocal.isDiscovering()) adaptateurLocal.startDiscovery();        
         }
         else if (trouve.equals(action)) {
            appareilDistant = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            Toast.makeText(Bluetooth.this, appareilDistant.getName()+" : "+appareilDistant.getAddress(), Toast.LENGTH_LONG).show();
         }
      }
   };               

   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
      registerReceiver(monitorerDecouverte, new IntentFilter(trouve));   
      registerReceiver(monitorerDecouverte, new IntentFilter(visibilite));
   }

   @Override
   protected void onStart() {
      super.onStart();
      startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), 0);     
   }

   @Override
   protected void onStop() {
      super.onStop();
      adaptateurLocal.disable();
      unregisterReceiver(monitorerDecouverte);
      finish();
   } 
}

Communication entre appareils Bluetooth

Pour établir une connexion entre deux appareils, vous devez impémenter chaque côté de votre logique de communication : le côté serveur et le côté client. Le client et le serveur sont considérés connectés lorsque chaque appareil possède un BluetoothSocket sur le même flux RFCOMM. C'est à ce moment que les appareils peuvent transmettre des données dans un sens ou dans l'autre.

Qu'il s'agissse du client ou du serveur, l'objet BluetoothSocket sera récupéré de façon différente. Pour le serveur, cet objet lui sera renvoyé lorqu'il aura accepté la demande de connexion alors que le client l'obtiendra après l'acceptation de la demande par le seveur et l'établissement du flux RFCOMM.

Protocole RFCOMM : Le protocole FRCOMM émule les paramètres de la ligne série câblée ainsi que le statut d'un port série RS-232. Il est utilisé pour permettre le transfert des données série. Ce protocole est notamment utilisé par les modems, les imprimantes et les ordinateurs. Le Bluetooth utilise ce protocole pour échanger des données.

Avant que votre apllication ne puisse communiquer, les appareils doivent être appariés (liés). Si deux périphériques doivent être appariés, l'utilisateur doit explicitement l'autoriser, soit par des réglages Bluetooth, soit lorsque votre application lui demandera au moment de la tentative de connexion.

Vous pouvez établir un canal de communication RFCOMM pour des communications bidirectionnelles en utilisant les classes suivantes :

  1. BluetoothServerSocket : Utilisée pour établir une socket d'écoute pour initier un lien entre périphériques. Pour établir une reconnaissance (handshake), l'un des périphériques agit en tant que serveur qui écoute et accepte les demandes entrantes.
  2. BluetoothSocket : Utilisée pour créer une nouvelle socket client à connecter à un serveur qui la renverra une une fois la connexion établie. Cela fait, les sockets sont utilisées par le serveur et le client pour transmettre les flux de données.

Lorsque vous créez une application qui utilise Bluetooth comme couche de transport, vous devez implémenter à la fois une socket côté serveur écoutant les demandes de connexion et une socket pour initier un nouveau canal et gérer les communications.

Une fois connectée, la socket serveur renvoie une socket Bluetooth utilisée ensuite pour envoyer et recevoir les données. Cette socket serveur Bluetooth est utilisée exactement de la même façon que la socket client. Les désignations serveur et client ne sont significatives que quant à la façon dont la connexion est établie. Elles n'affectent pas la circulation des flux une fois la connexion établie.

Créer un service Bluetooth

Une socket serveur est utilisée pour les demandes de connexion entrantes provenant des périphériques Bluetooth. Pour que deux appareils Bluetooth soient connectés, l'un doit agir comme serveur (écoutant et acceptant les demandes) et l'autre, comme client (initiant la demande de connexion au serveur). Une fois les deux appareils connectés, les communications sont gérés des deux côtés par une socket.

Pour connecter deux appareils, l'un doit donc faire office de serveur ou ouvrant une socket BluetoothServerSocket qui écoutera les requêtes entrantes. Si la requête est acceptée alors un objet BluetoothSocket connecté à l'autre appareil sera renvoyé. Pour créer une socket serveur et accepter la connexion de l'appareil distant, vous devez :

  1. Obtenir un objet BlutoothServerSocket en appelant la méthode listenUsingRfcommWithServiceRecord(String, UUID). Le premier paramètre est la chaîne représentant le nom du service. Le second paramètre est un identifiant unique qui identifie sans ambiguïté votre service. Lors de la demande de connexion, le client enverra également un identifiant unique qui devra être le même que celui spécifié ici pour que la connexion s'établisse.
  2. Débuter l'écoute des demandes de connexions en appelant la méthode accept(). Cette méthode ne retourne pas immédiatement et bloque l'exécution de votre application. La méthode retournera un objet BluetoothSocket dès qu'une demande de connexion sera acceptée avec un UUID correct. Vous pourrez utiliser cet objet pour transférer vos données.

    Si une demande de connexion est faite par un périphérique distant n'ayant jamais été apparié à l'adaptateur local, l'utilisateur se verra demander d'accepter l'appariement avant que l'appel à accept() ne renvoie un résultat. Cette demande est faite via une notification.

  3. Vous pouvez annuler l'écoute des demandes de connexions en appelant la méthode close() du BluetoothServerSocket. Dans tous les cas, appelez cette méthode pour libérer les ressources dès que vous n'aurez plus besoin de cet objet.

Du fait de la nature bloquante de la méthode accept(), vous devez toujours exécuter cette méthode hors du thread principal de l'interface au risuqe de bloquer complètement votre application. Il est nécessaire de créer un fil d'exécution à part qui se charge de la demande de connexion afin de ne pas bloquer l'exécution de l'application.

UUID : Un identifiant UUID est un identifiant unique (Universally Unique Identifier) représenté par une chaîne de 128 bits qui identifie l'information de façon unique. L'intérêt de l'UUID est qu'il est suffisamment grand pour que vous puissiez en générer un aléatoirement et avoir peu de chance d'entrer en collision avec le même identifiant.

Trouver un périphérique Bluetooth auquel se connecter

Il existe de nombreuses façons d'obtenir une référence à un périphérique Bluetooth distant et également quelques points importants à garder en tête, concernant les périphériques avec lesquels vous pouvez rentrer en communication. Pour pouvoir établir une connexion à un périphérique distant, les conditions suivantes doivent être réunies :

  1. Le périphérique distant doit être visible.
  2. Le périphérique distant doit accepter les connexions à l'aide d'une socket serveur.
  3. Les deux périphériques doivent être appariés (ou liés). Si tel n'est pas le cas, l'utilisateur se verra demander de le faire au moment de la demande de connexion.

Chaque objet Bluetooth représente un périphérique distant. Ces objets sont utilisés pour obtenir les propriétés des périphériques et pour initier les connexions. Il y a plusieurs façons d'obtenir un objet BluetoothDevice dans votre code.

Dans chaque cas, vous devez vérifier que le périphérique auquel vous vouslez vous conencter est visible et (optionnellement) déterminer si vous y êtes liés. Si vous ne pouvez pas découvrir le périphérique distant, vous devez demander à l'utilisateur d'activer la visibilité.

  1. Vous avez appris une technique pour découvrir les périphériques visibles plus haut dans cette section à l'aide de la méthode startDiscovery() et les actions de monitoring ACTION_FOUND. Vous avez appris que chaque Broadcast reçu incluait un extra BluetoothDevice.EXTRA_DEVICE contenant le périphérique découvert.
  2. Vous pouvez également utiliser la méthode getRemoteDevice() sur l'adaptateur local en spécifiant l'adresse matérielle du périphérique distant auquel vous voulez vous connecter.
    BluetoothAdapter adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
    BluetoothDevice appareilDistant = adaptateurLocal.getRemoteDevice("01:23:77:35:2F:AA");
  3. Pour trouver les périphériques appariés, appelez la méthode getBondedDevices() sur l'appareil local. Vous pouvez interroger l'ensemble renvoyé pour déterminer si un périphérique cible est aparié à l'adaptateur local.
    BluetoothAdapter adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
    Set<BluetoothDevice> appareils = adaptateurLocal.getBondedDevices();
    if (appareils.contains(appareilDistant) > 0) { ... }
Créer un client Bluetooth

La classe BluetoothSocket est utilisée sur le client pour initier un canal de communication depuis votre application vers un socket serveur à l'écoute. Avant de pouvoir demander une connexion à un appareil distant, vous devez posséder l'objet BluetoothDevice de ce dernier (soit en parcourant les appareils liés, soit en effectuant une nouvelle détection d'appareils). La création d'un client suit la logique suivante :

  1. Appelez la méthode createRfcommSocketToServiceRecord() de votre objet BluetoothDevice en spécifiant l'UUID du service désiré (le même que celui que vous aurez spécifié au BluetoothServerSocket). Cette méthode vous retourne un objet BluetoothSocket qui vous permettra de vous connecter à l'appareil plus tard.

    L'objet BluetoothDevice représente le serveur distant cible. Il doit avoir une socket serveur à l'écoute des demandes de connexion (comme nous l'avons décrit plus haut).

  2. Appelez la méthode connect() de votre instance BluetoothSocket. cet appel créera la connexion de communication RFCOMM avec l'autre appareil si le service distant accepte votre demande. Cette méthode est bloquante ; par conséquent placez toujours votre code dans un fil d'exécution indépendant.

    Si la méthode connect() est trop longue (par exemple lorsque l'appareil n'est pas suffisamment près ou si une recherche d'appareils Bluetooth est en train de s'exécuter en même temps), une exception sera lancée.

  3. Une fois connecté, vous pouvez vous déconnecter en appelant la méthode close() de l'objet BluetoothSocket. Dans tous les cas appelez cette méthode pour libérer les resources.
Transmettre les données à l'aide des sockets Bluetooth

Une fois le serveur et le client connectés, chacun va pouvoir échanger avec l'autre au travers de son objet BluetoothSocket. Effectivement, une fois la connexion établie, vous obtenez une socket sur chaque périphérique. A partir de maintenant, il n'y aura plus de distinction significative entre les deux : vous pouvez envoyer et recevoir des données en utilisant chacune des sockets.

Comme pour toute communication par les sockets, les méthodes et techniques sont les mêmes. Vous pouvez utiliser les méthodes getInputStream() et getOutputStream() pour récupérer les flux entrants et sortants.

Une fois les flux récupérés, vous pourrez utiliser les méthodes read() et write() respectivement pour lire et écrire des données. Ces méthodes étant bloquantes, vous devrez toujours les exécuter dans un fil d'exécution séparé.

Afin de valider toute ce chapitre concernant le bluetooth, je vous propose de nous servir du mobile pour piloter les robots LEGO NXT MINDSTORM uniquement par commande bluetooth. Dans ce premier exemple, le robot doit déjà être apparié à votre mobile pour que la communication se fasse correctement.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.bluetooth"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Bluetooth NXT" >
        <activity android:name="Bluetooth"  android:label="Robot NXT"  android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
</manifest>
src/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
   <EditText  
      android:id="@+id/adresse"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:gravity="center"
      android:text="00:16:53:0C:BC:BF" />
   <Button  
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:text="Connexion avec ce robot"
      android:onClick="recherche" />     
   <Button  
      android:layout_width="120dp" 
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:layout_margin="20dp"
      android:textColor="#0000FF"
      android:text="Avancer"
      android:onClick="avancer" />      
   <RelativeLayout 
      android:layout_width="fill_parent"
      android:layout_height="wrap_content">
      <Button  
         android:layout_width="120dp" 
         android:layout_height="wrap_content"
         android:layout_marginLeft="10dp"
         android:layout_alignParentLeft="true"
         android:textColor="#0000FF"
         android:text="Gauche"
         android:onClick="gauche" />          
      <Button  
         android:layout_width="120dp" 
         android:layout_height="wrap_content"
         android:layout_marginRight="10dp"
         android:layout_alignParentRight="true"
         android:textColor="#0000FF"
         android:text="Droite"
         android:onClick="droite" />          
   </RelativeLayout>    
   <Button  
      android:layout_width="120dp" 
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:layout_margin="20dp"
      android:textColor="#0000FF"
      android:text="Reculer"
      android:onClick="reculer" />      
   <Button  
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:layout_margin="20dp"
      android:textColor="#FF0000"
      android:text="Arrêter"     
      android:onClick="arreter" />         
      <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:padding="10dp">
         <Button  
            android:layout_width="0dp" 
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textColor="#FF00FF"
            android:text="40"     
            android:onClick="quarante" />      
         <Button  
            android:layout_width="0dp" 
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textColor="#FF00FF"
            android:text="60"     
            android:onClick="soixante" />  
         <Button  
            android:layout_width="0dp" 
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textColor="#FF00FF"
            android:text="80"     
            android:onClick="quatrevingt" />              
      </LinearLayout>
</LinearLayout>
fr.btsiris.bluetooth.Bluetooth.java
package fr.btsiris.bluetooth;

import android.app.Activity;
import android.bluetooth.*;
import android.content.*;
import android.os.*;
import android.view.View;
import android.widget.*;
import java.io.*;
import java.util.UUID;

public class Bluetooth extends Activity  {
   private BluetoothAdapter adaptateurLocal;
   private BluetoothDevice NXT;
   private BluetoothSocket robot;
   private InputStream entree;
   private OutputStream sortie;
   private EditText adresse;
   private boolean connecte = false;

   private UUID uuidNXT = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
   private final byte DIRECT_COMMAND_NOREPLY = (byte) 0x80;
   private final byte PLAY_TONE= (byte) 0x03; 
   private final byte SET_OUTPUT_STATE = (byte) 0x04;

   private final int MoteurA = 0;
   private final int MoteurC = 2;
   private enum Commande {Avancer, Reculer, GaucheAvant, GaucheArriere, DroiteAvant, DroiteArriere, Arreter};
   private Commande commande = Commande.Arreter;
   private int vitesse = 60;

   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      adresse = (EditText) findViewById(R.id.adresse);
      adaptateurLocal = BluetoothAdapter.getDefaultAdapter();   
   }

   @Override
   protected void onStart() {
      super.onStart();  
      if (!adaptateurLocal.isEnabled()) {
         startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 0);
      }
   }

   @Override
   protected void onStop() {    
      try {
         if (robot != null) robot.close();
         adaptateurLocal.disable();
         super.onStop();
      } 
      catch (IOException ex) { }
   }
   
   public void recherche(View vue) {
      if (connecte) return;
      try {
         connecte = true;
         NXT = adaptateurLocal.getRemoteDevice(adresse.getText().toString());
         robot = NXT.createRfcommSocketToServiceRecord(uuidNXT);
         robot.connect();
         entree = robot.getInputStream();
         sortie = robot.getOutputStream();
         Toast.makeText(Bluetooth.this, "Trouvé : "+NXT.getAddress(), Toast.LENGTH_LONG).show();
         beep(880, 1000);
      } 
      catch (IOException ex) {
         Toast.makeText(Bluetooth.this, "Problème de connexion", Toast.LENGTH_LONG).show();
      }
   }
   
   public void avancer(View vue) {  
      commande = Commande.Avancer;
      commandeMoteur(MoteurA, vitesse);
      commandeMoteur(MoteurC, vitesse);
   }
   
   public void gauche(View vue) { 
      if (commande==Commande.Avancer) commande = Commande.GaucheAvant;
      if (commande==Commande.DroiteAvant) commande = Commande.GaucheAvant;
      if (commande==Commande.Reculer) commande = Commande.GaucheArriere;
      if (commande==Commande.DroiteArriere) commande = Commande.GaucheArriere;
      switch (commande) {
         case GaucheAvant :          
            commandeMoteur(MoteurA, vitesse);
            commandeMoteur(MoteurC, vitesse/3);   break;
         case GaucheArriere :
            commandeMoteur(MoteurA, -vitesse);
            commandeMoteur(MoteurC, -vitesse/3); break;
      }
   }   
   
   public void droite(View vue) {  
      if (commande==Commande.Avancer) commande = Commande.DroiteAvant;
      if (commande==Commande.GaucheAvant) commande = Commande.DroiteAvant;
      if (commande==Commande.Reculer) commande = Commande.DroiteArriere;
      if (commande==Commande.GaucheArriere) commande = Commande.DroiteArriere;
      switch (commande) {
         case DroiteAvant :          
            commandeMoteur(MoteurA, vitesse/3);
            commandeMoteur(MoteurC, vitesse);   break;
         case DroiteArriere :
            commandeMoteur(MoteurA, -vitesse/3);
            commandeMoteur(MoteurC, -vitesse); break;
      } 
   }      
   
   public void reculer(View vue) {  
      commande = Commande.Reculer;
      commandeMoteur(MoteurA, -vitesse);
      commandeMoteur(MoteurC, -vitesse);
   }   
   
   public void arreter(View vue) {   
      commande = Commande.Arreter; 
      commandeMoteur(MoteurA, 0);
      commandeMoteur(MoteurC, 0);
   }

   public void quarante(View vue)   { vitesse = 40;  changerVitesse(vue);  }
   public void soixante(View vue)    { vitesse = 60;  changerVitesse(vue);  }    
   public void quatrevingt(View vue) { vitesse = 80;  changerVitesse(vue);  }
            
   private void changerVitesse(View vue) {
      Toast.makeText(Bluetooth.this, "Vitesse = "+vitesse, Toast.LENGTH_SHORT).show();
      switch (commande) {
         case Avancer : avancer(vue); break;
         case Reculer : reculer(vue); break;
         case GaucheAvant :
         case GaucheArriere : gauche(vue); break;
         case DroiteAvant :
         case DroiteArriere : droite(vue); break;
      }
   }
   
   private void beep(int frequence, int duree) {
      byte[] trame = new byte[6];
      trame[0] = DIRECT_COMMAND_NOREPLY;
      trame[1] = PLAY_TONE;
      trame[2] = (byte) frequence;
      trame[3] = (byte) (frequence >> 8);
      trame[4] = (byte) duree;
      trame[5] = (byte) (duree >> 8);
      envoiTrame(trame);
   }  
   
   private void commandeMoteur(int moteur, int vitesse) {
      byte[] trame = new byte[12];
      trame[0] = DIRECT_COMMAND_NOREPLY;
      trame[1] = SET_OUTPUT_STATE;
      trame[2] = (byte) moteur;
      if (vitesse==0) {
         trame[3] = 0x00;
         trame[4] = 0x00;
         trame[5] = 0x00;
         trame[6] = 0x00;     
         trame[7] = 0x00; 
      }
      else  trame[3] = (byte) vitesse;
      trame[4] = 0x03;
      trame[5] = 0x01;
      trame[6] = 0x00;
      trame[7] = 0x20;
      trame[8] = 0;
      trame[9] = 0;
      trame[10] = 0;
      trame[11] = 0;
      envoiTrame(trame);
   }
   
   private void envoiTrame(byte[] octets) {
      try {
         if (sortie == null) return;
         int longueur = octets.length;
         sortie.write(longueur);
         sortie.write(longueur >> 8);
         sortie.write(octets);
      } 
      catch (IOException ex) {
         Toast.makeText(Bluetooth.this, "Trame non reçue", Toast.LENGTH_LONG).show();
      }   
   }
}

Reprenons le même projet en ajoutant une autre activité qui permet de recenser les robots déjàs enregistrés sur votre mobile sous forme de liste. Autre particularité intéressante, nous ferons en sorte que les deux activités soient présentées sous forme de boîte de dialogue.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.bluetooth"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Bluetooth NXT" >
        <activity android:name="Bluetooth"  
                        android:label="Robot NXT" 
                        android:screenOrientation="portrait"
                        android:theme="@android:style/Theme.Dialog"> // Apparence d'une boîte de dialogue
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="ListeNXT" 
                   android:label=" Liste des robots"
                   android:screenOrientation="portrait"
                   android:theme="@android:style/Theme.Dialog"/>  // Apparence d'une boîte de dialogue
    </application>
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
</manifest>
src/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="3dp">
   <Button  
      android:layout_width="120dp" 
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:layout_margin="20dp"
      android:textColor="#0000FF"
      android:text="Avancer"
      android:onClick="avancer" />      
   <RelativeLayout 
      android:layout_width="fill_parent"
      android:layout_height="wrap_content">
      <Button  
         android:layout_width="120dp" 
         android:layout_height="wrap_content"
         android:layout_marginLeft="10dp"
         android:layout_alignParentLeft="true"
         android:textColor="#0000FF"
         android:text="Gauche"
         android:onClick="gauche" />          
      <Button  
         android:layout_width="120dp" 
         android:layout_height="wrap_content"
         android:layout_marginRight="10dp"
         android:layout_alignParentRight="true"
         android:textColor="#0000FF"
         android:text="Droite"
         android:onClick="droite" />          
   </RelativeLayout>    
   <Button  
      android:layout_width="120dp" 
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:layout_margin="20dp"
      android:textColor="#0000FF"
      android:text="Reculer"
      android:onClick="reculer" />      
   <Button  
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:layout_margin="20dp"
      android:textColor="#FF0000"
      android:text="Arrêter"     
      android:onClick="arreter" />         
      <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:padding="10dp">
         <Button  
            android:layout_width="0dp" 
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textColor="#FF00FF"
            android:text="40"     
            android:onClick="quarante" />      
         <Button  
            android:layout_width="0dp" 
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textColor="#FF00FF"
            android:text="60"     
            android:onClick="soixante" />  
         <Button  
            android:layout_width="0dp" 
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textColor="#FF00FF"
            android:text="80"     
            android:onClick="quatrevingt" />              
      </LinearLayout>
</LinearLayout>
fr.btsiris.bluetooth.Bluetooth.java
package fr.btsiris.bluetooth;

import android.app.Activity;
import android.bluetooth.*;
import android.content.*;
import android.os.*;
import android.view.*;
import android.widget.*;
import java.io.*;
import java.util.UUID;

public class Bluetooth extends Activity  {
   private BluetoothAdapter adaptateurLocal;
   private BluetoothDevice NXT;
   private BluetoothSocket robot;
   private InputStream entree;
   private OutputStream sortie;

   private UUID uuidNXT = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
   private final byte DIRECT_COMMAND_NOREPLY = (byte) 0x80;
   private final byte PLAY_TONE= (byte) 0x03; 
   private final byte SET_OUTPUT_STATE = (byte) 0x04;

   private final int MoteurA = 0;
   private final int MoteurC = 2;
   private enum Commande {Avancer, Reculer, GaucheAvant, GaucheArriere, DroiteAvant, DroiteArriere, Arreter};
   private Commande commande = Commande.Arreter;
   private int vitesse = 60;

   private final int BLUETOOTH_ACTIF = 1;
   private final int CHOIX_ROBOT = 2;
   
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); // La boîte de dialogue prend presque toute la largeur de l'écran
      adaptateurLocal = BluetoothAdapter.getDefaultAdapter();   
   }

   @Override
   protected void onStart() {
      super.onStart();  
      if (!adaptateurLocal.isEnabled()) {
         startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), BLUETOOTH_ACTIF); // Activation du Bluetooth
      }
      else choixNXT(); 
   }
 
   @Override
   protected void onStop() {    
      try {
         if (robot != null) robot.close();
         adaptateurLocal.disable();
         super.onStop();
      } 
      catch (IOException ex) { }
   }
   
   private void choixNXT() {
      setTitle(" Attente de connexion..."); 
      startActivityForResult(new Intent(this, ListeNXT.class), CHOIX_ROBOT); // Lancement de l'activité qui permet de choisir le robot
   }
   
   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
      switch (requestCode) {
         case BLUETOOTH_ACTIF :
            switch (resultCode) {
               case RESULT_OK : choixNXT(); break;
               case RESULT_CANCELED : Toast.makeText(this, "Arrêt : Bluetooth non actif", Toast.LENGTH_LONG).show();  break;
            }            
            break;
         case CHOIX_ROBOT :
            if (resultCode == RESULT_OK) {
               String adresse = data.getExtras().getString(ListeNXT.ADRESSE_MAC);  // Récupération de l'adresse MAC du robot
               setContentView(R.layout.main);
               connexion(adresse);
            }
            break;
      }
   }

   private void connexion(String adresse) {
      try {
         NXT = adaptateurLocal.getRemoteDevice(adresse);
         robot = NXT.createRfcommSocketToServiceRecord(uuidNXT);
         robot.connect();
         entree = robot.getInputStream();
         sortie = robot.getOutputStream();
         Toast.makeText(Bluetooth.this, "Trouvé : "+NXT.getAddress(), Toast.LENGTH_LONG).show();
         setTitle(" NXT - "+adresse);  // Changement du titre de l'activité principale
         beep(880, 1000); // Si le robot est bien connecté, ce dernier émet un La pendant une seconde
      } 
      catch (IOException ex) {
         Toast.makeText(Bluetooth.this, "Problème de connexion...", Toast.LENGTH_LONG).show();
      }
   }
  
   public void avancer(View vue) {  
      commande = Commande.Avancer;
      commandeMoteur(MoteurA, vitesse);
      commandeMoteur(MoteurC, vitesse);
   }
   
   public void gauche(View vue) { 
      if (commande==Commande.Avancer) commande = Commande.GaucheAvant;
      if (commande==Commande.DroiteAvant) commande = Commande.GaucheAvant;
      if (commande==Commande.Reculer) commande = Commande.GaucheArriere;
      if (commande==Commande.DroiteArriere) commande = Commande.GaucheArriere;
      switch (commande) {
         case GaucheAvant :          
            commandeMoteur(MoteurA, vitesse);
            commandeMoteur(MoteurC, vitesse/3);   break;
         case GaucheArriere :
            commandeMoteur(MoteurA, -vitesse);
            commandeMoteur(MoteurC, -vitesse/3); break;
      }
   }   
   
   public void droite(View vue) {  
      if (commande==Commande.Avancer) commande = Commande.DroiteAvant;
      if (commande==Commande.GaucheAvant) commande = Commande.DroiteAvant;
      if (commande==Commande.Reculer) commande = Commande.DroiteArriere;
      if (commande==Commande.GaucheArriere) commande = Commande.DroiteArriere;
      switch (commande) {
         case DroiteAvant :          
            commandeMoteur(MoteurA, vitesse/3);
            commandeMoteur(MoteurC, vitesse);   break;
         case DroiteArriere :
            commandeMoteur(MoteurA, -vitesse/3);
            commandeMoteur(MoteurC, -vitesse); break;
      } 
   }      
   
   public void reculer(View vue) {  
      commande = Commande.Reculer;
      commandeMoteur(MoteurA, -vitesse);
      commandeMoteur(MoteurC, -vitesse);
   }   
   
   public void arreter(View vue) {    
      commande = Commande.Arreter;
      commandeMoteur(MoteurA, 0);
      commandeMoteur(MoteurC, 0);
   }

   public void quarante(View vue)   { vitesse = 40;  changerVitesse(vue);  }
   public void soixante(View vue)    { vitesse = 60;  changerVitesse(vue);  }    
   public void quatrevingt(View vue) { vitesse = 80;  changerVitesse(vue);  }
            
   private void changerVitesse(View vue) {
      Toast.makeText(Bluetooth.this, "Vitesse = "+vitesse, Toast.LENGTH_SHORT).show();
      switch (commande) {
         case Avancer : avancer(vue); break;
         case Reculer : reculer(vue); break;
         case GaucheAvant :
         case GaucheArriere : gauche(vue); break;
         case DroiteAvant :
         case DroiteArriere : droite(vue); break;
      }
   }
   
   private void beep(int frequence, int duree) {
      byte[] trame = new byte[6];
      trame[0] = DIRECT_COMMAND_NOREPLY;
      trame[1] = PLAY_TONE;
      trame[2] = (byte) frequence;
      trame[3] = (byte) (frequence >> 8);
      trame[4] = (byte) duree;
      trame[5] = (byte) (duree >> 8);
      envoiTrame(trame);
   }  
   
   private void commandeMoteur(int moteur, int vitesse) {
      byte[] trame = new byte[12];
      trame[0] = DIRECT_COMMAND_NOREPLY;
      trame[1] = SET_OUTPUT_STATE;
      trame[2] = (byte) moteur;
      if (vitesse==0) {
         trame[3] = 0x00;
         trame[4] = 0x00;
         trame[5] = 0x00;
         trame[6] = 0x00;     
         trame[7] = 0x00; 
      }
      else  trame[3] = (byte) vitesse;
      trame[4] = 0x03;
      trame[5] = 0x01;
      trame[6] = 0x00;
      trame[7] = 0x20;
      trame[8] = 0;
      trame[9] = 0;
      trame[10] = 0;
      trame[11] = 0;
      envoiTrame(trame);
   }
   
   private void envoiTrame(byte[] octets) {
      try {
         if (sortie == null) return;
         int longueur = octets.length;
         sortie.write(longueur);
         sortie.write(longueur >> 8);
         sortie.write(octets);
      } 
      catch (IOException ex) {
         Toast.makeText(Bluetooth.this, "Trame non reçue", Toast.LENGTH_LONG).show();
      }   
   }
}
src/layout/liste_nxt.xml
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white">
    <ListView 
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:stackFromBottom="true"
        android:layout_weight="1"
        android:background="@android:color/white" />
</LinearLayout>
src/layout/nom_nxt.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:textSize="20sp"
    android:paddingLeft="5dp"
    android:paddingRight="5dp"
    android:paddingTop="10dp"
    android:paddingBottom="10dp"
    android:textColor="#FF9F00"
    android:textStyle="bold"
/>
fr.btsiris.bluetooth.ListeNXT.java
package fr.btsiris.bluetooth;

import java.util.Set;
import android.app.ListActivity;
import android.bluetooth.*;
import android.content.*;
import android.os.Bundle;
import android.view.*;
import android.widget.*;

public class ListeNXT extends ListActivity {
    static String ADRESSE_MAC = "adresseMAC";
    private BluetoothAdapter adaptateurLocal;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        setContentView(R.layout.liste_nxt);   
        ArrayAdapter<String> robots = new ArrayAdapter<String>(this, R.layout.nom_nxt);
        adaptateurLocal = BluetoothAdapter.getDefaultAdapter();
        Set<BluetoothDevice> appareilsDejaConnus = adaptateurLocal.getBondedDevices();  
        if (appareilsDejaConnus.size() > 0) {
            for (BluetoothDevice appareil : appareilsDejaConnus) {
                if (appareil.getAddress().startsWith("00:16:53"))                 // Début d'adresse MAC des robots NXT 
                   robots.add(appareil.getName()+"-"+appareil.getAddress());
            }
        }
        setListAdapter(robots);
        setResult(RESULT_CANCELED);
    }

   @Override
   protected void onListItemClick(ListView liste, View vue, int position, long id) {
      String info = ((TextView) vue).getText().toString();
      if (info.lastIndexOf('-') != info.length()-18) return;
      String addresseMAC = info.substring(info.lastIndexOf('-')+1);
      Intent intent = new Intent();
      Bundle data = new Bundle();
      data.putString(ADRESSE_MAC, addresseMAC);
      intent.putExtras(data);
      setResult(RESULT_OK, intent);
      finish();
    }
}

 

Choix du chapitre Réseau, connexions et services web

La plupart des applications sont communicantes. Difficile de s'y tromper, car toutes les permissions qui seront réclamées, la demande de connexion à Internet est l'une des plus fréquentes. Pour réaliser ce type d'applications, voici notre programme pour ce chapitre.

  1. La disponibilité du réseau.
  2. La gestion du Wi-Fi.
  3. L'utilisation des sockets.
  4. La création de requêtes pour accéder à des informations distantes.
  5. La réalisation de client pour des services web.

Disponibilité du réseau

Afin de concevoir intelligemment les applications qui nécessitent une connectivité au réseau, il est indispensable de vérifier au préalable la disponibilité de cette fameuse connexion.

Android diffuse des intentions qui décrivent les modifications dans la connexion réseau et offre des API qui permettent de contrôler les réglages réseau et les connexions.

Le réseau sous Android est principalement géré par la classe ConnectivityManager, un service qui vous permet de monitorer l'état de la connexion, de fixer votre connexion préféré et de gérer le basculement vers un autre type de connexion. Pour accéder au gestionnaire de connexion, utiliser la méthode getSystemService() en lui passant l'argument Context.CONNECTIVITY_SERVICE comme nom du service souhaité.

ConnectivityManager gestionConnexions =  (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);

Pour que cet extrait de code fonctionne, n'oubliez pas d'ajouter les permissions suivantes dans la section du manifeste du fichier de configuration de l'application, pour la lecture de l'état du réseau et pour d'éventuelles modifications :

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
Monitorer le réseau

Pour monitorer les détails du réseau, le gestionnaire de connexion fournit une vue de haut niveau sur les connexions réseau disponibles. L'utilisation des méthodes getActiveNetworkInfo() ou getNetworkInfo(TypeRéseau) renvoient des objets de type NetworkInfo qui contiennent les détails sur le réseau actif courant ou sur un réseau inactif du type spécifié.

  1. ConnectivityManager gestionConnexions =  (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo infoRéseau = gestionConnexions.getActiveNetworkInfo();
    NetworkInfo.State étatRéseau = infoRéseau.getState();
    if (étatRéseau.compareTo(State.CONNECTED) == 0) ... ;

    An passant la constante CONNECTIVITY_SERVICE à la méthode getSystemService(), nous obtenons notre gestionnaire réseau. A l'aide de ce dernier, nous récupérons une instance de la classe NetworkInfo. La méthode getState() appliqué à l'objet ainsi récupéré nous permet de tester l'état de la connexion (états possibles : CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED, SUSPENDED et UNKNOWN).

  2. ConnectivityManager gestionConnexions =  (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    // Récupère l'information du réseau actif
    NetworkInfo réseauActif = gestionConnexions.getActiveNetworkInfo();
    switch (réseauActif.getType())
    {
       case ConnectivityManager.TYPE_MOBILE : break;
       case ConnectivityManager.TYPE_WIFI : break;
    }
    // Récupère l'information du réseau mobile
    NetworkInfo réseauMobile = gestionConnexions.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
    NetworkInfo.State étatRéseauMobile = infoRéseau.getState();
    NetworkInfo.DetailledState étatDétailléRéseauMobile = infoRéseau.getDetailedState();
Déterminer et configurer les préférences réseau et contrôler le matériel radio

Le gestionnaire de connexion peut également être utilisé pour contrôler le matériel réseau et configurer les préférences de basculement. Android tentera de se connecter au réseau préféré à chaque fois qu'une application demandera une connexion Internet.

Vous pouvez déterminer et fixer le réseau courant en utilisant respectivement les méthodes getNetworkPreference() et setNetworkPreference(). Si la connexion préférée n'est pas disponible ou que la connectivité à ce réseau soit perdue, Android tentera automatiquement de se connecter au réseau secondaire.

ConnectivityManager connectivité =  (SensorManager) getSystemService(Context.CONNECTIVITY_SERVICE) ;
int préférence = connectivité.getNetworkPreference() ;
connectivité.setNetworkPreference(NetworkPreference.PREFER_WIFI) ;
Monitorer la connectivité réseau

L'une des fonctions les plus utiles du ConnectivityManager est de notifier les applications des changements dans la connectivité réseau.

Afin de monitorer celle-ci, créez votre propre implémentation de BroadcastReceiver écoutant les intentions venant de ConnectivityManager.CONNECTIVITY_ACTION. Ces intentions comptent plusieurs extras qui fournissent des détails additionnels sur les changements dans l'état de la connectivité. Vous pouvez accéder à chacun d'entre eux en utilisant l'une des constantes statiques de la classe ConnectivityManager :

  1. EXTRA_IS_FAILOVER : Booléen renvoyant true si la connexion courante est le résultat d'un basculement depuis le réseau préféré.
  2. EXTRA_NO_CONNECTIVITY : Booléen renvoyant true si l'appareil n'est connecté à aucun réseau.
  3. EXTRA_REASON : Si la diffusion associé représente un échec réseau, cette chaîne contiendra une description de la raison de l'échec.
  4. EXTRA_NETWORK_INFO : Renvoie un objet NetworkInfo contenant des détails plus fins sur le réseau associé à l'événement de connectivité courante.
  5. EXTRA_OTHER_NETWORK_INFO : Après une déconnexion réseau, cette valeur renverra un objet NetworkInfo contenant les détails des basculements possibles.
  6. EXTRA_EXTRA_INFO : Contient les détails de connexion additionnnels spécifiques au réseau.

Gérer le réseau Wi-FI

Le Wi-Fi est une fonctionnalité importante car elle offre aux appareils qui ne sont pas dotés de fonctions de téléphonie l'accès à l'Internet. De plus le réseau Wi-Fi offre actuellement un meilleur débit que le réseau téléphonique classique.

Pour manipuler les données relatives aux réseau Wi-Fi, vous devez faire appel à l'objet WifiManager. Cet objet sert d'interface entre le service système qui gère le Wi-Fi et votre application.

Le WifiManager représente le service de connectivité Wi-Fi d'Android. Il peut être utilisé pour configurer les connexions Wi-Fi, gérer la connexion courante, scanner des points d'accès et monitorer les changements dans la connectivité.

Comme pour le ConnectivityManager, vous accéder au WifiManager en utilisant la méthode getSystemService() et en lui passant en paramètre la constante Context.WIFI_SERVICE.

WifiManager wifi =  (WifiManager) getSystemService(Context.WIFI_SERVICE);

Pour utiliser le WifiManager, votre application doit avoir les permissions adéquates dans son manifeste, soit pour une simple consultation, soit pour que votre application soit capable d'effectuer un changement d'état :

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
Activer et désactiver le Wi-Fi

Le premier essentiel à aborder avec le Wi-Fi, c'est bien évidemment de pouvoir obtenir son état (activé ou non) et au besoin de le modifier.

Vous pouvez utiliser le WifiManager pour activer ou désactiver votre Wi-Fi en utilisant la méthode setWifiEnabled() ou demander l'état courant en utilisant les méthodes getWifiState() ou isWifiEnabled().

WifiManager wifi =  (WifiManager) getSystemService(Context.WIFI_SERVICE);
if (wifi.isWifiEnabled()) 
   if (wifi.getWifiState() != WifiManager.WIFI_STATE_ENABLING)  wifi.setWifiEnabled(true);
Monitorer la connectivité Wi-Fi

Le WifiManager diffuse des intentions à chaque fois sur le status de la connectivité du réseau Wi-Fi change, utilisant une action représentée par l'une des constantes suivantes de la classe WifiManager :

WIFI_STATE_CHANGED_ACTION
Indique que le status du matériel a changé pour l'une des valeurs "en cours d'activation", "activé", "en cours de désactivation", "désactivation" ou "inconnu". Elle contient deux extras, EXTRA_WIFI_STATE et EXTRA_PREVIOUS_STATE, qui fournissent respectivement le nouvel état et l'ancien.
SUPPLICANT_CONNECTION_CHANGED_ACTION
Cette intention est diffusée à chaque fois que l'état de la connexion avec le supplicant (point d'accès) actif change. Il est déclenché lorsqu'une nouvelle connexion est établie ou qu'une connexion existante est perdue, utilisant l'extra EXTRA_NEW_STATE, qui renvoie true dans le cas précédent.
NETWORK_STATE_CHANGED_ACTION

Déclenché à chaque fois que la connectivité Wi-Fi change. Cette intention contient deux extras. Le premier, EXTRA_NETWORK_INFO, contient un objet NetworkInfo qui détaille l'état du réseau courant et le second, EXTRA_BSSID, inclut le BSSID du point d'accès auquel vous êtes connecté.

RSSI_CHANGED_ACTION
Vous pouvez monitorer la force du signal du réseau auquel vous êtes connecté en écoutant cette intention. Ce Broadcast Intent inclut un extra entier, EXTRA_NEW_RSSI, qui contient la force du signal en cours. Pour l'utiliser, vous devez utiliser la méthode statique calculateSignalLevel() sur le WifiManager pour le convertir en une valeur entière sur une échelle de votre choix.
Monitorer les détails de la connexion active

Une fois qu'une connexion réseau a été établie, utilisez la méthode getConnectionInfo() sur le WifiManager pour trouver les informations su le statut de la connexion active. L'objet WifiInfo renvoyé contient le SSID, le BSSID, l'adresse MAC et l'adresse IP du point d'accès courant ainsi que la vitesse de liaison et la force du signal.

WifiManager wifi =  (WifiManager) getSystemService(Context.WIFI_SERVICE);
WifiInfo info = wifi.getConnectionInfo();
if (info.getBSSID() != null) {
   int force = WifiManager.calculateSignalLevel(info.getRssi(), 5);
   int vitesse = info.getLinkSpeed();
   String unités = WifiInfo.LINK_SPEED_UNITS;
   String ssid = info.getSSID();
   String information = String.format("Connecté à %s%s. Force %s/5", ssid, vitesse, unités, force);
}

Les sockets

L'utilisation des sockets, ou connecteurs réseau , est plus confidentielle sur une application mobile puisque de plus bas niveau, mais elle fait partie du large spectre des possibilités.

Rappel sur les sockets : Une socket est un flux que nous pouvons lire ou que nous pouvons écrire des données brutes. Le concept de socket permet à plusieurs processus de communiquer sur la même machine (en local) ou via un réseau. Pour cela, une socket est identifiable par une adresse IP spécifique et un numéro de port.

Il existe deux grands types d'utilisations et donc des communications
En mode connecté
Grâce au protocole TCP, pour établir une connexion durable.
En mode non connecté
Grâce au protocole UDP. Un mode non connecté implique qu'il n'y a pas de confirmation de réception des informations et pas de contrôle d'erreur. Ce mode est beaucoup plus performant pour des applications vidéo, pas exemple, mais moins fiable.
Le client Android

La mise en oeuvre d'une communication par socket avec est extrêmement simple avec Android, puisqu'il suffit d'appliquer exactement la même démarche qu'avec une application Java SE classique qui utilise la classe Socket et toutes les classes prévues par les flux de haut niveau. Par contre, comme les soumissions réseau sont relativement longue, prévoyez de mettre en oeuvre un environnement multi-tâches.

Côté permission, puisque nous avons besoin d'une connexion, il suffit d'ajouter android.permission.INTERNET à la section manifest du fichier de configuration de l'application comme suit :

<uses-permission android:name="android.permission.INTERNET" />
Je vous propose de prendre un exemple très simple qui interroge le service de l'heure GMT qui se trouve sur l'un des serveurs aux Etats-Unis dont le numéro de service est le 13 standard. Au préalable, je vous invite à le tester tout simplement à l'aide d'un client telnet.
  1. Lancez le logiciel client Telnet.
  2. Etablissez ensuite la connexion au serveur time.nist.org (Situé aux Etats-Unis à @IP 192.43.244.18) à l'aide du client Telnet. Pour cela nous utilisons la commande open. Rappelez-vous que par défaut ce service est réglé sur le port 13.
    open time.nist.gov 13
    ou
    open 192.43.244.18 13
  3. Que se passe-t-il ? Nous venons de nous connecter au service date que la plupart des serveurs UNIX implémentent en permanence. Le serveur auquel vous vous êtes connecté se trouve au NIST à Boulder dans le Colorado, et fournit l'heure d'une horloge atomique au césium. Naturellement, le temps affiché n'est pas parfaitement précis à cause des délais de propagation des informations sur le réseau. Par convention, le service date est toujours rattaché au port 13.



    Le programme du serveur fonctionne en permanence sur la machine distante, attendant un paquet du réseau qui essaierait de communiquer avec le port 13. Lorsque le système d'exploitation de l'ordinateur distant reçoit le paquet contenant une requête de connexion sur le port 13, le processus d'écoute du serveur est activé et la connexion est établie. Cette connexion demeure jusqu'à ce qu'elle soit arrêtée par l'une des deux parties.

    Lorsque vous avez ouvert une session Telnet sur le port 13 de l'ordinateur distant, une partie indépendante du logiciel réseau à converti la chaîne de texte time.nist.gov en une adresse IP associée, c'est à dire 192.43.244.18. Puis le logiciel a envoyé une requête de connexion à cet ordinateur, en spécifiant le port 13.

    Une fois que cette connexion a été établie, le programme de l'ordinateur distant a renvoyé un ensemble de données, puis il a terminé la connexion. Bien sûr, dans le cas plus général, les clients et les serveurs entament des dialogues beaucoup plus poussés avant que la connexion ne soit interrompue.

Après toutes ces considérations techniques, je vous porpose d'implémenter un client Android qui nous donne l'heure GMT exacte (presque) avec un formatage de la date qui nous donne le jour de la semaine et l'identification du mois en toute lettre.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.date"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Horloge Réseau" >
        <activity android:name="DateReseau" android:label="Horloge réseau">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>
src/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
   <TextView  
      android:id="@+id/date"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:gravity="center"
      android:textSize="20sp"
      android:textColor="#009F00"
      android:textStyle="bold"         
      android:text="Date" />
   <TextView  
      android:id="@+id/heure"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:gravity="center"
      android:textSize="20sp"
      android:textColor="#009F00"
      android:textStyle="bold"   
      android:paddingBottom="20sp"
      android:text="Heure" />
   <Button  
      android:layout_width="200dp" 
      android:layout_height="wrap_content" 
      android:layout_gravity="center"
      android:text="Nouvelle requête"
      android:onClick="requete"/>      
</LinearLayout>
fr.btsiris.date.DateReseau.java
package fr.btsiris.date;

import android.app.Activity;
import android.os.*;
import android.view.View;
import android.widget.*;
import java.io.*;
import java.net.*;
import java.text.DateFormat;
import java.util.*;

public class DateReseau extends Activity {
   private TextView heure, date;
   private boolean enExecution = false;
   
   private Handler client = new Handler() {
      @Override
      public void handleMessage(Message msg) {
         Bundle donnees = msg.getData();
         if (donnees.getBoolean("valide")) {
            Toast.makeText(DateReseau.this, "Soumission acceptée", Toast.LENGTH_LONG).show();
            heure.setText(donnees.getString("heure"));
            date.setText(donnees.getString("date")); 
         }
         else Toast.makeText(DateReseau.this, "Impossible d'atteindre le service", Toast.LENGTH_LONG).show();
      }   
   };
    
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      heure = (TextView) findViewById(R.id.heure);
      date = (TextView) findViewById(R.id.date);
   }

   @Override
   protected void onStart() {
      super.onStart();
      requete(null);
   } 
   
   public void requete(View vue) {
      if (!enExecution) new Thread(new Tache()).start();
   }
   
   class Tache implements Runnable {
      public void run() {
         enExecution = true;
         try {
            Socket serviceDate = new Socket("time.nist.gov", 13);
            Scanner reponse = new Scanner(serviceDate.getInputStream());
            if (reponse.hasNextLine()) {
               reponse.next();
               Scanner chaine = new Scanner(reponse.next());
               chaine.useDelimiter("-");
               int annee = 100+chaine.nextInt();
               int mois = chaine.nextInt()-1;
               int jour = chaine.nextInt();
               Date dateComplete = new Date(annee, mois, jour);    
               Bundle donnees = new Bundle();       
               donnees.putBoolean("valide", true);
               donnees.putString("heure", reponse.next()+ " GMT");
               donnees.putString("date", DateFormat.getDateInstance(DateFormat.FULL).format(dateComplete));   
               Message message = client.obtainMessage();
               message.setData(donnees);              
               client.sendMessage(message);                 
            }
            serviceDate.close();
         } 
         catch (IOException ex) {
             Bundle donnees = new Bundle(); 
             donnees.putBoolean("valide", false);
             Message message = client.obtainMessage();
             message.setData(donnees);               
             client.sendMessage(message);       
         }                 
         enExecution = false;
      }      
   }
}

Interroger un serveur web grâce au protocole HTTP

Au travers de ce dernier sujet, nous allons communiquer en travers de requête HTTP vers un service web REST, à partir d'un client Android.

Le protocole HTTP ainsi que le service web REST Java est largement évoqué dans l'étude suivante.
.

Rappel rapide sur le protocole HTTP

A une requête HTTP effectuée par un client, est associée une réponse d'un serveur. La requête client est constituée :

  1. D'une ligne de requête (qui peut être de plusieurs types : GET, POST, HEAD, etc.
  2. D'en-têtes (lignes facultatives qui permettent de communiquer des informations supplémentaires comme la version du client, très utile afin de pouvoir adapter les contenus aux mobiles).
  3. Du coprs de la requête qui peut être de différentes natures (types MIME : text/plain, application/xml, image/jpeg, etc.)
Utilisation de la classe HttpClient

Le kit de développement Android contient le client HttpClient d'Apache - dans une version adaptée à Android - qui propose un certain nombre de fonctionnalités pour utiliser le protocole HTTP dans son ensemble. Quel que soit le type de requête, GET, POST, PUT, DELETE, HEAD et OPTION, l'utilisation de ce client suit le schéma suivant :

  1. Création d'une instance de la classe DefaultHttpClient qui implémente l'interface HttpClient.
  2. Création d'une instance d'un objet représentant la requête qui spécialisera notre client.
  3. Réglage des propriétés de cette requête.
  4. Exécution de la requête grâce à l'instance de type HttpClient obtenue auparavant.
  5. Analyse et traitement de la réponse.
Je vous propose de prendre tout de suite un exemple très simple qui va nous permettre de communiquer avec le service web REST de gestion du personnel élaboré dans l'étude sur les web services. Il s'agit ici pour l'instant de n'utiliser que la méthode GET en donnant juste la valeur de l'identifiant de la personne concernée.

Il existe plusieurs méthodes GET implentée dans le web service REST. Nous utilisons la dernière méthode qui retourne les données de l'ensemble de la personne dans les en-têtes de la réponse HTTP.

Pour en savoir plus sur le projet de ce service web REST, retourneé dans l'étude suivante.
.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.rest"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="PersonnelREST" >
        <activity android:name="ClientPersonnel" android:label="Client Personnel">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>

Attention, pensez bien à donner les droits d'accès à la communication réseau.
.

src/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
   <EditText  
      android:id="@+id/identifiant"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Identifiant"
      android:inputType="number"/>
   <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Requête"
      android:onClick="soumettre" />    
</LinearLayout>






  
fr.btsiris.rest.ClientPersonnel.java
package fr.btsiris.rest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.io.IOException;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

public class ClientPersonnel extends Activity {
   private EditText identifiant;
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      identifiant = (EditText) findViewById(R.id.identifiant);
   }
   
   public void soumettre(View vue) throws IOException {
      String nom="", prenom="", telephone="";
      HttpClient client = new DefaultHttpClient();
      HttpGet requete = new HttpGet("http://172.16.34.235:8080/Personnel/rest/"+identifiant.getText().toString());
      HttpResponse reponse = client.execute(requete);
      if (reponse.getStatusLine().getStatusCode() == 200) {
         for (Header enTete : reponse.getAllHeaders()) {
            if (enTete.getName().equals("nom")) nom = enTete.getValue();
            if (enTete.getName().equals("prenom")) prenom = enTete.getValue();
            if (enTete.getName().equals("telephone")) telephone = enTete.getValue();
         }
         Toast.makeText(this, nom+" "+prenom+"\n"+telephone, Toast.LENGTH_LONG).show();
      }
      else Toast.makeText(this, "Identifiant inconnu", Toast.LENGTH_SHORT).show();
   }
}

Au delà de l'uitlisation de la classe DefaultHttpClient et de son interface HttpClient, vous devez prendre une classe correspond à la requête HTTP souhaitée, ici la requête GET. La classe correspondante se nomme tout simplement HttpGet. Pour récupérer la réponse à la requête choisissez cette fois-ci la classe spécialisée HttpResponse.

Je vous propose maintenant, toujours sur ce petit projet, d'utiliser la deuxième méthode GET, qui renvoie cette fois-ci un contenu au format JSON. Ce format étant textuel, je me contente ici d'afficher tout simplement le texte correspondant sans interprétation quelconque :

fr.btsiris.rest.ClientPersonnel.java
package fr.btsiris.rest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.io.*;
import java.util.Scanner;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

public class ClientPersonnel extends Activity {
   private EditText identifiant;
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      identifiant = (EditText) findViewById(R.id.identifiant);
   }
   
   public void soumettre(View vue) throws IOException {
      HttpClient client = new DefaultHttpClient();
      HttpGet requete = new HttpGet("http://172.16.34.235:8080/Personnel/rest/json/"+identifiant.getText().toString());
      HttpResponse reponse = client.execute(requete);
      if (reponse.getStatusLine().getStatusCode() == 200) {
         Scanner lecture = new Scanner(reponse.getEntity().getContent());
         StringBuilder contenu = new StringBuilder();
         while (lecture.hasNextLine()) contenu.append(lecture.nextLine()+'\n');
         Toast.makeText(this, contenu.toString(), Toast.LENGTH_LONG).show();
      }
      else Toast.makeText(this, "Identifiant inconnu", Toast.LENGTH_SHORT).show();
   }
}  
Le client Android JSON associé

JSON (JavaScript Object Notation) est un format de données qui permet de représenter de l'information structurée sous forme de texte. Sa représentation est plus légère que le format XML. Je vous propose de reprendre l'exemple précédent, et nous allons récupérer les informations atomiques constituant le personnel choisi :

fr.btsiris.rest.ClientPersonnel.java
package fr.btsiris.rest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.io.*;
import java.text.DateFormat;
import java.util.*;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.*;

public class ClientPersonnel extends Activity {
   private EditText identifiant;
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      identifiant = (EditText) findViewById(R.id.identifiant);
   }
   
   public void soumettre(View vue) throws IOException, JSONException {
      HttpClient client = new DefaultHttpClient();
      HttpGet requete = new HttpGet("http://172.16.34.235:8080/Personnel/rest/json/"+identifiant.getText().toString());
      HttpResponse reponse = client.execute(requete);
      if (reponse.getStatusLine().getStatusCode() == 200) {
         Scanner lecture = new Scanner(reponse.getEntity().getContent());
         StringBuilder contenu = new StringBuilder();
         while (lecture.hasNextLine()) contenu.append(lecture.nextLine()+'\n');      
         JSONObject json = new JSONObject(contenu.toString());
         String nom = json.getString("nom");
         String prénom = json.getString("prénom");
         Date naissance = new Date(json.getLong("naissance"));
         String téléphone = json.getString("téléphone");
         String message = nom+" "+prénom+"\n"+téléphone+"\n"+DateFormat.getDateInstance(DateFormat.FULL).format(naissance);
         Toast.makeText(this, message, Toast.LENGTH_LONG).show();
      }
      else Toast.makeText(this, "Identifiant inconnu", Toast.LENGTH_SHORT).show();
   }
}

Si vous devez récupérer un seul objet qui est envoyé en format org.json.JSON, il suffit de prendre la classe JSONObject et de créer une instance de cette classe avec la chaîne récupérée. Ensuite, pour mapper l'ensemble des attributs, il suffit d'utiliser les méthodes adaptées, comme getString(), getDouble(), getInt(), getLong(), etc. suivant le type de l'attribut.

Une autre classe intéressante et très souvent utile lorsque nous manipulons des fichiers JSON qui possèdent une collection d'objets, traduit sous forme de tableau. Il s'agit de la classe JSONArray. Dans le projet suivant et de façon la plus réduite possible pour bien maîtriser ce concept, nous allons récupérer la liste du personnel enregistré.

src/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
   <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Liste du personnel"
      android:onClick="soumettre" />    
</LinearLayout>
fr.btsiris.rest.ClientPersonnel.java
package fr.btsiris.rest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.io.*;
import java.util.*;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.*;

public class ClientPersonnel extends Activity {  
   @Override
   public void onCreate(Bundle savedInstanceState)  {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
   }
   
   public void soumettre(View vue) throws IOException, JSONException {
      HttpClient client = new DefaultHttpClient();
      HttpGet requete = new HttpGet("http://172.16.34.235:8080/Personnel/rest/tous/");
      HttpResponse reponse = client.execute(requete);
      if (reponse.getStatusLine().getStatusCode() == 200) {
         Scanner lecture = new Scanner(reponse.getEntity().getContent());
         StringBuilder contenu = new StringBuilder();
         while (lecture.hasNextLine()) contenu.append(lecture.nextLine()+'\n');             
         JSONArray tableauJSON = new JSONArray(contenu.toString());
         StringBuilder message = new StringBuilder();
         for (int i=0; i<tableauJSON.length(); i++) {
            JSONObject json = tableauJSON.getJSONObject(i);
            message.append(json.getString("nom")+' '+json.getString("prénom")+'\n');
         }
         Toast.makeText(this, message.toString(), Toast.LENGTH_LONG).show();
      }
      else Toast.makeText(this, "Identifiant inconnu", Toast.LENGTH_SHORT).show();
   }
}

Je vous propose de réaliser une dernière expérience qui consiste cette fois-ci à générer un document JSON à partir de données séparées. Pour cela, il suffit d'utiliser la même classe que nous venons d'étudier, savoir JSONObject, de compléter ensuite les éléments nécessaires au document au moyen des différents méthodes put(). Quand tout est bien complété, il suffit de faire appel à la méthode redéfinie toString().

src/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="3px">
    <EditText  
      android:id="@+id/nom"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Nom" />   
    <EditText  
      android:id="@+id/prenom"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Prénom" />   
    <EditText  
      android:id="@+id/age"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Âge"
      android:inputType="number"/>   
    <Button 
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Générer"
      android:onClick="soumettre" />    
</LinearLayout>
fr.btsiris.rest.ClientPersonnel.java
package fr.btsiris.rest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import org.json.*;

public class ClientPersonnel extends Activity {  
   private EditText nom, prenom, age;

   @Override
   public void onCreate(Bundle savedInstanceState)  {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      nom = (EditText) findViewById(R.id.nom);
      prenom = (EditText) findViewById(R.id.prenom);
      age = (EditText) findViewById(R.id.age);
   }
   
   public void soumettre(View vue) throws JSONException {
      JSONObject json = new JSONObject();
      json.put("nom", nom.getText().toString());
      json.put("prénom", prenom.getText().toString());
      json.put("âge", Integer.parseInt(age.getText().toString()));
      Toast.makeText(this, json.toString(), Toast.LENGTH_LONG).show();
   }
}
Exemple définitif complet de la gestion du personnel

Afin de valider toutes ces fonctionnalités de communication avec le service web REST de la gestion du personnel, je vous propose de construire un projet sous Android, avec une interface conviviale, qui permet de gérer la globalité de cette gestion du personnel.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="fr.btsiris.rest"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="Client REST Personnel" >
         <activity android:name="ListePersonnel" android:label="Liste du Personnel">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>       
        <activity android:name="ChoixServeur" android:label="Choix du serveur" android:theme="@android:style/Theme.Dialog" />        
        <activity android:name="Personnel" android:label="Edition du Personnel" android:theme="@android:style/Theme.Dialog"/>
    </application>
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>
res/drawable/fond.xml
<?xml version="1.0" encoding="UTF-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
   <gradient 
      android:startColor="#FF0000"
      android:endColor="#FFFF00"
      android:type="radial"
      android:gradientRadius="300" />
</shape>
res/layout/adresse.xml
<?xml version="1.0" encoding="UTF-8"?>
   <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:padding="2dp">
      <Button
         android:id="@+id/ok"
         android:layout_alignParentRight="true"
         android:layout_width="wrap_content" 
         android:layout_height="wrap_content" 
         android:text="OK" 
         android:onClick="ok"/>              
      <EditText
         android:layout_toLeftOf="@id/ok"
         android:id="@+id/adresse"
         android:layout_width="fill_parent" 
         android:layout_height="wrap_content" 
         android:hint="Adresse IP" />                   
   </RelativeLayout>
fr.btsiris.rest.ChoixServeur.java
package fr.btsiris.rest;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.*;
import android.widget.EditText;

public class ChoixServeur extends Activity {
   private EditText adresse;
   @Override
   public void onCreate(Bundle icicle) {
      super.onCreate(icicle);
      requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);  
      setContentView(R.layout.adresse);
      adresse = (EditText) findViewById(R.id.adresse);
   }
   
   public void ok(View vue) {
      Intent intent = new Intent();
      intent.putExtra("adresse", adresse.getText().toString());
      setResult(RESULT_OK, intent);
      finish();      
   }
}
res/layout/liste.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/fond"
    android:padding="3dp">  
   <Button
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:text="Nouvelle personne"
      android:onClick="edition" />
   <ListView  
      android:id="@android:id/list"
      android:layout_width="fill_parent" 
      android:layout_height="fill_parent" />
</LinearLayout>
fr.btsiris.rest.Personne.java
package fr.btsiris.rest;


import java.io.Serializable;

public class Personne implements Serializable {
   private long id;
   private String nom;
   private String prenom;
   private long naissance;
   private String telephone;

   public long getId() { return id;  }
   public void setId(long id) {  this.id = id;  } 

   public long getNaissance() { return naissance;  }
   public void setNaissance(long naissance) {  this.naissance = naissance;   }

   public String getNom() { return nom;  }
   public void setNom(String nom) {  this.nom = nom; }

   public String getTelephone() { return telephone;  }
   public void setTelephone(String telephone) { this.telephone = telephone; }

   public String getPrenom() { return prenom;   }
   public void setPrenom(String prenom) { this.prenom = prenom; }

   @Override
   public String toString() { return nom + " " + prenom;  }   
} 
fr.btsiris.rest.ListePersonnel.java
package fr.btsiris.rest;

import android.app.ListActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import java.io.IOException;
import java.util.*;
import org.apache.http.HttpResponse;
import org.apache.http.client.*;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.*;

public class ListePersonnel extends ListActivity {
    private ArrayList<Personne> personnes = new ArrayList<Personne>();
    private String adresse;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.liste);
       startActivityForResult(new Intent(this, ChoixServeur.class), 1);
    }
    
    protected void onActivityResult(int requestCode, int resultCode, Intent intention) {
       if (resultCode == RESULT_OK) {
          adresse = intention.getStringExtra("adresse");
          try {
             miseAJour();
          } 
          catch (Exception ex) {  Toast.makeText(this, "Réponse incorrecte", Toast.LENGTH_SHORT).show();  }
       }
    }

   @Override
   protected void onResume() {
      super.onResume();
      if (adresse!=null) try {
         miseAJour();
      } 
      catch (Exception ex) { }
   }

   @Override
   protected void onStop() {
      super.onStop();
      finish();
   }
   
    public void miseAJour() throws IOException, JSONException {
      HttpClient client = new DefaultHttpClient();
      HttpGet requete = new HttpGet("http://"+adresse+":8080/Personnel/rest/tous/");
      HttpResponse reponse = client.execute(requete);
      if (reponse.getStatusLine().getStatusCode() == 200) {
         Scanner lecture = new Scanner(reponse.getEntity().getContent());
         StringBuilder contenu = new StringBuilder();
         while (lecture.hasNextLine()) contenu.append(lecture.nextLine()+'\n');             
         JSONArray tableauJSON = new JSONArray(contenu.toString());
         StringBuilder message = new StringBuilder();
         personnes.clear();
         for (int i=0; i<tableauJSON.length(); i++) {
            JSONObject json = tableauJSON.getJSONObject(i);
            Personne personne = new Personne();
            personne.setId(json.getLong("id"));
            personne.setNom(json.getString("nom"));
            personne.setPrenom(json.getString("prénom"));
            personne.setNaissance(json.getLong("naissance"));
            personne.setTelephone(json.getString("téléphone"));
            personnes.add(personne);
         }
         setListAdapter(new ArrayAdapter<Personne>(this, android.R.layout.simple_list_item_1, personnes));
      }
      else Toast.makeText(this, "Problème de communication", Toast.LENGTH_SHORT).show();       
    }
    
    public void edition(View vue) {
       Intent intention = new Intent(this, Personnel.class);
       intention.putExtra("id", 0);
       intention.putExtra("adresse", adresse);
       startActivity(intention);
    }

    @Override
    protected void onListItemClick(ListView liste, View vue, int position, long id) {
       Personne personne = personnes.get(position);
       Intent intention = new Intent(this, Personnel.class);
       intention.putExtra("id", personne.getId());
       intention.putExtra("adresse", adresse);
       intention.putExtra("nom", personne.getNom());
       intention.putExtra("prenom", personne.getPrenom());
       intention.putExtra("naissance", personne.getNaissance());
       intention.putExtra("telephone", personne.getTelephone());
       startActivity(intention);      
    }      
}
res/layout/personnel.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/fond"
    android:padding="3px">
    <EditText  
      android:id="@+id/nom"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Nom" />   
    <EditText  
      android:id="@+id/prenom"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Prénom" />   
    <EditText 
      android:id="@+id/naissance"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="Date naissance"
      android:onClick="changeDate"/>   
    <EditText  
      android:id="@+id/telephone"
      android:layout_width="fill_parent" 
      android:layout_height="wrap_content" 
      android:hint="n° téléphone"/>         
      <LinearLayout 
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:gravity="center">
         <Button
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="Nouveau"
            android:onClick="nouveau"/>    
         <Button
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="Enregistrer" 
            android:onClick="enregistrer"/>            
         <Button
            android:id="@+id/supprimer"
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="Supprimer" 
            android:onClick="supprimer"/>              
      </LinearLayout>
</LinearLayout>
fr.btsiris.rest.Personnel.java
package fr.btsiris.rest;

import android.app.*;
import android.content.Intent;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.*;
import android.widget.*;
import java.io.IOException;
import java.util.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.impl.client.DefaultHttpClient;

public class Personnel extends Activity {  
   private EditText nom, prenom, naissance, telephone;
   private Button supprimer;
   private long id;
   private String adresse;
   private Calendar calendrier = Calendar.getInstance();
   
   @Override
   public void onCreate(Bundle savedInstanceState)  {
      super.onCreate(savedInstanceState);
      requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
      setContentView(R.layout.personnel);
      nom = (EditText) findViewById(R.id.nom);
      prenom = (EditText) findViewById(R.id.prenom);
      telephone = (EditText) findViewById(R.id.telephone);
      naissance = (EditText) findViewById(R.id.naissance);
      supprimer = (Button) findViewById(R.id.supprimer);
   }

   @Override
   protected void onStart() {
      super.onStart();
      Intent intention = getIntent();
      Bundle donnees = intention.getExtras();
      id = donnees.getLong("id");
      adresse = donnees.getString("adresse");
      if (id==0) toutEffacer();
      else {
         nom.setEnabled(false);
         prenom.setEnabled(false);      
         supprimer.setEnabled(true);
         nom.setText(donnees.getString("nom"));
         prenom.setText(donnees.getString("prenom"));
         long date = donnees.getLong("naissance");
         calendrier.setTimeInMillis(date);
         naissance.setText(DateFormat.format("EEEE dd MMMM yyyy", date)); 
         telephone.setText(donnees.getString("telephone"));               
      }      
   }
   
   private void toutEffacer() {
      id = 0;
      nom.setEnabled(true);
      prenom.setEnabled(true);
      supprimer.setEnabled(false);  
      nom.setText("");
      prenom.setText("");
      naissance.setText("");
      telephone.setText("");        
      calendrier = Calendar.getInstance();
   }
   
   private DatePickerDialog.OnDateSetListener evt = new DatePickerDialog.OnDateSetListener() {
      public void onDateSet(DatePicker dialog, int annee, int mois, int jour) {
         calendrier.set(annee, mois, jour);
         naissance.setText(DateFormat.format("EEEE dd MMMM yyyy", calendrier));
      }
   };
   
   public void changeDate(View vue) {
       new DatePickerDialog(this, evt, 
               calendrier.get(Calendar.YEAR), 
               calendrier.get(Calendar.MONTH),
               calendrier.get(Calendar.DAY_OF_MONTH)).show();      
   }
   
   public void nouveau(View vue) {
      toutEffacer();
   }
   
   public void enregistrer(View vue) throws IOException {
      if (id==0) nouveauPersonnel();
      else modifierPersonne();
   }
   
   public void supprimer(View vue) throws IOException {
      HttpClient client = new DefaultHttpClient();
      HttpDelete requete = new HttpDelete("http://"+adresse+":8080/Personnel/rest/"+id);
      client.execute(requete);
      Toast.makeText(this, "Personnel "+id+" supprimé", Toast.LENGTH_SHORT).show();   
      finish();
   }

   private void nouveauPersonnel() throws IOException {
      HttpClient client = new DefaultHttpClient();
      HttpPost requete = new HttpPost("http://"+adresse+":8080/Personnel/rest/");  
      requete.addHeader("nom", nom.getText().toString());
      requete.addHeader("prenom", prenom.getText().toString());
      requete.addHeader("date", ""+calendrier.getTimeInMillis());
      requete.addHeader("telephone", telephone.getText().toString());
      client.execute(requete);      
      Toast.makeText(this, "Nouveau personnel enregistré", Toast.LENGTH_SHORT).show();    
      finish();
   }

   private void modifierPersonne() throws IOException {
      HttpClient client = new DefaultHttpClient();
      HttpPut requete = new HttpPut("http://"+adresse+":8080/Personnel/rest/enTête/"+id);
      requete.setHeader("date", ""+calendrier.getTimeInMillis());
      requete.setHeader("telephone", telephone.getText().toString());
      client.execute(requete);      
      Toast.makeText(this, "Personnel modifié", Toast.LENGTH_SHORT).show();         
   }
}