Packager une application Android Wear dans la vraie vie

le 29/10/2014 par Pierre Degand
Tags: Software Engineering

Vous avez déjà une application Android ? C’est l’occasion rêvée d’être présent sur montre connectée grâce à Android Wear. Votre code est déjà prêt, votre graphiste connaît déjà le sujet, il suffit de se lancer. Mais votre application Android n’est pas aussi simple que les exemples de Google: suivez le guide pour ne pas tomber dans les pièges d’Android Wear !

Comme annoncé par Google, votre application pour Android Wear doit être packagée dans l’apk de l’application pour smartphone et celui-ci se charge ensuite de pousser l’apk de l’application Android Wear sur votre montre.

Au travers d’un exemple concret, nous allons découvrir les différentes méthodes qui permettent de packager correctement une application Android Wear dans l’apk d’une application Android de la vrai vie.


Note préliminaire sur Gradle

La version du plugin Android utilisée est la version 0.13. Cette version nécessite Gradle 2.1. Pour forcer le Gradle-Wrapper à utiliser cette version de Gradle, il faut modifier le fichier gradle-wrapper.properties pour ajouter la ligne suivante : distributionUrl=http://services.gradle.org/distributions/gradle-2.1-all.zip Puis dans le build.gradle parent du projet, il faut renseigner la version du plugin Android utilisé :

Voir le lien github

La version 0.13 est nécessaire pour avoir accès à la nouvelle API Outputs du plugin Android. Cette nouvelle API permet de référencer les fichiers de sorties de la compilation sans devoir mettre les liens en durs dans nos scripts.

1. Comme dans la doc Google: une application simpliste

Pour commencer notre exemple, nous allons partir d’une application très simple : pas de flavors, deux buildTypes (un de debug et un de release) et c’est tout. Le projet sous Android Studio doit donc comporter deux modules : un module :mobile qui contient l’application pour mobile et un module :wear qui contient le code pour l’application Android Wear.

En suivant simplement la documentation de Google sur Android Wear, on référence le module :wear dans les dépendances du module :mobile. Un point très important qu’il ne faut pas oublier : l’application Wear et l’application mobile doivent avoir exactement le même nom de package ! On obtient alors les build.gradle suivants :

Voir le lien github

Voir le lien github

On remarque que dans les 2 modules, une configuration de signature est définie. Ceci est nécessaire car Android n’installe automatiquement que des applications Android Wear qui sont signées en release.

Dans le module :mobile, la ligne wearApp project(':wear') indique une dépendance spéciale vers un projet Android Wear. Le système de build se charge donc lui-même de packager notre application Android Wear.

Voyons ça …

>> ./gradlew mobile:assembleRelease :mobile:preBuild :mobile:preReleaseBuild ... :mobile:generateReleaseResValues UP-TO-DATE :wear:preBuild ... :wear:validateProductionSigning :wear:packageRelease :wear:zipalignRelease :mobile:handleReleaseMicroApk :mobile:generateReleaseResources ... :mobile:assembleRelease

BUILD SUCCESSFUL

On constate que notre dépendance a ajouté une nouvelle task handleMicroApk et que cette tâche génère l’apk de notre application wear.

Essayons d’installer l’apk mobile sur un téléphone connecté à une montre Android Wear pour valider l’installation automatique de notre apk wear.

>> adb -d install mobile/build/outputs/apk/mobile-release.apk

L'application App est installée automatiquement sur le device

En allant voir la liste des applications sur le device Android Wear, on constate que l’apk wear a bien été poussé et installé.

En ne touchant quasiment rien dans notre build.gradle par rapport à un projet sans application Android Wear, on a réussi à packager un apk Android Wear dans l’apk de notre application mobile.

Plutôt simple non ? Mais comment cela se passe-t-il dans la vraie vie ?

2. Une application de la vraie vie

Quand on parle d’application de la vraie vie, on parle bien entendu de build customisé à coup de productFlavors. Les flavors permettent de générer plusieurs types d’apk en se basant sur une même base de code, à quelques changements près.

