Blog

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:
liste des assets ooxml compliqué

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!

admin

Written by

The author didnt add any Information to his profile yet

  • Même pendant les Fêtes ;)

  • Tschobber

    Hi,
    im really bad at french so I talk to you in english if thats good for you :) … I have the following problem – I always get the  following error when starting my app:

    XML-BEANS compiled schema: Could not locate compiled schema resource schemaorg_apache_xmlbeans/system/sADF9D9697439DFAA95C3BAFDA11C5681/index.xsb (schemaorg_apache_xmlbeans.system.sADF9D9697439DFAA95C3BAFDA11C5681.index) – code 0

    I know why this happens (because I have no index.xsb) but how could you « extract » those .xsb files and place them to sdcard? I only could find them in the POI ooxml package jar file but there I cant access them ….

    Every answer would be highly appreciated.

    Martin

  • Tschobber

     ok just added some code to Xmlbeans source and recompiled it to point to the sdcard where the .xsb files are…

  • Johann Hilbold

    sorry for the delayed answer: i recompiled ooxml, modifiying the sources where the xsb file is read in ooxml. 

    there’s probably a better way, but here’s how i’ve done it. (I commented out the original version of org.apache.xmlbeans.impl.schema.PathResourceLoader)
        public InputStream getResourceAsStream(String resourceName)    {    try { return new FileInputStream(« /sdcard/office/xsb/ »+resourceName.substring(resourceName.lastIndexOf(« / »)+1)); } catch (FileNotFoundException e1) { // TODO Auto-generated catch block e1.printStackTrace(); return null; }//     for (int i = 0; i < _path.length; i++)//        {//            InputStream result = _path[i].getResourceAsStream(resourceName);//            if (result != null)//                return result;//        }//        return null;    }

  • Ph0nyx

    Hey super mais je ne trouve pas la méthode plus simple Visualiser . . . android (3/3) ?

  • Franco

    Hi! Thanks for this very complete post. It’s was very useful for me. But now I have a problem. I need to open a docx document from my android application. I have tried a lot of things but I couldn’t get it.
    Have you tried this?

    Thanks in advance!

    (Please, excuse my poor english)

  • Johann

    @Ph0nyx:
    je suis tombé sur cet article http://openxmldeveloper.org/blog/b/openxmldeveloper/archive/2006/11/21/openxmlandjava.aspx et dans mes souvenirs du moins, je n’ai presque touché à rien pour le faire marcher. C’est pourquoi je n’ai finalement pas posté d’article à ce sujet. Le gros avantage, c’est que cette méthode est ultra légère.

    @Franco:
    I haven’t worked on this for a while (about a year ago!) but from what I remember, the process is somewhat different but works the same way. The thing is, at least a year ago, there was no ToHtml class in the « docx » package. It was easier to write the article by showing the « xslx » side. There might be a « ToHtml » class for docx now, I haven’t checked.
    I dont have all the sources here, but I’ll take a look some time soon.

  • Franco

    Thanks again.
    Please, let me explain you the steps I have followed.
    In my case I don’t need an HTML version of the file. I only need to instantiate a « org.apache.poi.xwpf.usermodel.XWPFDocument » from an InputStream and the process it.
    I tried with all of your jars within the « libsToSpecialDEX » folder without succeed. Then, I tried replacing « mypoibuild2.jar » (inside folder 1) with « mypoibuildWithOXML.jar » (inside « unused_Libs_But_Will_Be_Later » folder) and when I load the docx file I got this exception:

    E/WordDocxConverter( 321): Caused by: org.apache.xmlbeans.SchemaTypeLoaderException: Cannot resolve type for handle _XY_Q=space|R=space@http://www.w3.org/XML/1998/namespace (schemaorg_apache_xmlbeans.system.sE130CAA0A01A7CDE5A2B4FEB8B311707.cttext7f5btype) – code 13
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl$XsbReader.readHandle(SchemaTypeSystemImpl.java:2025)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl$XsbReader.readTypeRef(SchemaTypeSystemImpl.java:2099)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl$XsbReader.loadAttribute(SchemaTypeSystemImpl.java:2926)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl$XsbReader.readAttributeData(SchemaTypeSystemImpl.java:2918)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl$XsbReader.finishLoadingType(SchemaTypeSystemImpl.java:2535)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl.resolveHandle(SchemaTypeSystemImpl.java:3511)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.SchemaComponent$Ref.getComponent(SchemaComponent.java:113)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.SchemaType$Ref.get(SchemaType.java:872)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.schema.SchemaPropertyImpl.getType(SchemaPropertyImpl.java:92)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.schema.SchemaTypeImpl.createElementType(SchemaTypeImpl.java:995)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.values.XmlObjectBase.create_element_user(XmlObjectBase.java:927)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.store.Xobj.getUser(Xobj.java:1669)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.store.Cur.getUser(Cur.java:2659)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.store.Cur.getObject(Cur.java:2652)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.store.Cursor._getObject(Cursor.java:995)
    E/WordDocxConverter( 321): at org.apache.xmlbeans.impl.store.Cursor.getObject(Cursor.java:2904)
    E/WordDocxConverter( 321): at org.apache.poi.xwpf.usermodel.XWPFParagraph.(XWPFParagraph.java:124)
    E/WordDocxConverter( 321): at org.apache.poi.xwpf.usermodel.XWPFDocument.onDocumentRead(XWPFDocument.java:145)
    E/WordDocxConverter( 321): at org.apache.poi.POIXMLDocument.load(POIXMLDocument.java:159)
    E/WordDocxConverter( 321): at org.apache.poi.xwpf.usermodel.XWPFDocument.(XWPFDocument.java:122)
    E/WordDocxConverter( 321): … 7 more
    W/InputManagerService( 61): Window already focused, ignoring focus gain of: com.android.internal.view.IInputMethodClient$Stub$Proxy@40540430

    NOTE: I copied into « /sdcard/office/xsb/ » the xsb files needed.

    It would be great If you could give an advice.

    Thanks!