Programmation Android avec AndroidStudio


Cliquez ici pour retirer la feuille de style si vous souhaitez imprimer ce document (ou en cas de problème d'affichage).

Nous allons développer deux applications qui s'échangent des données par le bluetooth.

Le bluetooth est un standard de communication sans fils à faible consommation d'énergie disponnible sur la plupart des téléphones et device android. Nous n'allons pas étudier ici le fonctionnement détaillé des protocole bluetooth, mais simplement voir au travers d'une application simple comment utiliser les services de l'OS associés à cette technologie.

Prerequis

Socket

Un concept fondamental en réseau est le concept de Socket. Une socket désigne en fait une adresse (qui dépend du protocole utilisé) qui permet d'identifier une machine, plus un numéro de port, qui identifie au sein d'une machine une application.

En java "classique", il existe deux classes (et leurs sous classes) pour représenter une socket : Socket qui représente une socket coté client, et ServerSocket qui représente une socket coté serveur. La classe ServerSocket sert donc à instancier une Socket d'écoute, c'est à dire d'attente. On créé la ServerSocket, puis on invoque dessus la méthode accept() qui va provoquer l'attente d'une connexion. Si un client se connecte, la méthode est débloqué et elle renvoie une Socket, qu'on nomme socket de service, et qui va permettre la communication.

Coté client, c'est beaucoup plus simple : on créé directement l'objet Socket en renseignant l'adresse et le port de la machine que l'on cible puis on fait un connect() pour initialiser la connexion, et on peut commencer a recevoir et envoyer des données.

Dans les deux cas, pour communiquer, deux méthodes de Socket nous sont utiles : getInputStream() et getOutputStream(), qui renvoient respectivement un objet modélisant le flux d'entré (ce qui arrive comme donnés sur la machine) et le flux de sortie (ce qu'on veut envoyer). On peut alors utiliser la méthode read() sur l'inputStream et/ou write sur l'outputStream.

En pratique, on utilisera bien sur un sous type de Socket approprié au protocole que l'on veut utiliser et un sous type d'input/outputStram adapté au type de données que l'on souhaite envoyer/recevoir.

Exemple coté client :
public static void main(String[] argv) {
 
       Socket maSocket = ...; // on renseigne l'ip et le port du serveur
       try {
	  maSocket.connect();
       } catch (IOException connectException) {
           try {
               maSocket.close();
           } catch (IOException closeException) { }
           return;
       }
       InputStream is = maSocket.getInputStream();
       int data = ...
       try{
           data = is.read(); // lit un octet
       }catch(IOException e){
           ...
       }
       OutputStream os = maSocket.getOutputStream();
       data = ...
       try{
           os.write(data); // ecrit un octet
       }catch(IOException e){
           ...
       }
 
       try {
           maSocket.close();
       }catch (IOException closeException) { }
}
 
Et coté serveur c'est presque pareil :
public static void main(String[] argv) {
 
       ServerSocket ss = ...; // on renseigne le port d'écoute du programme
       Socket maSocket;
       try{
	  maSocket = ss.accept(); // le programme se met en attente d'une connexion
       }catch(IOException e){
          return;
       }
       InputStream is = maSocket.getInputStream();
       int data = ...
       try{
           data = is.read(); // lit un octet
       }catch(IOException e){
           ...
       }
       OutputStream os = maSocket.getOutputStream();
       data = ...
       try{
           os.write(data); // ecrit un octet
       }catch(IOException e){
           ...
       }
 
       try {
           maSocket.close();
       }catch (IOException closeException) { }
}
 

Thread

Bien sur, il n'est pas souhaitable que votre programme serveur ne puisse plus accepter d'autres clients pendant qu'il communique avec le premier. C'est pour cela qu'en général, une fois que qu'un premier client s'est connecté, le programme va crééer un nouveau thread pour traiter la communication et le thread principale par immédiatement refaire un appel à la méthode accept() pour attendre un deuxième client éventuel.