Voici quelques cas d’utilisation communs des flavors :

  • Une version gratuite et une version payante de la même application.
  • Une version dogFood pour votre beta interne et une version “Play Store”.
  • Plusieurs applications pour différentes marques avec des designs différents.

Nous allons donc complexifier notre projet de test et y ajouter des flavors. Nous voulons avoir 2 applications différentes: une pour la marque A et une pour la marque B, avec des applicationId différents pour les deux applications pour pouvoir avoir les deux sur le même device.

On rajoute donc la définitions des deux productFlavors dans le build.gradle du module “:mobile”

Voir le lien github

Un point important à prendre en compte : il ne faut pas oublier que l’applicationId de l’apk Wear doit être exactement le même que celui de l’application pour mobile ! Nous avons donc besoin des mêmes flavors dans le module :wear pour customiser aussi les applicationIds. On ajoute les même lignes dans le build.gradle du module :wear.

On peut maintenant builder 2 apk différents qui intégreront chacun l’apk de l’application wear associée. Essayons du coup de construire l’apk pour la flavor brandA :

>> ./gradlew mobile:assembleBrandARelease … BUILD SUCCESSFUL

L’application mobile a bien été construite mais… si on regarde les logs de compilation, notre application Wear n’a pas été construite ! Si on essaie d’installer l’apk de la marque A, aucune application Wear ne sera poussée sur la montre.

Le packaging automatique ne fonctionne donc plus si notre application intègre des flavors.

3. La solution : Packager manuellement une application Wear

Heureusement pour nous, Google explique comment packager manuellement un apk Wear dans un apk pour mobile.

Pour cela, il faut d’abord avoir un apk signé de l’application wear. Construisons le.

>> ./gradlew wear:assembleBrandARelease

Il faut maintenant placer l’apk généré dans le dossier assets/ du module :mobile

>> cp wear/build/outputs/apk/wear-brandA-release.apk mobile/src/brandA/assets/

Nous n’avons plus besoin du packaging automatique dans le module :mobile. On supprime la dépendance vers le module :wear.

Voir le lien github

Ensuite, il faut référencer notre application Wear dans le AndroidManifest.xml de l’application mobile. Pour cela, on référence un fichier xml qui va décrire l’application Wear en ajoutant les lignes suivantes dans le tag du manifest :

Voir le lien github

La dernière étape est de créer le fichier xml qui décrit l’application Wear. Comme l’application Wear pour la marque A n’est pas la même que l’application Wear pour la marque B, il faut créer un fichier de description par flavor. On crée donc dans le dossier /brandA/res/xml/ le fichier wear_desc.xml.

Voir le lien github

Ce fichier est assez simple à comprendre. On décrit le package de l’application Wear, ses différentes versions et enfin, on donne le chemin vers l’apk dans les assets.

Note : Dans son guide sur le package manuel, Google propose de mettre l’apk de la Wear dans le dossier res/raw/ de l’application mobile. Or, ce dossier est compressé automatiquement par le système de build. Un APK étant déjà compressé, la deuxième compression va donc “casser” l’APK et empêcher son déploiement automatique. Il y a donc deux solutions, la première est de désactiver la compression de res/raw/, la seconde est de mettre l’APK dans les assets. Mettre l’apk dans les assets permet d’éviter de configurer chaque environnement de build pour désactiver la compression et permet surtout de conserver la compression pour les ressources utilisées par l’application.

Il ne reste plus qu’a compiler le module “:mobile” pour avoir un apk mobile qui contient un apk Wear :

>> ./gradlew mobile:assembleBrandARelease

Puis on l’installe sur le mobile :

>> adb -d install mobile/build/outputs/apk/mobile-brandA-release.apk

Sur notre device Android Wear, on voit bien l’application de la marque A installée automatiquement.

On aimerait maintenant pouvoir compiler et installer l’application de la marque B. Il va falloir builder l’apk Wear de la marque B, le copier au bon endroit, créer le XML de la marque B avec les bonnes informations à l’intérieur puis enfin builder l’apk mobile de la marque B.

Et si je fais des modifications dans mon appli Wear ? Je vais devoir recompiler chaque flavor de la Wear, copier manuellement les apk dans les bons dossiers, et mettre à jour les fichiers de descriptions si les versions ont changées.

