Shlublu.org

Aller au contenu | Aller au menu | Aller à la recherche

mardi 7 août 2012

Niveau à Bulle Caméra en nouvelle version v1.7, Net Monitor Evaluation retiré du marché

Niveau à Bulle Caméra v1.7 a été publiée hier. Il est maintenant possible de configurer l'application et de conserver ces réglages. Entre autres choses, l'utilisateur peut maintenant choisir de désactiver la mise en veille automatique lorsque l'outil est en cours d'utilisation.

Plus de détails sur cette page.

Je travaille maintenant à la réalisation d'une fonction permettant à l'application de sonner lorsque l'angle désiré est atteint.


Net Monitor Evaluationl, la version limitée dans le temps de Net Monitor, a été retirée du marché, car avoir trois version d'un même produit était quelque peu illisible. Dorénavant, les seules versions disponibles sont Net Monitor, application payante et sans publicité, et Net Monitor Free, même application, mais gratuite et porteuse d'une publicité..

Plus de détails à propos de Net Monitor.

Camera Spirit Level v1.7 released yesterday, Net Monitor Trial discontinued

Camera Spirit Level 1.7 was released yesterday. It is now possible to configure it a little and to save these settings. Among other things, users can now choose to use a wakelock to prevent the auto-power-off from occuring while using the tool.

More details here.

I am now working on a function that will make the level to ring once reached the desired angle.


Net Monitor Trial, the time limited version of Net Monitor, has been discontinued as three versions of the same product was a bit unreadable. So now, we just have Net Monitor, paid app with no ads, and Net Monitor Free, same app but free and with an ad.

More details about Net Monitor.

mercredi 1 août 2012

Nouvelle version de Niveau à Bulle Camera: v1.6

Une nouvelle version de Niveau à Bulle Caméra est parue hier. Consultez cette page pour plus de détails.

L'application embarque dorénavant une publicité, c'est pourquoi elle demande l'accès au réseau. J'ai tâché de la rendre discrète, mais n'hésitez pas à revenir vers moi si vous la trouvez trop encombrante.

A bientôt !

New version released yesterday: Camera Spirit Level v1.6

A new version of Camera Spirit Level has been released yesterday. Check out this page for details.

The application now includes an ad, and this is why the application requires a network permission. I have tried to make it discreet, but let me know should it be too cumbersome.

Talk to you soon!

lundi 30 juillet 2012

Parution ce jour de Compas de Relèvement v3.0

Une nouvelle version de Compas de Relèvement a été publiée aujourd'hui. Consultez cette page pour en connaitre les détails!

Sighting Compass v3.0 has been released today

A new version of Sighting Compass has been released today. Check out this page for details!

dimanche 24 juillet 2011

Chiffrer et déchiffrer des contenus de tous types

Ce billet est issu d'une question posée sur Stack Overflow et relative à la sécurisation de contenus sur téléphones Android.

On a parfois besoin de chiffrer des contenus afin qu'ils ne puissent être copiés, ou simplement visualisés en dehors de l'application à laquelle ils appartiennent. Ces contenus peuvent être créés par l'application elle-même (contenus confidentiels définis par l'utilisateur et protégés par mot de passe, par exemple) et stockés localement, être téléchargés depuis une source externe (fichiers de niveau pour un jeu, questions/réponses pour un quizz, ou encore cartes pour un logiciel de navigation), ou encore être produit par l'application et stockés à distance, sur un serveur, pour être ensuite accédés par téléchargement. Et ces contenus peuvent être de tous types.

Quel algorithme ?

Ce qui est proposé ici, c'est d'utiliser l'algorithme AES. Il s'agit d'un algorithme de chiffrement symétrique, ce qui signifie que la clef utilisée pour chiffrer est la même que celle utilisée pour déchiffrer.

Il existe d'autres techniques de chiffrement. Sans rentrer plus avant dans les considérations liées à la sécurité informatique - un billet n'y suffirait pas, et devrait de toutes façons être écrit par un spécialiste en la matière, ce que je ne suis pas - le choix d'AES, ici, est un choix de compromis:

  • il est simple à mettre en place
  • il couvre l'essentiel des besoins en garantissant un résistance suffisante à une tentative de déchiffrage par force brute. Par résistance suffisante, il faut entendre qu'il est au moins aussi difficile de déchiffrer ce contenu sans en connaître la clé que d'y accéder par d'autres moyens: vol de cette clé, accès à la source avant chiffrement, accès par utilisation de l'application et recopie du contenu, etc - ces moyens dépendent du contexte. Inutile de surprotéger un contenu stocké sur un téléphone mobile ou une tablette si l'on est pas certain que toute la chaîne par lequel ce contenu passe n'offre pas une protection au moins égale.
  • il est rapide. C'est important car l'utilisateur n'accepterait pas, par exemple, d'attendre plusieurs secondes qu'une image enregistrée sur son mobile soit déchiffrée avant qu'elle ne s'affiche.