Un thread est un fils d'execution qui s'éxecute en parallèle (si il y a plus de processeurs que de threads) ou en concurrence (sinon) du thread principal du programme. Dans les deux cas, l'utilisateur à l'impression que les deux codes s'executent "en même temps".

Sous Android, une application n'a pas de thread dédié : vous avez un thread dit Thread UI qui est responsable d'executer tout le code en relation avec l'interface graphique. Il est manipulable (c'est à dire que vous pouvez lui faire executer du code) comme vous le savez à partir des méthodes "crochets" que vous pouvez redefinir (par exemple la méthode onCreate() de Activity).

La particularité de la plateforme android, c'est que si une activité monopolise trop longtemps le thread UI, l'OS peut décider de tuer l'application. Il faut donc a tout prix éviter d'écrire du code dont le traitement dure longtemps directement dans les méthodes "crochets" du thread UI.

Malheureusement pour nous, ici nous voulons créer une connexion bluetooth, puis l'utiliser pour s'échanger des données pendant un temps potentielement relativement long : il faudra donc crééer un nouveau Thread dédié à faire ce travail.

Voici comment créer un nouveau Thread en java, et donc également sous Android (méthode concise) :

Thread nouveauThread = new Thread(){
    @Override
    public void run(){
      // code éxecuté au sein du nouveau thread.
    }
};

Nous avons en fait ici créé une nouvelle classe anonyme qui hérite de la classe Thread et qui redéfinie sa méthode run(). Si vous souhaitez créer plusieurs threads qui éxécutent le même code, il sera préféreable de créé d'abord une classe qui implémente l'interface Runnable, comme dans l'exemple ci dessous :

- dans un fichier MyRunnable.java :

public class MyRunnable implements Runnable {
    @Override
    public void run(){
      // code executé dans un nouveau thread
    }
}

- dans le code qui doit démarrer un nouveau thread :

Thread nouveauThread = new Thread(new MyRunnable());

L'avantage de cette méthode est de découpler l'objet Runnable (qui représente le code), de l'objet Thread (qui représente le fils d'exécution chargé d'exécuter le code). De plus, cela permet d'ajouter dans MyRunnable des attributs et des constructeurs.

Une fois votre thread créé, il suffit de le démarrer avec la méthode start() : 

nouveauThread.start();

Le code de la méthode run() démarre dans un nouveau fils d'éxécution et le thread courrant continue à exécuter le code qui suit l'instruction start().

Vous pouvez aussi parfois trouver un mixte entre les deux, car il n'est plus trop d'usage de redéfinir directement la méthode run() de la classe Thread :

Thread nouveauThread = new Thread(new Runnable(){
    @Override
    public void run(){
      // code éxecuté au sein du nouveau thread.
    }
  }).start();

Il est possible également d'appeler une méthode cancel() quand on veut arreter un thread (méthode qui peut être surchargée, par exemple pour fermer correctement les Socket dans notre cas).

Pour en savoir plus sur le cycle de vie des thread sous android, je vous invite à lire la doc : https://developer.android.com/guide/components/processes-and-threads.html

 

Attention, il sera impossible de manipuler directement des objets de l'interface graphique depuis un autre thread que le thread UI. Il existe dans Android plusieurs mécanismes  permettant aux autres threads de demander au thread UI d'exécuter des traitements, le plus simple ici sera d'utiliser la méthode post() de la classe View, qui prend en parametre un Runnable qui permettra d'encapsuler le code que l'on veut faire exécuter par le thread UI.

Le permier exemple de la doc illustre cela :