Et si je rajoute une nouvelle flavor ? Il va falloir encore compiler un nouvel apk à la main, le copier dans les assets de la nouvelle flavor, et faire le nouvel XML de description.

Quel est le problème de cette méthode manuelle? La réponse est dans la question: elle est manuelle ! Cela devient donc difficile de faire compiler nos flavors par un Jenkins. À la moindre modification, on risque d’introduire des erreurs entre les différents fichiers. Bref, on n’aime pas du tout cette solution manuelle.

4. Automatisons le processus : Gradle à la rescousse !

Heureusement pour nous, les applications Android sont compilées avec Gradle, un outil merveilleusement puissant.

Récapitulons les différentes étapes du packaging manuel:

  1. Construire l’apk Wear du même flavor que l’apk mobile que l’on veut construire
  2. Copier cette apk dans les assets de l’application mobile
  3. Créer un fichier XML dans res/xml/ qui décrit l’apk Wear que l’on vient de construire
  4. Construire l’apk mobile avec l’apk Wear à l’intérieur

Avant de commencer, uniformisons les numéros de versions de l’application mobile et de l’application Wear.

Pour cela, définissons 2 variables externes dans le build.gradle parent qui seront utilisées dans les build.gradle du module :wear et :mobile.

Voir le lien github

Voir le lien github

Nous modifions cela pour garantir que les 2 applications ont des numéros de version identiques.

Passons à l’étape n°2 du packaging manuel : c’est la seule modification du module :wear que nous allons devoir effectuer. Nous voulons qu’à la fin de la compilation, Gradle copie le résultat du build dans le dossier main/assets/ du module :mobile.

Il faut rajouter les lignes suivantes à la fin du build.gradle du module :wear.

Voir le lien github

Dans ce script, on récupère chaque variante (une variante est une combinaison buildType / productFlavor) du projet et pour chaque variante, on copie les fichiers de sorties (outputs) dans le dossier assets du module mobile. On a terminé de modifier le module “:wear”.

Concentrons nous sur l’étape n°1 : construire l’apk de la Wear avant de construire l’apk mobile. Pour cela, il faut terminer la compilation et la copie de l’apk Wear avant que les assets soient générés (je rappelle qu’on colle l’apk dans les assets des sources et non les assets générés). Il va donc falloir lancer la tâche “wear:assembleFlavorBuildType” avant la génération des assets.

Heureusement, grâce à Gradle, on peut injecter dynamiquement des dépendances entre les tâches. On va donc indiquer au processus de compilation que la tâche “generateFlavorBuildType” dépend de la tâche qui génère l’apk Wear.

On ajoute le code suivant au build.gradle du module “:mobile” :

Voir le lien github

Ici, on utilise la récupération de tâche dynamique en se basant sur le nom de la tâche. Comme la tâche generateFlavorBuildTypeAssets est une tâche générée au runtime par le plugin Android, on ne peut la référencer qu’après que le projet a été évalué. C’est pourquoi tout ce script est exécuté après l’évaluation du projet. On note qu’on ne lance la compilation du module :wear que si on est en train de faire une compilation de type “release”. Cela évite de polluer les compilations de debug utilisées quotidiennement par les développeurs.

L’étape n°3 consiste à générér le fichier res/xml/wear_desc.xml. La méthode la plus simple est de préparer ce fichier avec des clés à l’intérieur qui seront dynamiquement modifiées une fois que les ressources auront été copiées par le build.

On crée donc un fichier wear_desc.xml dans main/res/xml/. Ce fichier servira de squelette pour plus tard. Voici son contenu :

Voir le lien github

Dans le build.gradle de l’application mobile, créons une nouvelle tâche pour réécrire ce fichier quand il aura été copié dans dossiers intermédiaires. Cette tâche sera exécutée quand les ressources seront disponibles. On ajoute le code suivant à l’intérieur du if :

Voir le lien github

Cela va créer une tâche pour chaque output de la variant en cours afin d’avoir accès à l’API Output. Celle-ci nous évite d’avoir à hardcoder le chemin vers le répertoire des ressources dans le dossier intermédiaire de compilation. Pour récupérer la version et l’applicationId, on utilise variant.mergedFlavor. Cet objet est la fusion entre la variant en cours et la defaultConfig.