Quel format de stockage ?

Le chiffrement d'un contenu par l'algorithme AES produit un code binaire, et ce même si le contenu avant chiffrement est en mode texte.

Si les contenus à chiffrer sont générés par l'application et stockés localement, il n'y a pas de question à se poser : ils peuvent être stockés en mode binaire sans que cela ne pose le moindre problème.

Si par contre ces contenus doivent transiter par le réseau (stockage sur un serveur distant, ou téléchargement), on peut souhaiter ou se voir imposer, selon que l'on maîtrise ou non la partie serveur destinée à gérer ces contenus, que cet échange s'effectue en mode texte par HTTP plutôt qu'en mode binaire par FTP ou encore par l'emploi d'une connection directe par socket. Deux raisons à cela : HTTP est très simple à mettre en place côté client, Android offrant tous les outils nécessaires pour ce faire, mais aussi côté serveur, car il est assez simple de construire un service Java ou PHP destiné à gérer l'échange et le stockage de ces contenus sous la forme désirée (système de fichiers, base de données, etc). Nous ne nous intéresserons pas plus avant à la partie transfert dans ce billet[1] mais nous intéresserons aux conversions entre mode texte et mode binaire des contenus chiffrés.

Comment passe-t-on de binaire à texte ?

Pour effectuer ces conversions, base64 est tout indiqué. Cet algorithme permet de convertir n'importe quel flux binaire[2] en un code texte. Ce code résultant est approximativement 30% plus volumineux que sa donnée source, et cette conversion demande un peu de temps machine, mais le base64 a le mérite d'être très simple à mettre en place et d'être un standard.

Android intègre base64 depuis la version 8 de son API et nous verrons dans un prochain billet comment encoder et décoder du base64 en restant compatible API 3.

Côté implémentation, ça donne quoi?

Ça donne ce qui suit:

  • On construit une classe chargée d'effectuer le travail de base, à savoir le chiffrement et le déchiffrement:
public class AESEncrypter {
        // Notre algorithme de chiffrement est AES, et notre 
        // générateur de nombre aléatoire utilisera l'algorithme
        // SHA1PRNG, seul supporté par Android à ce jour.
        // (De quoi s'agit-il ?? On y vient plus bas):
        private static final String CIPHER_ALGO = "AES"; 
        private static final String RANDOM_ALGO = "SHA1PRNG";
 
        // On utilise une clef de 128 bits. AES admet des clefs plus longues
        // mais pas forcément disponibles sur tous systèmes. 
        // 128 est toujours disponible et suffit pour la majorité des usages,
        // eu égard à ce que l'on a vu au début de ce billet:
        private static final int KEY_SIZE = 128; 
 
        // Cette méthode à pour objet de chiffrer les contenus.
        // Elle reçoit le mot de passe désiré et le contenu 
        // à chiffrer (clear), sous forme de tableaux d'octets.
       private static byte[] encrypt(byte[] password, byte[] clear) 
                   throws AESEncrypterException {
            try {
            	// On crée une spécification de clef à partir de
                // notre mot de passe. (On y revient plus bas).
                final SecretKeySpec skeySpec = getKeySpec(password);
 
                // On crée également un outil de chiffrement AES, et
                // on l'initialise avec le mode souhaité (ici ENCRYPT)
                // et avec notre spécification de clef.
                final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
                cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
 
                // Et on retourne notre contenu chiffré:
                return cipher.doFinal(clear);
 
            } catch (Exception e) {
                // On ne devrait pas non plus arriver
                // ici, sauf à passer une clef raw invalide
                throw new AESEncrypterException(e);
            }
        }
 
        // Cette méthode a pour objet de déchiffrer un
        // contenu.
        // Elle reçoit le mot de passe et le contenu chiffré en AES
        private static byte[] decrypt(byte[] password, byte[] encrypted) 
                   throws AESEncrypterException {
            try {
                // L'opération est la même que dans la méthode encrypt()...
            	final SecretKeySpec skeySpec = getKeySpec(password);
                final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
 
                // ... si ce n'est que l'on est en DECRYPT_MODE
                cipher.init(Cipher.DECRYPT_MODE, skeySpec);
 
                return cipher.doFinal(encrypted);
 
            } catch (Exception e) {
                // Si l'on se trompe de mot de passe, la
                // clef AES résultante est invalide. 
                // On arrive alors ici !
                throw new AESEncrypterException(e);
            }
        }
 
