- Kotlin 100%
|
|
||
|---|---|---|
| .gitea/workflows | ||
| app | ||
| gradle/wrapper | ||
| .gitignore | ||
| build.gradle.kts | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle.kts | ||
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_NETWORKsteht als String im Manifest, der Laufzeit-Check nutztBuild.VERSION.SDK_INT < 37als Literal statt einer SDK-Konstante. Ein Anheben vontargetSdkgenü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:
- Icon-Pack (ADW-Format, launcher-unabhängig). Für Fineline: Arcticons (FOSS, F-Droid, ~10.000 Strich-Icons).
- Monochrome Ebene des Adaptive Icons — seit Android 13 liefern viele Apps sie mit, ganz ohne Pack.
- 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 innetwork_security_config.xml. - Dateisuche ist Namenssuche. Kein Volltext, keine Indizierung eigener
Inhalte —
MediaStore.Filesliefert 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:
- Obtainium installieren (F-Droid/IzzyOnDroid oder GitHub-Release).
- „Add App" → als Quelle die Repo-URL eintragen:
https://git.nerdtech.info/L_cke_text/nerdtech-launcherObtainium erkennt Gitea/Forgejo-Instanzen automatisch an der URL und zieht Versionsstand + APK aus den Releases, genau wie oben im CI-Workflow beschrieben. - 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.