Pour appeler la tâche de réécriture du fichier XML, on indique que la tâche qui traite le Manifest dépend de notre tâche custom. On utilise la tâche de traitement de Manifest car cette tâche est exécutée après que les ressources ont été copiées dans le dossier intermédiaire.

Note : Avec Gradle, il existe 2 manières d’effectuer des actions à des instants donnés de la compilation. Soit on définit une tâche custom et on demande à une des tâches standards de compilation de dépendre de notre tâche custom (comme ici). Soit on demande à une tâche particulière de réaliser une action une fois que la tâche est terminée. Par exemple, à la place de faire output.processManifest.dependsOn(...), on aurait pu écrire :

variant.mergeResources << { // Ici on met l’action à réaliser après la tâche mergeResources. // On met donc le contenu de notre tâche custom }

Cette deuxième méthode semble plus propre car elle évite de créer des tâches customs et elle permet de mieux comprendre que notre action doit être réalisés une fois que les ressources sont prêtes. En revanche, comme le processus de compilation sur Android est optimisé, si les ressources de l’application ne changent pas, la tâche mergeResources ne sera pas appelée du tout. Notre action custom ne sera donc pas réalisée non plus. Par contre, même si le build se rend compte qu’il ne doit pas re-traiter le manifest, il va quand même appeler notre tâche custom.

On a maintenant un build de release qui va automatiquement lancer le build de release du module :wear, celui-ci va copier l’apk de sorti vers le dossier assets du module :mobile, le fichier wear_desc.xml va être édité pour contenir exactement les bonnes informations.

On en profite pour rajouter une dernière petite optimisation. On voudrait que le build supprime automatiquement les apk wear du dossier assets car on ne voudrait pas les commiter par inadvertance. On ajoute ces quelques lignes, qui ressemblent beaucoup à celles ajoutées dans le module :wear :

Voir le lien github

On note qu’une tâche Ant est utilisée car il n’est pas possible avec Gradle de supprimer des fichiers en utilisant les wildcards.

Il ne nous reste plus que l’étape n° 4 : construire l’application mobile !

>> ./gradlew clean mobile:assembleBrandARelease :mobile:clean :wear:clean :mobile:preBuild ... :wear:preBuild <- On build l’application Wear ... :wear:assembleBrandARelease <- Elle est prête Copying Wear APK inside mobile <- Elle est copiée dans les assets :mobile:generateBrandAReleaseAssets :mobile:mergeBrandAReleaseAssets <- Les assets sont mergés, l’apk wear avec ... :mobile:mergeBrandAReleaseResources <- Les ressources sont prêtes :mobile:handleBrandAReleaseWearDesc <- On écrit le fichier xml de description :mobile:processBrandAReleaseManifest ... :mobile:assembleBrandARelease <- L’apk mobile est près ! BUILD SUCCESSFUL

Enfin, il ne reste plus qu’à envoyer l’APK sur notre mobile pour vérifier que l’application est bien installée sur le wearable

>> adb -d install adb -d install mobile/build/outputs/apk/mobile-brandA-release.apk

On peut même construire l’application de la marque B pour vérifier que tout est bien dynamique.

>> ./gradlew mobile:assembleBrandBRelease … >> adb -d install adb -d install mobile/build/outputs/apk/mobile-brandB-release.apk

Et voici le résultat sur le wearable.

On a donc maintenant un build entièrement automatisé. Votre application mobile avec l’apk Wear à l’intérieur peut être compilée avec Jenkins sans problème et lors de la prochaine livraison sur le store, vos heureux utilisateurs possesseurs d’une montre Android Wear auront votre application Wear automatiquement installée sur leur device.

N’hésitez pas à customiser les différents scripts pour qu’ils s’adaptent exactement à vos besoins, car chaque application étant unique, chaque script de build l’est aussi.

Le code source du petit projet de test est disponible sur le dépôt Github suivant : https://github.com/pdegand/android-wear-packaging-sample. Vous êtes libre de récupérer les scripts si besoin.