        // Cette méhtode génére une clef AES à partir du mot de passe  
        // désiré. 
        // AES nécessite en effet des clefs de taille bien précise et obéissant
        // à certaines spécifications.
        // Le paramètre passwordest notre mot de passe sous forme de 
        // tableau d'octets.
        private static SecretKeySpec getKeySpec(byte[] password) 
                    throws AESEncrypterException {
 
            try {
                // On crée le générateur de clefs.
                final KeyGenerator kgen = KeyGenerator.getInstance(CIPHER_ALGO);
 
                // On crée un générateur de nombres aléatoires
                final SecureRandom sr = 
                       SecureRandom.getInstance(RANDOM_ALGO);
 
                // On initialise ce dernier avec notre mot de passe, ce qui rend
                // la suite de nombres aléatoires tout à fait prédictible.
                sr.setSeed(password);
 
                // On initialise le générateur de clefs avec ce générateur
                // de nombres aléatoires
                kgen.init(KEY_SIZE, sr); 
 
                // Et on génère une clef. Comme notre mot de passe
                // est utilisé pour initialiser le générateur de nombres
                // aléatoires, lequel initialise le générateur de clef,
                // on a la garantie qu'un même mot de passe génère
                // toujours une même clef AES.
                final SecretKey skey = kgen.generateKey();
 
                // On retourne notre clef sous forme exploitable
                return new SecretKeySpec(skey.getEncoded(), CIPHER_ALGO);
            } catch (Exception e) {
                // On ne devrait jamais arriver ici sur Android : les paramètres
                // CIPHER_ALGO, RANDOM_ALGO et KEY_SIZE ont
                // été choisis car ils existent dans toutes les versions de cet OS
                throw new AESEncrypterException(e);
            }
        }
   }

Pour que cette classe compile, on aura besoin de lui adjoindre cette exception:

public class AESEncrypterException extends Exception {
        public AESEncrypterException (Throwable e) { super(e); }
    }

On a maintenant notre classe de base AESEncrypter, encore dépourvue de méthodes publiques. Il s'agit pour le moment de notre moteur de chiffrement/déchiffrement : tout ce qui suit ne sera que de la gestion de fichiers ou de base64.

On voit dans ce code que l'on utilise un générateur de nombres aléatoires de façon... à le rendre déterministe. C'est simplement un moyen technique d'associer, pour tout mot de passe donnée, une valeur pouvant être utilisée comme source valide par le générateur de clef.

On peut également noter que les mots de passe sont passés sous forme binaire à getKeySpec(). C'est qu'ils sont gérés ainsi nativement. Les mots de passe texte ne sont que des cas particuliers ayant la particularité d'être rédigés en Unicode.

  • Maintenant, ajoutons des méthodes publiques à notre AESEncrypter afin de pouvoir chiffrer et déchiffrer des contenus stockés localement sous forme de fichiers ou à distance sous forme de base64:
...
 
        // Cette méthode chiffre un contenu binaire et stocke le résultat
        // dans un fichier. Elle reçoit le mot de passe désiré, le contenu
        // sous forme binaire, et le fichier de destination.
        public static void encryptBytesToBinaryFile(String password, 
                  byte[] bytes, 
                  File file) 
                  throws AESEncrypterException {
 
            try {
                // On ouvre notre fichier de sortie en écriture
                final FileOutputStream ostream = new FileOutputStream(file, false);
 
                // Et on y écrit notre contenu après l'avoir chiffré
                ostream.write(encrypt(password.getBytes(), bytes));
 
                ostream.flush();
                ostream.close();
 
            } catch (IOException e) {
                throw new AESEncrypterException(e);
            }
        }
 
        // Cette méthode déchiffre un contenu binaire et retourne le résultat
        // binaire déchiffré.
        public static byte[] decryptBytesFromBinaryFile(String password, File file) 
                  throws AESEncrypterException {
 
            try {
                // On ouvre notre fichier d'entrée et on crée
                // un tableau d'octets de la taille de ce fichier (AES
                // préserve la longueur des contenus)
                final FileInputStream istream = new FileInputStream(file);
                final byte[] buffer = new byte[(int)file.length()];
 
                // On lit notre contenu chiffré
                istream.read(buffer);
 
                // Et on retourne son déchiffrement
                return decrypt(password.getBytes(), buffer);
 
            } catch (IOException e) {
                throw new AESEncrypterException(e);
            }
        }
 
        // Cette méthode chiffre un contenu binaire et retourne
        // le résultat chiffré sous forme de base64
        public static String encryptBytesToBase64(String password, byte[] bytes) 
                  throws AESEncrypterException {
 
            // Ici, au lieu de stocker le résultat chiffré dans un fichier
            // on le retourne après l'avoir convertir en texte base 64
            return Base64.encodeToString(encrypt(password.getBytes(), 
                                                 bytes), 
                                                 Base64.DEFAULT);
        }
 
       // Cette méthode déchiffre un contenu binaire préalablement 
       // chiffré et converti en base 64 
       public static byte[] decryptBytesFromBase64(String password, 
                 String encrypted) 
                 throws AESEncrypterException {
 
            try {
                final byte[] enc = Base64.decode(encrypted, Base64.DEFAULT);
 
                return decrypt(password.getBytes(), enc);
 
            } catch (IllegalArgumentException e) {
                throw new AESEncrypterException(e);
            }
        }

