Le logs des lancers de Dés est une fonctionnalité que j’ai en tête depuis le début du développement de SRDice, mais je voulais d’abord me familiariser avec des aspects plus simple (entrée utilisateur-traitement-affichage retour) avant de me lancer dans le stockage et la restitution d’information. Un premier pas dans ce sens avait été franchi avec la mise en place des préférences, même si Android propose justement sur ce point un fonctionnement léger et surtout dédié.
Il est maintenant temps de s’intéresser au stockage et à la restitution d’information, en l’occurrence le log des lancers de Dés. Ceci posera notamment la base d’une fonctionnalité à venir : la gestion des Tests Étendus de Shadowrun, à savoir des tests utilisant plusieurs lancers (Ou pas… Le test étendu utilisera certainement une fonctionnalité de stockage de l’information moins pérenne que le log).
Un petit parcours rapide des différentes options pour stocker de l’information dans le cadre d’un développement Android. Bien que décousu coté exemples de code, je vous met en lien cette page qui a l’avantage de présenter clairement les solutions à notre disposition : http://www.mti.epita.fr/blogs/2010/11/17/le-stockage-sous-android/ (traduction FR de http://developer.android.com/guide/topics/data/data-storage.html )
Les solutions sont donc :
- Les préférences => Voir à ce sujet mon billet sur la mise en place des préférences, spécifiquement dédié à la gestion des préférences, ça ne va pas être utile ici.
- Le stockage interne => Écrire des fichiers sur la mémoire interne de l’Android
- Le stockage externe => Écrire des fichiers sur la mémoire externe de l’Android, comprendre une carte SD (SDCard) la plupart du temps
- La base de donnée => Stocker l’information dans une base de donnée de type SQLite
J’ai choisi le stockage externe pour la bonne et simple raison que je préfère garder l’application aussi légère que possible pour la mémoire interne, beaucoup d’appareils tournant sur Android ayant une mémoire interne assez limitée.
Etape 0 : Déclarer la permission dans le Manifest.xml
Vu que SRDice va maintenant écrire un fichier de log sur la carte SD, il faut qu’il en ai la Permission. Pour cela, cette permission doit être déclarée dans le fichier qui sert de présentation de l’application : AndroidManifest.xml
C’est dans ce fichier qu’on déclare la version actuelle de l’appli (point que je mets à jour à chaque fois que je commence à travailler sur une nouvelle fonctionnalité, j’en suis à la 1.8), si l’application peut (doit) s’installer sur la Carte SD : installLocation= »preferExternal » (personnellement je vire de mon Android toutes les appli incapables de faire ça…), les icônes de l’appli et les activity à lancer (avec le chemin).
Et aussi donc, les Permissions :
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="fr.cyol.android.shadowrundice" android:versionCode="8" android:versionName="1.8" android:installLocation="preferExternal"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Un autre truc qu’il faudra que je pense à faire lors de la mise à disposition de cette version : Expliquer le pourquoi de cette Permission supplémentaire dans la présentation des nouveautés. Je pense qu’il s’agit là d’un minimum de politesse envers les utilisateurs qui voient une application changer ses demandes de Permissions.
(Personnellement j’ai tendance à virer de mon Android ou à ne pas installer d’applications qui demandent des Permissions qui me semblent non justifiées par ses fonctionnalités présentées. A quoi servent ces autres Permissions, que fait l’appli comme action qui ne m’est pas présenté, dont je n’ai pas l’utilité ?)
Etape 1 : vérifier que la carte SD est disponible pour ne proposer la fonctionnalité que dans ce cas
Je disais que cette page : http://developer.android.com/guide/topics/data/data-storage.html était décousue coté exemples de code, je m’explique: Il nous donne effectivement un bout de code bien utile pour vérifier s’il y a une mémoire externe de disponible et si on peut écrire dessus mais après, il se contente de donner la fonction getExternalFilesDir sans trop détailler son fonctionnement. Mais voyons déjà le code proposé (et utile!) :
boolean mExternalStorageAvailable = false; boolean mExternalStorageWriteable = false; String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { // We can read and write the media mExternalStorageAvailable = mExternalStorageWriteable = true; } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { // We can only read the media mExternalStorageAvailable = true; mExternalStorageWriteable = false; } else { // Something else is wrong. It may be one of many other states, but all we need // to know is we can neither read nor write mExternalStorageAvailable = mExternalStorageWriteable = false; }
Que dire de plus ? C’est un bel exemple de code bien commenté qui répond à toutes les questions (en tous cas aux miennes ! Si vous en avez, n’hésitez pas à les poser.)
Sur la fenêtre de restitution du résultat du jet, là où je proposais juste un bouton « OK »,si mExternalStorageAvailable et mExternalStorageWriteable sont à true, je propose désormais en plus un bouton « Save ».
Etape 2 : Créer et écrire un fichier de log
Maintenant qu’on s’est assuré de la possibilité d’écrire sur la mémoire externe, écrivons y le log ! Tout d’abord quelques liens sources d’inspiration et de compréhensions sur le sujet :
- http://developer.android.com/reference/android/content/Context.html#getExternalFilesDir%28java.lang.String%29
- http://developer.android.com/guide/topics/data/data-storage.html#filesExternal
- http://blog.developpez.com/android23/p8541/android/creer-un-sd-card-ajouter-des-fichiers-et/
Quand l’utilisateur clique sur le bouton « Save », je déclenche cette fonction :
/** * @desc inscrire le résultat dans un fichier de log * @param bundle contient les résultats */ private void _saveResult(Bundle bundle) { String LOG ="log.txt"; File logFile = new File(Environment.getExternalStorageDirectory(), LOG); String currentDateTimeString = _getCurrentDateTimeString(); String resultMessage = bundle.getString("resultats"); try { logFile.createNewFile(); FileWriter filewriter = new FileWriter(logFile,false); filewriter.write(currentDateTimeString + " :\n"); filewriter.write(resultMessage); filewriter.close(); Toast.makeText(getApplicationContext(), R.string.log_saved,Toast.LENGTH_SHORT).show(); } catch (IOException e) { e.printStackTrace(); Toast.makeText(getApplicationContext(), R.string.log_error,Toast.LENGTH_SHORT).show(); } }
Pour la fonction _getCurrentDateTimeString() je vous renvoie au billet Dév Android : attention à l’import de classe Date
Petits problèmes en l’état :
- Le log s’inscrit à la racine de la carte SD et non pas comme j’avais cru le comprendre dans un dossier dédié de l’application
- A chaque fois que j’écris, j’écrase ce qu’il y avait déjà dans le fichier de log.
Résolution de ces problèmes :
Je n’ai pas trouvé de méthode magique ciblant un dossier dédié à l’application, donc on le crée
String DIR = "SRDiceInfos"; String LOG = "log.txt"; File sdCard = Environment.getExternalStorageDirectory(); File dir = new File (sdCard.getAbsolutePath() + "/" + DIR); dir.mkdirs(); File logFile = new File(dir, LOG);
Un truc qui me dérange maintenant est qu’on ne va pas créer le dossier s’il existe déjà. Après vérification dans la doc, il semble que mkdirs() retourne true si le dossier a été créé, false dans le cas contraire. Dans le false, il y a la possibilité que le dossier existe déjà mais aussi celle où la création a échoué pour une autre raison, du coup, je fais une vérification supplémentaire avec isDirectory() avant d’essayer d’écrire :
if(dir.mkdirs() == true || dir.isDirectory() == true)
createNewFile() n’est visiblement pas le plus nécessaire, ils proposent plutôt d’en utiliser d’autre qui font aussi la création de fichier tout en offrant la possibilité de lire ou d’écrire. En fait FileWriter() que j’utilise le fait par exemple.
Pour le second problème, la solution est bien simple et est due à une erreur de copier-coller. La fonction FileWriter() prend en second argument facultatif un booléen disant si on écrase le contenu du fichier (false) ou si on écrit à la suite (true).
Etape 3 : Consultation des logs
Cette fois, contrairement au résultat d’un lancer, je ne veux pas d’une espèce de popup, mais j’aimerais essayer un véritable nouvel écran, ce qui implique, si j’ai bien tout compris une nouvelle Activity !
Bon, par contre, ça fera le sujet d’un nouveau billet et d’une nouvelle version. Pour l’instant je veux aller au plus vite et donc réutiliser le maximum de fonctions déjà en place. On ajoute un nouvel élément de menu proposant de consulter les logs.
Le menu pourra faire lui aussi le sujet d’un billet dédié, mais un truc rapide en passant : Pour un élément de menu, on peut définir un icône.
android:icon="@android:drawable/ic_menu_info_details
Android en propose une bonne base. Et voici un site où les visualiser tous pour mieux choisir celui qu’il vous faut : http://androiddrawableexplorer.appspot.com/
Avant d’afficher la fenêtre de dialogue je récupère le contenu du fichier :
/** * Lit le fichier de log.txt * @return Bundle */ private Bundle _getLogs() { Bundle logBundle = new Bundle(); if (dir.mkdirs() == true || dir.isDirectory() == true) { File logFile = new File(dir, LOG); // Read text from file StringBuilder text = new StringBuilder(); try { BufferedReader br = new BufferedReader(new FileReader(logFile)); String line; while ((line = br.readLine()) != null) { text.append(line); text.append('\n'); } logBundle.putString("resultats", text.toString()); } catch (IOException e) { e.printStackTrace(); logBundle = null; } } return logBundle; }
if (dir.mkdirs() == true || dir.isDirectory() == true) me sert à vérifier la présence du dossier avant d’essayer de lire le fichier.
Utiliser un BufferedReader est conseillé dans le cas d’un FileReader pour des questions de performances (Informations sur le site de référence de développement Android ). Il faudra que je vois à creuser cette question, je ne suis pas certain de l’utilité de la chose dans mon cas.
Les deux boutons sont : OK pour fermer la fenêtre de dialogue et l’autre pour vider les logs.
Pour les vider, je me contente de supprimer le fichier log.txt :
File logFile = new File(dir, LOG); boolean deleted = logFile.delete()
J’ai encore quelques points à revoir, notamment une optimisation à mener sur la façon dont je gère les Dialog. Ce sera le prochain billet.
Quelques idées pour la suite :
- Proposer dans les préférences un choix « Logguer tous les jets », « Demander à chaque jet », « Ne jamais logguer »
- Enregistrer les log de manière antéchronologique (nouveau log en haut du fichier et non pas en bas)
[…] est ma constante actant si la SDCard est inscriptible ou pas (Dans l’Etape 1 : vérifier que la carte SD est disponible pour ne proposer la fonctionnalité que dans ce c…) et que SDCard est ma constante contenant le chemin de la SDCard : elle vaut […]