public void onClick(View v){
    newThread(newRunnable(){
        public void run(){ // on créé un nouveau thread pour executer un code long
            // a potentially  time consuming task
            final Bitmap bitmap =
		    processBitMap("image.png"); // c'est long :)
            // ca y est, c'est fini, maintenant on veut mettre à jour l'IG, mais on ne peut pas le faire dans ce thread
            mImageView.post(newRunnable(){ // on envoie donc le code au Thread UI
                public void run(){
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}
La méthode onClick() est celle que l'on va redefinir sur une view, par exemple un Button. mImageView est une référence vers une view de l'activity qui représente une image. Il faut que cette référence soit connue par le code. Elle peut par exemple etre récupérer avec un R.findViewbyId(), ou simplement passée en paramètre au constructeur du runnable si on utilise une classe explicite...

Le bluetooth

Les services offert par Android nous permettent de récupérer facilement un objet de type Socket que nous pourrons connecter et sur lequel nous pourrons recevoir et/ou envoyer des données.

La classe BluetoothAdapter représente un objet permettant de manipuler le module bluetooth. Pour obtenir une instance de cet objet, on fait appel à la méthode statique BluetoothAdapter.getDefaultAdapter();

Si cette méthode renvoie null, ou si la méthode isEnabled() sur l'instance renvoie false, c'est que le bluetooth n'est pas activé sur l'appareil (regardez la doc https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html).

Il est possible alors de demander à l'utilisateur d'activer le bluetooth :

Intent enableBluetooth = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBluetooth, REQUEST_CODE_ENABLE_BLUETOOTH);

Et pour ajouter du code a executer lorsque l'activity du sytème revient, redéfinissez la méthode onActivityResult() de Activity.

Ici nous ne verrons pas comment appairer les appareils dans l'application. Nous laisserons les utilisateurs faire les appairages grace au paramètres du système et considérons que les appareils qui vont communiquer ont été appairé correctement.

 

 

Ce qu'il faut faire en séance  TP 

Vous allez réaliser deux applications (donc il faudra créér deux projets dans AndroidStudio). Ces deux applications vont fonctionner ensemble, chacune sur un terminal différent. La premiere, le client, va se connecter au serveur pour lui envoyer un message (bonjour dans un premier temps, puis vous pourrez ajouter un textbox à l'application pour que l'utilisateur saisisse ce qu'il veut envoyer). La seconde, le serveur, va attendre qu'un client se connecte et lui envoie des données qu'elle va afficheri (dans un terminal de debug dans un premier temps, puis dans un Toast ou un textbox ensuite).

Application Client (Producteur de données)

Cette application devra récupérer le BluetoothAdapter par défaut, et appeller dessus la méthode getBoundedDevices(). Le résultat de cette méthode devra etre présenté dans un combobox (https://fr.wikipedia.org/wiki/Bo%C3%AEte_combin%C3%A9e), qui sous android est représenté par la View Spinners (https://developer.android.com/guide/topics/ui/controls/spinner.html).

Une fois récupéré le bon BluetoothDevice, vous allez pouvoir créer et démarrer un nouveau Thread.

Voici un squelette de thread Client que vous pouvez adapter :

private class ConnectThread extends Thread {
   private final BluetoothSocket mmSocket;
   private final BluetoothDevice mmDevice;
 
   public ConnectThread(BluetoothDevice device) {
       BluetoothSocket tmp = null;
       mmDevice = device;
       try {
           tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
       } catch (IOException e) { }
       mmSocket = tmp;
   }
 
   public void run() {
       try {
           mmSocket.connect();
       } catch (IOException connectException) {
           try {
               mmSocket.close();
           } catch (IOException closeException) { }
           return;
       }
        manageConnectedSocket(mmSocket);
   }
 
   public void cancel() {
       try {
           mmSocket.close();
       } catch (IOException e) { }
   }
 
}

Le MY_UUID correspond a un UID unique qui dépend des applications. Ici nous n'utilisons pas de bluetooth sécurisé, nous nous contentons du mode SSP et l'UUID a utiliser est 00001101-0000-1000-8000-00805F9B34FB (regardez la doc pour voir comment creer une UUID a partir d'une chaine de caractères.

Il ne vous reste plus qu'a écrire une méthode manageConnectedSocket(), qui va appeller sur la socket la méthode getOutputSteam() pour récupérer un flux d'écriture.

Dans une boucle, on appelera la méthode write() sur ce flux pour écrire sur la socket toutes les secondes (utiliser Thread.sleep() pour attendre entre deux écritures).

Application Server (consommateur de données)

C'est quasiement la même chose, sauf que le server n'initie pas la communication, il doit d'abord se mettre en attente d'un client qui demande à démarrer une communication.

Pour cela on utilisera une classe intermediaire appellé ServerSocket. Voici un squelette de code à adapter :

private class AcceptThread extends Thread {
   private final BluetoothServerSocket mmServerSocket;
 
   public AcceptThread() {
       BluetoothServerSocket tmp = null;
       try {
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
       } catch (IOException e) { }
       mmServerSocket = tmp;
   }
 
   public void run() {
       BluetoothSocket socket = null;
       while (true) {
           try {
               socket = mmServerSocket.accept();
           } catch (IOException e) {
               break;
           }
 
           if (socket != null) {
               manageConnectedSocket(socket);
               mmServerSocket.close();
               break;
           }
       }
   }
 
   public void cancel() {
       try {
           mmServerSocket.close();
       } catch (IOException e) { }
   }
}

Comme vous le voyez, il faut d'abord faire un appel a accept sur la serverSocket, qui est un appel bloquant. Quand un client se connecte, la méthode renvoie une socket, qu'on appelle socket de service, qui va nous servir pour la connection.

Cette fois il faudra récupérer sur la socket un InputStream (grace a getInputStream()) et utiliser la méthode read() sur cet objet.

Dans un premier temps, afficher dans le terminal de debug ce que vous recevez (necessite que le terminal soit branché). Ensuite il faudra demander à l'UI d'afficher un Toast, ou de mettre a jour un TextBlock, grace à sa méthode post() (voir plus haut).

 

Vos deux programmes doivent pouvoir communiquer entre eux si executés sur deux terminaux différents !

Application pilotage du robot 

Vous avez tous les éléments nécessaires pour faire une petite application avec des boutons permettant d'envoyer des instructions au robot par le bluetooth !

Votre application devra comporter une seule activité présentant le spinner pour choisir le périphérique bluetooth et cinq bouttons.

Elle devra démarrer un Thread client qui envoit périodiquement un caractère stocké comme attribut de classe si ce caractère n'est pas 'p'.

Les bouttons permettent de changer cet attibut pour qu'il prenne une des valeurs 'u', 'd', 'l' ou 'r' (pour up, down, left et right).

Vous pouvez également rajouter un boutton pour réinitialiser la connexion (tue le thread, et le recréé et le redémarre)

Lorsque vous avez un prototype fonctionnel, vous pouvez si vous le souhaitez améliorer l'application pour par exemple proposer d'autres activités pour controler le Robot avec des gestes ou tout ce que vous voulez. 

 

À rendre

- un petit rapport (2 pages) sur le TP 2 dans lequel vous expliquez la notion de Thread sous android (https://developer.android.com/guide/components/processes-and-threads.html), les différences avec une application Java normale.

- une courte vidéo (3 minutes max) montrant votre réalisation finale (l'application qui pilote le robot si tout va bien)

- les fichiers .java correspondants à votre (vos?) activités et si ils existent les autres classes (Runnable, Thread...) que vous auriez écrites. Si vous avez developpé un "serveur" pour débugger votre application (une autre application communiquant avec elle a la place du robot, en gros), vous pouvez également donner les .java corresponfant.

- un rapport de projet expliquant le fonctionnement de l'application et son interaction avec le robot. Un bon rapport doit permettre à quelqu'un possedant les connaissances de base de reproduire votre application, sans pour autant lister le code source.

Vous pouvez inclure le rapport du TP2 comme annexe du rapport de projet.

 

Vous mettez tout ca dans une archive propre nommée NOM1_NOM2_EIG2022_2019 et vous la partagez avec Damien MASSON et Christophe DELABIE sur le drive de l'école.