Ici, les mots de passe sont des chaînes de caractères. Mais vous pouvez tout à fait modifier ces méthodes de façon à utiliser des mots de passe binaires. Il suffit pour cela de modifier le type du paramètre password en byte et de le passer directement aux méthodes encrypt() et decrypt().

Ajoutons maintenant quelques méthodes de confort permettant de gérer nativement des contenus texte plutôt que binaires:

...
        // Cette méthode chiffre un contenu texte et stocke le résultat
        // dans un fichier. Elle reçoit le mot de passe désiré, le contenu
        // sous forme binaire, et le fichier de destination.
        public static void encryptTextToBinaryFile(String password, 
                  String cleartext, 
                  File file) 
                  throws AESEncrypterException {
 
            encryptBytesToBinaryFile(password, cleartext.getBytes(), file);
        }
 
        // Cette méthode déchiffre un contenu texte depuis un fichier chiffré
        public static String decryptTextFromBinaryFile(String password, File file) 
                  throws AESEncrypterException {
 
            return new String(decryptBytesFromBinaryFile(password, file));
        }
 
        // Cette méthode chiffre un contenu texte et retourne
        // le résultat chiffré sous forme de base64
        public static String encryptTextToBase64(String password, String cleartext) 
                  throws AESEncrypterException {
 
            return encryptBytesToBase64(password, cleartext.getBytes());
        }
 
        // Et cette méthode, enfin,  déchiffre un contenu texte 
        // préalablement chiffré et converti en base64
        public static String decryptTextFromBase64(String password, 
                  String encrypted) 
                  throws AESEncrypterException {
 
            return new String(decryptBytesFromBase64(password, encrypted));
        }
    }

Et voilà, notre classe de chiffrement / déchiffrement est complète[3]. Le code source correspondant est disponible en téléchargement à la fin de ce billet.

Nous pouvons l'utiliser comme suit:

  • Pour les contenus locaux (fichiers binaires)
try {
 	// Chiffrement / déchiffrement de contenu local (fichier binaire):
        final File file = getFileStreamPath(<nom_de_votre_fichier>);
 
        AESEncrypter.encryptTextToBinaryFile(<mot_de_passe>,<texte>, file); 
 
        ...
 
        final String text = AESEncrypter.decryptTextFromBinaryFile(<mot_de_passe>, file);
 
        ...
} catch (AESEncrypterException e) {
        // Attention ! Utiliser un mauvais mot de passe 
        // au déchiffrement vous emmène ici !
        ...
}
  • Pour les contenus base64 (chaînes base64)
try {
        // Chiffrement / déchiffrement de contenu distant (base64):
        final String aes = 
               AESEncrypter.encryptTextToBase64(<mot_de_passe>, <texte>);
 
        ...
 
        final String text = 
               AESEncrypter.decryptTextFromBase64(<mot_de_passe>, aes);
 
        ...        	
} catch (AESEncrypterException e) {
        // Attention ! Utiliser un mauvais mot de passe
        //  au déchiffrement vous emmène ici !
	...
}

Et quel mot de passe, alors ?

Si vos contenus sont générés par l'utilisateur et pour son seul usage, un mot de passe entré par ses soins convient parfaitement.

Si par contre il revient à l'application d'administrer le mot de passe, ou d'utiliser un mot de passe utilisé pour déchiffrer des contenus téléchargés sous forme chiffrée, il vaut mieux éviter un mot de passe simplement codé en dur (sous forme de private static final String, par exemple) car même après obfuscation par ProGuard ceux-ci sont assez simples à retrouver. Il vaut mieux utiliser le résultat d'une méthode ou d'un calcul déterministe, ou encore le nom d'une méthode ou d'une classe obtenu par introspection car ceci est plus difficile à retrouver en décompilant l'application.

