La façon normale d’avoir un écran d’accueil Android programmable, c’est de garder le launcher livré avec le téléphone et de lui greffer sur le côté une appli d’automatisation genre Tasker. Le combo est mature, il est sur le Play Store, il a dix ans de recettes de la communauté derrière lui et, pour la plupart des gens, il fait le boulot sans que personne ait à toucher à un build system.
Moi, j’ai pris l’autre direction et j’ai construit Dikciz, un launcher qui est lui-même la plateforme d’automatisation, pas un truc que l’automatisation vient tripoter de l’extérieur. Des widgets HTML avec un bridge JavaScript, des scripts Lua sandboxés qui réagissent aux événements du téléphone, un WebSocket et un serveur MCP sur le loopback de l’appareil, et tout ça configuré par de simples fichiers dans /sdcard/Dikciz/. Voilà comment les deux approches se comparent pour de vrai, un point à la fois.
Sur Quoi C’est Construit
Un launcher d’origine, c’est le produit fermé de quelqu’un, et l’appli d’automatisation par-dessus, c’est celui de quelqu’un d’autre. Tu te tapes deux calendriers de release et aucun des deux ne sait que l’autre existe.
Dikciz part de Fossify Launcher, GPLv3, parce qu’écrire un launcher de zéro, c’est se noyer dans des cas limites chiants autour des icônes, des drawers et du rôle d’accueil d’Android. Toute la source Fossify reste intacte comme base upstream. Le code Dikciz vit isolé dans un product flavour Gradle sous app/src/dikciz/ (plus des source sets combinés comme app/src/dikcizDebug/), donc les sources, les ressources et les traductions de Fossify restent remplaçables. Une release Fossify plus récente arrive par un chemin contrôlé:
make dikciz-upstream-compare DIKCIZ_UPSTREAM_DIR=path/to/fossify-source
make dikciz-upstream-refresh-dry-run DIKCIZ_UPSTREAM_DIR=path/to/fossify-source
DIKCIZ_UPSTREAM_REFRESH=1 make dikciz-upstream-refresh \
DIKCIZ_UPSTREAM_DIR=path/to/fossify-sourceÇa met la source de côté, vérifie une sauvegarde, préserve les chemins qui appartiennent à Dikciz et s’arrête net sur une collision ou un patch raté, et ça refuse de remplacer quoi que ce soit si le marqueur de la release Fossify choisie n’est pas plus récent que celui du repo. Pas de merde du genre “on merge et on croise les doigts”.
Le build que tu fais vraiment tourner, c’est dikcizDebug, package eu.psyb0t.dikciz.launcher.debug. C’est lui qui garde allumés les control planes, la config publique, les logs, le bridge HTML et toute la surface d’automatisation.
Où Vit la Config
Avec un launcher d’origine, ton écran d’accueil est une base de données privée que tu ne vois jamais. Tu as droit à un écran de réglages, et l’appli d’automatisation garde ses profils à elle, dans son format à elle. Faire un diff de l’un ou de l’autre, le sauvegarder en texte ou le générer depuis un script, ça va de pénible à impossible.
Dans Dikciz, tout ce que l’utilisateur peut éditer vit sous /sdcard/Dikciz/, avec config.json comme racine. Pages, widgets, scripts, politiques d’automatisation, thèmes, polices, fond d’écran, paquets de la bibliothèque de widgets, config d’auth et logs ont chacun leur chemin. Dikciz surveille l’arborescence et applique les modifs valides sans redémarrer l’appli. Tu pousses un fichier par ADB et l’accueil change. Mets tout ça dans git si ça te chante.
La config fournie embarque des limites dures pour qu’une édition faite avec les pieds ne bouffe pas le téléphone: 24 pages max, 128 widgets par page, 8 widgets HTML par page, 1 MiB de config, 192 KiB par document de widget HTML. Si un truc part quand même en couille, le safe mode ouvre la première page fournie sans écraser ta config sauvegardée, garde les diagnostics et les logs accessibles, et bloque les modifs de l’accueil et les changements sur l’appareil jusqu’à ce que tu règles le problème.
Agencer l’Écran d’Accueil
Les launchers d’origine te donnent une grille et un geste de glisser. Très bien pour des humains, inutile pour tout ce qui veut désigner un endroit de l’écran par son nom.
Les pages Dikciz sont posées sur une grille à deux axes et adressées par coordonnées: 1H1V, c’est l’accueil, 2H1V, une à droite, 1H2V, une en dessous. Chaque page est une grille native avec un viewport fixe qui ne grandit jamais. Tu ajoutes un widget, il prend la première case libre, de gauche à droite puis vers le bas. Page pleine? L’ajout est refusé. Pas de chevauchement, pas d’icônes poussées en douce hors de l’écran.
Ce qu’on peut mettre sur une page:
- Des widgets HTML avec HTML, CSS, JavaScript locaux et état persistant, redimensionnables, avec des actions natives à l’appui long et une hauteur ajustée au contenu.
- Des raccourcis d’applis natifs et des groupes d’applis. Tu lâches un raccourci sur un autre et ça fait un groupe.
- De vrais AppWidgets Android tiers. Ils gardent leur propre
AppWidgetHostViewet ne sont jamais fourrés dans une WebView. - Un dashboard de scripts natif qui liste tes scripts Lua avec statut, dernière exécution, logs, et des boutons pour activer, désactiver et éditer.
Thèmes, polices (Inter, Space Grotesk, JetBrains Mono et Comic Neue sont fournies, évidemment), fond d’écran, styles de cartes et verrous complètent le tout. Le logo Dikciz à côté du rail de pages ouvre le drawer d’applis, l’ajout à la page, la gestion de la page et les réglages. La loupe à côté fait la recherche de pages, en listant chaque page sous la forme Name (address), par exemple Notes (2H1V). Le drawer d’applis est natif, pas une WebView, et ses actions couvrent l’ajout de raccourci, les infos de l’appli, la désinstallation et le force stop, qui n’apparaît que si l’appareil accorde le root.
Écrire Ton Propre Widget
Côté stock, un widget perso, ça veut dire soit écrire une appli Android complète avec un AppWidgetProvider et des RemoteViews, soit installer encore une appli de création de widgets avec son propre éditeur. Les deux marchent. Les deux sont une putain de corvée pour une carte qui affiche trois chiffres.
Un widget Dikciz, c’est une page web. Chaque widget HTML reçoit window.dikciz, window.selfWidget et un événement dikciz-ready. À partir de là, il peut:
- appeler n’importe quelle commande du launcher avec
dikciz.command(type, fields), les mêmes commandes qu’utilisent les clients WebSocket et MCP - lancer des actions typées via
dikciz.dispatch({ actions }) - lire la carte des pages avec
dikciz.pages()etdikciz.home() - patcher l’état d’un autre widget avec
dikciz.patchWidgetState(address, values) - modifier le DOM rendu sans l’enregistrer, via
dikciz.patchDom(address, selector, values) - publier des événements et s’y abonner avec
dikciz.emitEventetdikciz.onEvent
const result = await dikciz.patchDom(
"1H1V-device-card",
"#temperature",
{
text: "18 C",
className: "fresh",
attributes: { "data-updated": "now" }
},
);Les patches d’état mettent à jour le document existant. Ils ne rechargent pas la WebView, donc le widget ne clignote pas comme l’enseigne d’un motel miteux à chaque fois qu’une valeur change.
Réagir aux Événements du Téléphone
C’est là que l’appli d’automatisation greffée mérite sa réputation. Profils, contextes, tâches, une énorme bibliothèque de déclencheurs. Son point faible, c’est que le launcher n’a aucune idée que tout ça existe, donc l’automatisation tripote l’écran d’accueil de l’extérieur en espérant que le résultat ait la bonne gueule.
Dans Dikciz, la gestion des événements fait partie du launcher. Lua n’est pas un type de widget, il tourne sous forme de scripts depuis l’arborescence publique des scripts, sur LuaJ. Un script reçoit un événement et renvoie un statut plus une liste d’actions typées:
function on_event(event)
if event.type ~= "example.pipeline.request" then
return { status = "Waiting for a pipeline request" }
end
local message = event.payload.message or "Pipeline request received"
return {
status = "Updated the widget",
actions = {
{
type = "patchWidget",
widgetAddress = "1H1V-example-widget",
values = { state = { message = message } }
},
{
type = "emitEvent",
event = "example.pipeline.done",
payload = { message = "Lua finished: " .. message }
}
}
}
endLa liste des événements est longue comme un jour sans pain: heure, alarmes, batterie, charge, état thermique, écran allumé et éteint, connectivité, Bluetooth, capteurs, localisation, nombre de pas Health Connect, agenda, contacts, état du téléphone, SMS entrants, presse-papiers, notifications publiées et supprimées, sessions média, fenêtres d’accessibilité, état device admin, changements de widgets, déclencheurs manuels et n’importe quel événement custom que tu inventes.
Les actions aussi sont typées: selectPage, patchWidget, patchState, patchDom, launchApp, appAction, postNotification, sendSms, explicitIntent, emitEvent, mediaControl, mediaVolume, notificationControl, lockDevice et d’autres. Comme le script parle directement au launcher, patchWidget tombe sur un widget nommé à une adresse nommée, pas sur une approximation de “le machin en haut à gauche”.
Jusqu’où un Script Peut Tout Casser
Les applis d’automatisation ont tendance à filer à une tâche un gros tas de pouvoir: lancer du shell, écrire des fichiers, balancer des intents arbitraires. Pratique, et c’est aussi exactement comme ça qu’une recette copiée sur un forum finit par faire un truc que tu n’as jamais lu.
Dikciz est plus radin, exprès. Chaque action et chaque capacité d’événement qu’un script utilise doit être listée dans la politique d’automatisation publique de ce script, et le runtime Lua est mis en cage. require, package, load, loadfile, dofile, debug et print ont sauté. Pas de shell, pas de filesystem, pas d’objets Android bruts. Des actions typées ou tu dégages.
Les logs atterrissent en JSON Lines bornés sous /sdcard/Dikciz/logs/, lisibles depuis le dashboard, le viewer des Settings, le WebSocket scriptLogs et le MCP dikciz_script_logs. Les valeurs qui ont une tête d’identifiants sont masquées avant d’arriver dans le fichier. Une exécution Lua ratée peut poster une notification Android, avec une période de silence par script pour qu’elle ne te spamme pas jusqu’à la mort.
Laisser un Agent IA Prendre le Volant
C’est pour ça que j’ai vraiment construit le truc. Confie un launcher d’origine à un agent LLM et la réponse habituelle, c’est “fais une capture, devine les coordonnées, tape, prie”. Ça, c’est pas de l’automatisation, c’est un mec bourré avec un stylet.
Dikciz fait tourner deux listeners versionnés sur le loopback de l’appareil:
- WebSocket sur
ws://127.0.0.1:19001/v1/automation - Streamable HTTP MCP sur
http://127.0.0.1:19002/mcp
Les deux tapent sur la même cible de commandes: config, snapshots sémantiques de l’UI et interaction, pages, widgets, captures d’écran, dumps d’UI, diagnostics, logs de scripts, le catalogue d’applis, les actions d’applis, les intents Android et les commandes shell. Le côté MCP expose des outils comme dikciz_snapshot, dikciz_tap, dikciz_find, dikciz_wait_for, dikciz_screenshot, dikciz_app_catalogue et dikciz_shell. Branche un agent dessus et l’agent inspecte le launcher sémantiquement au lieu de deviner des pixels.
Les commandes shell demandent su par défaut. Passe root: false et elles tournent sous l’utilisateur Android propre à l’appli Dikciz. Que le root existe et soit accordé, c’est le téléphone qui décide, pas moi.
Active le service d’Accessibilité de Dikciz et Dikciz va aussi au-delà de son propre écran d’accueil: les clients locaux reçoivent un arbre borné de la fenêtre active de n’importe quelle appli, plus accessibilityAction, accessibilityGesture et accessibilityGlobalAction. Lua reçoit la même fenêtre sous context.accessibility.
Comme “n’importe quoi sur le loopback peut piloter mon téléphone” est un défaut débile pour un appareil que tu trimballes partout, tu peux exiger un mot de passe bearer pour les deux control planes depuis les Dikciz Settings. La config publique stocke un vérificateur bcrypt, jamais le mot de passe en clair.
Le repo fournit aussi un skill d’agent dikciz-launcher qui apprend à un agent à installer l’APK par ADB et à piloter le launcher, avec des références de setup et de capacités, et un skill séparé dikciz-android-lab qui envoie les builds depuis la source et le boulot sur l’émulateur vers android-lab. Fais tourner ton agent dans un truc comme aicodebox et la boucle est bouclée: l’agent lit le skill, se connecte à l’endpoint MCP, pilote ton téléphone.
L’Installer sur un Téléphone
Là, le combo d’origine gagne haut la main: tu appuies sur installer dans le Play Store et c’est fini. Dikciz ne passe pas par Google Play et n’y passera jamais. Tu le compiles toi-même.
Je n’installe pas Android Studio. Jamais. Le build et l’émulateur tournent sur android-lab, mon setup Docker pour le SDK Android et l’émulateur. Le Makefile veut les images android-lab:0.13.0 et android-lab:0.13.0-emulator-api36 construites en local, et s’il en manque une, il s’arrête et te renvoie aux instructions de build d’android-lab au lieu de télécharger de la merde dans ton dos.
git clone https://github.com/psyb0t/dikciz-launcher.git
cd dikciz-launcher
make build
make dikciz-runmake build assemble dikcizDebug. Les conteneurs d’outils prennent le nombre de CPU de l’hôte Docker, plafonné à 10, avec autant de workers Gradle. Sur une machine plus faiblarde:
make build ANDROID_LAB_TOOL_CPU_LIMIT=2 ANDROID_LAB_GRADLE_MAX_WORKERS=2make dikciz-run l’installe sur l’émulateur partagé, accorde l’accès aux fichiers, le définit comme Android Home et redirige les control planes vers l’hôte sur 19101 (WebSocket) et 19102 (MCP). L’écran de l’émulateur est dans ton navigateur à:
http://127.0.0.1:61326/vnc.html?autoconnect=true&resize=scaleEnsuite, tu lui parles avec le client fourni:
make control CONTROL_ARGS='websocket controlStatus --port 19101 --pretty'
make control CONTROL_ARGS='mcp-tools --port 19102 --pretty'
make control CONTROL_ARGS='websocket scriptLogs --port 19101 --pretty'Si tu as activé l’auth, exporte DIKCIZ_REMOTE_BEARER_TOKEN depuis un fichier env ignoré. Ne le mets pas en argument de commande comme un sauvage. make dikciz-reset rase uniquement Dikciz et /sdcard/Dikciz puis démarre l’accueil de départ. make stop arrête la stack sans supprimer le téléphone sauvegardé. L’état (cache Gradle, identité de signature, disque de l’émulateur, artefacts) vit dans .android-lab/.
Les tests sont des tests d’UI sur ce même émulateur, avec des cibles étroites comme make dikciz-test-accessibility ou make dikciz-test-app-drawer-entry, et make dikciz-test pour la suite large, qui balance captures et dumps d’UI dans .android-lab/shared/artifacts/ et remet le téléphone sur l’accueil de départ à la fin, que ça passe ou que ça casse.
Quand Garder le Launcher d’Origine et Tasker
Si tu veux un téléphone qui marche le jour où tu l’achètes, qui s’installe depuis le Play Store et qui fait de l’automatisation via une GUI avec des années de recettes partagées derrière, garde le launcher d’origine et l’appli greffée. Pareil si tu ne comptes jamais éditer un fichier de config, écrire une ligne de Lua ou brancher un agent sur ton téléphone. Dikciz te demande de le compiler toi-même dans Docker, et ça ne vaut le coup que si tu veux que ton écran d’accueil soit des fichiers, du code, un socket et un serveur MCP.
Si cette dernière partie ressemble à ce qui te manquait, c’est sur github.com/psyb0t/dikciz-launcher.