Calea normală spre un home screen de Android programabil e să păstrezi ce launcher a venit cu telefonul și să-i lipești pe lângă o aplicație de automatizare gen Tasker. Combinația e matură, e în Play Store, are în spate zece ani de rețete de la comunitate și, pentru majoritatea oamenilor, își face treaba fără ca cineva să se atingă de vreun build system.
Eu am luat-o invers și am făcut Dikciz, un launcher care e el însuși platforma de automatizare, nu ceva în care automatizarea scormonește din afară. Widgeturi HTML cu bridge JavaScript, scripturi Lua în sandbox care reacționează la evenimentele telefonului, un WebSocket și un server MCP pe loopback-ul device-ului, și toate configurate prin fișiere simple în /sdcard/Dikciz/. Uite cum se compară de fapt cele două abordări, pe rând, punct cu punct.
Pe Ce E Construit
Un launcher stock e produsul închis al cuiva, iar aplicația de automatizare de deasupra e produsul altcuiva. Trăiești cu două calendare de release și niciunul nu știe că celălalt există.
Dikciz pornește de la Fossify Launcher, GPLv3, pentru că un launcher scris de la zero înseamnă să te îneci în edge case-uri plictisitoare cu iconițe, drawere și rolul de home al lui Android. Tot codul Fossify rămâne intact, ca bază upstream. Codul Dikciz stă izolat într-un product flavour Gradle sub app/src/dikciz/ (plus source set-uri combinate ca app/src/dikcizDebug/), așa că sursele, resursele și traducerile Fossify rămân înlocuibile. Un release Fossify mai nou intră pe un drum verificat:
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-sourcePune sursa deoparte, verifică un backup, păstrează căile care țin de Dikciz și se oprește pe loc la o coliziune sau la un patch eșuat, iar dacă markerul release-ului Fossify ales nu e mai nou decât cel din repo, refuză să înlocuiască ceva. Fără căcaturi de tip “dăm merge și ne rugăm”.
Build-ul pe care îl rulezi de fapt e dikcizDebug, pachetul eu.psyb0t.dikciz.launcher.debug. Ăla e cel care ține pornite control plane-urile, configul public, logurile, bridge-ul HTML și toată partea de automatizare.
Unde Stă Configul
La un launcher stock, home screen-ul tău e o bază de date privată pe care n-o vezi niciodată. Primești un ecran de setări, iar aplicația de automatizare își ține profilurile ei, în formatul ei. Să faci diff pe oricare dintre ele, backup ca text sau să-l generezi dintr-un script e undeva între chinuit și imposibil.
În Dikciz tot ce poate edita utilizatorul stă sub /sdcard/Dikciz/, cu config.json ca rădăcină. Paginile, widgeturile, scripturile, politicile de automatizare, temele, fonturile, wallpaperul, pachetele din biblioteca de widgeturi, configul de auth și logurile au fiecare calea lor. Dikciz urmărește arborele și aplică editările valide fără restart de aplicație. Împingi un fișier prin ADB și se schimbă home-ul. Ține-l în git dacă ai chef.
Configul inclus vine cu limite fixe, ca o editare făcută cu picioarele să nu-ți mănânce telefonul: maxim 24 de pagini, 128 de widgeturi pe pagină, 8 widgeturi HTML pe pagină, 1 MiB de config, 192 KiB per document de widget HTML. Dacă tot se duce ceva în pizda mă-sii, safe mode deschide prima pagină inclusă fără să-ți suprascrie configul salvat, lasă diagnosticele și logurile la îndemână și blochează editările de home și modificările pe device până rezolvi.
Cum Așezi Home Screen-ul
Launcherele stock îți dau un grid și un gest de drag. Bun pentru oameni, inutil pentru orice vrea să se refere la un loc de pe ecran după nume.
Paginile Dikciz stau pe un grid cu două axe și au adresă după coordonate: 1H1V e home, 2H1V e una la dreapta, 1H2V e una mai jos. Fiecare pagină e un grid nativ cu un viewport fix care nu crește niciodată. Adaugi un widget și ocupă prima celulă liberă, de la stânga la dreapta și apoi în jos. Pagina e plină? Adăugarea e refuzată. Fără suprapuneri, fără iconițe împinse pe tăcute în afara ecranului.
Ce poți pune pe o pagină:
- Widgeturi HTML cu HTML, CSS, JavaScript locale și stare persistentă, redimensionabile, cu acțiuni native la apăsare lungă și înălțime potrivită după conținut.
- Scurtături native de aplicații și grupuri de aplicații. Lași o scurtătură peste alta și ai un grup.
- AppWidget-uri Android third-party adevărate. Își păstrează propriul
AppWidgetHostViewși nu sunt niciodată îndesate într-un WebView. - Un dashboard nativ de scripturi care îți listează scripturile Lua cu status, ultima rulare, loguri și butoane de activare, dezactivare și editare.
Temele, fonturile (Inter, Space Grotesk, JetBrains Mono și Comic Neue sunt incluse, că na, evident), wallpaperul, stilurile de card și lacătele completează tabloul. Sigla Dikciz de lângă bara de pagini deschide drawerul de aplicații, adăugarea pe pagină, administrarea paginii și setările. Lupa de lângă ea face căutarea de pagini, listând fiecare pagină ca Name (address), gen Notes (2H1V). Drawerul de aplicații e nativ, nu WebView, iar acțiunile lui acoperă adăugare de scurtătură, info aplicație, dezinstalare și force stop, care apare doar când device-ul dă root.
Cum Îți Scrii Propriul Widget
Pe partea stock, un widget custom înseamnă ori să scrii o aplicație Android întreagă cu AppWidgetProvider și RemoteViews, ori să instalezi încă o aplicație de făcut widgeturi, cu editorul ei. Merg amândouă. Amândouă sunt o durere în cur pentru un card care arată trei numere.
Un widget Dikciz e o pagină web. Fiecare widget HTML primește window.dikciz, window.selfWidget și un eveniment dikciz-ready. De acolo poate:
- să cheme orice comandă de launcher cu
dikciz.command(type, fields), aceleași comenzi pe care le folosesc clienții WebSocket și MCP - să ruleze acțiuni tipizate prin
dikciz.dispatch({ actions }) - să citească harta paginilor cu
dikciz.pages()șidikciz.home() - să modifice starea altui widget cu
dikciz.patchWidgetState(address, values) - să schimbe DOM-ul randat fără să-l salveze, prin
dikciz.patchDom(address, selector, values) - să publice și să se aboneze la evenimente cu
dikciz.emitEventșidikciz.onEvent
const result = await dikciz.patchDom(
"1H1V-device-card",
"#temperature",
{
text: "18 C",
className: "fresh",
attributes: { "data-updated": "now" }
},
);Patch-urile de stare actualizează documentul existent. Nu reîncarcă WebView-ul, deci widgetul nu clipește ca firma unui motel de doi lei de fiecare dată când se schimbă o valoare.
Cum Reacționezi la Evenimentele Telefonului
Aici își câștigă aplicația de automatizare lipită pe lângă reputația. Profiluri, contexte, task-uri, o bibliotecă uriașă de triggere. Punctul ei slab e că launcherul habar n-are că există ceva din toate astea, așa că automatizarea împunge home screen-ul din afară și speră că rezultatul arată cum trebuie.
În Dikciz, tratarea evenimentelor face parte din launcher. Lua nu e un tip de widget, rulează ca scripturi din arborele public de scripturi, pe LuaJ. Un script primește un eveniment și întoarce un status plus o listă de acțiuni tipizate:
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 }
}
}
}
endLista de evenimente e lungă rău de tot: ora, alarme, baterie, încărcare, starea termică, ecran pornit și oprit, conectivitate, Bluetooth, senzori, locație, pașii din Health Connect, calendar, contacte, starea telefonului, SMS-uri primite, clipboard, notificări postate și șterse, sesiuni media, ferestre de accesibilitate, starea de device admin, schimbări de widgeturi, triggere manuale și orice eveniment custom pe care ți-l inventezi.
Și acțiunile sunt tipizate: selectPage, patchWidget, patchState, patchDom, launchApp, appAction, postNotification, sendSms, explicitIntent, emitEvent, mediaControl, mediaVolume, notificationControl, lockDevice și altele. Fiindcă scriptul vorbește direct cu launcherul, patchWidget nimerește un widget cu nume, la o adresă cu nume, nu o aproximare de “chestia aia din stânga sus”.
Cât Poate Strica un Script
Aplicațiile de automatizare tind să-i dea unui task o grămadă de putere: shell, scris de fișiere, intent-uri arbitrare. Util, și tot pe-aici ajunge o rețetă copiată de pe vreun forum să facă ceva ce n-ai citit niciodată.
Dikciz e mai zgârcit, intenționat. Fiecare acțiune și fiecare capabilitate de evenimente folosită de un script trebuie trecută în politica publică de automatizare a scriptului ăluia, iar runtime-ul Lua e închis în cutie. require, package, load, loadfile, dofile, debug și print au dispărut. Fără shell, fără filesystem, fără obiecte Android brute. Acțiuni tipizate sau la muie.
Logurile ajung ca JSON Lines limitate sub /sdcard/Dikciz/logs/ și se citesc din dashboard, din viewerul din Settings, prin WebSocket scriptLogs și prin MCP dikciz_script_logs. Valorile care arată a credențiale sunt cenzurate înainte să ajungă în fișier. O rulare Lua eșuată poate posta o notificare Android, cu o perioadă de liniște per script, ca să nu te spameze până crăpi.
Când Îl Lași pe un Agent AI la Volan
Asta e dimensiunea pentru care am făcut de fapt chestia asta. Dai un launcher stock pe mâna unui agent LLM și răspunsul obișnuit e “fă un screenshot, ghicește coordonatele, apasă, roagă-te”. Aia nu e automatizare, aia e un bețiv cu un stylus.
Dikciz rulează doi listeneri versionați pe loopback-ul device-ului:
- WebSocket la
ws://127.0.0.1:19001/v1/automation - Streamable HTTP MCP la
http://127.0.0.1:19002/mcp
Amândoi ajung la aceeași țintă de comenzi: config, snapshot-uri semantice de UI și interacțiune, pagini, widgeturi, screenshot-uri, dump-uri de UI, diagnostice, loguri de scripturi, catalogul de aplicații, acțiuni de aplicații, intent-uri Android și comenzi shell. Partea MCP expune tooluri ca dikciz_snapshot, dikciz_tap, dikciz_find, dikciz_wait_for, dikciz_screenshot, dikciz_app_catalogue și dikciz_shell. Îndrepți un agent spre el și agentul inspectează launcherul semantic, în loc să ghicească pixeli.
Comenzile shell cer su implicit. Dai root: false și rulează ca userul Android al aplicației Dikciz. Dacă root există și e aprobat e decizia telefonului, nu a mea.
Pornești serviciul de Accessibility al Dikciz și Dikciz ajunge și dincolo de propriul home screen: clienții locali primesc un arbore limitat al ferestrei active din orice aplicație, plus accessibilityAction, accessibilityGesture și accessibilityGlobalAction. Lua primește aceeași fereastră ca context.accessibility.
Cum “orice de pe loopback îmi poate conduce telefonul” e un default tâmpit pentru un device pe care îl cari peste tot, poți cere o parolă bearer pentru ambele control plane-uri din Dikciz Settings. Configul public stochează un verifier bcrypt, niciodată parola în clar.
Repo-ul vine și cu un skill de agent dikciz-launcher care învață un agent să instaleze APK-ul prin ADB și să conducă launcherul, cu referințe de setup și de capabilități, plus un skill separat dikciz-android-lab care trimite build-urile din sursă și treaba cu emulatorul către android-lab. Rulezi agentul în ceva gen aicodebox și ăsta e tot cercul: agentul citește skillul, se conectează la endpointul MCP, îți conduce telefonul.
Cum Ajunge pe Telefon
Aici combinația stock câștigă detașat: apeși install în Play Store și gata. Dikciz nu trece prin Google Play și n-o să treacă niciodată. Ți-l construiești singur.
Eu nu instalez Android Studio. Niciodată. Partea de build și de emulator rulează pe android-lab, setup-ul meu Docker pentru Android SDK și emulator. Makefile-ul vrea imaginile android-lab:0.13.0 și android-lab:0.13.0-emulator-api36 construite local, iar dacă lipsește vreuna se oprește și te trimite la instrucțiunile de build din android-lab, în loc să-ți tragă mizerii în numele tău.
git clone https://github.com/psyb0t/dikciz-launcher.git
cd dikciz-launcher
make build
make dikciz-runmake build asamblează dikcizDebug. Containerele de tooluri iau numărul de CPU-uri ale hostului Docker, plafonat la 10, cu tot atâția workeri Gradle. Pe o mașină mai slabă:
make build ANDROID_LAB_TOOL_CPU_LIMIT=2 ANDROID_LAB_GRADLE_MAX_WORKERS=2make dikciz-run îl instalează pe emulatorul comun, dă acces la fișiere, îl setează ca Android Home și forwardează control plane-urile către host pe 19101 (WebSocket) și 19102 (MCP). Ecranul emulatorului îl ai în browser la:
http://127.0.0.1:61326/vnc.html?autoconnect=true&resize=scaleApoi vorbești cu el prin clientul inclus:
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'Dacă ai pornit auth-ul, exportă DIKCIZ_REMOTE_BEARER_TOKEN dintr-un fișier env ignorat. Nu-l pune ca argument de comandă, ca un animal. make dikciz-reset rade doar Dikciz și /sdcard/Dikciz și pornește home-ul de start. make stop oprește stack-ul fără să șteargă telefonul salvat. Starea (cache-ul Gradle, identitatea de semnare, discul emulatorului, artefactele) stă în .android-lab/.
Testele sunt teste de UI pe același emulator, cu ținte înguste ca make dikciz-test-accessibility sau make dikciz-test-app-drawer-entry, și make dikciz-test pentru suita largă, care varsă screenshot-uri și dump-uri de UI în .android-lab/shared/artifacts/ și resetează telefonul la home-ul de start la final, fie că trece, fie că pică.
Când Să Rămâi la Launcherul Stock și la Tasker
Dacă vrei un telefon care merge din ziua în care l-ai cumpărat, se instalează din Play Store și primește automatizare printr-un GUI cu ani de rețete împărtășite în spate, păstrează launcherul stock și aplicația lipită pe lângă. La fel dacă n-ai de gând să editezi vreodată un fișier de config, să scrii o linie de Lua sau să îndrepți un agent spre telefonul tău. Dikciz îți cere să ți-l construiești singur în Docker și te răsplătește doar dacă vrei ca home screen-ul să fie fișiere, cod, un socket și un server MCP.
Dacă partea asta din urmă sună exact a ce-ți lipsea, îl găsești la github.com/psyb0t/dikciz-launcher.