"Plus difficile" ne signifie pas impossible, mais selon le même raisonnement que plus haut, on peut considérer ceci comme étant suffisant dans la majorité des cas. L'idée, ici, est de proposer une solution simple destinée à rendre les contenus suffisamment difficiles à déchiffrer pour que cela soit décourageant et n'en vaille pas la peine. Pas de les rendre inviolables.

Notes

[1] Message caché : nous en reparlerons.

[2] Et a fortiori texte, mais ce n'est pas l'objet ici : on s'intéresse au code AES binaire à échanger au travers du réseau.

[3] Navré pour la mise en forme de ce code... Si quelqu'un a des suggestions de plug-in de formattage de code sous DotClear...

Bug report: Sighting Compass under Samsung Galaxy S (Android 2.3)

This article concerns the trial version of Sighting Compass. The standard version is not concerned.

The following bug has been reported under Samsung Galaxy S:

At startup time, the application displays an error message saying the network is unavailable, while 3G and Wi-Fi are actually operational. The app stops a few seconds later.

This problem was not reported with other hardware than Galaxy S so far. We are investigating on it and will fix it as soon as we will be able to.

Without this to be clearly reported, we suspect that Net Monitor Trial is impacted as well by this bug.

EDIT, 2011/07/25:

The problem was identified and solved. It was actually impacting both applications.
New versions were delivered a few minutes ago.

EDIT, 2011/07/30:

Due to other issues reported with the Samung Galaxy S, we have decided to exclude this device from the list of compatible hardware. The applications Sighting Compass and Sighting Compass Trial are therefore no longer available for this device on Android Market.

We are nevertheless investigating on the reported issues. Once they will be fixed, Sighting Compass will be available again for Samsung Galaxy S.

EDIT, 2011/08/27:

Finally the bug was identified. Weare going to republish the application and reinclude the Galaxy S to the compatible hardware list withing the next few hours.

Rapport d'erreur: Compas de relèvement sous Samsung Galaxy S (Android 2.3)

Ce billet concerne la version évaluation de Compas de relèvement. La version standard n'est pas affectée.

L'erreur suivante à été reportée sous Samsung Galaxy S:

Au démarrage, l'application se plaint de ne pouvoir se connecter au réseau, alors que la connectivité est bien établie, en 3G comme en WiFi. L'application se ferme quelques secondes plus tard.

Ce problème n'a pas été rapporté sous d'autre matériel pour le moment. Nous tâchons de le diagnostiquer et le régler au plus vite.

Sans que cela n'ait été clairement établi, l'application Net Monitor Évaluation devrait également être touchée par ce problème.

EDIT, 25/07/2011:

Le problème a été identifié et résolu. Les deux applications étaient bien touchées.
Elles ont été relivrées il y a quelques instants.

EDIT, 30/07/2011:

D'autres dysfonctionnements relatifs aux appareils Samsung Galaxy S nous ont été remontés, si bien que nous avons décidé de les exclure de la liste des matériels compatibles avec les applications Compas de Relèvement et Compas de Relèvement Evaluation. Ces applications ne sont donc plus disponibles pour les appareils de cette gamme sur Android Market.

Nous continuons cependant nos recherches destinées à régler ces problèmes. Dès qu'elles auront abouti et que les problèmes rencontrés auront été corrigés, les applications Compas de Relèvement et Compas de Relèvement Evaluation seront de nouveau disponibles pour Samsung Galaxy S.

EDIT, 27/08/2011:

La bug a finalement été identifié et traité. Nous allons dans les prochaines heures publier une mise à jour de l'application et réintroduire le Galaxy S dans la liste des matériels compatibles.

mardi 12 juillet 2011

Conversion d'InputStream texte en String : refactoring

Il arrive assez fréquemment que l'on ait besoin de convertir en String le contenu d'un InputStream texte retourné par telle ou telle méthode. Il peut s'agir de contenus de fichiers textes, de pages lues sur internet, etc[1].

