De normale manier om een programmeerbaar Android-homescreen te krijgen: je houdt de launcher die bij de telefoon zat en je schroeft er een automatiseringsapp als Tasker naast. Die combo is volwassen, staat in de Play Store, heeft tien jaar communityrecepten achter zich en doet voor de meeste mensen gewoon z’n werk zonder dat iemand aan een buildsysteem hoeft te zitten.
Ik ging de andere kant op en bouwde Dikciz, een launcher die zelf het automatiseringsplatform is in plaats van iets waar automatisering van buitenaf in zit te porren. HTML-widgets met een JavaScript-bridge, Lua-scripts in een sandbox die reageren op telefoonevents, een WebSocket en een MCP-server op de loopback van het device, en alles geconfigureerd via platte bestanden in /sdcard/Dikciz/. Zo verhouden de twee aanpakken zich echt, punt voor punt.
Waar Het op Gebouwd Is
Een stock launcher is het gesloten product van de een, en de automatiseringsapp erbovenop is dat van een ander. Je zit vast aan twee releaseschema’s en geen van beide weet dat de ander bestaat.
Dikciz is gebaseerd op Fossify Launcher, GPLv3, want een launcher vanaf nul schrijven betekent verzuipen in saaie edge cases rond app-iconen, drawers en de home-rol van Android. De volledige Fossify-broncode blijft intact als upstream-basis. Alle Dikciz-code leeft geïsoleerd in een Gradle-product flavour onder app/src/dikciz/ (plus gecombineerde source sets zoals app/src/dikcizDebug/), zodat de eigen bronnen, resources en vertalingen van Fossify vervangbaar blijven. Een nieuwere Fossify-release komt binnen via een gecontroleerde route:
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-sourceDe route zet de broncode klaar, controleert een backup, laat de paden van Dikciz met rust en stopt meteen bij een botsing of een mislukte patch, en weigert iets te vervangen zolang de releasemarker van de gekozen Fossify-release niet nieuwer is dan de ingecheckte. Geen “mergen en hopen”-gezeik.
De build die je echt draait is dikcizDebug, package eu.psyb0t.dikciz.launcher.debug. Dat is degene die de control planes, de publieke config, de logs, de HTML-bridge en het hele automatiseringsoppervlak aan laat staan.
Waar de Config Woont
Bij een stock launcher is je homescreen een privédatabase die je nooit te zien krijgt. Je krijgt een instellingenscherm, en de automatiseringsapp houdt z’n eigen profielen bij in z’n eigen formaat. Een van de twee diffen, als tekst back-uppen of uit een script genereren zit ergens tussen omslachtig en onmogelijk.
In Dikciz staat alles wat de gebruiker kan bewerken onder /sdcard/Dikciz/, met config.json als root. Pagina’s, widgets, scripts, automatiseringspolicies, thema’s, fonts, wallpaper, pakketten uit de widgetbibliotheek, auth-config en logs krijgen elk hun eigen pad. Dikciz houdt de boom in de gaten en past geldige wijzigingen toe zonder de app te herstarten. Push een bestand via ADB en het homescreen verandert. Zet de boom in git als je daar zin in hebt.
De meegeleverde config heeft harde limieten, zodat een verneukte edit je telefoon niet opvreet: max 24 pagina’s, 128 widgets per pagina, 8 HTML-widgets per pagina, 1 MiB config, 192 KiB per HTML-widgetdocument. Gaat er toch iets naar de klote, dan opent safe mode de meegeleverde eerste pagina zonder je opgeslagen config te overschrijven, houdt diagnostiek en logs bereikbaar en blokkeert homescreen-bewerkingen en wijzigingen aan het device tot je het opgelost hebt.
Het Homescreen Indelen
Stock launchers geven je een grid en een sleepgebaar. Prima voor mensen, nutteloos voor alles wat een plek op het scherm bij naam wil aanspreken.
Dikciz-pagina’s liggen op een grid met twee assen en worden geadresseerd per coördinaat: 1H1V is home, 2H1V is er een naar rechts, 1H2V is er een naar beneden. Elke pagina is een native grid met een vaste viewport die nooit groeit. Voeg je een widget toe, dan pakt die de eerste vrije cel, van links naar rechts en dan naar beneden. Pagina vol? Dan wordt het toevoegen geweigerd. Geen overlap, geen iconen die stiekem van het scherm geduwd worden.
Wat er op een pagina kan:
- HTML-widgets met lokale HTML, CSS, JavaScript en persistente state, schaalbaar, met native long-press-acties en een hoogte die zich aan de inhoud aanpast.
- Native app-snelkoppelingen en app-groepen. Sleep de ene snelkoppeling op de andere en je hebt een groep.
- Echte Android-AppWidgets van derden. Die houden hun eigen
AppWidgetHostViewen worden nooit in een WebView gepropt. - Een native scriptdashboard dat je Lua-scripts toont met status, laatste run, logs en knoppen om ze aan te zetten, uit te zetten en te bewerken.
Thema’s, fonts (Inter, Space Grotesk, JetBrains Mono en Comic Neue zitten erbij, uiteraard), wallpaper, kaartstijlen en locks maken het af. Het Dikciz-logo naast de paginabalk opent de app-drawer, toevoegen aan pagina, pagina beheren en de instellingen. Het vergrootglas ernaast doet de paginazoekfunctie en toont elke pagina als Name (address), zoals Notes (2H1V). De app-drawer is native, geen WebView, en z’n acties dekken snelkoppeling toevoegen, app-info, verwijderen en force stop, die alleen verschijnt als het device root geeft.
Je Eigen Widget Schrijven
Aan de stock-kant betekent een eigen widget óf een complete Android-app schrijven met een AppWidgetProvider en RemoteViews, óf nog maar een widgetbouwapp installeren met z’n eigen editor. Beide werken. Allebei zijn ze een kuttaak voor een kaartje dat drie getallen laat zien.
Een Dikciz-widget is een webpagina. Elke HTML-widget krijgt window.dikciz, window.selfWidget en een dikciz-ready-event. Van daaruit kan hij:
- elk launchercommando aanroepen met
dikciz.command(type, fields), dezelfde commando’s die WebSocket- en MCP-clients gebruiken - getypeerde acties uitvoeren via
dikciz.dispatch({ actions }) - de paginakaart lezen met
dikciz.pages()endikciz.home() - de state van een andere widget patchen met
dikciz.patchWidgetState(address, values) - gerenderde DOM aanpassen zonder die op te slaan, via
dikciz.patchDom(address, selector, values) - events publiceren en erop abonneren met
dikciz.emitEventendikciz.onEvent
const result = await dikciz.patchDom(
"1H1V-device-card",
"#temperature",
{
text: "18 C",
className: "fresh",
attributes: { "data-updated": "now" }
},
);State-patches werken het bestaande document bij. Ze herladen de WebView niet, dus de widget knippert niet als het neonbord van een louche motel elke keer dat er een waarde verandert.
Reageren op Telefoonevents
Hier verdient de vastgeschroefde automatiseringsapp z’n reputatie. Profielen, contexten, taken, een gigantische bibliotheek aan triggers. Het zwakke punt: de launcher heeft geen idee dat er ook maar iets van bestaat, dus de automatisering port van buitenaf in het homescreen en hoopt dat het resultaat er goed uitziet.
In Dikciz is de eventafhandeling onderdeel van de launcher. Lua is geen widgettype, het draait als scripts uit de publieke scriptboom op LuaJ. Een script krijgt een event binnen en geeft een status plus een lijst getypeerde acties terug:
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 }
}
}
}
endDe eventlijst is godsgruwelijk lang: tijd, alarmen, batterij, opladen, thermische status, scherm aan en uit, connectiviteit, Bluetooth, sensoren, locatie, Health Connect-stappen, agenda, contacten, telefoonstatus, inkomende sms’jes, klembord, geplaatste en verwijderde notificaties, mediasessies, toegankelijkheidsvensters, device-admin-status, widgetwijzigingen, handmatige triggers en elk eigen event dat je verzint.
De acties zijn ook getypeerd: selectPage, patchWidget, patchState, patchDom, launchApp, appAction, postNotification, sendSms, explicitIntent, emitEvent, mediaControl, mediaVolume, notificationControl, lockDevice en meer. Omdat het script rechtstreeks met de launcher praat, landt patchWidget op een widget met een naam op een adres met een naam, in plaats van op een benadering van “dat ding linksboven”.
Hoeveel een Script Kapot Kan Maken
Automatiseringsapps geven een taak meestal een flinke berg macht: shell draaien, bestanden schrijven, willekeurige intents afvuren. Handig, en ook precies hoe een gekopieerd recept van een of ander forum uiteindelijk iets doet wat je nooit gelezen hebt.
Dikciz is expres krenteriger. Elke actie en elke event-capability die een script gebruikt moet in de publieke automatiseringspolicy van dat script staan, en de Lua-runtime zit in een kooi. require, package, load, loadfile, dofile, debug en print zijn weg. Geen shell, geen filesystem, geen rauwe Android-objecten. Getypeerde acties of opflikkeren.
Logs komen als begrensde JSON Lines onder /sdcard/Dikciz/logs/, leesbaar vanuit het dashboard, de viewer in Settings, WebSocket scriptLogs en MCP dikciz_script_logs. Waarden die op credentials lijken worden weggelakt voordat ze in het bestand belanden. Een mislukte Lua-run kan een Android-notificatie plaatsen, met een stilteperiode per script zodat die je niet doodspamt.
Een AI-Agent achter het Stuur Zetten
Dit is waar ik het ding eigenlijk voor gebouwd heb. Geef een stock launcher aan een LLM-agent en het gebruikelijke antwoord is “maak een screenshot, gok coördinaten, tik, bid”. Dat is geen automatisering, dat is een zatlap met een stylus.
Dikciz draait twee geversioneerde listeners op de loopback van het device:
- WebSocket op
ws://127.0.0.1:19001/v1/automation - Streamable HTTP MCP op
http://127.0.0.1:19002/mcp
Allebei komen ze uit bij hetzelfde commandodoel: config, semantische UI-snapshots en interactie, pagina’s, widgets, screenshots, UI-dumps, diagnostiek, scriptlogs, de appcatalogus, app-acties, Android-intents en shellcommando’s. De MCP-kant biedt tools als dikciz_snapshot, dikciz_tap, dikciz_find, dikciz_wait_for, dikciz_screenshot, dikciz_app_catalogue en dikciz_shell. Richt een agent erop en de agent inspecteert de launcher semantisch in plaats van pixels te gokken.
Shellcommando’s vragen standaard om su. Geef root: false mee en ze draaien als de eigen Android-appgebruiker van Dikciz. Of root echt bestaat en goedgekeurd wordt, beslist de telefoon, niet ik.
Zet de Accessibility-service van Dikciz aan en Dikciz reikt ook verder dan z’n eigen homescreen: lokale clients krijgen een begrensde boom van het actieve venster van elke app, plus accessibilityAction, accessibilityGesture en accessibilityGlobalAction. Lua krijgt hetzelfde venster als context.accessibility.
Omdat “alles op de loopback mag mijn telefoon besturen” een debiele default is voor een apparaat dat je overal mee naartoe sleept, kun je vanuit Dikciz Settings een bearer-wachtwoord eisen voor beide control planes. De publieke config slaat een bcrypt-verifier op, nooit het wachtwoord in platte tekst.
De repo levert ook een agent-skill dikciz-launcher mee die een agent leert de APK via ADB te installeren en de launcher te besturen, met setup- en capability-referenties, en een aparte skill dikciz-android-lab die builds vanaf broncode en emulatorwerk naar android-lab doorstuurt. Draai je agent in iets als aicodebox en dat is de hele kringloop: de agent leest de skill, verbindt met het MCP-endpoint, bestuurt je telefoon.
Het op een Telefoon Krijgen
Hier wint de stock-combo met vlag en wimpel: tik op installeren in de Play Store en klaar. Dikciz gaat niet via Google Play en gaat dat ook nooit doen. Je bouwt het zelf.
Ik installeer geen Android Studio. Nooit. De build- en emulatorkant draait op android-lab, mijn Docker-setup voor de Android SDK en de emulator. De Makefile wil lokaal gebouwde images android-lab:0.13.0 en android-lab:0.13.0-emulator-api36, en als er een ontbreekt stopt hij en wijst je naar de buildinstructies van android-lab, in plaats van namens jou troep binnen te trekken.
git clone https://github.com/psyb0t/dikciz-launcher.git
cd dikciz-launcher
make build
make dikciz-runmake build bouwt dikcizDebug. Toolcontainers nemen het aantal CPU’s van de Docker-host, gemaximeerd op 10, met evenveel Gradle-workers. Op een zwakkere bak:
make build ANDROID_LAB_TOOL_CPU_LIMIT=2 ANDROID_LAB_GRADLE_MAX_WORKERS=2make dikciz-run installeert het op de gedeelde emulator, geeft bestandstoegang, stelt het in als Android Home en forwardt de control planes naar de host op 19101 (WebSocket) en 19102 (MCP). Het emulatorscherm staat in je browser op:
http://127.0.0.1:61326/vnc.html?autoconnect=true&resize=scaleDaarna praat je ermee via de meegeleverde client:
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'Heb je auth aangezet, exporteer dan DIKCIZ_REMOTE_BEARER_TOKEN uit een genegeerd env-bestand. Zet hem niet als een holbewoner in een commando-argument. make dikciz-reset veegt alleen Dikciz en /sdcard/Dikciz leeg en boot het starthomescreen. make stop stopt de stack zonder de opgeslagen telefoon te wissen. De state (Gradle-cache, signing-identiteit, emulatordisk, artifacts) staat in .android-lab/.
De tests zijn UI-tests tegen diezelfde emulator, met smalle targets als make dikciz-test-accessibility of make dikciz-test-app-drawer-entry, en make dikciz-test voor de brede suite, die screenshots en UI-dumps in .android-lab/shared/artifacts/ dumpt en de telefoon daarna terugzet naar het starthomescreen, geslaagd of niet.
Wanneer Je bij de Stock Launcher en Tasker Blijft
Wil je een telefoon die werkt vanaf de dag dat je hem koopt, installeert vanuit de Play Store en automatisering krijgt via een GUI met jaren gedeelde recepten erachter, hou dan de stock launcher en de vastgeschroefde app. Hetzelfde als je nooit van plan bent een configbestand te bewerken, een regel Lua te schrijven of een agent op je telefoon los te laten. Dikciz vraagt je om het zelf in Docker te bouwen, en dat betaalt zich alleen terug als je wilt dat je homescreen bestaat uit bestanden, code, een socket en een MCP-server.
Klinkt dat laatste als wat je miste, dan staat het op github.com/psyb0t/dikciz-launcher.