LeBlog OXiane
2011
Visualiser un fichier Office (doc, xls, ppt…) sous Android (2/3)
Comme expliqué dans mon dernier post, j’ai cherché (et réussi) à afficher des documents MS Office sur une plateforme Android.
L’objet de mon dernier article était de montrer comment afficher des fichiers MS Office « binaires », c’est à dire pré 2007, sous Android. Ici, je vais montrer comment faire la même chose avec des fichiers Office Open Document (c’est à dire MS Office 2007+, docx, pptx, xlsx…)
Je répète le postulat, car il est important: comme mon application ne doit à priori pas permettre l’édition de ces documents, je pars du principe qu’il suffit de convertir ces fichiers MS Office au format HTML pour ensuite les afficher dans une WebView.
La solution pour convertir les fichiers binaires était relativement simple, il suffisait d’ajouter les jars au classpath et d’appeler la bonne méthode. Ici, ça se complique sévèrement. Et ce, pour plusieurs raisons:
Tout d’abord, parce qu’on ne peut PAS ajouter les jars simplement au build path dans un projet Android. En effet, les jars utilisés par la conversion des fichiers OOxml pèsent très lourd: plus de 24 Mo. Voir ici la liste des composants nécessaires et la liste des dépendances de ces composants.
Si on ajoute tous ces jars à notre projet, on obtient un joli « java heap space exception » avant même de pouvoir uploader l’apk sur le téléphone. En fait, pour être plus précis, il s’agit non pas d’un problème de taille des jars, mais surtout du fait que les classes contenues dans ces jars contiennent énormément de méthodes. En effet, la transformation des fichiers .class en un fichier DEX (Dalvik EXecutable) n’autorise pas plus de 64k méthodes.
Pour info/rappel, lorsque vous cliquez sur votre joli bouton vert sur Eclipse, Eclipse appelle un script ANT de commandes du SDK Android, et il va, compiler vos .java en .class (ça tout le monde connait, c’est javac!). Puis, il va référencer toutes les ressources de votre répertoire /res. Puis, il transforme tous vos .class en un seul fichier DEX, qui contient du bytecode lisible par la machine virtuelle Dalvik. C’est la commande dx, il y’a d’ailleurs un fichier dx.bat dans votre répertoire androidSDK/platform-tools. Et le tout (un fichier classes.dex et toutes les ressources xml ou image) est archivé dans un fichier APK.
Et donc, cette « dexisation » n’accepte que 64k méthodes. L’équipe de développement Android a d’ailleurs posté une solution à ce problème. Ce lien est très intéressant, je le recommande chaudement (et puis sinon vous comprendrez difficilement la suite!)
Donc j’ai fait ce qu’ils ont dit: j’ai chargé les .class de tous mes jars additionnels dans plusieurs fichiers .dex que j’ai chargé après, dynamiquement, au lancement de l’application. Tout comme c’est expliqué dans le lien du blog Android. A la seule différence que moi, j’ai créé plusieurs fichiers DEX étant donné que je n’arrivais pas à tout caser dans un seul fichier DEX additionnel.
Voici à quoi ressemblent mes assets dans mon projet explorer:

Le répertoire « libsToSpecialDEX » que je n’ai pas affiché en entier contient tous les jars indiqués dans les dépendances de POI pour la partie ooxml. Et même, légèrement plus compliqué que cela, j’ai divisé le jar « schemas-ooxml » en 5 jars différents (un pour chaque package). J’ai donc un jar (et un dex) pour le package word, un jar pour le package excel, etc.
Au final, j’ai donc créé 9 fichiers DEX que je vais charger dynamiquement au lancement de mon application. Pour créer les fichiers DEX, j’ai légèrement modifié le fichier ANT fourni dans l’exemple du blog Android de la façon suivante:
hasCode = false. Skipping...
Notez que je n’ai copié que le target « DEX » et que j’ai tronqué pour n’afficher que les deux premiers fichiers DEX que j’ai créés. (Comme expliqué plus haut, j’ai finalement fait 9 fichiers DEX additionnels!)
Donc, une fois les fichiers DEX créés, il faut les charger dans le classloader de mon application. Cette opération est faite à l’exécution de l’application. Pour des raisons de simplicité, j’ai tout laissé dans le onCreate de mon application. Mais c’est une opération lourde, il faudrait donc la mettre dans un thread séparé!
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//si le fichier de démo "SampleSS.xlsx" n'est pas déjà présent sur la SDCard, on le copie
if(!new File("/sdcard/office/SampleSS.xlsx").exists())
{
//la méthode copyFilesToPrivate est la même que celle utilisée dans l'article précédent // pour copier les assets vers la SD
copyFilesToPrivate("SampleSS.xlsx");
}
//chargement de la classe qui convertit le xlsx en html
Class cl0 = loadClass( "org.apache.poi.ss.examples.html.ToHtml");
try {
//invoquer la méthode main de la classe "ToHtml" avec deux arguments
//(merci Java pour le cast obligatoire et non intuitif)
main.invoke(o, (Object) new String[] {"/sdcard/office/SampleSS.xlsx", "/sdcard/office/SampleSS.html"});
} catch (InstantiationException e) {
TestOfficeAndroidActivity.this.getClassLoader().clearAssertionStatus();
e.printStackTrace();
} catch (IllegalAccessException e) {
TestOfficeAndroidActivity.this.getClassLoader().clearAssertionStatus();
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
TestOfficeAndroidActivity.this.getClassLoader().clearAssertionStatus();
}
}
Vous noterez cette fois ci l’invocation de la méthode « main » par réflexion, car, lors de la compilation du projet, on n’a pas de référence aux classes de POI! En effet, celles-ci ne sont chargées qu’à l’exécution, dans la méthode loadClass() que voici:
private Class loadClass(String className) {
// Internal storage where the DexClassLoader writes the optimized dex file to
new File("/sdcard/office/opti").mkdirs();
final File optimizedDexOutputPath = new File("/sdcard/office/opti");
//the dexloader is given all the jars that we need and loads them into the activity's classloader
DexClassLoader cl = new DexClassLoader(onary.getAbsolutePath()+":"+secondary.getAbsolutePath(),
optimizedDexOutputPath.getAbsolutePath(),
null,
TestOfficeAndroidActivity.this.getClassLoader());
Class libProviderClazz = null;
try {
// Load the library.
libProviderClazz = cl.loadClass(className);
}
catch(Exception e)
{e.printStackTrace();}
return libProviderClazz;
}
Ici, la ligne réellement importante est celle avec le new DexClassLoader. En effet, c’est ici qu’on va lire les fichiers dex (précédemment stockés sur la SD card), les optimiser, et surtout, les charger en mémoire dans le classLoader de notre activité. Cela permettra, par la suite, d’appeler la méthode « main » de la classe « ToHtml ».
Dans l’exemple ci dessus, j’ai tronqué un peu la méthode pour plus de clarté (je rappelle que dans mon projet, ce n’est pas 2 fichiers DEX, mais 9 qu’il a fallu ajouter au classLoader!)
Et là… c’est le drame!
On aurait pu penser que cette méthode marcherait comme ça, mais non! Ici, on obtient un message pas très clair:
W/System.err(370): Caused by: java.lang.RuntimeException: Installation Problem??? Couldn’t load messages: Can’t find resource for bundle ‘org.apache.xmlbeans.impl.regex.message_en_US’, key »
Après un peu de recherches, je me rends compte qu’il s’agit d’un message d’erreur causé par l’une des dépendances (XmlBeans, en l’occurence) lorsqu’un fichier de ressources n’est pas présent. Mais pourtant, ce fichier, il est bien dans mon jar! Bizarre bizarre… En fait, pas du tout! C’est parfaitement logique. La « dexisation » a pour unique but de transformer les .class en un fichier DEX. Mais si un fichier autre que .class se trouve aussi dans le jar qu’on transforme, il est est ignoré!
Après un petit hack pas très propre (j’ai recompilé XmlBeans en modifiant la ligne qui recherche ce fichier de ressources), un nouveau problème du même genre se pose: il se trouve que dans le jar « ooxml-schemas » de POI, il y’a tout un tas de fichiers « .xsb ». Ceux-ci sont en fait des schémas qui permettent à XmlBeans de générer des classes qui modélisent les objets Office Open. Et ils sont donc fondamentaux au fonctionnement de la partie « ooxml » de POI.
Après un second hack pas très propre (j’ai recompilé ooxml-schemas en spécifiant que les fichiers XSB se trouvaient non pas à la racine du projet, mais dans un répertoire de ma SDcard), c’était presque bon! Plus d’erreur bizarres à cause de fichiers manquants, simplement une bonne vieille erreur ClassNotFoundException: java.awt.Color
Je vous le donne en mille: il a fallu retirer dans toutes les classes de POI toutes les références à java.awt.* car ce package n’existe pas sous Android! Hé oui, le projet d’Apache est prévu pour Java, et non pour Android. C’est d’ailleurs toute la difficulté exprimée dans cet article.
Donc c’était fastidieux, mais pas impossible. Il a suffit de remplacer les références à java.awt.Color par des android.graphics.Color et les java.awt.Dimension par une petite classe à moi qui fait presque tout comme Dimension.
Et là… ça marche!!!
Pour ceux qui voudraient voir tout le code, il est « checkoutable » ici.
Alors bien sur… C’est pas très optimisé tout ça. J’ai tenté, comme pour la version qui marche avec des fichiers binaires (voir post précédent), de supprimer l’inutile. Mais c’est une prise de tête sans fin. D’ailleurs, j’ai demandé aux devs de POI. Ils m’ont bien confirmé qu’il est impossible d’enlever des grosses parties de leurs jars pour n’utiliser que la fonction « extraction au format HTML ».
D’ailleurs, mon APK pèse dans les 8Mo et il décompresse tous les fichiers XSB (soit une taille supplémentaire de 15 Mo) sur la SDCard, ce qui fait donc vraiment beaucoup trop pour une application qui ne fait que de la lecture de fichier OOxml.
Mais au moment de commencer à vraiment optimiser le projet, un cher collègue qui se reconnaîtra m’a fait part d’une solution alternative infiniment plus simple, que je vais vous présenter dans mon prochain post!
2011
Visualiser un fichier Office (doc, xls, ppt…) sous Android (1/3)
Après avoir bien galéré pour les fichiers PDF (cf mon article précédent), je me suis attaqué à l’affichage de fichiers Microsoft Office sous Android.
Et figurez vous que le problème, bien que très différent, possède lui aussi une solution tout à fait particulière.
Comme mon application ne demande à priori pas la fonctionnalité « édition » des fichiers Office, je suis parti du postulat qu’il « suffisait » de convertir ceux ci au format HTML et à les afficher ainsi dans une WebView.
Pour faire moins dense, je vais découper la solution en plusieurs posts:
- - Convertir au HTML un fichier Office binaire (fichiers doc et non docX, par exemple) avec Apache POI
- - Convertir au HTML un fichier Office Open Document (xlsx, docx…) avec Apache POI
- - Convertir au HTML un fichier Office Open Document (xlsx, docx…) SANS Apache POI (c’est plus simple, mais moins intéressant!)
Donc, pour commencer… Convertir un fichier MSOffice binaire en HTML est relativement simple:
1) Il suffit d’aller chercher les bons jars sur le site de POI.
Dans mon exemple, je n’ai eu besoin que d’utiliser poi-scratchpad-3.8-beta4-20110826.jar, poi-3.8-beta4-20110826.jar et commons-codec-1.5.jar. Ce dernier est une dépendance et est disponible ici.
2) J’ai créé un projet Android sous Eclipse, ajouté des fichiers de test dans mon répertoire assets, ajouté les jars ci dessus au build path de mon projet et voila le résultat:

3) Directement dans le onCreate de mon activité (c’est pas bien, mais c’est suffisant pour ce test), j’ai copié les fichiers à convertir depuis mon répertoire Assets vers la SDCard. Puis, j’ai simplement appelé les méthodes de conversion du package Apache POI. Et voila!
Histoire de donner un bout de code, voilà mon onCreate():
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//copier les fichiers de test depuis mon répertoire assets vers la SDCard
copyAssetToSDCard("test.doc");
copyAssetToSDCard("testxls.xls");
//lancer la conversion avec les jars d'apache POI
WordToHtmlConverter.main(new String[]{"/mnt/sdcard/test.doc", "/mnt/sdcard/test.html"});
ExcelToHtmlConverter.main(new String[]{"/mnt/sdcard/testxls.xls", "/mnt/sdcard/testxls.html"});
}
Donc j’ai une méthode qui copie un fichier depuis les Assets Android vers la SDCard et j’ai simplement appelé les méthodes « main » des classes Apache POI WordToHtmlConverter et ExcelToHtmlConverter. Ci dessous le détail de ma méthode copyAssetToSDCard:
private final int BUF_SIZE = 8192;
private File copyAssetToSDCard(String assetName) {
File copiedFileStoragePath = new File("/mnt/sdcard/"+assetName);
BufferedInputStream bis = null;
OutputStream dexWriter = null;
try {
bis = new BufferedInputStream(getAssets().open(assetName));
dexWriter = new BufferedOutputStream(new FileOutputStream(copiedFileStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
} catch (Exception e) {e.printStackTrace();
return null;}
Log.d("OfficeCopy", "copied "+assetName+" to /sdcard");
return copiedFileStoragePath;
}
Et voila! Il suffit de lire les fichiers HTML créés et de les insérer dans une WebView avec la méthode loadURL:
WebView v = (WebView) findViewById(R.id.ma_web_view);
v.loadUrl("file://mnt/sdcard/test.html");
Et pour les malins qui testeraient ce même code avec un fichier docx, xlsx ou autre Office Open Document… vous obtiendriez le message suivant:
org.apache.poi.poifs.filesystem.OfficeXmlFileException: The supplied data appears to be in the Office 2007+ XML. You are calling the part of POI that deals with OLE2 Office Documents. You need to call a different part of POI to process this data (eg XSSF instead of HSSF)
Bien essayé! ça aurait été trop simple. La solution dans le prochain post!
Pour conclure, mon apk final pèse 1,2 Mo, ce qui est beaucoup et finalement pas tant que ça, quand on considère ce qu’il fait. Cette notion de taille de l’apk, on le verra par la suite, a toute son importance!
On pourrait, pour diminuer la taille de l’apk final, « s’amuser » à ne prendre que la partie qui s’occupe de la conversion dans les jars Apache. Pour cela, j’ai téléchargé les sources sur le site de POI, puis j’ai lancé la conversion en mode DEBUG. Et j’ai regardé, aussi bien que je pouvais, quelles classes étaient utilisées.
Hum, honnêtement, j’ai essayé, je me suis cassé la tête pendant deux jours et au final, j’ai réduit la taille finale de mon apk à 800ko. C’est un peu mieux, mais le casse tête ne vaut probablement pas les 400ko gagnés. La méthode que j’ai utilisé n’était peut-être pas la bonne, et il y’a surement plus de choses qu’on peut supprimer.
2011
OXiane studio : urbancycle à l’honneur
Un petit clin d’oeil à Urbancycle et tous les coursiers parisiens !
Rue 89 consacre un article pas mal fait à la course et en particulier à Urbancycle qui fêtait ses 10 ans cette année dans la chistole générale.
Co-fondateur d »Urban Cycle, la plus respectée et ancienne des sociétés de livraison où ne sévissent que des cyclistes, après avoir été lui-même à longueur de journées sur le pavé, Patrick, la quarantaine, a accompagné l’émergence du phénomène.
Dans les années 90, il a débuté dans une filiale d’une boîte américaine. Après sa chute pour des défauts de gestion, il a décidé de se lancer avec deux anciens collègues.

Avec une belle photo de notre JB national.
Tout ça augure d’un superbe pre-event Parisien aux championnats du monde 2013 qui devraient se dérouler à Loz !
2011
iOS & Modal Partial Curl
Il y a quelques jours une personne me demandait comment récupérer une donnée d’une vue modale s’affichant avec une transition « Partial Curl » (soulèvement partiel de la vue parente comme une page d’un livre) dans son UIViewController parent.
Son problème étant que son code fonctionne parfaitement sous iOS 4 mais pas sous iOS 5.
Je regarde son code et là je remarque que tout est bien compliqué pour une chose aussi simple.
En effet, il observe la valeur de sa vue modale et si elle est à nil (l’animation de fermeture de la vue a eu lieu) alors il déclenche un callback sensé transmettre une donnée à sa vue parente, représentée par une propriété de type UIViewController, sachant que la vue modale se ferme elle-même.
Seulement, le changement d’état de la vue modale n’est pas intercepté et donc la valeur n’est pas transmise.
Outre ce soucis, il aurait dû paraître étrange que la vue modale se supprime et qu’elle possède une propriété UIViewController car dans iOS (et bien d’autres langages objets) on prend la bonne habitude d’étendre les classes de base et ce pour permettre la réutilisabilité par exemple.
Dans iOS, un design pattern est très souvent utilisé, c’est le delegate (délégation ou proxy).
Le principe est simple : déléguer à un autre objet des traitements, c’est une inversion de responsabilité.
Pour démontrer que c’est simple, je vais vous indiquer les étapes à suivre sous iOS 5 avec les storyboards :
- créez un nouveau projet de type « Single View Application » dans iOS
- déposez un UIButton dans la vue du contrôleur principal de votre storyboard
- donnez lui un titre
- déposez un nouveau UIViewController dans votre storyboard
- faîtes un segue modal (ça se prononce ségoué) à partir du bouton vers le nouveau UIViewController
|
|
(pour rappel : le segue va instancier le nouveau UIViewController et faire la transition comme définie dans le code ou l’inspecteur)
- cliquez sur le segue et donnez lui un identifiant

- cliquez sur le nouveau UIViewController et fixez le style de transition à « Partial Curl » ou bien faîtesle directement sur le segue.
- ajoutez un UIButton par exemple

- créez une nouvelle classe héritant de UIViewController : PageCurlController et ajouter un protocole PageCurlDelegate déclarant une méthode (style dismissWithData:), dans votre déclaration de classe ainsi qu’une propriété de type id répondant à ce protocole afin d’être réutilisable. Vous pouvez l’appeler… delegate ![]()
Ce qui est important : la référence vers cet objet est faible (« weak ») car le contraire n’aurait aucun sens ici, si la référence était forte alors si PageCurlController disparaît le délégué aussi mais comme c’est son parent… tout disparaît ! On ne veut pas cela n’est-ce pas ?
@protocol PageCurlDelegate <NSObject>
-(void)dismissWithData:(NSString *)data;
@end
@interface PageCurlController : UIViewController
@property(nonatomic, weak) id<PageCurlDelegate> delegate;
@end
- n’oubliez pas de la synthétiser :
@synthesize delegate = _delegate;
- retournez dans le storyboard et changer la classe du nouveau UIViewController en PageCurlController (par exemple) dans l’inspecteur d’identité

- reliez le bouton à l’action closePage, c’est ici que l’on fait appel au délégué afin de lui dire que PageCurlController est prêt à disparaître et qu’il lui transmet une donnée.
- (IBAction)closePage:(UIButton *)sender
{
//faisons appel au delegate afin de transmettre nos données
//et de fermer cette vue
[_delegate dismissWithData:sender.titleLabel.text];
}
- on complète notre contrôleur principal (logiquement le pointeur est donc fort sur page) :
@interface PageTrickViewController : UIViewController<PageCurlDelegate>
@property(nonatomic, strong) IBOutlet PageCurlController * page;
@end
- et dans l’implémentation (on n’oublie pas de synthétiser) :
@synthesize page = _page;
//méthode déclenchée à chaque fois qu'un segue est déclenché
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
//vérification du segue déclenché
if([segue.identifier isEqualToString:@"modalCurl"]){
//ici l'exemple est simple mais il serait judicieux de tester la classe
//avec isKindOfClass !
_page = segue.destinationViewController;
//très important !
_page.delegate = self;
}
}
-(void)dismissWithData:(NSString *)data
{
//ici j'ai juste affiché dans le debug la String transmise
//mais on peut imaginer la stocker dans une ivar ou l'employer dans une autre méthode
NSLog(@"%@", data);
//c'est notre contrôleur principal qui supprime la vue !
[_page dismissModalViewControllerAnimated:YES];
//si vous n'avez plus besoin de _page
_page = nil;
}
Et voici le résultat en image :
Pour les curieux, voici comment Apple voit la délégation : http://developer.apple.com/library/ios/#documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html
2011
Bonnes fêtes de fin d’année !

Toutes les équipes d’OXiane se joignent à moi pour vous souhaiter d’agréables fêtes de fin d’année et une excellente année 2012.