On se dit que c'est une boucle simple, que cela prend deux lignes... puis l'on s'aperçoit qu'il y a des Exceptions à intercepter, et l'on se surprend parfois même à utiliser le coûteux opérateur + de la classe String plutôt que d'utiliser un StringBuilder (ou un StringBuffer si l'on est en API 1.x).

Enfin, vu que l'on a plusieurs flux à convertir de cette façon et en des endroits très différents du code, on finit avec le même bloc de 8 à 10 lignes copié-collé un peu partout.

Et c'est mal.

Pour traiter ce genre de cas proprement, il est pratique d'avoir dans sa librairie personnelle une classe utilitaire de ce type :

import java.io.IOException;
import java.io.InputStream;
 
public class TextHelper {
	/**
	 * Convertit un InputStream en String.
	 * @param in InputStream, non nul, à lire
	 * @param bufSize Taille du buffer de lecture, en octets
	 * @return String Chaîne contenant l'intégralité de l'InputStream in
	 */
	public static String InputStreamToString (InputStream in, int bufSize) {
 
	    // En API 1.x, il faut utiliser un StringBuffer 
	    // à la place du StringBuilder ci-dessous :
	    final StringBuilder out = new StringBuilder(); 
 
	    // Buffer de lecture
	    final byte[] buffer = new byte[bufSize]; 
 
	    try {
	    	    // On ajoute le contenu du flux au StringBuilder
	    	    for (int ctr; (ctr = in.read(buffer)) != -1;) {
	    	    	    out.append(new String(buffer, 0, ctr));
	    	    }
	    } catch (IOException e) {
	    	    throw new RuntimeException("Cannot convert stream to string", e);
	    }
 
	    // On retourne la chaîne contenant les données de l'InputStream
	    return out.toString(); 
	}
 
	/**
	 * Convertit un InputStream en String.
	 * @param in InputStream, non nul, à lire
	 * @return String Chaîne contenant l'intégralité de l'InputStream in
	 */
	public static String InputStreamToString (InputStream in) {
 
	    // On appelle la méthode précedente avec une taille de buffer par défaut
	    return InputStreamToString(in, 1024);
	}
}

Et voilà.

Il suffit dorénavant d'appeler TextHelper.InputStreamToString(InputStream) pour obtenir une chaîne contenant l'intégralité des données du flux. Si l'on en connaît la taille approximative, ou si l'on souhaite optimiser l'usage fait de la mémoire, on préférera appeler appeler TextHelper.InputStreamToString(InputStream, int) afin de spécifier la taille du buffer à utiliser.

Si une exception se produit lors du traitement, trois écoles :

  • Soit on l'intègre à une RuntimeException car aucun traitement applicatif ne donnerait satisfaction: c'est le choix effectué ici.
  • Soit on l'intègre à son propre type d'exception que l'on déclare par un throws en lieu et place de la RuntimeException remontée dans cet exemple.
  • Soit enfin on la remonte directement, auquel cas on supprime le bloc try/catch pour ne garder que la boucle for ( ; ; ) de lecture du flux et l'on déclare un throws IOException.

La particularité de la RuntimeException est de ne pas nécessiter de déclaration throws spécifique, l'exception n'ayant pas vocation à être interceptée (plus de détails ici). Ce type d'exceptions, dont NullPointerException, ClassCastException ou encore IndexOutOfBoundException font partie, est idéal pour tous les cas d'erreurs ne pouvant être traités par l'application elle-même car relevant d'une erreur de programmation ou encore de matériel.

Les deux derniers choix sont quant à eux appropriés dans le cas de flux liés au réseau par exemple, afin de permettre la gestion d'une coupure réseau par l'application.

En fonction des cas d'utilisation, on pourra donc être amené à préférer l'une ou l'autre de ces options, ce qui peut conduire à l'implémentation de plusieurs méthodes.

Enfin, cette classe peut-être enrichie de toutes les méthodes de traitement de données texte dont on a fréquemment besoin.

Notes

[1] En faisant tout de même attention à la mémoire.

lundi 11 juillet 2011

Utiliser des layouts différents pour portrait et paysage

Nous avons vu dans le précédent billet comment éviter la réinitialisation de l'activité en cours lors du changement d'orientation du terminal.

Toujours sans que l'activité ne soit réinitialisée, il arrive que l'on souhaite utiliser des layouts différents en mode portrait et en mode paysage.

Pour ce faire, on peut procéder comme suit:

1) On commence par gérer l’évènement de changement d'orientation au niveau du fichier AndroidManifest.xml.

2) On définit ensuite deux layouts, l'un correspondant au mode portrait, l'autre au mode paysage.

On les nomme respectivement layoutHorizontal.xml et layoutVertical.xml, ce qui nous permettra de nous y référer dans le code en tant que R.layout.layoutHorizontal et R.layout.layoutVertical.

Dans cet exemple, ces layouts ne comportent qu'un seul champ de type TextView et nommé myTextView. On s'y réfère dans le code en tant que R.id.myTextView.

3) On définit dans notre activité une méthode permettant d'associer à l'activité le layout que l'on désire.

4) On y définit ensuite une méthode dédiée au remplissage de notre TextView.

5) Et enfin, on branche tout ceci. Sur la méthode onCreate() pour initialiser notre interface, et sur la méthode onConfigurationChanged() pour gérer les changements d'orientation du terminal lors de l'exécution de notre activité.

