No description
Find a file
nerdtech 19c30f09de
All checks were successful
release-apk / build-and-release (push) Successful in 6m0s
release workflow: SIGPIPE von 'yes' bei sdkmanager --licenses abfangen
2026-07-28 02:44:25 +02:00
.gitea/workflows release workflow: SIGPIPE von 'yes' bei sdkmanager --licenses abfangen 2026-07-28 02:44:25 +02:00
app Ruckeln beim Wischen 2026-07-28 01:53:32 +02:00
gradle/wrapper nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00
.gitignore nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00
build.gradle.kts nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00
CHANGELOG.md Ruckeln beim Wischen 2026-07-28 01:53:32 +02:00
CLAUDE.md nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00
gradle.properties nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00
gradlew nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00
gradlew.bat nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00
LICENSE nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00
README.md Runner test 2026-07-28 02:05:13 +02:00
settings.gradle.kts nerdTech: minimalist GrapheneOS launcher 2026-07-23 12:26:04 +02:00

nerdTech

Minimalistischer Launcher für GrapheneOS. Gedacht als portables Frontend für selbstgehostete Webapps — plus die installierten Android-Apps im selben Raster.

Kotlin + Jetpack Compose. Keine Analytics, keine Third-Party-Abhängigkeiten außer AndroidX und kotlinx.serialization.

Open Source, MIT-lizenziert. Frei nutzbar, veränderbar und weiterverbreitbar, auch kommerziell — ohne jede Garantie oder Haftung für Schäden oder Fehlverhalten, siehe LICENSE. Forks und Pull Requests sind ausdrücklich willkommen.


Kompilieren

Voraussetzungen

JDK 17 oder neuer
Gradle 8.11.1 (Wrapper)
Android Gradle Plugin 8.7.2
compileSdk / targetSdk 35 — auf 37 heben, sobald platforms;android-37 lokal installiert ist
minSdk 31 (Android 12)

JDK setzen. Gradle 9.x verträgt sich nicht mit AGP 8.7.2 — der Wrapper ist bewusst auf 8.11.1 festgenagelt. Die JDK aus Android Studio ist die einfachste Wahl:

export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
java -version    # muss 17+ zeigen

Dauerhaft in die ~/.zshrc. Alternativ brew install --cask temurin@21.

Bauen

./gradlew --stop          # Daemons mit falscher JVM wegräumen
./gradlew assembleDebug

Fehlt der Wrapper, einmalig:

gradle wrapper --gradle-version 8.11.1 --distribution-type bin

Installieren

adb install -r app/build/outputs/apk/debug/app-debug.apk

Eine unsignierte Release-APK lässt sich nicht installieren — Android verweigert jedes APK ohne gültige Signatur, unabhängig vom Übertragungsweg. Die Debug-APK ist mit dem automatischen Debug-Keystore signiert und funktioniert.

Für eine echte Release-APK einmalig einen Keystore anlegen:

keytool -genkey -v -keystore ~/nerdtech.jks -keyalg RSA \
  -keysize 4096 -validity 10000 -alias nerdtech

und in app/build.gradle.kts einen signingConfig ergänzen. Den Keystore sichern — ohne ihn sind keine Updates über die installierte Version möglich.

Signaturwechsel. Ändert sich der Debug-Keystore (neue Maschine, neu installiertes Android Studio), verweigert adb install -r die vorhandene Installation mit INSTALL_FAILED_UPDATE_INCOMPATIBLE. Hilft nur: adb uninstall de.nerdtech.launcher — das löscht Layout, Webapp-Liste und Einstellungen auf dem Gerät, danach normal neu installieren.

adb ohne Kabel

