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.
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.
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.
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.
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.
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.
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.).
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
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 :
// 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);
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);
Android possède quelques fournisseurs de contenu disponibles nativement permettant d'exposer certaines informations contenues dans les bases de données du système.
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 :
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 + ; 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); } }
<?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 :
<?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>
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.
// 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);
// 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);
// 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);
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 :
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.
Ce chapitre n'est pas erminé et sera traité dans un avenir proche.
.
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.
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> :
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.
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).
// 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.
stopService(new Intent(this, monService.getClass()));
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.
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 :
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é.
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.
<?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>
<?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>
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, , Toast.LENGTH_LONG).show(); } @Override public void onDestroy() { super.onDestroy(); Toast.makeText(this, , 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.
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( +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)); } }
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())); } }
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 :
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.
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 :
A l'aide du Location Manager vous pouvez :
Lorsqu'il s'agit de déterminer la position courante de l'appareil, plusieurs problèmes peuvent être rencontrés :
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 :
// 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.
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.
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é :
Le critère de coût n'est jamais modifié. Si aucun fournisseur n'est trouvé, la valeur null est renvoyée.
.
<?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>
<?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>
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( +position.getLatitude()); longitude.setText( +position.getLongitude()); } else latitude.setText( ); } }
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.
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( +position.getLatitude()); longitude.setText( +position.getLongitude()); } else latitude.setText( ); } }
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);
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é. } }
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.
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( +position.getLatitude()); longitude.setText( +position.getLongitude()); } else latitude.setText( ); Toast.makeText(Geolocalisation.this, , 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é.
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( +position.getLatitude()); longitude.setText( +position.getLongitude()); } else latitude.setText( ); Toast.makeText(Geolocalisation.this, , 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) { } } }
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);
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().
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 = ; 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( +position.getLatitude()); longitude.setText( +position.getLongitude()); } else latitude.setText( ); } 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 ? : ; Toast.makeText(Geolocalisation.this, alerte, Toast.LENGTH_LONG).show(); } } }
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 :
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.
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.
<?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>
<?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>
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( +lat); longitude.setText( +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( ); Toast.makeText(Geolocalisation.this, , 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) { } } }
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.
<?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>
<?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>
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() + ; 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( +position.getLatitude()); longitude.setText( +position.getLongitude()); } } catch (IOException ex) { Toast.makeText(this, , Toast.LENGTH_LONG).show(); } } }
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 :
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.
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 :
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.
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 :
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
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 :
<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.
<uses-permission android:name="android.permission.INTERNET" />
<?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>
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; } }
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 :
<?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.
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);
<?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>
<?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>
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); } }
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.
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; } }
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.
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 :
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.
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);
Paint pinceau = new Paint(); pinceau.setARGB(255, 255, 255, 0);
canevas.drawText("C'est ici !", pointEcran.x, pointEcran.y, pinceau);
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 :
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.
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( , pointEcran.x+10, pointEcran.y-10, pinceau); } } @Override public boolean onTap(GeoPoint centre, MapView carte) { return false; } }
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); } }
L'API Android propose également un calque prédéfini spécifique qui peut répondre à deux problématiques usuelles :
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().
<?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>
<?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" />
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) { } } }
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 :
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.
<?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>
<?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>
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( ); 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, , Toast.LENGTH_LONG).show(); } } } }
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() + ; getIntent().putExtra( , adresse); setResult(RESULT_OK, getIntent()); finish(); } }
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.
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.
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.
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.
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.
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.
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);
<?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" />
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)); } }
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.
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.
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 :
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.
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. |
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 :
Cela ouvre quelques perspectives inhabituelles à vos applications. En monitorant l'orientation, la direction et le mouvement, vous pouvez :
Vous devrez toujours vérifier la disponibilité des capteurs requis et vous assurer que votre application se terminera proprement s'ils sont absents.
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.
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 :
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.
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.
<?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>
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( +evt.values[0]); longitudinal.setText( +evt.values[1]); vertical.setText( +evt.values[2]); } } public void onAccuracyChanged(Sensor capteur, int precision) { } }
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.
<?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>
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.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 = + 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.
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 :
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.
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.
<?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>
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( +evt.values[0]); tangage.setText( +evt.values[1]); roulis.setText( +evt.values[2]); } } public void onAccuracyChanged(Sensor capteur, int precision) { } }
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.
<?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>
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 = ; else if (x>11.25 && x<33.75) cap = ; else if (x>33.75 && x<56.25) cap = ; else if (x>56.25 && x<78.75) cap = ; else if (x>78.75 && x<101.25) cap = ; else if (x>101.25 && x<123.75) cap = ; else if (x>123.75 && x<146.25) cap = ; else if (x>146.25 && x<168.75) cap = ; else if (x>168.75 && x<191.25) cap = ; else if (x>191.25 && x<213.75) cap = ; else if (x>213.75 && x<236.25) cap = ; else if (x>236.25 && x<258.75) cap = ; else if (x>258.75 && x<281.25) cap = ; else if (x>281.25 && x<303.75) cap = ; else if (x>303.75 && x<326.25) cap = ; else if (x>326.25 && x<348.75) cap = ; azimut.setText(cap); } } public void onAccuracyChanged(Sensor capteur, int precision) { } }
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
<?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>
<?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>
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.
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( ); 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( ); 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 = { , , , , , , , }; 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(); } }
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.
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( ); 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( ); 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 = ; 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 = ; break; case 12 : cap = ; break; case 18 : cap = ; 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); } }
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.
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) { } }
J'aimerais, au travers de cet exemple, puisque je ne l'ai pas encore fait, vous parler des tracés personnalisés.
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 :
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.
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 :
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.
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.
<?xml version="1.0" encoding="utf-8"?> <color xmlns:android="http://schemas.android.com/apk/res/android" android:color="#FFCC00" />
<?xml version="1.0" encoding="UTF-8"?> <resources> <color name="texte">#0000FF</color> </resources>
<?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>
/* 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; } }
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.
Je reprend l'exemple précédent, mais cette fois-ci, ce sont les TextView qui profitent du fond créer dans le drawable.
<?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>
<?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>
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.
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 :
<?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>
<?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>
<?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>
<?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>
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.
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 :
<?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" />
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.
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.
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.
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 :
<?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>
<?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" />
<?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>
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>
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.
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) :
<?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>
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(), ); 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( )) { Bitmap vignette = data.getParcelableExtra( ); image.setImageBitmap(vignette); } } else { File fichier = new File(Environment.getExternalStorageDirectory(), ); Bitmap photo = BitmapFactory.decodeFile(fichier.getPath()); image.setImageBitmap(photo); } } } }
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" />
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 :
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 :
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);
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 } });
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 :
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.
<?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°.
<?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>
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( , ex.getMessage()); } } } public void surfaceDestroyed(SurfaceHolder holder) { if (appareil!=null) { appareil.stopPreview(); appareil.release(); } } }
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.
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 :
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.
<?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>
<?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>
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( , 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( ); os.write(image); os.close(); } catch (IOException ex) { Log.e( , ); } appareil.startPreview(); } public void onShutter() { Log.d(getClass().getSimpleName(), ); } }
Vous avez ci-dessous une alternative qui permet d'archiver votre photo dans un emplacement spécifique au stockage de médias.
.
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( , 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, ); valeurs.put(Media.DESCRIPTION, ); 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( , ); } appareil.startPreview(); } public void onShutter() { Log.d(getClass().getSimpleName(), ); } }
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.
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.
<?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>
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( , 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( ); os.write(image); os.close(); ExifInterface exif = new ExifInterface( ); poids.setText( +exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)); largeur.setText( +exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)); } catch (IOException ex) { Log.e( , ); } appareil.startPreview(); } public void onShutter() { Log.d(getClass().getSimpleName(), ); } }
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 :
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.
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" />
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.
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).
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.
<?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>
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 = ; 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.
<?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>
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 = ; break; case BluetoothAdapter.STATE_ON : texte = adaptateurLocal.getName()+ +adaptateurLocal.getAddress(); break; case BluetoothAdapter.STATE_TURNING_OFF : texte = ; break; case BluetoothAdapter.STATE_OFF : texte = ; break; } Toast.makeText(Bluetooth.this, texte, Toast.LENGTH_LONG).show(); } } }
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.
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 :
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.
<?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>
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 = ; break; case BluetoothAdapter.SCAN_MODE_CONNECTABLE : texte = ; break; case BluetoothAdapter.SCAN_MODE_NONE : texte = ; break; } Toast.makeText(Bluetooth.this, texte, Toast.LENGTH_LONG).show(); } } }
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.
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.
<?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>
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, , Toast.LENGTH_LONG).show(); break; } } else if (depart.equals(action)) Toast.makeText(Bluetooth.this, , Toast.LENGTH_LONG).show(); else if (fin.equals(action)) Toast.makeText(Bluetooth.this, , 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, +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.
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(); } }
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 :
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.
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 :
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.
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.
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 :
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é.
BluetoothAdapter adaptateurLocal = BluetoothAdapter.getDefaultAdapter(); BluetoothDevice appareilDistant = adaptateurLocal.getRemoteDevice("01:23:77:35:2F:AA");
BluetoothAdapter adaptateurLocal = BluetoothAdapter.getDefaultAdapter(); Set<BluetoothDevice> appareils = adaptateurLocal.getBondedDevices(); if (appareils.contains(appareilDistant) > 0) { ... }
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 :
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).
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.
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é.
<?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>
<?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>
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( ); 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, +NXT.getAddress(), Toast.LENGTH_LONG).show(); beep(880, 1000); } catch (IOException ex) { Toast.makeText(Bluetooth.this, , 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, 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, , 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.
<?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>
<?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>
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( ); 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( ); 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, , 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, +NXT.getAddress(), Toast.LENGTH_LONG).show(); setTitle( +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, , 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, 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, , Toast.LENGTH_LONG).show(); } } }
<?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>
<?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" />
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 = ; 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( )) // 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(); } }
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.
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" />
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é.
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).
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();
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) ;
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 :
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" />
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);
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 :
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é.
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); }
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.
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" />
open time.nist.gov 13 ou open 192.43.244.18 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.
<?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>
<?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>
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( )) { Toast.makeText(DateReseau.this, , Toast.LENGTH_LONG).show(); heure.setText(donnees.getString( )); date.setText(donnees.getString( )); } else Toast.makeText(DateReseau.this, , 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( , 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( , true); donnees.putString( , reponse.next()+ ); donnees.putString( , 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( , false); Message message = client.obtainMessage(); message.setData(donnees); client.sendMessage(message); } enExecution = false; } } }
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.
.
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 :
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 :
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.
.
<?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.
.
<?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>
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( +identifiant.getText().toString()); HttpResponse reponse = client.execute(requete); if (reponse.getStatusLine().getStatusCode() == 200) { for (Header enTete : reponse.getAllHeaders()) { if (enTete.getName().equals( )) nom = enTete.getValue(); if (enTete.getName().equals( )) prenom = enTete.getValue(); if (enTete.getName().equals( )) telephone = enTete.getValue(); } Toast.makeText(this, nom+ +prenom+\n +telephone, Toast.LENGTH_LONG).show(); } else Toast.makeText(this, , 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 :
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( +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, , Toast.LENGTH_SHORT).show(); } }
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 :
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( +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( ); String prénom = json.getString( ); Date naissance = new Date(json.getLong( )); String téléphone = json.getString( ); 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, , 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é.
<?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>
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( ); 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( )+' '+json.getString( )+'\n'); } Toast.makeText(this, message.toString(), Toast.LENGTH_LONG).show(); } else Toast.makeText(this, , 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().
<?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>
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.getText().toString()); json.put( , prenom.getText().toString()); json.put( , Integer.parseInt(age.getText().toString())); Toast.makeText(this, json.toString(), Toast.LENGTH_LONG).show(); } }
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.
<?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>
<?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>
<?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>
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.getText().toString()); setResult(RESULT_OK, intent); finish(); } }
<?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>
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; } }
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( ); try { miseAJour(); } catch (Exception ex) { Toast.makeText(this, , 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( +adresse+ ); 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( )); personne.setNom(json.getString( )); personne.setPrenom(json.getString( )); personne.setNaissance(json.getLong( )); personne.setTelephone(json.getString( )); personnes.add(personne); } setListAdapter(new ArrayAdapter<Personne>(this, android.R.layout.simple_list_item_1, personnes)); } else Toast.makeText(this, , Toast.LENGTH_SHORT).show(); } public void edition(View vue) { Intent intention = new Intent(this, Personnel.class); intention.putExtra( , 0); intention.putExtra( , 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( , personne.getId()); intention.putExtra( , adresse); intention.putExtra( , personne.getNom()); intention.putExtra( , personne.getPrenom()); intention.putExtra( , personne.getNaissance()); intention.putExtra( , personne.getTelephone()); startActivity(intention); } }
<?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>
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( ); adresse = donnees.getString( ); if (id==0) toutEffacer(); else { nom.setEnabled(false); prenom.setEnabled(false); supprimer.setEnabled(true); nom.setText(donnees.getString( )); prenom.setText(donnees.getString( )); long date = donnees.getLong( ); calendrier.setTimeInMillis(date); naissance.setText(DateFormat.format( , date)); telephone.setText(donnees.getString( )); } } 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( , 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( +adresse+ +id); client.execute(requete); Toast.makeText(this, +id+ , Toast.LENGTH_SHORT).show(); finish(); } private void nouveauPersonnel() throws IOException { HttpClient client = new DefaultHttpClient(); HttpPost requete = new HttpPost( +adresse+ ); requete.addHeader( , nom.getText().toString()); requete.addHeader( , prenom.getText().toString()); requete.addHeader( , +calendrier.getTimeInMillis()); requete.addHeader( , telephone.getText().toString()); client.execute(requete); Toast.makeText(this, , Toast.LENGTH_SHORT).show(); finish(); } private void modifierPersonne() throws IOException { HttpClient client = new DefaultHttpClient(); HttpPut requete = new HttpPut( +adresse+ +id); requete.setHeader( , +calendrier.getTimeInMillis()); requete.setHeader( , telephone.getText().toString()); client.execute(requete); Toast.makeText(this, , Toast.LENGTH_SHORT).show(); } }