Interface graphique évènementielle, applications Android
par Albert Cohen

 Login :  Mot de passe :

Objectifs :

0. Installation de l'environnement de développement pour Android

Il est nécessaire de suivre au préalable la procédure installation de l'Android SDK sur votre ordinateur personnel.

1. Programmation graphique événementielle

Nous nous baserons sur un squelette d'application disponible ici. Téléchargez cette application et importez-la sous Eclipse : il suffit d'extraire les fichiers (pas directement dans le workspace d'eclipse), puis de créer un projet Android en sélectionnant "Create project from existing sources". Pour le moment, ce programme affiche seulement quelques éléments graphiques interactifs. Il est possible de cliquer sur le bouton et/ou de taper des caractères dans la zone de texte, mais rien ne se produit.

Squelette de l'application :

En examinant le projet sous Eclipse, on peut distinguer plusieurs répertoires. Les plus importants dans notre cas sont src et res. Le premier contient le code source Java du projet. C'est dans le fichier TD5.java (du package com.android.td5) que nous allons développer notre application.
Une application Android doit être dérivée de la classe Activity (avec le mot clé extends en Java). Pour le moment, seule la méthode onCreate() est surchargée : elle contient un appel à son ancêtre super.onCreate(...) pour initialiser l'activité, et un appel à la méthode setContentView() pour dessiner les éléments graphiques de la couche main.
Ces éléments graphiques sont définis dans le fichier main.xml situé dans le répertoire res/layout. Ce fichier représente tous les éléments graphiques de la fenêtre. Il peut être édité en mode texte (onglet inférieur main.xml) au format XML, ou alors graphiquement (onglet inférieur Layout) sous Eclipse (ajout à la souris de nouveaux éléments graphiques).

Vous pouvez lire cette section pour plus d'informations sur la gestion des éléments graphiques.

Découverte de le l'interface graphique :

Le but de ce TD est de compter les nombres premiers inférieurs à une certaine valeur en utilisant le crible d'Ératosthène avec une interface graphique sous Android. L'implémentation de l'algorithme sera faite dans l'exercice 2, nous allons dans un premier temps compléter l'interface graphique.
Il est donc nécessaire de pouvoir rentrer un nombre entier positif (via la zone de texte numberEditText) et de lancer le calcul en cliquant sur le bouton Start (ButtonStart).

Travail à faire :
  1. Examiner entièrement la structure du projet: les sources Java, le fichier contenant l'interface graphique main.xml, et le fichier de chaînes de caractères strings.xml.
    Lire et comprendre le code Java fourni en intéraction avec les enseignants.

  2. Modifier l'application de telle sorte qu'un message "Bonjour" s'affiche dans le TextView resultTextView lors d'un clic sur le bouton Start.

    1. S'inspirer, dans la documentation, de la section "Class Overview" de la classe Button, sachant qu'un objet d'identifiant "@+id/XXX" dans main.xml est appelé R.id.XXX en Java, pour capturer l'appui sur le bouton, puis afficher le message.

  3. Modifier cet affichage afin d'imprimer la valeur rentrée dans la zone de texte numberEditText. On pourra utiliser la méthode getText() de la classe EditText pour obtenir un objet Editable, que l'on pourra ensuite convertir en chaîne avec la méthode toString().

    Par la suite, on cherchera également à convertir la chaîne de caractères en entier. Pour cela, il existe la fonction Integer.parseInt() que l'on peut utiliser de la façon suivante :

    try {
      int a = Integer.parseInt(String s) ;
      // ...
    } catch(NumberFormatException e) {
      // Afficher un message d'erreur dans ce cas
    }
    
    Mais pour gagner du temps, une méthode parseNumber() est fournie dans Sieve.java qui fourni la solution clef en main.

Pour en savoir plus :

Les mécanismes mis en jeu dans l'exercice 1 sont illustrés dans la correction de l'exercice 2 ci-dessous.

2. Crible d'Ératosthène (version séquentielle)

Le crible d'Ératosthène permet de trouver les nombres premiers entre 1 et un entier number. Le site Wikipedia contient une animation sur le déroulement de ce crible : version française et version anglaise.

L'algorithme prend en entrée cet entier number et construit un tableau de booléens, que nous appelons isPrime, dont les cases sont toutes initialisées à true. Le principe est d'éliminer tous les multiples de chaque nombre premier jusqu'à ce que tous les nombres aient été parcourus.
Les éléments dont la valeur reste à true à la fin de l'algorithme sont les nombres premiers. La sortie est ainsi le nombre d'entiers premiers contenus entre 1 et number.

Algorithme :

Nous présentons ici une version très simple du crible sans chercher à optimiser sa complexité algorithmique :

Entrées :
	number   intervalle d'entiers à tester
Sortie :
	primes	 nombre d'entiers premiers entre 1 et number
Algorithme :
	Créer un tableau isPrime de booléen contenant number+1 cases
	Initialiser ce tableau à true
	Initialiser primes à 0
	Pour i allant de 2 à number
		Si isPrime[i] est vrai, Alors
			Incrémenter primes
			Pour j allant de 2 jusqu'à number/i, Faire
				Marquer isPrime[j*i] à faux
                        FinPour
                FinSi
        FinPour

Travail à faire :

  1. Implémenter le crible dans l'application afin de calculer le nombre d'entiers premiers en fonction du nombre entré dans la zone de texte numberEditText. Ce calcul devra se lancer lorsque l'utilisateur appuie sur le bouton Start.
    Les variables number, isPrime et primes doivent rester des champs privés de la classe SieveModel.

  2. Tester cette implémentation avec plusieurs valeurs.
    Que remarquez-vous lorsque number est grand (par exemple, number = 10000) ?
    L'application répond-elle lorsque le calcul est en cours ?

Merci de déposer une archive .zip de votre projet, plutôt que les fichiers séparément.

Télécharger la Correction de l'exercice 2.

3. Concurrence avec le thread en charge de l'interface graphique

Afin de pallier le problème de réactivité identifié lors de l'exercice précédent, nous allons devoir rendre le calcul concurrent avec l'interface graphique.
Ainsi, lorsque l'utilisateur demande un calcul (appui sur le bouton Start, nous allons transférer le calcul à un thread de service, rendant ainsi la main à l'interface graphique.

Le squelette de code suivant permet de créer et de lancer un thread t lors de l'appel à la méthode événementielle onClick():

private Thread t = new Thread () {
	public void run() {
		// ...
		// Travail à faire par le thread
		// ...
	}
} ;

public void onClick(View v) {
	t.start() ;
}

Travail à faire :

  1. Ajouter un champ privé final de type Thread nommé t. Sa méthode run() devra contenir le calcul du crible (mais pas l'affichage du résultat, nous nous occuperons de l'affichage plus tard).
    La méthode onClick() doit donc lancer ce thread au lieu de faire le calcul (méthode start() de la classe Thread).

  2. Tester le code précédent avec plusieurs valeurs de number. L'application plante alors parfois. Utiliser le débugger afin de déterminer l'erreur (le debugger d'Eclipse donne le nom de l'exception levée lors de l'arrêt du programme).

    Le debugger se lance grâce à l'option Debug as... située dans le menu contextuel du projet. L'application prend un peu plus de temps à se lancer car elle doit être controlée par le debugger depuis la machine hôte. Le programme se comporte comme avant, vous pouvez tester plusieurs valeurs de number. Lorsque l'application se termine anormalement, Eclipse change de perspective et permet de regarder la pile d'appels (ensemble des méthodes appelées lors de l'arrêt du programme) de tous les threads présents. Généralement, Eclipse affiche en premier le thread qui a causé la terminaison du programme, avec le nom de l'exception levée.

    Vous pouvez également utiliser la sortie standard ou la sortie d'erreur de votre application, avec les méthodes System.out.println() et System.err.println() classiques. Dans ce cas il faut visualiser la fenêtre LogCat en mode debugguer sous Eclipse, et parcourir les logs (faire défiler l'ascenseur jusqu'en bas pour les logs récents).

  3. On propose de recycler le thread créé afin d'éviter le problème. Au démarrage de l'activité (méthode onCreate()), le thread devra être créé. Tant que l'utilisateur ne demande pas de calcul, ce thread devra alors attendre. On va lui attacher pour cela une boucle de traitement de file de message, configurée pour bloquer le thread en l'absence de message. On utilisera la classe Looper. Inspirez vous de l'exemple et de la documentation fournies pour que le thread de service attende les requêtes de calcul.
    Modifiez ensuite la méthode de calcul pour qu'elle s'interrompe (assez rapidement, pas forcément instantanément) lorsque que l'utilisateur clique sur Start, afin de laisser la place au calcul suivant.

Merci de déposer une archive .zip de votre projet, plutôt que les fichiers séparément.

Les mécanismes mis en jeu dans l'exercice 3 sont illustrés avec la correction de l'exercice 4 ci-dessous.

4. Mise à jour des éléments graphiques

Maintenant que notre calcul s'effectue dans un thread à part, l'application répond lorsque le crible est en cours. En revanche, nous avons retiré l'affichage de la solution dans le label.

Travail à faire :

  1. Ajouter l'affichage du résultat dans le thread de calcul, une fois le crible terminé.
    Tester l'application et utiliser une nouvelle fois le debugger pour connaître l'erreur. Expliquer la levée d'exception.

  2. La mise à jour des élements graphiques doit être faite par le thread principal. Il est alors nécessaire d'envoyer un message comportant une méthode de mise-à-jour à la fin du calcul au lieu de changer le texte du label directement dans le thread de calcul.
    Ajouter un nouvel objet comportant une méthode run() mettant à jour le nombre d'entier premiers (nouvel objet implémentant l'interface Runnable). À la fin du crible, le thread de calcul devra alors envoyer un message (ce nouvel objet) au thread principal pour mettre à jour l'affichage graphique. Le mécanisme s'appuie sur le même principe que le Looper vu précédemment. Le thread principal de l'interface graphique possède en effet sa propre boucle d'attente pour des messages issus aussi bien des composants graphiques que de l'application elle même. L'affichage sera donc réalisé via un appel à la méthode post() d'une instance de la classe Handler, et l'objet Handler doit être préalablement instancié dans le thread principal.

Merci de déposer une archive .zip de votre projet, plutôt que les fichiers séparément.

Télécharger la Correction des exercices 2 à 4.

5. Crible d'Ératosthène en version parallèle

Notre application permet d'exécuter le crible en utilisant plusieurs threads. Mais le calcul en lui-même est toujours séquentiel (les threads servent à l'interface graphique). Nous allons à présent paralléliser notre calcul.

Dans ce but, il est nécessaire d'extraire le plus de calculs indépendants de notre application. Les marquages de multiples d'un nombre premier donné semblent être de bons candidats pour l'extraction de threads de calcul indépendant. Malheureusement ces séquence de marquage ne sont pas réellement indépendantes : le mêmes entier peut être marqué plusieurs fois à faux dans le tableau isPrime en tant que multiple de plusieurs nombre premiers distincts. Une parallélisation directe induira donc des courses critiques. On remarquera en revanche que ces courses sont "bénignes" : la même valeur (faux) sera écrite par tous les threads concurrents accédant à un même élément du tableau isPrime. On s'en accomodera donc, car le modèle mémoire Java tolère ce type de course critiques sur des types de base de taille inférieure à 32 bits (atomicité des écritures).

Travail à faire :

  1. En dehors de la course critique bénigne, il existe une autre dépendance qui limite les possibilités de marquage en parallèle de plusieurs séries de multiples. Quelle est elle ? Est il correct de l'ignorer ? Discuter.
  2. Une possibilité serait de créer un thread à chaque nombre premier découvert : le thread de calcul principal parcourt le tableau et, à chaque fois que la valeur est true, il lance un nouveau thread qui va marquer les multiples de ce nombre. C'est bien entendu irréaliste pour toute valeur "intéressante" de number.

    Proposer et implémenter une autre solution, faisant appel à un "pool" de threads fixé (inférieur ou égal à 4, pour un téléphone ou une tablette typique). Ces threads seront en concurrence pour chercher la "graine" suivante dont les multiples sont marqués en parallèle (par exemple, avec une section critique autour de la recherche du prochain entier non marqué). Vous êtes encouragés à utiliser à nouveau le mécanisme de boîte aux lettres avec un Looper dans chacun de ces threads. Afin d'optimiser les performances, vous ferez en sorte de minimiser le temps passé en section critique.

Merci de déposer une archive .zip de votre projet, plutôt que les fichiers séparément.

6. Animation du crible d'Ératosthène

En parallèle avec le calcul, on souhaite afficher un tableau composé de rectangles colorés indiquant la position des nombres premiers identifiés ou en cours d'identification. Le menu permet de contrôler le délai entre chaque affichage d'un nouvel entier (sous la forme d'un rectangle) parcouru par un thread donné. Si le délai est non nul, on utilisera Thread.sleep() sur ce délai en millisecondes entre chaque marquage.

Travail à faire :

Analyzer et compléter le template SieveSurfaceView pour réaliser cette animation, en vous aidant de la documentation de SurfaceView. En particulier, on remarquera que la classe SurfaceView offre un moyen de déroger à la règle de l'accès exlusif à l'interface graphique dans le thread principal. Il est possible de transférer l'accès exclusif à un autre thread (ici, celui qui raffraichit le dessin) via le mécanisme ci-dessous.

    // Access and get exclusive access to the rectangular Canvas
    Canvas c = sieveSurfaceHolder.lockCanvas();
    
    // draw on the canvas
    
    // Release the rectangular Canvas
    sieveSurfaceHolder.unlockCanvasAndPost(c);
  

En option, vous pourrez également étudier le fonctionnement du RadioGroup de l'interface graphique et permettre à l'utilisateur de varier le nombre de threads entre 1 et 4 d'un calcul à l'autre.

Merci de déposer une archive .zip de votre projet, plutôt que les fichiers séparément.

Télécharger la correction.

7. Extension client-serveur de l'application

On souhaite enfin combiner l'application avec un mode serveur, capable de recevoir des requêtes de criblage jusqu'à un nombre donné, à travers la connexion d'un client. On utilisera des sockets (INET, TCP) pour cela, et on reposera sur la classe Net.java qui fournit les éléments pour des entrées/sorties simples et pour l'établissement de connexions TCP entre un client et un serveur.

Travail à faire :

Analyzer la classe Net, puis ajouter à l'interface graphique des composants permettant de saisir l'addresse IP sur laquelle s'exécute un serveur de calcul du crible d'Ératosthène, et le port sur lequel ce serveur attend des connexions (par exemple, 7777). Réaliser en Java (pas nécessairement sous Android) un serveur attendant les connexions entrantes sur un ServerSocket et répondant avec le calcul du crible. On n'échangera que la borne supérieure du crible et le nombre de nombres premiers correspondant, sans chercher à communiquer le tableau du crible ou l'image résultante.

En option, vous pourrez faire en sorte que le serveur s'exécute également dans l'application Android, laquelle disposera donc d'un mode client et d'un mode serveur, avec un selecteur approprié dans l'interface graphique. Une application en mode client pourra se connecter sur une autre application en mode serveur, cette dernière réalisant également l'animation du calcul.