URL: https://linuxfr.org/news/minipy-un-serveur-python-dans-son-android Title: minipy, un serveur Python dans son Android Authors: omc palm123, BAud, Davy Defaud, Benoît Sibaud, Cyprien et gUI Date: 2019年07月07日T10:46:50+02:00 License: CC By-SA Tags: android, ubuntu, minipy et python Score: 37 Minipy est une application Android dont [les sources](https://gitlab.com/damien.andre/minipy) sont distribuées sous [licence MIT](https://gitlab.com/damien.andre/minipy/blob/master/LICENSE). Cette application permet d’exécuter des petites commandes Python depuis son smartphone ou sa tablette Android. En elle‐même, cette application ne fait pas grand’chose et il faut bien reconnaître que son utilité est très limitée. Toutefois, son intérêt n’est pas dans le _pourquoi_, mais plutôt dans le *comment*. Cette dépêche va donc expliquer *comment* cette application fonctionne et va détailler les différentes étapes nécessaires à la création de cette application jusqu’à son empaquetage plutôt non conventionnel. Dans une seconde partie, cette dépêche va s’intéresser à l’architecture de Minipy qui permet de faire tourner « en local » un serveur Python [Tornado](https://www.tornadoweb.org/en/stable/) qui dialogue avec une page HTML (rendue par un [WebView](https://developer.android.com/reference/android/webkit/WebView)) via des [WebSockets](https://fr.wikipedia.org/wiki/WebSocket).  ---- [Code source de minipy sur GitLab](https://gitlab.com/damien.andre/minipy) [Fiche Google Play de minipy](https://play.google.com/store/apps/details?id=minipy.fr.test) [Bibliothèque Python Kivy ](https://kivy.org/#home) [Projet Buildozer](https://pypi.org/project/buildozer/) [Serveur Web léger Tornado ](https://pypi.org/project/tornado/) ---- Partie I : construire un APK _from scratch_ avec Kivy et Buildozer ================================================================= Il existe plusieurs solutions pour intégrer du Python dans une application Android. Parmi elles, le cadriciel Python [Kivy](https://kivy.org/) a l’avantage de proposer des [_widgets_](https://fr.wikipedia.org/wiki/Composant_d%27interface_graphique). Ces _widgets_ permettent de réaliser des interfaces graphiques avec une gestion multi‐plate‐forme : Windows, GNU/Linux, macOS et Android. ## Une simple application « hello world! » avec Kivy ## Sur la dernière Ubuntu 18.04 LTS, Kivy est disponible dans les paquets officiels de la distribution et s’installe très facilement avec un simple `sudo apt install python3-kivy`. L’installation peut ensuite être testée en réalisant une application « _hello world!_ » minimaliste telle que le montre le script suivant [(script tiré du wiki officiel)](https://en.wikibooks.org/wiki/Getting_Started_With_Kivy_Tutorial/Hello_World!) : ```python from kivy.app import App from kivy.uix.widget import Widget from kivy.uix.label import Label class Lesson1App(App): def build(self): lbl=Label(text='Hello World!') return lbl if __name__ == '__main__': Lesson1App().run() ``` Après avoir exécuté ce script, vous devriez voir une fenêtre graphique ressemblant à l’image suivante :  L’intérêt principal de Kivy est d’utiliser un code multi‐plate‐forme qui donnera le même rendu et le même comportement sur GNU/Linux, macOS et Android. ## Créer son APK avec Buildozer ## Une fois l’application réalisée, vient l’étape de création de l’[_Android application package_](https://en.wikipedia.org/wiki/Android_application_package) (APK). Pour aider à cette tâche plutôt ingrate, [Buildozer](https://pypi.org/project/buildozer/), un projet lui‐même basé sur [Python-For-Android](https://python-for-android.readthedocs.io/en/latest/) (souvent abrégé par _p4a_) permet de créer un APK sans ~~trop de~~ douleur. Buildozer est un script Python qui permet de : 1. télécharger et d’installer les API et SDK Android ; 2. télécharger les différentes sources des modules Python (ceux‐ci doivent être déclarés dans un fichier de configuration `buildozer.spec`) ; 3. effectuer une compilation croisée de ces sources pour les terminaux Android _(généralement pour les plates‐formes [armeabi-v7a](https://stackoverflow.com/questions/48339427/is-armeabi-32bit-or-64bit))_ ; 4. rédiger le [manifeste](https://developer.android.com/guide/topics/manifest/manifest-intro) (_manifest_) Android pour, finalement, créer l’APK correspondant. Essayons cela avec notre mini‐programme « _hello world!_ ». ## Installation de Buildozer ## Tout d’abord, il faut installer Buildozer. Celui‐ci peut être installé via `pip` avec la commande (testé sur Ubuntu 18.04 LTS) : ```bash sudo python3 -m pip install --upgrade buildozer ``` Pour la dernière Ubuntu LTS, la [documentation](https://buildozer.readthedocs.io/en/latest/installation.html) recommande aussi de faire les manipulations suivantes : ```bash sudo pip install --upgrade cython==0.28.6 sudo dpkg --add-architecture i386 sudo apt update sudo apt install build-essential ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 ``` _Attention, utiliser pip en mode "super utilisateur" n'est pas une pratique très recommandée, vous pouvez utiliser des méthodes plus sûres d'installation comme [expliquées ici](https://dev.to/elabftw/stop-using-sudo-pip-install-52mn)_ ## Création du fichier de config Buildozer ## Une fois l’installation de buildozer réalisée, vous devez vous placer dans le répertoire de votre script Python et lancer la commande `buildozer init`. Celle‐ci crée un fichier de configuration par défaut qui se nomme `buildozer.spec`. Votre répertoire courant doit donc maintenant contenir deux fichiers : ``` . ├── buildozer.spec └── main.py ``` _Notez que le script Python doit se nommer `main.py`._ Maintenant, il ne vous reste plus qu’à brancher votre appareil Android sur votre PC en activant le [mode de débogage Android](https://www.frandroid.com/comment-faire/tutoriaux/229753_questcequelemodedebogageusb), puis exécuter la commande : ```script buildozer android debug deploy run ``` _La première fois que vous exécutez cette commande, cela peut être assez long... Soyez patient !_ Cette commande va : 1. créer un APK Android en mode débogage (option `android debug`) ; 2. la déployer sur le téléphone/tablette Android (option `deploy`) ; 3. exécuter cet APK sur votre téléphone/tablette (option `run`). À l’issue de cette commande, vous devriez voir votre application s’exécuter sur votre appareil Android. De plus, votre répertoire courant doit maintenant contenir : ``` . ├── .buildozer ├── bin │ └── myapp-0.1-debug.apk ├── buildozer.spec └── main.py ``` Aux deux précédents fichiers `buildozer.spec` et `main.py`, ce sont rajoutés un dossier caché `.buildozer` qui contient tout un tas de fichiers (notamment, les sources des modules Python utilisés par `main.py`) et un nouveau dossier `bin` dans lequel se trouve votre APK. Maintenant, si vous souhaitez que Monsieur et Madame Michu puissent utiliser votre application, vous voulez sans doute la distribuer sur [Google Play](https://play.google.com/). Or, en l’état, cet APK ne peut pas y être déposée. La partie suivante montre comment rendre l’APK compatible avec ggplay. ## Rendre l’APK compatible avec Google Play ## _Cette partie traite d’un service privateur. Pour distribuer votre application, vous pouvez vous tourner aussi vers des magasins d’applications alternatifs ouverts et respectueux de votre vie privée, tels que [F‐Droid](https://www.f-droid.org:/)._ Tout d’abord, il va falloir créer un APK en mode _release_. Pour cela, il faut d’abord changer la valeur par défaut du champ `package.domain` du fichier `buildozer.spec`, puis lancer buildozer avec la commande : ``` buildozer android release ``` À ce stade, il semble y avoir une erreur ! Buildozer cherche à copier l’APK depuis un sous‐dossier `release-unsigned` alors que celui-ci se trouve dans un dossier nommé `release`. Nous allons donc copier à la main ce fichier dans le répertoire `bin` avec la commande : ``` cp .buildozer/android/platform/build/dists/myapp/build/outputs/apk/release/myapp-release-unsigned.apk ./bin ``` L’APK `myapp-release-unsigned.apk` se trouve maintenant dans le répertoire `bin`. À présent, nous allons créer une clé pour signer cette application grâce à _keytool_, disponible dans les [_security tools_ d’OpenJDK](https://openjdk.java.net/tools/index.html) avec la commande : ``` keytool -genkey -v -keystore my-app.keystore -alias cb-play -keyalg RSA -keysize 2048 -validity 10000 ``` Aprés avoir répondu à différentes questions, cette commande crée un fichier `my-app.keystore` qui contient votre clé de chiffrement. Le programme _jarsigner_, disponible également dans les [_security tools_ d’OpenJDK](https://openjdk.java.net/tools/index.html), va nous servir à signer l’APK avec la commande : ``` jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ./my-app.keystore ./bin/myapp-release-unsigned.apk cb-play ``` Une dernière étape consiste maintenant à « [zipaligner](https://developer.android.com/studio/command-line/zipalign) » votre application. Pour trouver l’exécutable `zipalign`, il faut un peu fouiner dans les entrailles de buildozer. Chez moi, celui‐ci se trouve dans le dossier `~/.buildozer/android/platform/android-sdk/build-tools/29.0.0`. Pour « zipaligner » l’APK, il suffit de lancer la commande : ``` ~/.buildozer/android/platform/android-sdk/build-tools/29.0.0/zipalign -v 4 ./bin/myapp-release-unsigned.apk ./bin/myapp.apk ``` **Et Voilà !** Le fichier `myapp.apk` se trouvant dans le répertoire `bin` peut être déposé sur Google Play. Cette première partie a permis d’introduire très brièvement Kivy et le cycle de création d’un APK grâce à Buildozer et aux _security tools_ d’OpenJDK. La partie suivante propose de revenir sur minipy en détaillant son architecture particulière _client Web_ / _serveur Python_. Partie II : retour sur minipy ============================== Un défaut majeur de Kivy est son rendu plutôt simpliste, peu compatible avec les critères esthétiques des applications d’aujourd’hui. Par exemple, l’image suivante (issue de la doc de Kivy) montre une fenêtre de connexion. Vous en conviendrez, c’est plutôt ~~moche~~ minimaliste :  L’idée est donc de transformer notre application en WebApp, c’est‐à‐dire une application dont la partie graphique sera rendue par des pages Web via des [WebView](https://developer.android.com/guide/webapps/webview). En effet, les technologies HTML5, CSS et JavaScript disposent de bibliothèques comme [Bootstrap](https://getbootstrap.com/) qui permettent des rendus de grande qualité graphique avec un minimum d’effort. Aussi, la communication entre Python et les pages Web sera assurée par un serveur Python léger du nom de [Tornado](https://www.tornadoweb.org/en/stable/) qui permet de prendre en charge le protocole standard [WebSocket](https://en.wikipedia.org/wiki/WebSocket) largement utilisé dans les technologies Web. Voici un petit schéma qui synthétise l’architecture retenue : ``` !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! APK ANDROID ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! PYTHON WEBVIEW ! ! ############## ############## ! ! # tornado # websocket # html # ! ! # numpy # <-------> # css # ! ! # etc. # # javascript # ! ! ############## ############## ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ``` ## Une WebApp pour s’affranchir des widgets Kivy ## Minipy montre comment implémenter cette architecture « serveur Python‐client Web". À ce titre, c’est un simple « démonstrateur » qui se contente d’illustrer cette solution et rien de plus ! Aussi, l’utilisation de minipy consiste simplement à : 1. entrer des commandes Python dans le client HTML ; 2. ces commandes sont ensuite envoyées au serveur Tornado via des WebSocket du client vers le serveur ; 3. le serveur Python interprète ces commandes puis retourne le résultat via (encore) des WebSockets, mais cette fois du serveur vers le client ; 4. le client Web reçoit le résultat (via un événement JavaScript) et affiche ce résultat dans une fenêtre modale. Les deux captures d’écran suivantes montrent minipy en action :  ## Exécuter minipy sur GNU/Linux ## Si vous clonez minipy, avec la commande `git clone https://gitlab.com/damien.andre/minipy.git`, vous verrez apparaître l’arborescence de fichiers : ```bash ├── apk │ └── minipy-0.102.apk ├── LICENSE ├── README.md └── src ├── buildozer.spec # fichier de config buildozer ├── data # dossier contenant quelques fichiers pour Android dont : │ ├── logo.png # l’icône de l’app dans le menu Android │ ├── logo.svg # le source SVG │ ├── splash.png # l’image « splash screen » affichée au lancement de l’app │ └── splash.svg # le source SVG ├── html # dossier contenant les sources pour le client HTML dont : │ ├── favicon.png # l’image favicon │ ├── header.png # l’image d’en‐tête │ ├── header.svg # le source SVG │ ├── minipy.css # le fichier CSS │ ├── minipy.html # le fichier HTML │ ├── minipy.js # le fichier JavaScript │ └── vendor # le dossier contenant des bibliothèques tierces dont : │ ├── jquery-ui.min.js # jQuery │ └── waitingfor.js # bibliothèque pour la boîte de dialogue « patientez » └── main.py # le serveur Python ``` Le démarrage de minipy consiste simplement à exécuter le script `main.py`. Aussi, il suffit de faire : ``` cd src python3 ./main.py ``` _Dans le cas d’Android, notez que le script `main.py` est exécuté automatiquement._ ## Autopsie de la partie serveur `main.py` ## Le script `main.py` doit assurer les fonctions suivantes : 1. implémenter le serveur Tornado (c’est‐à‐dire paramétrer son comportement) ; 2. exécuter le serveur Tornado dans un fil d’exécution séparé ; 3. exécuter le client Web dans un fil d’exécution séparé. L’objet de cette dépêche n’est pas l’implémentation d’un serveur Tornado. Aussi, il ne sera pas abordé en détail le point n^(o) 1. En revanche, les points n^(os) 2 et 3 nécessitent quelques astuces qui vont être détaillées ici. Tout se passe dans le constructeur `Wv` invoqué au démarrage du script. `Wv` est une classe qui dérive de [Kivy.Widget](https://kivy.org/doc/stable/api-kivy.uix.widget.html). C’est une astuce car, en fait, le _widget_ n’affiche rien. Aussi, le constructeur donne : ```python class Wv(Widget): def __init__(self, **kwargs): self.f2 = self.create_webview # sans ça... c’est le bogue ! super(Wv, self).__init__(**kwargs) # invoque le constructeur parent self.visible = False # le widget n’affiche rien WebServer().start() # serveur tornado (voir plus loin) if platform == 'android': # on démarre les clients... Clock.schedule_once(self.create_webview, 0) # ... Android else: Clock.schedule_once(self.open_web_browser, 0) # ... autres qu’Android ``` ### Démarrage automatique du client Web ### Ici, il est nécessaire de traiter à part le cas d’Android, car le client WebView doit être démarré dans le fil d’exécution graphique principal : ```python ## class Wv(Widget): ## ... ... ... ... @run_on_ui_thread def create_webview(self, *args): webview = webView(activity) webview.getSettings().setJavaScriptEnabled(True) wvc = webViewClient(); webview.setWebViewClient(wvc); activity.setContentView(webview) file_path = "file:///" + getcwd() + "/html/minipy.html" webview.loadUrl(file_path) ``` Ici, le décorateur [`@run_on_ui_thread`](https://gist.github.com/tito/5844528) est utilisé afin de créer un objet [WebView](https://developer.android.com/reference/android/webkit/WebView) de l’API d’Android qui pointe vers le fichier `minipy.html`. Le WebView est créé grâce à [PyJnius](https://pyjnius.readthedocs.io/en/stable/), une bibliothèque Python qui sert de passerelle pour manipuler des classes et objets Java. Ce sera ici la seule ~~compromission avec~~ utilisation du langage Java. En revanche, si minipy est exécutée sur une plate‐forme autre qu’Android, le démarrage du client Web est réalisé simplement grâce au module `webbrowser` selon : ```python ## class Wv(Widget): ## ... ... ... ... def open_web_browser(self, *args): import webbrowser webbrowser.open_new_tab('./html/minipy.html') ``` ### Implémentation du serveur Tornado ### La ligne `WebServer().start()` du constructeur de `Wv` permet de créer et d’exécuter le serveur Tornado dans un nouveau fil d’exécution (non graphique, cette fois). Pour cela, la classe `WebServer` dérive de la classe [`Thread`](https://docs.python.org/3/library/threading.html#threading.Thread) et implémente la méthode `run(self)`. Cette méthode démarre le serveur Tornado qui écoute des WebSockets sur le port 9999 : ```python class WebServer(Thread): def __init__(self): super(WebServer, self).__init__() def run(self): Logger.info("Awaiting connection on port 9999") asyncio.set_event_loop(asyncio.new_event_loop()) application = tornado.web.Application([(r'/ws', EchoWebSocket),]) application.listen(9999) tornado.ioloop.IOLoop.instance().start() ``` Le comportement du serveur Tornado est défini au travers de la classe `EchoWebSocket`, qui dérive de [`websocket.WebSocketHandler`](https://www.tornadoweb.org/en/stable/websocket.html). Cette classe surcharge les méthodes `open(self)`, `on_message(self, message)`, `on_close(self)` et `check_origin(self, origin)` : ```python class (websocket.WebSocketHandler): def open(self): # lorsqu’une connexion s’ouvre ... def on_message(self, message): # lorsqu’un message du client arrive ... def on_close(self): # lorsque la connexion s’arrête ... def check_origin(self, origin): # pour certifier l’origine de la connexion return True # toujours vrai car tout se passe en local ``` Ces méthodes sont exécutées lors des différents événements énumérés ci‐dessus. Il suffit donc de surcharger ces méthodes pour obtenir les comportements souhaités. Dans le cas de minipy, le plus intéressant se passe dans la méthode `on_message(self, message)`, dont voici un aperçu : ```python3 def on_message(self, message): json_data = json.loads(message) if (json_data['cmd'] == 'interpret'): res = str(eval(json_data['content'], self.global_env, self.local_env)) msg = "your cmd '{}' gives '{}'".format(json_data['content'], res) self.send_msg(msg) def send_msg(self, msg): json_data = {} json_data['cmd'] = 'msg' json_data['content'] = msg self.write_message(json.dumps(json_data)) ``` Dans le cas de minipy, les messages venant des WebSockets sont encodés et décodés via [JSON](https://fr.wikipedia.org/wiki/JavaScript_Object_Notation "JavaScript Object Notation"), un format qui a l’avantage d’être très bien géré par Python et JavaScript. Dans minipy, le choix a été fait d’utiliser la clé `cmd` pour spécifier le type de comportement voulu par le client. Si le champ associé à la clé `cmd` est la chaîne de caractères `interpret`, alors : 1. minipy va interpréter en Python (avec la fonction interne `eval()`) le message envoyé par le client (champ associé à la clé `content`) ; 2. puis le résultat `res` est renvoyé au client (toujours au format JSON) grâce à la méthode `write_message` héritée de la classe parente `websocket.WebSocketHandler`. Voici donc un bref aperçu de la partie serveur, la suite concerne maintenant le client Web et plus particulièrement le code JavaScript permettant d’implémenter les WebSockets. ## Le client JavaScript `minipy.js` Lors du chargement de la page Web `minipy.html`, le fichier JavaScript `minipy.js` est automatiquement exécuté. Celui‐ci tient en quelques lignes dont voici un aperçu : ```javascript // création d’un websocket lors du chargement de la page var ws = new WebSocket("ws://127.0.0.1:9999/ws"); // lorsqu’un message arrive ws.onmessage = function(e){ json = JSON.parse(e.data); if(json["cmd"]=="msg") { $('#modal-msg-content').html(json["content"]); $('#modal-msg').modal('show'); } } ``` Ici, le traitement des messages suit la même procédure que pour Python. Le message encodé en JSON doit contenir la clé `cmd`. Si le champ associé à cette clé est `msg` alors une [fenêtre modale Bootstrap](https://getbootstrap.com/docs/4.0/components/modal/) apparaît. Finalement, le contenu de cette fenêtre modale a été préalablement défini par le champ associé à la clé `content` du message JSON (voir partie serveur). Conclusion ========== Si vous êtes un « ~~fanatique~~ enthousiaste de Python » et que vous voulez tout faire avec ce langage, alors cette dépêche vous donne quelques clés pour développer et déployer votre application Android codée en Python grâce à Kivy et Buildozer. Toutefois, le cadriciel Kivy pêche un peu par son rendu graphique. Aussi, minipy propose une solution originale pour contrecarrer ce problème : le rendu graphique est déporté grâce à un client Web, tout en conservant Python pour la partie serveur. En résumé, cette dépêche montre et détaille les quelques étapes clés permettant de coder des applications Android au design sympa grâce aux technologies Web et avec votre langage préféré... _a.k.a_ Python !