Voici comment cela prend forme :

1) AndroidManifest.xml : on intercepte le changement d'orientation grâce à android:configChanges :

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 
        [...]
 
        <activity android:name=".Test"
               android:label="@string/app_name"
               android:configChanges="orientation">
 
               <intent-filter>
                     <action android:name="android.intent.action.MAIN" />
                     <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter> 
        </activity>
 
        [...]
 
</manifest>

2) Les fichiers layout, nommés res/layout/layoutHorizontal.xml et res/layout/layoutVertical.xml: ici, la seule différence entre ces deux layouts est l'orientation du LinearLayout et le texte. Ceci permettra d'avoir un changement à observer lorsque l'on testera.

<!-- layoutHorizontal.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:orientation="horizontal"
  	android:layout_width="fill_parent"
   	android:layout_height="fill_parent"
>
 
   	<TextView  
	   	android:layout_width="fill_parent"
		android:layout_height="wrap_content" 
		android:text="Je suis horizontal !" 
	/>
 
   	<TextView  android:id="@+id/myTextView"
	   	android:layout_width="fill_parent"
		android:layout_height="wrap_content" 
		android:text="" 
	/>
 
</LinearLayout>
 
 
<!-- layoutVertical.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:orientation="vertical"
  	android:layout_width="fill_parent"
   	android:layout_height="fill_parent"
>
 
   	<TextView  
	   	android:layout_width="fill_parent"
		android:layout_height="wrap_content" 
		android:text="Je suis vertical !" 
	/>
 
   	<TextView  android:id="@+id/myTextView"
	   	android:layout_width="fill_parent"
		android:layout_height="wrap_content" 
		android:text="" 
	/>
 
</LinearLayout>

3) Dans notre activité (Test, dans cet exemple), on définit une méthode setLayout() permettant d'affecter le layout désiré à l'activité en fonction de l'orientation de du terminal :

public class Test extends Activity {
 
    [...]
 
    private void setLayout(int orientation) {
    	// Les layouts R.layout.layoutHorizontal et R.layout.layoutVertical ci-dessous 
    	// correspondent aux fichiers layoutHorizontal.xml et layoutVertical.xml 
    	// définis dans le répertoire res/layout de l'arborescence projet (cf étape 
    	// précédente).
 
    	final int res = (orientation == Configuration.ORIENTATION_LANDSCAPE ? 
    	    	    	     R.layout.layoutHorizontal : 
    	    	    	     R.layout.layoutVertical);
 
    	setContentView(res);
    }

4) on définit ensuite la méthode dédiée au remplissage de notre TextView :

private void populate() {
        // Attention ! 
        // Vous devez impérativement retrouver votre TextView par findViewById() 
        // sans le conserver dans une variable membre de votre activité : le fait
        // d'avoir appelé setContentView() lors de l'étape précédente fait
        // que le layout est rechargé, et le TextView recréé. 
        // La référence conservée ne serait donc plus valide.
 
        final TextView text = (TextView)findViewById(R.id.myTextView);
 
        text.setText("Je suis myTextView et je suis ici !");
    }

5) On initialise notre interface graphique lors du onCreate() :

@Override 
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        // Le code ci-dessous permet une initialisation correcte quelle
        // que soit l'orientation du terminal
 
        setLayout(getResources().getConfiguration().orientation);
        populate();
    }

6) Et enfin, on gère nos changements d'orientation lors du onConfigurationChanged :

@Override
    public void onConfigurationChanged(Configuration newConfig) {        
        super.onConfigurationChanged(newConfig);
 
        setLayout(newConfig.orientation);
        populate();
    }
 
    [...]
}

Et voilà ! Vous pouvez constater que le layout utilisé est layoutVertical.xml en mode portrait et layoutHorizontal.xml en mode paysage. Pour autant, l'activité Test n'est jamais réinitialisée.

Eviter la réinitialisation de l'activité lors du passage portrait / paysage

Par défaut, lorsque l'utilisateur fait passer son terminal du mode portrait au mode paysage (ou de paysage à portrait), l'activité en cours d'exécution redémarre : celle-ci est réinitialisée et la méthode onCreate() est de nouveau appelée.

Ce comportement est utile lorsque le changement d'orientation ne permet pas de conserver l'activité en l'état. Mais il arrive également qu'il soit perturbant et que l'on préfère simplement voir l'interface se redessiner dans la nouvelle orientation sans que l'état de l'activité ne se trouve modifié.