Falls USB nicht erkannt wird (GrapheneOS härtet den Port: Einstellungen → Sicherheit → USB-C-Anschluss, und bei „Nur Laden, wenn gesperrt" muss das Gerät beim Anstecken schon entsperrt sein):

Entwickleroptionen → Drahtloses Debugging → Mit Kopplungscode koppeln

adb pair <ip>:<pairing-port>     # Port + Code stehen IM Kopplungsdialog
adb connect <ip>:<debug-port>    # anderer Port, aus der Hauptansicht
adb devices

Die beiden Ports sind verschieden — genau da hakt es meistens. Der Kopplungsport lebt nur, solange der Dialog offen ist.

Erster Start

nerdTech aus dem App-Drawer starten, nicht über die Home-Taste. So läuft es als normale Activity und der bisherige Launcher bleibt unangetastet. Dabei logcat mitlaufen lassen:

adb logcat -c
adb logcat | grep -i -E "nerdTech|de.nerdtech.launcher|AndroidRuntime"

Erst wenn das sauber startet, Home-Taste drücken und nerdTech auswählen. Zweiten Launcher installiert lassen. Zurück geht es jederzeit mit:

adb shell cmd package set-home-activity com.android.launcher3/.uioverrides.QuickstepLauncher

Android 17 (API 37)

Drei Plattformänderungen betreffen dieses Projekt direkt:

Lokaler Netzwerkzugriff ist blockiert. Apps mit Ziel-SDK 37 kommen ohne die neue Laufzeitberechtigung ACCESS_LOCAL_NETWORK nicht mehr an Geräte im lokalen Netz — also auch nicht an selbstgehostete Webapps auf nas.local oder 192.168.x.x. Sie steht im Manifest und wird kontextbezogen angefragt: erst beim Start einer Webapp, deren Host lokal aussieht. Eine Abfrage beim ersten Launcher-Start wäre zusammenhanglos und würde eher abgelehnt.

Certificate Transparency ist standardmäßig aktiv. Unter Android 16 mussten Apps das noch selbst einschalten. Zertifikate einer eigenen CA stehen nicht in CT-Logs und werden dadurch abgelehnt. network_security_config.xml enthält einen auskommentierten Block, um CT pro Domain abzuschalten — nicht global.

Hinweis: Diese Verschärfungen greifen erst ab targetSdk = 37. Das Projekt baut aktuell gegen 35, weil API 37 die Canary-Version von Android Studio voraussetzt. Der Code ist vorbereitet: ACCESS_LOCAL_NETWORK steht als String im Manifest, der Laufzeit-Check nutzt Build.VERSION.SDK_INT < 37 als Literal statt einer SDK-Konstante. Ein Anheben von targetSdk genügt.

Kein Opt-out mehr bei Orientierung und Resizability auf Geräten mit sw > 600 dp. screenOrientation und resizeableActivity=false werden ignoriert. Das entsprechende Manifest-Attribut ist deshalb entfernt; das Raster rechnet seine Zellmaße ohnehin aus den Constraints.

Nicht umgesetzt, aber naheliegend: Android 17 erlaubt es, per Long-Press auf ein Launcher-Icon eine App als schwebende Bubble zu öffnen.


Funktionen

Homescreen. Freie Platzierung auf einem konfigurierbaren Raster (4×6 bis 6×8), Lücken erlaubt. Ordner durch Ziehen und 600 ms Verweilen, Umsortieren innerhalb eines Ordners per Drag. Dock mit vier Plätzen, beim ersten Start automatisch befüllt.

Widgets. Echte AppWidgetHost-Integration, gemischt mit App-Icons auf denselben Seiten. Verschieben per Long-Press-Drag im Bearbeiten-Modus, Größenänderung über Presets (2×2/4×2/4×4) im Kontextmenü, Entfernen entweder über dasselbe Menü oder über eine eigene Markierung, die im Bearbeiten-Modus auf jedem Widget erscheint — unabhängig davon, ob das Widget selbst interaktiv ist und Touch-Events sonst abfangen würde.

Benachrichtigungs-Markierung. Optionaler Punkt auf App-Icons mit aktiven Benachrichtigungen (Einstellungen → „symbole"). Ausgewertet wird ausschließlich das Vorhandensein pro Paket, nie Titel, Text oder sonstiger Inhalt der Benachrichtigung.

Sprache. Deutsch und Englisch, folgt standardmäßig der Systemsprache (Deutsch bei deutscher Systemsprache, sonst Englisch als Fallback — es sind nur diese beiden Übersetzungen gepflegt). Lässt sich in den Einstellungen fest auf eine der beiden Sprachen stellen, unabhängig vom Systemwert.

Apps vom Homescreen ausblenden. Long-Press auf eine App-Kachel → „Vom Homescreen ausblenden" nimmt sie vom Raster, ohne sie zu deinstallieren — sie bleibt über die Suche voll erreichbar und dort auch nicht optisch abgesetzt. Rückweg: in der Suche lang auf die App drücken → „Auf Homescreen einblenden", oder zentral in den Einstellungen unter „apps" per Toggle pro App. Initial ist alles sichtbar; das Ausblenden ist ein bewusster Opt-out, kein impliziter Zustand.

Webapps. Eigene Kachel mit Monogramm und Akzentfarbe, in Ordner einsortierbar wie jede App. Zwei Startarten: STANDALONE in einer randlosen WebView ohne Browser-Chrom, oder BROWSER an den Standardbrowser. Pro Webapp einstellbar: Desktop-User-Agent und Session-Persistenz. Die STANDALONE-WebView läuft im Vollbild — die Statusleiste wird beim Start versteckt (Wisch von der Kante holt sie kurz zurück), damit eigene Navigationselemente der Webseite (z. B. ein Hamburger-Menü oben links) nicht darunter verschwinden.

Der Kniff im Datenmodell: Webapps haben keine eigene Cell-Variante, sondern stecken als Cell.App mit dem Key web:<id> im Layout. Dadurch funktionieren Ordner, Verschieben, Tauschen und Aufräumen ohne jede Sonderbehandlung — nur die Darstellungsebene prüft das Prefix.

Suche. Wisch nach unten zum Öffnen, im Ergebnisraster weiter nach oben wischen zum Schließen (Gegenstück zur Öffnen-Geste — funktioniert auch dann, wenn die Trefferliste selbst noch scrollbar ist, weil die Schließen-Geste nur den Leftover übernimmt, den das Raster nicht mehr braucht). Zeigt standardmäßig alle Apps und Webapps, die Eingabe filtert. Ranking: Präfix > Wortanfang > Teilstring, Umlaute NFD-normalisiert. Apps und Webapps in einer gemeinsamen Rangfolge.

Geräteweite Suche (optional, Einstellungen → „suche"). Bezieht zusätzlich Dateinamen aus Downloads, Dokumenten und lokalen Cloud-Sync-Ordnern mit ein — reiner Dateiname, kein Volltext. Braucht die Berechtigung „Zugriff auf alle Dateien" (MANAGE_EXTERNAL_STORAGE), weil die granularen READ_MEDIA_*- Rechte ab API 33 nur Bilder/Video/Audio abdecken, nicht die PDFs und Textdateien, um die es bei synchronisierten Cloud-Ordnern meist geht. Diese Berechtigung hat keinen Laufzeitdialog, sondern führt in die Systemeinstellungen; der Toggle stößt das automatisch an. Default aus — kein stiller Opt-in in eine derart weitreichende Berechtigung.

Browsersuche als Fallback. Liefert weder App- noch Webapp- noch Dateisuche einen Treffer, erscheint statt „keine treffer" ein Button "<eingabe>" im Browser suchen. Versucht zuerst ACTION_WEB_SEARCH, fällt ohne registrierten Suchanbieter (z. B. kein Google-App auf einem gedebloateten GrapheneOS-Gerät) auf eine direkte DuckDuckGo-URL im Standardbrowser zurück.

Icons. Drei Stufen, kein Weg allein deckt alle Apps ab:

  1. Icon-Pack (ADW-Format, launcher-unabhängig). Für Fineline: Arcticons (FOSS, F-Droid, ~10.000 Strich-Icons).
  2. Monochrome Ebene des Adaptive Icons — seit Android 13 liefern viele Apps sie mit, ganz ohne Pack.
  3. Monogramm in einem konturierten Squircle. Besser als ein einzelnes buntes Original zwischen lauter Strichzeichnungen. Dateitreffer der geräteweiten Suche bekommen nach demselben Prinzip die Dateiendung als Monogramm statt eines Vorschaubilds — MediaStore liefert nur Metadaten, ein Thumbnail-Request pro Treffer wäre ein weiterer I/O-Weg für wenig Nutzen bei Nicht-Bilddateien.

Einstellungen. Long-Press auf freie Fläche öffnet ein Kontextmenü (Home-Bildschirm bearbeiten, Webapp erstellen, Darstellung) — ein Launcher hat kein eigenes App-Icon, das ist der einzige Weg hinein. Unter „Darstellung“: Icon-Stil, Icon-Pack, Akzentfarbe per Hex-Eingabe, Icon-Größe und Border-Radius per Regler (inklusive separat koppelbarem Dock-Radius), Rastergröße, Beschriftungen, Kiosk-Modus, Webapp-Verwaltung, App-Sichtbarkeit auf dem Homescreen, geräteweite Suche.


Berechtigungen

Bewusst eng gehalten — jede Berechtigung ist an eine konkrete Funktion gebunden, keine ist beim ersten Start pauschal angefragt:

Berechtigung Wofür Wann angefragt
INTERNET WebView der eigenen Webapps Install-Time, normal
EXPAND_STATUS_BAR Statusleiste nach Immersive-/Kiosk-Modus wieder einblenden Install-Time, normal
REQUEST_DELETE_PACKAGES Deinstallations-Dialog aus dem Kontextmenü Install-Time, normal
ACCESS_LOCAL_NETWORK Webapps mit lokalem Host (*.local, 192.168.x.x) — erst ab API 37 überhaupt relevant Beim Start einer LAN-Webapp
MANAGE_EXTERNAL_STORAGE Geräteweite Dateisuche (Dateinamen in Downloads/Dokumenten/Cloud-Sync) Beim Einschalten des Suche-Toggles, führt in die Systemeinstellungen

Kein QUERY_ALL_PACKAGES — die App-Sichtbarkeit kommt aus einem eng gefassten <queries>-Block im Manifest (Launcher-Activities plus ein paar Icon-Pack-Theme-Intents).


Architektur

data/
  AppRepository        LauncherApps, Icon-Auflösung, Suche       (off-main)
  FileSearchRepository Dateiname-Suche ueber MediaStore.Files     (off-main)
  IconPackResolver     appfilter.xml parsen                      (off-main)
  LayoutStore          Cell-Modell, v1→v2-Migration, Reflow
  Settings             Raster, Akzent, Skalierung, Radius, versteckte Apps
  WebApp                Modell, URL-Normalisierung
  JsonFile              atomares Schreiben, Quarantäne
ui/
  HomeScreen            Pager, Platzierung, Drag & Drop, Dock
  SearchOverlay         Spotlight, Hit-Typen App/Webapp/Datei, Browser-Fallback
  SettingsScreen        alle Einstellungen + Webapp-Editor
  FolderOverlay         Ordnerinhalt
  theme/IconMetrics     eine Quelle für alle Größen
WebAppActivity          randlose, vollflächige WebView mit sichtbaren Fehlern

Persistenz läuft über JsonFile: geschrieben wird in .tmp, dann renameTo — atomar auf demselben Dateisystem. Lässt sich eine Datei nicht parsen, wandert sie mit Zeitstempel nach .corrupt.<millis>, statt beim nächsten Speichern überschrieben zu werden. save() liefert Result; gemeldet wird an genau einer Stelle in MainActivity.

Größen kommen ausschließlich aus IconMetrics, berechnet aus Zellbreite × Skalierungsfaktor und per CompositionLocal verteilt. Keine hartkodierten dp-Werte in der Darstellungsschicht.

Versteckte Apps sind kein eigener Datentyp, sondern ein Set<String> von App-Keys in Settings. Der Sync-Effekt in MainActivity, der Apps ins Layout einsortiert, filtert sie einfach heraus — AppRepository und damit die Suche wissen davon nichts, dort bleiben sie voll sichtbar. Dieselbe Trennung wie beim web:-Präfix: ein Zustand, eine einzige Stelle, die ihn auswertet.

Suchtreffer laufen asynchron. SearchOverlay.onQuery ist suspend, weil die Dateisuche an einer ContentResolver-Abfrage hängt, die nicht auf den Hauptthread gehört. App- und Webapp-Suche bleiben synchron schnell (In-Memory- Listen), nur der Datei-Teil hängt tatsächlich an I/O.


Bekannte Grenzen

  • Wallpaper-Blur. Das Blur wirkt auf eigenen Inhalt, nicht auf das Wallpaper — das liegt hinter dem Fenster und ist ohne eigene Wallpaper-Surface samt Permission nicht samplebar.
  • Dock-Drag. placeDock() existiert, die Drop-Zone fehlt.
  • Getrennte Webapp-Sessions. Alle STANDALONE-Webapps teilen sich Cookies und Storage. Trennung bräuchte WebView.setDataDirectorySuffix — nur einmal pro Prozess setzbar, also einen eigenen Prozess je Webapp.
  • Selbstsignierte Zertifikate werden bewusst nicht stillschweigend akzeptiert. Die WebView benennt den Fehler; eigene CA gehört als PEM nach res/raw/ und in network_security_config.xml.
  • Dateisuche ist Namenssuche. Kein Volltext, keine Indizierung eigener Inhalte — MediaStore.Files liefert nur, was das System ohnehin schon gescannt hat. Cloud-Dateien, die nur als Platzhalter (Stub) ohne lokale Kopie vorliegen, tauchen unter Umständen gar nicht erst auf.
  • SF Pro ist aus Lizenzgründen nicht beigelegt. Die Monospace-Labels machen den Look ohnehin eigenständiger.

Releases

Jeder Push auf main baut über .gitea/workflows/release.yml automatisch eine signierte Release-APK und hängt sie als Asset an einen Gitea/Forgejo- Release. Der Signing-Keystore liegt nie im Repo, sondern ausschließlich als Repo-Secret auf dem Server (Details und Einrichtung im Kommentarkopf der Workflow-Datei). Wer nur bauen will, ohne CI: siehe „Kompilieren" oben.

Installation & Updates

Einmalig, manuell. Neueste APK von der Releases-Seite laden und installieren — „Unbekannte Apps installieren" muss für die App, aus der heraus installiert wird (Browser, Dateimanager), einmalig erlaubt werden.

Mit automatischer Update-Prüfung: Obtainium. Kein zentraler Store, kein Tracking — Obtainium fragt regelmäßig die Gitea/Forgejo-API des Repos nach neuen Releases:

  1. Obtainium installieren (F-Droid/IzzyOnDroid oder GitHub-Release).
  2. „Add App" → als Quelle die Repo-URL eintragen: https://git.nerdtech.info/L_cke_text/nerdtech-launcher Obtainium erkennt Gitea/Forgejo-Instanzen automatisch an der URL und zieht Versionsstand + APK aus den Releases, genau wie oben im CI-Workflow beschrieben.
  3. Ab dann zeigt Obtainium neue Versionen an und installiert sie mit einem Tap — Signatur muss dabei mit der bereits installierten Version übereinstimmen (siehe „Signaturwechsel" oben), das ist bei allen Releases aus diesem Repo automatisch der Fall, da sie durchgehend mit demselben CI-Keystore signiert sind.

Das Repo muss dafür auf der Forgejo-Instanz öffentlich einsehbar sein (Repo-Einstellung, nicht Teil dieses Workflows) — Obtainium liest die Releases über die normale, unauthentifizierte API.

Mitmachen

Issues und Pull Requests sind willkommen — es gibt keine formale Beitragsvorlage, ein kurzer Hinweis auf das „Warum" in der Commit-Message/PR-Beschreibung reicht. Es gibt keine automatisierte Test-Suite (app/src hat keine test/androidTest-Sources); vor einem PR mindestens ./gradlew assembleDebug und ./gradlew lint lokal laufen lassen.

Lizenz

MIT — siehe LICENSE. Kurz gesagt: nutzen, verändern, weiterverbreiten, auch kommerziell, ohne Rückfrage — ohne jede Garantie und ohne Haftung für Schäden oder Fehlverhalten, die aus der Nutzung entstehen.