Pour ce faire, il suffit de modifier le fichier AndroidManifest.xml de façon à intercepter le changement d'orientation du terminal, et de surcharger la méthode onConfigurationChanged() de l'activité dont on souhaite conserver l'état :

  • AndroidManifest.xml: on définit l'attribut android:configChanges de l'activité :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 
        [...]
 
        <activity android:name=".Test"
               android:label="@string/app_name"
               android:configChanges="orientation">
 
               <intent-filter>
                     <action android:name="android.intent.action.MAIN" />
                     <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter> 
        </activity>
 
        [...]
 
</manifest>
  • Activité liée (.Test, dans cet exemple): on surcharge onConfigurationChanged():
public class Test extends Activity {
 
        [...]    
 
         @Override
        public void onConfigurationChanged(Configuration newConfig) {        
                super.onConfigurationChanged(newConfig);
                // Rien à faire
        }
 
        [...]
}

Ainsi, à chaque changement d'orientation, la méthode onConfigurationChanged() sera appelée et ne fera rien. L'interface sera quant à elle redessinée selon la nouvelle orientation suivant le jeu des fill_parent et wrap_content défini dans votre layout.

Si vous souhaitez effectuer une opération plus complexe, comme utiliser un layout différent suivant l'orientation, c'est également possible avec cette méthode. Cela fera l'objet du prochain billet.

A bientôt !

Mentions légales

Ce blog est un service de communication au public en ligne édité à titre non professionnel au sens de l'article 6, III, 2° de la loi 2004-575 du 21 juin 2004.

Ce blog est accessible à tous. La possibilité de commenter les articles publiés sur ce journal est ouverte à tous les visiteurs, et ces commentaires reflètent uniquement l'avis de leurs auteurs. A des fins de modération, l'éditeur se réserve le droit de supprimer tout ou partie des commentaires publiés, sans justification ni information préalable.

Propriété intellectuelle

Les articles de ce blog ainsi que leurs commentaires sont mis à votre disposition sous un contrat Creative Commons BY-NC-ND 2.0. Vous êtes libre de reproduire tout ou partie de ces contenus à l'identique et à des fins non commerciales. Vous devez simplement citer leurs auteurs.

Déposer un commentaire sur ce blog signifie accepter sa mise à disposition du public sous licence Creative Commons selon les modalités définies ci-dessus.

Du code source est disponible dans certains articles. A défaut de mention contraire explicite, celui-ci est mis à votre disposition sous un contrat Creative Commons BY-NC 2.0. Vous êtes libre de reproduire tout ou partie de ces contenus à des fins non commerciales. Vous êtes également libre de les modifier et de les adapter. Vous devez simplement citer leurs auteurs.

Réclamations

En cas de réclamation sur le contenu de ce blog, commentaire ou billet, je vous propose de m'adresser un courrier électronique à l'adresse shlublu@gmail.com

La loi vous permet de vous adresser directement à l'hébergeur de ce blog :
OVH
2 rue Kellermann
59100 Roubaix
France

Conformément à la loi, mes éléments d'identification personnelle lui ont été communiqués.

Sachez que l'article 6, I, 4° de la loi 2004-575 du 21 juin 2004 stipule : "Le fait, pour toute personne, de présenter aux [hébergeurs du site] un contenu ou une activité comme étant illicite dans le but d'en obtenir le retrait ou d'en faire cesser la diffusion, alors qu'elle sait cette information inexacte, est puni d'une peine d'un an d'emprisonnement et de 15 000 EUR d'amende."

Utilisation de Google Analytics

« Ce site utilise Google Analytics, un service d'analyse de site internet fourni par Google Inc. (« Google »). Google Analytics utilise des cookies , qui sont des fichiers texte placés sur votre ordinateur, pour aider le site internet à analyser l'utilisation du site par ses utilisateurs. Les données générées par les cookies concernant votre utilisation du site (y compris votre adresse IP) seront transmises et stockées par Google sur des serveurs situés aux Etats-Unis. Google utilisera cette information dans le but d'évaluer votre utilisation du site, de compiler des rapports sur l'activité du site à destination de son éditeur et de fournir d'autres services relatifs à l'activité du site et à l'utilisation d'Internet. Google est susceptible de communiquer ces données à des tiers en cas d'obligation légale ou lorsque ces tiers traitent ces données pour le compte de Google, y compris notamment l'éditeur de ce site. Google ne recoupera pas votre adresse IP avec toute autre donnée détenue par Google. Vous pouvez désactiver l'utilisation de cookies en sélectionnant les paramètres appropriés de votre navigateur. Cependant, une telle désactivation pourrait empêcher l'utilisation de certaines fonctionnalités de ce site. En utilisant ce site internet, vous consentez expressément au traitement de vos données nominatives par Google dans les conditions et pour les finalités décrites ci-dessus. »