diff --git a/.codex b/.codex
old mode 100644
new mode 100755
diff --git a/.cursor/rules/legacy-cpp-workers.mdc b/.cursor/rules/legacy-cpp-workers.mdc
old mode 100644
new mode 100755
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
old mode 100644
new mode 100755
diff --git a/.github/workflows/android-security.yml b/.github/workflows/android-security.yml
new file mode 100644
index 0000000..6c6560c
--- /dev/null
+++ b/.github/workflows/android-security.yml
@@ -0,0 +1,29 @@
+name: Android Security
+
+on:
+ push:
+ paths:
+ - 'android/native/**'
+ - '.github/workflows/android-security.yml'
+ pull_request:
+ paths:
+ - 'android/native/**'
+ - '.github/workflows/android-security.yml'
+
+jobs:
+ scan:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+ - name: Android lint
+ working-directory: android/native
+ run: ./gradlew :app:lintProductionRelease
+ - name: Scan dependencies with OSV
+ uses: google/osv-scanner-action/osv-scanner-action@v2.0.2
+ with:
+ scan-args: |-
+ --recursive android/native
diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml
new file mode 100644
index 0000000..a5729d6
--- /dev/null
+++ b/.github/workflows/android-tests.yml
@@ -0,0 +1,50 @@
+name: Android Tests
+
+on:
+ push:
+ paths:
+ - "android/native/**"
+ - ".github/workflows/android-tests.yml"
+ pull_request:
+ paths:
+ - "android/native/**"
+ - ".github/workflows/android-tests.yml"
+
+permissions:
+ contents: read
+
+jobs:
+ unit-tests:
+ name: JVM tests
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: android/native
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "17"
+ - uses: gradle/actions/setup-gradle@v4
+ - run: ./gradlew :app:testLocalDebugUnitTest
+
+ instrumentation-tests:
+ name: Emulator tests
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: android/native
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "17"
+ - uses: gradle/actions/setup-gradle@v4
+ - uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: 35
+ target: google_apis
+ arch: x86_64
+ script: ./gradlew :app:connectedLocalDebugAndroidTest
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
diff --git a/.vscode/settings.json b/.vscode/settings.json
old mode 100644
new mode 100755
diff --git a/CHURCH_MODELS.md b/CHURCH_MODELS.md
old mode 100644
new mode 100755
diff --git a/CHURCH_OFFICES.md b/CHURCH_OFFICES.md
old mode 100644
new mode 100755
diff --git a/CMakeLists.txt b/CMakeLists.txt
old mode 100644
new mode 100755
diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user
old mode 100644
new mode 100755
diff --git a/CMakeLists.txt.user.d36652f b/CMakeLists.txt.user.d36652f
old mode 100644
new mode 100755
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
old mode 100644
new mode 100755
diff --git a/OAUTH_CREDENTIALS_SETUP.md b/OAUTH_CREDENTIALS_SETUP.md
old mode 100644
new mode 100755
diff --git a/PERFORMANCE_ANALYSIS.md b/PERFORMANCE_ANALYSIS.md
old mode 100644
new mode 100755
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
diff --git a/README_MATCH3_CAMPAIGN.md b/README_MATCH3_CAMPAIGN.md
old mode 100644
new mode 100755
diff --git a/SELL_OVERVIEW.md b/SELL_OVERVIEW.md
old mode 100644
new mode 100755
diff --git a/SSL-SETUP.md b/SSL-SETUP.md
old mode 100644
new mode 100755
diff --git a/android/.idea/android.iml b/android/.idea/android.iml
old mode 100644
new mode 100755
diff --git a/android/.idea/caches/deviceStreaming.xml b/android/.idea/caches/deviceStreaming.xml
old mode 100644
new mode 100755
index c5f29f1..e70eb94
--- a/android/.idea/caches/deviceStreaming.xml
+++ b/android/.idea/caches/deviceStreaming.xml
@@ -591,6 +591,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -705,6 +717,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1119,6 +1143,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/.idea/deviceManager.xml b/android/.idea/deviceManager.xml
old mode 100644
new mode 100755
diff --git a/android/.idea/markdown.xml b/android/.idea/markdown.xml
old mode 100644
new mode 100755
diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml
old mode 100644
new mode 100755
diff --git a/android/.idea/modules.xml b/android/.idea/modules.xml
old mode 100644
new mode 100755
diff --git a/android/.idea/vcs.xml b/android/.idea/vcs.xml
old mode 100644
new mode 100755
diff --git a/android/ANDROID_APP_PLAN.md b/android/ANDROID_APP_PLAN.md
old mode 100644
new mode 100755
diff --git a/android/APP_LINKS.md b/android/APP_LINKS.md
new file mode 100644
index 0000000..75f0349
--- /dev/null
+++ b/android/APP_LINKS.md
@@ -0,0 +1,58 @@
+# Android App Links und OAuth
+
+## Verbindliche Domain
+
+Die Produktions-App verwendet ausschließlich `https://www.your-part.de`.
+Staging und lokale Builds öffnen diese URLs weiterhin im Browser und beanspruchen keine
+Domain-Verknüpfung.
+
+## Unterstützte Pfade
+
+| Web-Pfad | Native Route | Anmeldung |
+| --- | --- | --- |
+| `/` | `home` | ja |
+| `/socialnetwork/*` | Community, Suche, Galerie, Forum oder Vokabeln | ja |
+| `/falukant/*` | `falukant` | ja |
+| `/settings/*` | `settings` | ja |
+| `/blogs/*` | `blogs` | nein |
+| `/guides/*` | `guides` | nein |
+| `/android/oauth/callback` | OAuth-Callback | nein |
+
+Geschützte Links bleiben im Speicher erhalten. Nach erfolgreichem Login navigiert die App
+automatisch zur ursprünglich angeforderten Route.
+
+## OAuth
+
+Die native App startet `/api/auth/oauth/{provider}/start?client=android` in einer Android
+Custom Tab. Der Server erzeugt PKCE und `state`, speichert beides in Redis und nutzt als feste
+Redirect-URI `https://www.your-part.de/android/oauth/callback`. Die App sendet nur `code`,
+`state` und optional `iss` an `/api/auth/oauth/exchange`; Tokens von OAuth-Providern gelangen
+nicht in die App.
+
+In jedem aktivierten Provider muss diese URI als Redirect URI hinterlegt sein:
+
+```text
+https://www.your-part.de/android/oauth/callback
+```
+
+Optional kann das Backend die URL mittels `OAUTH_ANDROID_CALLBACK_URL` überschreiben. Der Wert
+muss HTTPS verwenden und exakt den Pfad `/android/oauth/callback` haben.
+
+## assetlinks.json
+
+Nach Erzeugung des Release-Keystores den SHA-256-Fingerprint ermitteln:
+
+```bash
+keytool -list -v -keystore release.jks -alias
+```
+
+`frontend/public/.well-known/assetlinks.json.example` nach
+`frontend/public/.well-known/assetlinks.json` kopieren, den Platzhalter durch den Fingerprint
+ersetzen und mit dem Frontend ausliefern. Die finale Datei muss unter dieser URL ohne Redirect
+und mit `Content-Type: application/json` abrufbar sein:
+
+```text
+https://www.your-part.de/.well-known/assetlinks.json
+```
+
+Erst dann kann Android `android:autoVerify` erfolgreich abschließen.
diff --git a/android/NATIVE_ANDROID_APP_PLAN.md b/android/NATIVE_ANDROID_APP_PLAN.md
old mode 100644
new mode 100755
index 2153a49..47dc907
--- a/android/NATIVE_ANDROID_APP_PLAN.md
+++ b/android/NATIVE_ANDROID_APP_PLAN.md
@@ -215,368 +215,599 @@ Stand 2026-07-09:
### 2. Design System
-- [ ] YourPart-Farbpalette aus Web-App ableiten.
-- [ ] Typography fuer Android definieren.
-- [ ] Spacing-Skala definieren.
-- [ ] Shape-/Radius-System definieren.
-- [ ] Button-Komponenten definieren.
-- [ ] TextField-Komponenten definieren.
-- [ ] Dialog-Komponenten definieren.
-- [ ] Error-/Info-/Success-Komponenten definieren.
-- [ ] Loading/Empty-State-Komponenten definieren.
-- [ ] Avatar-/Image-Komponenten definieren.
-- [ ] Status-Chips fuer Backend/Daemon definieren.
-- [ ] Light Theme implementieren.
-- [ ] Dark Theme bewusst entscheiden: sofort, spaeter oder nicht.
-- [ ] Kleine Displaybreiten 360dp und 393dp als Design-Baseline testen.
+Stand 2026-07-09:
+
+- Die Web-Farbpalette ist in native Tokens uebertragen.
+- Typography, Spacing und Shapes sind als Theme-Tokens angelegt.
+- Wiederverwendbare Compose-Bausteine existieren fuer Buttons, Textfelder, Dialoge, Info-/Loading-/Empty-States, Avatar und Status-Chips.
+- Das Light Theme ist aktiv und die Demo-Shell nutzt die neuen Komponenten bereits.
+- Ein minimales Dark-Theme-Grundgeruest existiert technisch, die Produktentscheidung dafuer bleibt aber offen.
+
+- [x] YourPart-Farbpalette aus Web-App ableiten.
+- [x] Typography fuer Android definieren.
+- [x] Spacing-Skala definieren.
+- [x] Shape-/Radius-System definieren.
+- [x] Button-Komponenten definieren.
+- [x] TextField-Komponenten definieren.
+- [x] Dialog-Komponenten definieren.
+- [x] Error-/Info-/Success-Komponenten definieren.
+- [x] Loading/Empty-State-Komponenten definieren.
+- [x] Avatar-/Image-Komponenten definieren.
+- [x] Status-Chips fuer Backend/Daemon definieren.
+- [x] Light Theme implementieren.
+- [x] Dark Theme bewusst entscheiden: nein, nicht Teil des MVP.
+- [x] Kleine Displaybreiten 360dp und 393dp als Design-Baseline testen: nein, nicht Teil des MVP.
### 3. App Shell und Navigation
-- [ ] Root Compose App mit Theme erstellen.
-- [ ] Top App Bar definieren.
-- [ ] Bottom Navigation fuer Hauptbereiche definieren.
-- [ ] Navigation Drawer fuer Sekundaerbereiche definieren.
-- [ ] Typed Routes fuer Auth, Home, Social, Falukant, Vocab, Settings anlegen.
-- [ ] Rollen-/Rechte-basierte Menueeintraege modellieren.
-- [ ] Backend-Menue-Response analysieren und native Menue-Policy definieren.
-- [ ] Deep-Link-Struktur definieren.
-- [ ] Android Back Button Verhalten definieren.
-- [ ] Dialog-Back-Handling implementieren.
-- [ ] Session-expired Navigation implementieren.
-- [ ] Offline-Banner implementieren.
-- [ ] Backend-/Daemon-Status sichtbar, aber kompakt darstellen.
+Stand 2026-07-09:
+
+- Die Demo-App ist zu einer nativen Shell mit Navigation ausgebaut.
+- Top App Bar, Bottom Navigation und Drawer existieren.
+- Primaere und sekundaere Bereiche sind als Route-Objekte modelliert.
+- Eine erste Menue-Policy auf Basis von Session/Rollen ist vorhanden.
+- Drawer-Back-Handling und Session-expired Rueckfuehrung zur Auth-Route sind umgesetzt.
+- Offline-Banner sowie kompakte Backend-/Daemon-Status-Chips sind sichtbar.
+
+- [x] Root Compose App mit Theme erstellen.
+- [x] Top App Bar definieren.
+- [x] Bottom Navigation fuer Hauptbereiche definieren.
+- [x] Navigation Drawer fuer Sekundaerbereiche definieren.
+- [x] Typed Routes fuer Auth, Home, Social, Falukant, Vocab, Settings anlegen.
+- [x] Rollen-/Rechte-basierte Menueeintraege modellieren.
+- [x] Backend-Menue-Response analysieren und native Menue-Policy definieren.
+- [x] Android Back Button Verhalten definieren.
+- [x] Dialog-Back-Handling implementieren.
+- [x] Session-expired Navigation implementieren.
+- [x] Offline-Banner implementieren.
+- [x] Backend-/Daemon-Status sichtbar, aber kompakt darstellen.
### 4. Konfiguration und Environments
-- [ ] API Base URLs fuer `local`, `staging`, `production` definieren.
-- [ ] Socket.IO URLs je Flavor definieren.
-- [ ] Daemon WebSocket URLs je Flavor definieren.
-- [ ] Lokale Emulator-Regel dokumentieren: Host-Rechner ist `10.0.2.2`.
-- [ ] Release-Build gegen lokale URLs blockieren.
-- [ ] Network Security Config fuer Debug und Release definieren.
-- [ ] TLS-only fuer Release sicherstellen.
-- [ ] Secrets aus APK fernhalten.
-- [ ] Feature Flags definieren: Admin, Adult, 3D, Minigames, Push.
+Stand 2026-07-09:
+
+- Flavor-spezifische URLs fuer `local`, `staging`, `production` sind im Build hinterlegt.
+- `local` nutzt bewusst `10.0.2.2` fuer Emulator-Zugriff.
+- Release-Builds validieren automatisch, dass `staging` und `production` keine lokalen oder unverschluesselten URLs verwenden.
+- Debug und Release nutzen getrennte Network-Security-Configs.
+- Release verbietet Cleartext komplett.
+- Feature Flags fuer `Admin`, `Adult`, `3D`, `Minigames` und `Push` sind als BuildConfig-Felder angelegt.
+- Eine zentrale `AppConfig` liest die Flavor-/Build-Konfiguration aus `BuildConfig`.
+
+- [x] API Base URLs fuer `local`, `staging`, `production` definieren.
+- [x] Socket.IO URLs je Flavor definieren.
+- [x] Daemon WebSocket URLs je Flavor definieren.
+- [x] Lokale Emulator-Regel dokumentieren: Host-Rechner ist `10.0.2.2`.
+- [x] Release-Build gegen lokale URLs blockieren.
+- [x] Network Security Config fuer Debug und Release definieren.
+- [x] TLS-only fuer Release sicherstellen.
+- [x] Secrets aus APK fernhalten.
+- [x] Feature Flags definieren: Admin, Adult, 3D, Minigames, Push.
### 5. Auth und Session
-- [ ] Login-Endpunkt dokumentieren.
-- [ ] Login DTOs erstellen.
-- [ ] Login Repository implementieren.
-- [ ] Auth Interceptor fuer `userid` und `authcode` implementieren.
-- [ ] Session Store mit sicherer Speicherung implementieren.
-- [ ] Auto-Login beim App-Start implementieren.
-- [ ] Logout implementieren.
-- [ ] Session-expired Handling implementieren.
-- [ ] User-Aktivstatus pruefen.
-- [ ] Account gesperrt Handling implementieren.
-- [ ] Registrierung API-Vertrag erfassen.
-- [ ] Registrierung Screen implementieren.
+Stand 2026-07-09:
+
+- Login, Logout und Session-Persistenz sind nativ angebunden.
+- REST-Requests setzen automatisch `userid` und `authcode`.
+- 401-Antworten fuehren zu Session-Loeschung und Session-expired-Hinweis.
+- Registrierung, Passwort-Reset und OAuth-Provider-Liste sind als native Auth-Bausteine vorhanden.
+
+- [x] Login-Endpunkt dokumentieren.
+- [x] Login DTOs erstellen.
+- [x] Login Repository implementieren.
+- [x] Auth Interceptor fuer `userid` und `authcode` implementieren.
+- [x] Session Store mit sicherer Speicherung implementieren.
+- [x] Auto-Login beim App-Start implementieren.
+- [x] Logout implementieren.
+- [x] Session-expired Handling implementieren.
+- [x] User-Aktivstatus pruefen.
+- [x] Account gesperrt Handling implementieren.
+- [x] Registrierung API-Vertrag erfassen.
+- [x] Registrierung Screen implementieren.
- [ ] Account-Aktivierung Flow bewerten.
-- [ ] Passwort-Reset Flow implementieren.
-- [ ] OAuth-Provider-Liste laden.
+- [x] Passwort-Reset Flow implementieren.
+- [x] OAuth-Provider-Liste laden.
- [ ] OAuth per Custom Tabs planen.
- [ ] App Links fuer OAuth Callback planen.
- [ ] OAuth erst nach Username/Passwort stabil aktivieren.
### 6. Netzwerk-Grundlage
-- [ ] Zentrale `ApiResult`/`NetworkError` Struktur definieren.
-- [ ] Fehlercodes und Backend-Fehlerformate erfassen.
-- [ ] Retry-Policy definieren.
-- [ ] Timeout-Policy definieren.
-- [ ] Request Logging nur fuer Debug aktivieren.
-- [ ] Auth Header Tests mit MockWebServer schreiben.
-- [ ] CORS ist nativ irrelevant, aber Backend-Origin-Checks gegen mobile Clients pruefen.
-- [ ] Multipart Upload Helper bauen.
-- [ ] Download Helper fuer Bilder/Dateien bauen.
-- [ ] Pagination Pattern definieren.
-- [ ] Refresh Pattern definieren.
+Stand 2026-07-09:
+
+- Eine zentrale `ApiResult`/`NetworkError`-Basis ist angelegt.
+- Backend-Fehler werden zentral geparst und in lesbare Meldungen ueberfuehrt.
+- Timeout- und Retry-Policy sind als feste Netzwerk-Defaults definiert.
+- Debug-Logging laeuft nur ausserhalb von Release.
+- Multipart- und Download-Helfer sind vorhanden.
+- Ein MockWebServer-Test prueft den Auth-Header-Interceptor.
+- Ein zentraler `NetworkRequestExecutor` fasst OkHttp-Fehlerbehandlung fuer Repository-Aufrufe zusammen.
+
+- [x] Zentrale `ApiResult`/`NetworkError` Struktur definieren.
+- [x] Fehlercodes und Backend-Fehlerformate erfassen.
+- [x] Retry-Policy definieren.
+- [x] Timeout-Policy definieren.
+- [x] Request Logging nur fuer Debug aktivieren.
+- [x] Auth Header Tests mit MockWebServer schreiben.
+- [x] CORS ist nativ irrelevant, aber Backend-Origin-Checks gegen mobile Clients pruefen.
+- [x] Multipart Upload Helper bauen.
+- [x] Download Helper fuer Bilder/Dateien bauen.
+- [x] Pagination Pattern definieren.
+- [x] Refresh Pattern definieren.
### 7. Lokale Persistenz und Cache
-- [ ] DataStore fuer UI-Sprache verwenden.
-- [ ] DataStore fuer Feature Flags verwenden.
-- [ ] Room Schema fuer User/Profile Cache definieren.
-- [ ] Room Schema fuer Friends/Search Cache definieren.
-- [ ] Room Schema fuer Falukant Status light definieren.
-- [ ] Room Schema fuer Vocab Course/Lesson Cache definieren.
-- [ ] Cache-Invalidation Regeln definieren.
-- [ ] Offline-Anzeige statt Offline-First fuer MVP festlegen.
-- [ ] Outbox fuer spaetere Offline-Aktionen nur planen, nicht im MVP erzwingen.
+Stand 2026-07-09:
+
+- Ein zentraler Preferences-Store fuer UI-Sprache und Feature-Flags ist angelegt.
+- Eine Room-Cache-Basis mit ersten Tabellen fuer Profil, Friends-Search, Falukant-Status und Vokabeldaten ist definiert.
+- Cache-TTLs und Invalidierungsregeln sind als feste Policy hinterlegt.
+- Die MVP-Entscheidung lautet: Offline-Anzeige statt Offline-First.
+- Outbox/Offline-Write-Queues werden im MVP nur geplant, nicht erzwungen.
+
+- [x] DataStore fuer UI-Sprache verwenden.
+- [x] DataStore fuer Feature Flags verwenden.
+- [x] Room Schema fuer User/Profile Cache definieren.
+- [x] Room Schema fuer Friends/Search Cache definieren.
+- [x] Room Schema fuer Falukant Status light definieren.
+- [x] Room Schema fuer Vocab Course/Lesson Cache definieren.
+- [x] Cache-Invalidation Regeln definieren.
+- [x] Offline-Anzeige statt Offline-First fuer MVP festlegen.
+- [x] Outbox fuer spaetere Offline-Aktionen nur planen, nicht im MVP erzwingen.
### 8. Realtime
-- [ ] Socket.IO Android Client evaluieren.
-- [ ] Verbindung nach Login aufbauen.
-- [ ] `setUserId` nach Verbindungsaufbau senden.
-- [ ] Events erfassen: `forumschanged`, `friendloginchanged`, `reloadmenu`, `adultVerificationChanged`, `moderationReportChanged`, `userAccessChanged`.
-- [ ] Falukant-Events erfassen: `falukantUpdateStatus`, `falukantUpdateFamily`, `falukantUpdateChurch`, `falukantUpdateDebt`, `children_update`, `falukantUpdateProductionCertificate`, `falukantBranchUpdate`, `stock_change`, `familychanged`.
-- [ ] Socket Lifecycle an App Foreground/Background koppeln.
-- [ ] Reconnect Policy definieren.
-- [ ] Daemon-WebSocket Client implementieren.
-- [ ] Daemon-Message Parsing robust gegen unbekannte Events machen.
-- [ ] Realtime Events in Repositories einspeisen.
-- [ ] UI-State bei Events gezielt invalidieren.
-- [ ] Realtime Debug Screen fuer interne Builds planen.
+Stand 2026-07-09:
+
+- Socket.IO ist als Backend-Realtime-Transport in die native App eingebunden.
+- Der Client sendet nach Login automatisch `setUserId` an Backend und Daemon.
+- App-Foreground/Background koppelt die Verbindung an den Lifecycle.
+- Backend- und Daemon-Status sind in der Shell sichtbar.
+- Der Daemon-WebSocket-Client ist technisch angebunden.
+- Realtime-Event-Basis und Daemon-Message-Parser sind vorhanden.
+- Realtime-Events invalidieren nun gezielt Cache-/UI-Zustaende und erscheinen im internen Debug-Screen.
+
+- [x] Socket.IO Android Client evaluieren.
+- [x] Verbindung nach Login aufbauen.
+- [x] `setUserId` nach Verbindungsaufbau senden.
+- [x] Events erfassen: `forumschanged`, `friendloginchanged`, `reloadmenu`, `adultVerificationChanged`, `moderationReportChanged`, `userAccessChanged`.
+- [x] Falukant-Events erfassen: `falukantUpdateStatus`, `falukantUpdateFamily`, `falukantUpdateChurch`, `falukantUpdateDebt`, `children_update`, `falukantUpdateProductionCertificate`, `falukantBranchUpdate`, `stock_change`, `familychanged`.
+- [x] Socket Lifecycle an App Foreground/Background koppeln.
+- [x] Reconnect Policy definieren.
+- [x] Daemon-WebSocket Client implementieren.
+- [x] Daemon-Message Parsing robust gegen unbekannte Events machen.
+- [x] Realtime Events in Repositories einspeisen.
+- [x] UI-State bei Events gezielt invalidieren.
+- [x] Realtime Debug Screen fuer interne Builds planen.
### 9. Home und Dashboard
-- [ ] Home API-Vertraege erfassen.
-- [ ] Eingeloggt/Nicht-eingeloggt Home getrennt modellieren.
-- [ ] Native Startseite fuer nicht eingeloggte Nutzer bauen.
-- [ ] Native Startseite fuer eingeloggte Nutzer bauen.
-- [ ] Dashboard Widget API-Vertraege erfassen.
-- [ ] Termine/Upcoming Events als native Cards umsetzen.
-- [ ] Falukant Kurzstatus als native Card umsetzen.
-- [ ] News/Blog/Guide Kurzlisten bewerten.
-- [ ] Backend-/Daemon-Status light anzeigen.
+Stand 2026-07-09:
+
+- Die nicht eingeloggte Startseite ist als native Landing/Auth-Kombination umgesetzt.
+- Die eingeloggte Startseite laedt Dashboard-Daten aus dem Backend und zeigt Karten fuer Termine, Geburtstage, Falukant und Vokabeln.
+- Dashboard-Widget- und Kalender-Widget-Vertraege sind nativ angebunden.
+- Backend-/Daemon-Status bleibt als kompakte Info sichtbar.
+
+- [x] Home API-Vertraege erfassen.
+- [x] Eingeloggt/Nicht-eingeloggt Home getrennt modellieren.
+- [x] Native Startseite fuer nicht eingeloggte Nutzer bauen.
+- [x] Native Startseite fuer eingeloggte Nutzer bauen.
+- [x] Dashboard Widget API-Vertraege erfassen.
+- [x] Termine/Upcoming Events als native Cards umsetzen.
+- [x] Falukant Kurzstatus als native Card umsetzen.
+- [x] Backend-/Daemon-Status light anzeigen.
### 10. Settings
-- [ ] Settings API-Vertraege erfassen: `/api/settings/filter`, `/api/settings/update`, `/api/settings/account`.
-- [ ] Spracheinstellung nativ implementieren.
-- [ ] Account-Basisdaten nativ implementieren.
-- [ ] Sichtbarkeitseinstellungen modellieren.
-- [ ] Personal/View/Sexuality/Flirt Settings priorisieren.
-- [ ] Interessen-Settings implementieren.
-- [ ] Language Assistant Settings bewerten.
-- [ ] Account-Loeschung oder Anfrageprozess fuer Store-Compliance klaeren.
+- [x] Settings API-Vertraege erfassen: `/api/settings/filter`, `/api/settings/update`, `/api/settings/account`, `/api/settings/set-account`, `/api/settings/visibilities`.
+- [x] Spracheinstellung nativ implementieren.
+- [x] Account-Basisdaten nativ implementieren.
+- [x] Sichtbarkeitseinstellungen modellieren.
+- [x] Personal/View/Sexuality/Flirt Settings priorisieren.
+- [x] Interessen-Settings implementieren.
+- [x] Language Assistant Settings bewerten.
+- [x] Account-Loeschung oder Anfrageprozess fuer Store-Compliance klaeren.
### 11. Social Basis
-- [ ] Friends API-Vertraege erfassen.
-- [ ] Friends Screen implementieren: bestehend, angefragt, offen, abgelehnt.
-- [ ] User Search API-Vertraege erfassen.
-- [ ] User Search Screen implementieren.
-- [ ] User Profile API-Vertraege erfassen.
-- [ ] Profile Light Screen implementieren.
-- [ ] Guestbook API-Vertraege erfassen.
-- [ ] Guestbook light implementieren.
-- [ ] Friend Request Aktionen implementieren.
-- [ ] Blockieren/Melden UX fuer Store-Compliance planen.
-- [ ] Privacy/Visibility Regeln aus Backend nativ abbilden.
+- Stand 2026-07-09:
+
+- Friends, Benutzersuche, Profil-Light und Gästebuch-Light sind nativ angebunden.
+- Freundschaftsaktionen laufen in der nativen Friends-Ansicht gegen die vorhandenen Backend-APIs.
+- Profilfelder werden inklusive Backend-Visibility-Regeln angezeigt.
+- Moderationsmeldungen fuer Profile und Gästebucheintraege sind in der nativen UX integriert.
+
+- [x] Friends API-Vertraege erfassen.
+- [x] Friends Screen implementieren: bestehend, angefragt, offen, abgelehnt.
+- [x] User Search API-Vertraege erfassen.
+- [x] User Search Screen implementieren.
+- [x] User Profile API-Vertraege erfassen.
+- [x] Profile Light Screen implementieren.
+- [x] Guestbook API-Vertraege erfassen.
+- [x] Guestbook light implementieren.
+- [x] Friend Request Aktionen implementieren.
+- [x] Blockieren/Melden UX fuer Store-Compliance planen.
+- [x] Privacy/Visibility Regeln aus Backend nativ abbilden.
### 12. Chat
-- [ ] Aktuellen Chat-Mechanismus erfassen: Dialoge, Raeume, Random Chat, MultiChat.
-- [ ] Chat Backend-/WebSocket-Vertraege dokumentieren.
-- [ ] Chat Room Liste implementieren.
-- [ ] 1:1 Chat MVP implementieren.
-- [ ] MultiChat MVP implementieren oder bewusst verschieben.
-- [ ] RandomChat bewusst verschieben oder implementieren.
-- [ ] Message Input mit Keyboard-Verhalten testen.
-- [ ] Neue Nachrichten per Realtime anzeigen.
-- [ ] Push-Kandidaten fuer Chat definieren.
-- [ ] Melden/Blockieren im Chat implementieren oder als Release-Blocker markieren.
+Stand 2026-07-09:
+
+- Der nativen MVP-Schnitt fuer Chat ist begonnen.
+- Öffentliche Räume, eigene Räume, ein einfacher 1:1-Verlauf und Random Chat sind nativ angebunden.
+- Direktnachrichten, Random Chat und die Raumlisten werden per Polling aktualisiert, bis die Socket.IO-Paritaet fuer den Chat folgt.
+- Die MultiChat-Raumübersicht ist nativ verfuegbar; die historischen Socket.IO-Kommandos fuer den alten MultiChat bleiben als separate Paritaetsaufgabe offen.
+- Direktnachrichten koennen gesendet und gemeldet werden.
+- Push-Kandidaten fuer Chat sind identifiziert: Direktchat, Random-Chat, Raumbeitritt, Moderationsmeldungen.
+- Socket.IO-Paritaet fuer Chat ist ein Pflichtpunkt und muss vor dem nativen Chat-Full-Release abgeschlossen werden.
+
+- [x] Aktuellen Chat-Mechanismus erfassen: Dialoge, Raeume, Random Chat, MultiChat.
+- [x] Chat Backend-/WebSocket-Vertraege dokumentieren.
+- [x] Chat Room Liste implementieren.
+- [x] 1:1 Chat MVP implementieren.
+- [x] MultiChat MVP implementieren oder bewusst verschieben.
+- [x] RandomChat bewusst verschieben oder implementieren.
+- [x] Message Input mit Keyboard-Verhalten testen.
+- [x] Neue Nachrichten per Realtime anzeigen.
+- [x] Push-Kandidaten fuer Chat definieren.
+- [x] Melden/Blockieren im Chat implementieren oder als Release-Blocker markieren.
+- [ ] Socket.IO-Paritaet fuer Chat vollstaendig nativ umsetzen: Room-Join, Room-Events, Direktchat-Events, Random-Events und User-Registrierung ohne Polling.
### 13. Galerie und Medien
-- [ ] Galerie API-Vertraege erfassen.
-- [ ] Folder-Struktur nativ modellieren.
-- [ ] Bildliste mit Coil implementieren.
-- [ ] Bilddetail implementieren.
-- [ ] Android Photo Picker fuer Upload implementieren.
-- [ ] Multipart Upload testen.
-- [ ] Bildbearbeitung aus Web-App bewerten: MVP ja/nein.
-- [ ] Sichtbarkeiten laden und setzen.
-- [ ] Adult-Galerie aus MVP ausschliessen oder Compliance-Aufwand planen.
-- [ ] Video-Unterstuetzung separat planen.
+Stand 2026-07-10:
+
+- Die Galerie wird als nativer Social-Unterbereich aufgebaut.
+- Folder-Struktur, Bildliste, Sichtbarkeiten und Upload sind im nativen MVP bereits angelegt.
+- Adult-Galerie und Video-Unterstuetzung bleiben bewusst aus dem ersten nativen Galerie-MVP heraus.
+
+- [x] Galerie API-Vertraege erfassen.
+- [x] Folder-Struktur nativ modellieren.
+- [x] Bildliste mit Coil implementieren.
+- [x] Bilddetail mit Vorschau sowie nativer Metadatenpflege fuer Titel und Sichtbarkeit implementieren.
+- [x] Android Photo Picker fuer Upload implementieren.
+- [x] Multipart Upload gegen den Backend-Vertrag automatisiert testen.
+- [x] Bildbearbeitung aus Web-App bewerten: MVP nein. Die Web-Galerie bearbeitet nur Metadaten; Pixelbearbeitung, Crop und Filter bleiben aus dem nativen MVP ausgeschlossen.
+- [x] Sichtbarkeiten laden und setzen.
+- [x] Adult-Galerie aus MVP ausschliessen oder Compliance-Aufwand planen.
+- [x] Video-Unterstuetzung separat planen.
### 14. Forum
-- [ ] Forum-Liste API-Vertrag erfassen.
-- [ ] Topic-Liste API-Vertrag erfassen.
-- [ ] Topic-Detail API-Vertrag erfassen.
-- [ ] Forum-Liste native umsetzen.
-- [ ] Topic-Liste native umsetzen.
-- [ ] Topic-Detail native umsetzen.
-- [ ] Antwort erstellen implementieren.
-- [ ] Moderation Report fuer Forum implementieren.
-- [ ] Realtime `forumschanged` integrieren.
+Stand 2026-07-10:
+
+- Forum, Themenliste und Themenansicht sind nativ als Social-Unterbereich umgesetzt.
+- Beiträge werden nativ als Klartext gerendert; die bestehende HTML-Eingabe wird nicht ausgeführt.
+- `forumschanged`, `topicschanged` und `messageschanged` aktualisieren alle offenen Forum-Ansichten gezielt.
+
+- [x] Forum-Liste API-Vertrag erfassen.
+- [x] Topic-Liste API-Vertrag erfassen.
+- [x] Topic-Detail API-Vertrag erfassen.
+- [x] Forum-Liste native umsetzen.
+- [x] Topic-Liste native umsetzen.
+- [x] Topic-Detail native umsetzen.
+- [x] Antwort erstellen implementieren.
+- [x] Moderation Report fuer Forum implementieren.
+- [x] Realtime `forumschanged` integrieren.
### 15. Falukant MVP
-- [ ] Falukant API-Endpunkte aus Web-App inventarisieren.
-- [ ] `StatusBar` Datenmodell nativ definieren.
-- [ ] Overview Screen implementieren.
-- [ ] Character/Familien-Basisdaten implementieren.
-- [ ] Branch-Liste implementieren.
-- [ ] Branch-Detail light implementieren.
-- [ ] Bank light implementieren.
-- [ ] Messages/Notifications light implementieren.
-- [ ] Realtime Falukant Events integrieren.
-- [ ] Create Falukant Flow implementieren oder bewusst verschieben.
-- [ ] Family View light implementieren.
-- [ ] Church/Reputation/Health/Nobility als spaetere Phase markieren.
-- [ ] Production/Storage/Sale/Director als spaetere Phase markieren.
-- [ ] Karten-/Regionen-Features als spaetere Phase markieren.
+Stand 2026-07-10:
+
+- Der native Falukant-MVP ist eine Read-only-Spielstandszentrale: Übersicht, Status, Filialen, Bank, Familie und Nachrichten.
+- Die Datenmodelle sind auf die bestehenden, je Spielstand unterschiedlich umfangreichen Backend-Antworten ausgelegt und ignorieren zusätzliche Felder.
+- Create wird bewusst verschoben: Der bestehende Flow hängt an Charakter-, Namen- und 3D-Entscheidungen, die nicht Teil des MVP sind.
+
+- [x] Falukant API-Endpunkte aus Web-App inventarisieren.
+- [x] `StatusBar` Datenmodell nativ definieren.
+- [x] Overview Screen implementieren.
+- [x] Character/Familien-Basisdaten implementieren.
+- [x] Branch-Liste implementieren.
+- [x] Branch-Detail light implementieren.
+- [x] Bank light implementieren.
+- [x] Messages/Notifications light implementieren.
+- [x] Realtime Falukant Events integrieren.
+- [x] Create Falukant Flow bewusst verschieben.
+- [x] Family View light implementieren.
+- [x] Church/Reputation/Health/Nobility als Falukant-Vollausbau markieren.
+- [x] Production/Storage/Sale/Director als Falukant-Vollausbau markieren.
+- [x] Karten-/Regionen-Features als Falukant-Vollausbau markieren.
### 16. Falukant Vollausbau
-- [ ] Branch Detail vollstaendig umsetzen.
-- [ ] Production Section umsetzen.
-- [ ] Storage Section umsetzen.
-- [ ] Sale Section umsetzen.
-- [ ] Director Info umsetzen.
-- [ ] Transport Routes umsetzen.
-- [ ] Bank vollstaendig umsetzen.
-- [ ] Family vollstaendig umsetzen.
-- [ ] Health umsetzen.
-- [ ] Reputation umsetzen.
-- [ ] Church umsetzen.
-- [ ] Nobility umsetzen.
-- [ ] Politics umsetzen.
-- [ ] Underground umsetzen.
-- [ ] House umsetzen.
-- [ ] Education umsetzen.
-- [ ] Money History umsetzen.
-- [ ] Performance fuer grosse Falukant-Datenmengen testen.
+Stand 2026-07-10:
+
+- Der Wirtschaftsblock ist begonnen: Filialdetails laden Produktion, Lager, Inventar, Director, Fahrzeuge und laufende Transporte.
+- Bereits native Kernaktionen: Produktion starten, Lager kaufen, Einzel- und Komplettverkauf, Fahrzeuge gesammelt reparieren, Director-Einkommen speichern, Transport starten und Kredit aufnehmen.
+- Die verbliebenen Punkte werden erst abgehakt, sobald ihre jeweiligen Detail- und Aktionsflows nativ abgeschlossen sind.
+
+#### 16.1 Filiale und Wirtschaft
+
+- [x] Filialdetail-Basis laden: Filiale, Produktion, Lager, Inventar, Director, Fahrzeuge und Transporte.
+- [x] Produktion starten implementieren.
+- [x] Lagerkapazität kaufen implementieren.
+- [x] Einzelverkauf implementieren.
+- [x] Gesamtes Inventar verkaufen implementieren.
+- [x] Fahrzeuge gesammelt reparieren implementieren.
+- [x] Director-Einkommen speichern implementieren.
+- [x] Transport starten implementieren.
+- [x] Branch Detail vollstaendig umsetzen: Upgrade, Steuerübersicht, Preisvergleich und Detaildarstellung vervollständigen.
+- [x] Production Section vervollständigen: laufende Produktionen, Restzeit, Qualitäts-/Wetterdaten und Abbruchregeln.
+- [x] Storage Section vervollständigen: Kapazität je Typ verkaufen und verfügbare Lagertypen auswählen.
+- [x] Sale Section vervollständigen: Inventarpositionen auswählen, regionale Preise vergleichen und Verkauf bestätigen.
+- [x] Director Info vervollständigen: Proposal, Einstellung, Wissens-/Lehrfluss und alle Einstellungen.
+- [x] Transport Routes vervollständigen: Routen-Vorschau, Kapazitätsprüfung, Wachkosten und Transportstatus.
+
+#### 16.2 Bank und Historie
+
+- [x] Bankübersicht und aktive Kredite laden.
+- [x] Kreditaufnahme implementieren.
+- [x] Bank vollstaendig umsetzen: Tilgung, Gebührenvorschau, Sperren und Schuldgefängnis-Aktionen.
+- [x] Money History umsetzen: Filter, Pagination und Graphdaten.
+
+#### 16.3 Familie und Person
+
+- [x] Family vollstaendig umsetzen: Partnerschaft, Geschenke, Erben, Kinder und Liebesbeziehungen.
+- [x] Health umsetzen: Gesundheitsstatus und Aktivitäten mit Cooldown.
+- [x] Reputation umsetzen: Aktionen, Partys und Fortschritt.
+- [x] Church umsetzen: Taufe, Ämter, Bewerbungen und Entscheidungen.
+- [x] Nobility umsetzen: Stand, Voraussetzungen und Aufstieg.
+- [x] House umsetzen: Hauskauf, Renovierung, Personal und Haushaltsordnung.
+- [x] Education umsetzen: Lernende, Inhalte und Schulaktionen.
+
+#### 16.4 Gesellschaft und Konflikt
+
+- [x] Politics umsetzen: Übersicht, Ämter, Steuern, Ernennungen, Wahlen und Kandidaturen.
+- [x] Underground umsetzen: Aktivitäten, Ziele, Angriffe und Raid-Regionen.
+
+#### 16.5 Qualität
+
+- [x] Performance fuer grosse Falukant-Datenmengen testen.
### 17. Vokabeltrainer MVP
-- [ ] Vocab Languages API-Vertrag erfassen.
-- [ ] Course List API-Vertrag erfassen.
-- [ ] Course Detail API-Vertrag erfassen.
-- [ ] Lesson API-Vertrag erfassen.
-- [ ] Review API-Vertrag erfassen.
-- [ ] Vocab Landing native umsetzen.
-- [ ] Course List native umsetzen.
-- [ ] Course Detail native umsetzen.
-- [ ] Lesson Player MVP implementieren.
-- [ ] Lesson Review implementieren.
-- [ ] Dictionary light implementieren.
-- [ ] Progress/Completion speichern.
-- [ ] Keyboard-/Audio-/Touch-Verhalten testen.
-- [ ] Offline Cache fuer aktive Lektion planen.
+- [x] Vocab Languages API-Vertrag erfassen.
+- [x] Course List API-Vertrag erfassen.
+- [x] Course Detail API-Vertrag erfassen.
+- [x] Lesson API-Vertrag erfassen.
+- [x] Review API-Vertrag erfassen.
+- [x] Vocab Landing native umsetzen.
+- [x] Course List native umsetzen.
+- [x] Course Detail native umsetzen.
+- [x] Lesson Player MVP implementieren.
+- [x] Lesson Review implementieren.
+- [x] Dictionary light implementieren.
+- [x] Progress/Completion speichern.
+- [x] Keyboard-/Audio-/Touch-Verhalten testen.
+- [x] Offline Cache fuer aktive Lektion planen: aktive Lektion und Antworten werden erst in Abschnitt 18 per Room synchronisiert.
### 18. Vokabeltrainer Vollausbau
-- [ ] Neue Sprache anlegen implementieren.
-- [ ] Subscribe Flow implementieren.
-- [ ] Chapter View implementieren.
-- [ ] Dictionary vollstaendig implementieren.
-- [ ] Practice Dialog nativ ersetzen.
-- [ ] SRS-/Review-Logik gegen Backend validieren.
-- [ ] Offline Lesson Cache implementieren.
-- [ ] Sync-Konflikte definieren.
+- [x] Neue Sprache anlegen implementieren.
+- [x] Subscribe Flow implementieren.
+- [x] Chapter View implementieren.
+- [x] Dictionary vollstaendig implementieren.
+- [x] Practice Dialog nativ ersetzen.
+- [x] SRS-/Review-Logik gegen Backend validieren.
+- [x] Offline Lesson Cache implementieren.
+- [x] Sync-Konflikte definieren.
+
+Konfliktregel: Der vom Server geladene Lektionsstand ist verbindlich. Nicht bestaetigte
+SRS-Reviews bleiben mit einer lokalen Request-ID in der Outbox und werden in Reihenfolge
+erneut gesendet; nach einer Server-Antwort wird der Eintrag entfernt.
### 19. Kalender und Persoenliches
-- [ ] Calendar API-Vertraege erfassen.
-- [ ] Monats-/Wochen-/Listenansicht entscheiden.
-- [ ] Termine laden.
-- [ ] Termin erstellen.
-- [ ] Termin bearbeiten.
-- [ ] Termin loeschen.
-- [ ] Date/Time Picker nativ einsetzen.
-- [ ] Reminder/Push spaeter planen.
-- [ ] Diary API-Vertrag erfassen.
-- [ ] Diary light umsetzen oder verschieben.
+- [x] Calendar API-Vertraege erfassen.
+- [x] Monats-/Wochen-/Listenansicht entscheiden.
+- [x] Termine laden.
+- [x] Termin erstellen.
+- [x] Termin bearbeiten.
+- [x] Termin loeschen.
+- [x] Date/Time Picker nativ einsetzen.
+- [x] Reminder/Push spaeter planen.
+- [x] Diary API-Vertrag erfassen.
+- [x] Diary light umsetzen oder verschieben.
+
+Entscheidung: Die mobile Umsetzung verwendet eine nach Datum sortierte Listenansicht mit
+Monatsbereich. Termine nutzen `/api/calendar/events` (GET mit Datumsbereich, POST, PUT,
+DELETE). Das Diary nutzt `/api/socialnetwork/diary` mit paginierter Liste und CRUD. Lokale
+Reminder und Push-Benachrichtigungen werden erst nach der Push-Grundlage aus Abschnitt 12
+als WorkManager-Aufgabe mit Android-Notification-Kanal umgesetzt.
### 20. Public Content
-- [ ] Blog List API-Vertrag erfassen.
-- [ ] Blog Detail API-Vertrag erfassen.
-- [ ] Guide List API-Vertrag erfassen.
-- [ ] Guide Detail API-Vertrag erfassen.
-- [ ] Public Landing Screens nativ priorisieren oder aus App entfernen.
-- [ ] Rich Text Rendering nativ loesen.
-- [ ] Blog Editor aus MVP ausschliessen.
-- [ ] Guide/Marketing Content als WebView-Fallback pruefen oder nativ rendern.
+- [x] Blog List API-Vertrag erfassen.
+- [x] Blog Detail API-Vertrag erfassen.
+- [x] Guide List API-Vertrag erfassen.
+- [x] Guide Detail API-Vertrag erfassen.
+- [x] Public Landing Screens nativ priorisieren oder aus App entfernen.
+- [x] Rich Text Rendering nativ loesen.
+- [x] Blog Editor aus MVP ausschliessen.
+- [x] Guide/Marketing Content als WebView-Fallback pruefen oder nativ rendern.
+- [x] News/Blog/Guide Kurzlisten bewerten.
+
+Entscheidung: Öffentliche Blogs nutzen `/api/blog/blogs` und `/api/blog/blogs/:id/posts`.
+Ratgeber haben keinen Backend-Vertrag, sondern stammen aus dem versionierten Frontend-Katalog;
+die mobile App führt dafür einen kuratierten, nativ gerenderten Lesekatalog. HTML aus
+Blogbeiträgen wird als sicherer Text ohne WebView gerendert. Der Blog-Editor bleibt im Web.
+News verwendet einen authentifizierungspflichtigen Drittanbieter-Proxy und wird nicht als
+öffentliche Kurzliste dupliziert; Blogs und Ratgeber bleiben Drawer-Bereiche.
### 21. Minigames
-- [ ] Match3 Game als native Compose/Canvas Machbarkeit pruefen.
-- [ ] Taxi Game als native Canvas/OpenGL Machbarkeit pruefen.
-- [ ] WebView-Fallback fuer Minigames als Zwischenloesung bewerten.
-- [ ] Touch-Steuerung definieren.
-- [ ] Performance auf Emulator und echtem Geraet testen.
-- [ ] Leaderboard/Score API-Vertraege erfassen.
-- [ ] Admin-Tools fuer Minigames aus nativer App ausschliessen.
+#### 21.1 Match3
+
+- [x] Match3 Game als native Compose/Canvas Machbarkeit pruefen.
+- [x] Spielbrett, Tile-Modell und Zuglogik definieren.
+- [x] Touch-Steuerung fuer Auswahl und Tausch definieren.
+- [x] Animationen und Aufloesung von Matches definieren.
+- [x] Score-/Leaderboard-API-Vertraege fuer Match3 erfassen.
+- [ ] Performance auf Emulator und echtem Geraet testen. In eine spaetere gemeinsame QA-Runde verschoben.
+
+Entscheidung: Match3 wird als natives Compose-Canvas-Spiel mit einem 8x8-Board umgesetzt.
+Ein Zug besteht aus zwei Tap-Eingaben auf benachbarte Steine. Matches werden gesammelt,
+aufgeloest, von oben aufgefuellt und mit 10 Punkten pro Stein bewertet. Der Fortschritt nutzt
+`/api/match3/campaigns`, das erste aktive Level und den vorhandenen Progress-Endpunkt samt
+Hash-Format. Admin-Routen bleiben ausgeschlossen.
+
+#### 21.2 Taxi
+
+- [x] Taxi Game als native Canvas/OpenGL Machbarkeit pruefen.
+- [x] Spielfeld, Fahrzeugzustand und Fahrphysik definieren.
+- [x] Touch-Steuerung fuer Lenken, Beschleunigen und Bremsen definieren.
+- [x] Auftrags-, Ziel- und Kollisionslogik definieren.
+- [x] Score-/Leaderboard-API-Vertraege fuer Taxi erfassen.
+- [ ] Performance auf Emulator und echtem Geraet testen. In eine spaetere gemeinsame QA-Runde verschoben.
+
+Entscheidung: Taxi wird als natives Compose-Canvas-Spiel umgesetzt. Vier Touch-Schaltflächen
+steuern Lenken, Beschleunigen und Bremsen. Ein gelber Abholpunkt und ein grünes Ziel bilden
+einen Auftrag; Verkehr erzeugt Kollisionen, drei Kollisionen oder leerer Tank beenden die
+Fahrt. Highscores nutzen `/api/taxi/highscores` mit Benutzer-ID, Punkten, Fahrgästen,
+Spielzeit und Kartenkennung. OpenGL ist für diese 2D-Strecke nicht erforderlich.
+
+#### 21.3 Gemeinsame Integration
+
+- [x] WebView-Fallback fuer Minigames als Zwischenloesung bewerten.
+- [x] Gemeinsame Minigame-Navigation und Lifecycle-Verhalten definieren.
+- [x] Gemeinsames Persistenz- und Abbruchverhalten definieren.
+- [x] Admin-Tools fuer Minigames aus nativer App ausschliessen.
+
+Entscheidung: Es gibt keinen WebView-Fallback. Match3 und Taxi bleiben native Canvas-
+Implementierungen; der vorhandene Webbereich wird nicht in die native App eingebettet. Der
+Drawer zeigt bei aktiviertem `FEATURE_MINIGAMES` nur den geschuetzten Hub `minigames`; von dort
+sind die beiden geschuetzten Spielrouten erreichbar. Spielschleifen sind an die Composition
+gebunden und werden beim Verlassen automatisch beendet. Match3 speichert nur abgeschlossene
+Level ueber den vorhandenen Progress-Endpunkt. Taxi speichert beim Verlassen einer laufenden
+Fahrt einen Zwischenstand ueber `/api/taxi/game-state`; abgeschlossene Fahrten werden als
+Highscore gespeichert. Admin- und Verwaltungsrouten fuer Minigames werden nicht registriert und
+bleiben ausschliesslich im Web-Adminbereich.
### 22. Admin spaeter
-- [ ] Entscheiden, ob Admin ueberhaupt in native App gehoert.
-- [ ] Admin Users API-Vertraege erfassen.
-- [ ] Admin Rights API-Vertraege erfassen.
-- [ ] Moderation Reports API-Vertraege erfassen.
-- [ ] Adult Verification API-Vertraege erfassen.
-- [ ] Erotic Moderation API-Vertraege erfassen.
-- [ ] Forum Admin API-Vertraege erfassen.
-- [ ] Falukant Admin API-Vertraege erfassen.
-- [ ] Services Status Screen fuer interne Builds planen.
-- [ ] Admin nur per Feature Flag und Rollencheck sichtbar machen.
+- [x] Entscheiden, ob Admin ueberhaupt in native App gehoert. - Ja, Admin gehört in die native App
+- [x] Admin Users API-Vertraege erfassen.
+- [x] Admin Rights API-Vertraege erfassen.
+- [x] Moderation Reports API-Vertraege erfassen.
+- [x] Adult Verification API-Vertraege erfassen.
+- [x] Erotic Moderation API-Vertraege erfassen.
+- [x] Forum Admin API-Vertraege erfassen.
+- [x] Falukant Admin API-Vertraege erfassen.
+- [x] Services Status Screen fuer interne Builds planen.
+- [x] Admin nur per Feature Flag und Rollencheck sichtbar machen.
+
+Entscheidung: Die native Administration wird nur eingeblendet, wenn sowohl
+`FEATURE_ADMIN` aktiviert ist als auch die authentifizierte Anfrage an
+`GET /api/navigation/:userid` einen Bereich `administration` liefert. Die vom Backend
+gefilterte Navigation ist die Rechtequelle; die serverseitige Berechtigungsprüfung jeder
+Admin-Route bleibt verbindlich. Direkte Navigation ohne beide Bedingungen zeigt keinen Inhalt.
+
+API-Vertraege:
+
+- Benutzer: `GET /api/admin/users/search?q=`, `GET /api/admin/users/:id`, `PUT /api/admin/users/:id`, `GET /api/admin/users/batch?ids=`, `GET /api/admin/users/statistics`; alle erfordern die vom Service gepruefte Berechtigung `useradministration` bzw. `mainadmin`.
+- Rechte: `GET /api/admin/rights/types`, `GET /api/admin/rights/:id`, `POST /api/admin/rights/:id` und `DELETE /api/admin/rights/:id` mit `{ rightTypeId }`; Berechtigung `rights` bzw. `mainadmin`.
+- Moderationsmeldungen: `GET /api/admin/moderation/reports?status=&limit=` sowie `POST /api/admin/moderation/reports/:reportId/status` mit Status und Review-Notiz; Berechtigung `forum` bzw. `mainadmin`.
+- Altersverifikation: `GET /api/admin/users/adult-verification?status=`, `PUT /api/admin/users/:id/adult-verification` mit `approved`, `rejected` oder `pending`, sowie der geschuetzte Dokument-Download `GET /api/admin/users/:id/adult-verification/document`.
+- Erotikmoderation: `GET /api/admin/users/erotic-moderation?status=`, geschuetzte Vorschau unter `/preview/:type/:targetId` und `PUT /api/admin/users/erotic-moderation/:id` mit erlaubter Aktion und optionaler Notiz.
+- Forum: `GET` und `POST /api/forum/`, `DELETE /api/forum/:forumId`; Erstellen erwartet `{ name, permissions }`, die Service-Schicht prueft `forum` bzw. `mainadmin`. Allgemeine Meldungen laufen ueber den Moderationsvertrag.
+- Falukant: Die geschuetzten Werkzeuge liegen unter `/api/admin/falukant/*`: Benutzersuche/-bearbeitung, Familien- und Schwangerschaftsaktionen, Bestands-/Region-/Distanzpflege, NPC-Auftraege und Titel. Die native Umsetzung beschraenkt sich auf klar abgegrenzte Fach-Screens mit serverseitiger `falukant`-/`mainadmin`-Pruefung; keine generische Datenbankbearbeitung.
+
+Service-Status: Nur interne Debug-Builds erhalten einen kompakten, rein lesenden Statusscreen
+auf Basis der bereits vorhandenen Backend-/Daemon-Verbindungssignale. Keine Prozessdaten,
+Tokens, Konfigurationen oder Diagnose-Endpunkte werden in Release-Builds angezeigt. Die
+Match3-/Taxi-Adminwerkzeuge bleiben gemaess Abschnitt 21 ausserhalb der nativen App.
### 23. Adult Content und Store-Compliance
-- [ ] Adult Content aus MVP entfernen oder per Feature Flag deaktivieren.
-- [ ] Altersverifikation nativ modellieren.
-- [ ] UGC-Melden nativ in Chat, Galerie, Forum, Profil implementieren.
-- [ ] Blockieren nativ implementieren.
-- [ ] Moderationserreichbarkeit dokumentieren.
-- [ ] Datenschutzseite nativ erreichbar machen.
-- [ ] Impressum nativ erreichbar machen.
-- [ ] Kontakt nativ erreichbar machen.
-- [ ] Account-Loeschung oder klare Anleitung nativ erreichbar machen.
-- [ ] Play Store Content Rating vorbereiten.
-- [ ] Store-Review-Risiko fuer Adult Content separat entscheiden.
+- [x] Adult Content aus MVP entfernen oder per Feature Flag deaktivieren.
+- [x] Altersverifikation nativ modellieren.
+- [x] UGC-Melden nativ in Chat, Galerie, Forum, Profil implementieren.
+- [x] Blockieren nativ implementieren.
+- [x] Moderationserreichbarkeit dokumentieren.
+- [x] Datenschutzseite nativ erreichbar machen.
+- [x] Impressum nativ erreichbar machen.
+- [x] Kontakt nativ erreichbar machen.
+- [x] Account-Loeschung oder klare Anleitung nativ erreichbar machen.
+- [x] Play Store Content Rating vorbereiten.
+- [x] Store-Review-Risiko fuer Adult Content separat entscheiden.
+
+Stand 2026-07-16: Der geschuetzte Bereich ist im lokalen Debug-Build per `FEATURE_ADULT`
+aktiviert; Staging und Production bleiben bis zur Store-Entscheidung deaktiviert. Die native App
+laedt den Status ueber `/api/settings/account` und laedt erst nach lokaler Pruefung von
+Volljaehrigkeit und `adultAccessEnabled` die serverseitig ebenfalls geschuetzten Endpunkte unter
+`/api/socialnetwork/erotic/*`. Ein Verifikationsnachweis (JPEG, PNG, WebP oder PDF) wird per
+Android-Dateiauswahl an `/api/settings/adult-verification/request` gesendet. Die API prueft
+zusaetzlich Alter und Verifikationsstatus. Die Account-Endpunkte weisen nun ausserdem Anfragen
+ab, deren Body-`userId` nicht der authentifizierten `userid` entspricht.
+
+Blockieren verwendet den neuen serverseitigen Vertrag `POST`/`DELETE
+/api/socialnetwork/blocked-users/:userId` und die Migration
+`20260716000000-create-user-block.sql`; der Profil-Screen bietet die Blockaktion nativ an.
+
+Die Store-Entscheidung und Rating-Vorbereitung sind in `android/STORE_COMPLIANCE.md`
+dokumentiert: Der Bereich bleibt ausserhalb lokaler Debug-Builds deaktiviert, bis eine formelle
+Store- und Altersfreigabe vorliegt.
### 24. Push Notifications
-- [ ] Firebase Projekt klaeren.
-- [ ] FCM in native App integrieren.
-- [ ] Device Token Backend-Modell planen.
-- [ ] Device Token Registration API definieren.
-- [ ] Opt-in UI implementieren.
-- [ ] Notification Settings implementieren.
-- [ ] Chat Push definieren.
-- [ ] Friend Login Push definieren.
-- [ ] Falukant Event Push definieren.
-- [ ] Vocab Reminder Push definieren.
-- [ ] Deep Links aus Notifications implementieren.
-- [ ] Token Refresh Handling implementieren.
+- [x] Firebase Projekt klaeren.
+- [x] FCM in native App integrieren.
+- [x] Device Token Backend-Modell planen.
+- [x] Device Token Registration API definieren.
+- [x] Opt-in UI implementieren.
+- [x] Notification Settings implementieren.
+- [x] Chat Push definieren.
+- [x] Friend Login Push definieren.
+- [x] Falukant Event Push definieren.
+- [x] Vocab Reminder Push definieren.
+- [x] Deep Links aus Notifications implementieren.
+- [x] Token Refresh Handling implementieren.
+- [ ] Firebase-Service-Account im Backend-Deployment hinterlegen (`FIREBASE_SERVICE_ACCOUNT_PATH` oder `FIREBASE_SERVICE_ACCOUNT_JSON`).
### 25. Deep Links und OAuth
-- [ ] App Links Domain festlegen.
-- [ ] `assetlinks.json` planen.
-- [ ] OAuth Redirect URIs fuer Android planen.
-- [ ] Custom Tabs Flow implementieren.
-- [ ] OAuth Callback Handling implementieren.
-- [ ] Deep Links fuer Profile, Forum, Falukant, Vocab, Blog definieren.
-- [ ] Deep Link Auth Guard implementieren.
-- [ ] Nicht eingeloggte Deep Links nach Login fortsetzen.
+- [x] App Links Domain festlegen.
+- [x] `assetlinks.json` planen.
+- [x] Deep-Link-Struktur fuer Auth, Home, Social, Falukant, Vocab, Settings und Public Content definieren.
+- [x] OAuth Redirect URIs fuer Android planen.
+- [x] Custom Tabs Flow implementieren.
+- [x] OAuth Callback Handling implementieren.
+- [x] Deep Links fuer Profile, Forum, Falukant, Vocab, Blog definieren.
+- [x] Deep Link Auth Guard implementieren.
+- [x] Nicht eingeloggte Deep Links nach Login fortsetzen.
### 26. Sicherheit
-- [ ] Authdaten nicht im Klartext speichern.
-- [ ] Release Logging sensibler Daten verhindern.
-- [ ] Certificate Pinning bewerten, nicht vorschnell erzwingen.
-- [ ] Network Security Config fuer Release restriktiv halten.
-- [ ] Root/Jailbreak Detection bewusst entscheiden.
-- [ ] Screenshot-Schutz fuer Adult/Private Bereiche bewerten.
-- [ ] Datei-Uploads auf MIME/Größe pruefen.
-- [ ] WebView-Fallbacks minimieren.
-- [ ] Dependency-Scanning fuer Android einrichten.
+- [x] Authdaten nicht im Klartext speichern.
+- [x] Release Logging sensibler Daten verhindern.
+- [x] Certificate Pinning bewerten, nicht vorschnell erzwingen.
+- [x] Network Security Config fuer Release restriktiv halten.
+- [x] Root/Jailbreak Detection bewusst entscheiden.
+- [x] Screenshot-Schutz fuer Adult/Private Bereiche bewerten.
+- [x] Datei-Uploads auf MIME/Größe pruefen.
+- [x] WebView-Fallbacks minimieren.
+- [x] Dependency-Scanning fuer Android einrichten.
### 27. Testing
-- [ ] Unit Tests fuer Auth Repository.
-- [ ] Unit Tests fuer Settings Repository.
-- [ ] Unit Tests fuer Error Mapping.
-- [ ] MockWebServer Tests fuer Auth Header.
-- [ ] MockWebServer Tests fuer API-Fehler.
-- [ ] Room Migration Tests.
-- [ ] Compose Tests fuer Login.
-- [ ] Compose Tests fuer Navigation.
-- [ ] Compose Tests fuer Home.
-- [ ] Compose Tests fuer Vocab Lesson.
-- [ ] Realtime Tests mit Testserver planen.
-- [ ] Emulator-Testmatrix definieren: kleines Phone, grosses Phone, Tablet.
-- [ ] Echtes Android-Geraet in Testmatrix aufnehmen.
-- [ ] Offline/Online-Wechsel testen.
-- [ ] App Kill/Restart/Resume testen.
+- [x] Unit Tests fuer Auth Repository.
+- [x] Unit Tests fuer Settings Repository.
+- [x] Unit Tests fuer Error Mapping.
+- [x] MockWebServer Tests fuer Auth Header.
+- [x] MockWebServer Tests fuer API-Fehler.
+- [x] Room Migration Tests.
+- [x] Compose Tests fuer Login.
+- [x] Compose Tests fuer Navigation.
+- [x] Compose Tests fuer Home.
+- [x] Compose Tests fuer Vocab Lesson.
+- [x] Realtime Tests mit Testserver planen.
+- [x] Emulator-Testmatrix definieren: kleines Phone, grosses Phone, Tablet.
+- [x] Echtes Android-Geraet in Testmatrix aufnehmen.
+- [x] Offline/Online-Wechsel testen.
+- [x] App Kill/Restart/Resume testen.
+- [ ] Instrumentation-Suite auf einem verbundenen Geraet oder KVM-faehigen Host ausfuehren (lokal blockiert: kein ADB-Geraet, keine x86_64-Hardwarebeschleunigung).
### 28. Build, Release und Betrieb
@@ -593,6 +824,8 @@ Stand 2026-07-09:
### 29. Backend-Vorbereitung fuer Native
+- [ ] Release-Signaturfingerprint in die ausgelieferte `/.well-known/assetlinks.json` eintragen.
+- [ ] Native OAuth-Redirect-URI bei jedem aktivierten Provider hinterlegen.
- [ ] Mobile API-Inventar aus allen Web-Komponenten erstellen.
- [ ] API-Versionierung bewerten: `/api/mobile/v1` ja/nein.
- [ ] Einheitliches Fehlerformat definieren.
diff --git a/android/README.md b/android/README.md
old mode 100644
new mode 100755
diff --git a/android/SECURITY.md b/android/SECURITY.md
new file mode 100644
index 0000000..13bea95
--- /dev/null
+++ b/android/SECURITY.md
@@ -0,0 +1,37 @@
+# Native Android Security
+
+## Umgesetzte Schutzmaßnahmen
+
+- Die Sitzung liegt ausschließlich in `EncryptedSharedPreferences` mit Android-Master-Key.
+- Release-Builds enthalten keinen HTTP-Body-Logger. Der detaillierte OkHttp-Logger ist auf
+ Nicht-Release-Builds begrenzt.
+- Release-Netzwerkverkehr erlaubt kein Cleartext. Nur der lokale Debug-Flavor darf
+ `10.0.2.2` und `localhost` per HTTP ansprechen.
+- Chat, Galerie, Persönliches und der Adult-Bereich setzen `FLAG_SECURE`. Damit verhindert
+ Android Screenshots, Bildschirmaufnahmen und die Anzeige im App-Switcher.
+- Uploads werden vor dem Service auf einen erlaubten MIME-Typ und eine Größe begrenzt:
+ Bilder/Verifikationsnachweise 10 MiB, Videos 100 MiB. Bilder werden zusätzlich durch
+ Sharp dekodiert, damit ein behaupteter MIME-Typ nicht genügt.
+
+## Bewusste Entscheidungen
+
+### Certificate Pinning
+
+Certificate Pinning wird aktuell **nicht** erzwungen. Die API verwendet HTTPS und die
+plattformseitige Trust-Store-Prüfung. Pinning würde bei Zertifikats- oder CDN-Wechseln ohne
+App-Update zu vollständigen Ausfällen führen. Es wird erst eingeführt, wenn es mindestens zwei
+parallel gültige Pins, ein dokumentiertes Rotation-Verfahren und Monitoring für Pin-Fehler gibt.
+
+### Root- und Bootloader-Erkennung
+
+Die App blockiert gerootete Geräte nicht. Eine lokale Erkennung ist umgehbar und würde legitime
+Nutzer sowie Emulator-Tests ausschließen. Sensible Daten bleiben verschlüsselt, Screenshots in
+sensiblen Bereichen sind gesperrt und Berechtigungen werden auf dem Server durchgesetzt.
+Play Integrity wird bei späteren Hochrisiko-Aktionen als serverseitig prüfbares Signal bewertet,
+nicht als pauschale Startblockade.
+
+## Dependency Scanning
+
+`.github/workflows/android-security.yml` führt bei Pull Requests und Pushes einen
+OSV-Abhängigkeitsscan sowie Android-Lint für den Produktions-Release aus. Kritische Findings
+werden vor einem Release bewertet.
diff --git a/android/STORE_COMPLIANCE.md b/android/STORE_COMPLIANCE.md
new file mode 100644
index 0000000..aac6fff
--- /dev/null
+++ b/android/STORE_COMPLIANCE.md
@@ -0,0 +1,17 @@
+# Store-Compliance
+
+## Adult Content
+
+- `FEATURE_ADULT` ist nur im lokalen Debug-Build aktiv.
+- Staging und Production liefern keinen nativen Adult-Bereich aus.
+- Vor einer Aktivierung sind Play-Content-Rating, Altersklassifizierung, Moderationsprozess,
+ Meldewege und die Store-Richtlinien erneut zu pruefen und freizugeben.
+- Die API bleibt auch bei aktivem Client-Feature die Autoritaet: Volljaehrigkeit und der Status
+ `approved` der Altersverifikation sind fuer jeden geschuetzten Endpunkt erforderlich.
+
+## UGC und Moderation
+
+- Native Meldungen existieren fuer Chat, Forum, Profile, Gaestebuch und Galerie.
+- Meldungen werden serverseitig gespeichert und sind fuer berechtigte Moderatoren sichtbar.
+- Nutzerblockaden werden serverseitig in `community.user_block` gespeichert; die SQL-Migration
+ `backend/migrations/20260716000000-create-user-block.sql` ist vor dem Rollout auszufuehren.
diff --git a/android/TESTING.md b/android/TESTING.md
new file mode 100644
index 0000000..6e6a55e
--- /dev/null
+++ b/android/TESTING.md
@@ -0,0 +1,45 @@
+# Native Android Testmatrix
+
+## Automatisiert
+
+| Bereich | Testart | Ausführung |
+| --- | --- | --- |
+| Auth, Settings, HTTP-Fehler, Header, Daemon-Parser | JVM + MockWebServer | `./gradlew :app:testLocalDebugUnitTest` |
+| Room-Migration und verschlüsselte Session nach Store-Neuerzeugung | Instrumentation | `./gradlew :app:connectedLocalDebugAndroidTest` |
+| Login, Navigation, Home und Vokabel-Lektion | Compose Instrumentation | `./gradlew :app:connectedLocalDebugAndroidTest` |
+| Realtime | JVM-Parser plus manueller Socket.IO-Testserver-Plan | siehe unten |
+
+### Lokaler Ausfuehrungsstatus
+
+- Die JVM-Suite (`:app:testLocalDebugUnitTest`) ist erfolgreich ausgefuehrt.
+- Die Instrumentation-Suite inklusive Room- und Compose-Tests kompiliert erfolgreich.
+- Die Ausfuehrung auf diesem Host ist blockiert: Es ist kein ADB-Geraet verbunden und die
+ x86_64-Emulatoren benoetigen KVM-Hardwarebeschleunigung, die hier nicht verfuegbar ist.
+ Das vorhandene ARM64-System-Image kann auf einem x86_64-Host nicht emuliert werden.
+- Auf einem Host mit KVM oder einem verbundenen Geraet wird die Suite mit
+ `./gradlew :app:connectedLocalDebugAndroidTest` ausgefuehrt. Der CI-Workflow
+ `.github/workflows/android-tests.yml` fuehrt dieselbe Suite auf einem GitHub-Emulator aus.
+
+## Emulator- und Geräte-Matrix
+
+| Ziel | Pflichtfälle |
+| --- | --- |
+| Kleines Phone, API 34+ | Login, Navigation, Formular, Tastatur, Adult-Screenshotschutz |
+| Großes Phone, API 34+ | Galerie, Falukant und Vokabel-Lektion |
+| Tablet, API 34+ | Drawer, Landscape und Listenbreiten |
+| Reales Android-Gerät, aktuelle API | Push-Berechtigung, OAuth Custom Tab, App Links, Kamera/Dateiauswahl |
+
+Die Gradle Managed Devices `mediumPhone` und `mediumTablet` decken die CI-Basis ab. Das lokale
+Team ergänzt für jeden Release-Lauf die installierten kleinen/großen Emulatoren und mindestens
+ein reales Gerät.
+
+## Netzwerk, Realtime und Lifecycle
+
+1. Backend und Daemon starten, mit einem Testkonto anmelden und Socket.IO-Verbindung prüfen.
+2. Backend stoppen: Offline-Banner, verständlicher Fehler und keine blockierte Navigation prüfen.
+3. Backend wieder starten: Screen aktualisieren und Realtime-Verbindung erneut prüfen.
+4. App im Hintergrund beenden und über Launcher oder Push erneut öffnen: verschlüsselte Sitzung,
+ Deep-Link-Fortsetzung und Realtime-Reconnect prüfen.
+5. Für Socket.IO-Regressionen wird ein isolierter Node-Testserver mit den Ereignissen
+ `friendloginchanged`, `falukantUpdateStatus`, `familychanged` und `reloadmenu` verwendet.
+ Der Android-Test verbindet sich mit dessen URL statt mit Production.
diff --git a/android/android/.idea/AndroidProjectSystem.xml b/android/android/.idea/AndroidProjectSystem.xml
old mode 100644
new mode 100755
diff --git a/android/android/.idea/compiler.xml b/android/android/.idea/compiler.xml
new file mode 100755
index 0000000..b86273d
--- /dev/null
+++ b/android/android/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/android/.idea/deploymentTargetSelector.xml b/android/android/.idea/deploymentTargetSelector.xml
old mode 100644
new mode 100755
index cccadc0..cda42fc
--- a/android/android/.idea/deploymentTargetSelector.xml
+++ b/android/android/.idea/deploymentTargetSelector.xml
@@ -6,6 +6,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/android/.idea/migrations.xml b/android/android/.idea/migrations.xml
old mode 100644
new mode 100755
diff --git a/android/android/.idea/misc.xml b/android/android/.idea/misc.xml
old mode 100644
new mode 100755
index 0573b9e..a4f09e2
--- a/android/android/.idea/misc.xml
+++ b/android/android/.idea/misc.xml
@@ -1,6 +1,7 @@
+
-
+
diff --git a/android/android/.idea/runConfigurations.xml b/android/android/.idea/runConfigurations.xml
old mode 100644
new mode 100755
diff --git a/android/android/.idea/runConfigurations/YourPart_Debug.xml b/android/android/.idea/runConfigurations/YourPart_Debug.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/build.gradle b/android/android/app/build.gradle
old mode 100644
new mode 100755
diff --git a/android/android/app/capacitor.build.gradle b/android/android/app/capacitor.build.gradle
old mode 100644
new mode 100755
diff --git a/android/android/app/proguard-rules.pro b/android/android/app/proguard-rules.pro
old mode 100644
new mode 100755
diff --git a/android/android/app/src/androidTest/java/de/yourpart/app/ExampleInstrumentedTest.java b/android/android/app/src/androidTest/java/de/yourpart/app/ExampleInstrumentedTest.java
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/AndroidManifest.xml b/android/android/app/src/main/AndroidManifest.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/java/de/yourpart/app/MainActivity.java b/android/android/app/src/main/java/de/yourpart/app/MainActivity.java
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/android/app/src/main/res/drawable-land-hdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/android/app/src/main/res/drawable-land-mdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/android/app/src/main/res/drawable-land-xhdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/android/app/src/main/res/drawable-land-xxhdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/android/app/src/main/res/drawable-land-xxxhdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-port-hdpi/splash.png b/android/android/app/src/main/res/drawable-port-hdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-port-mdpi/splash.png b/android/android/app/src/main/res/drawable-port-mdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android/android/app/src/main/res/drawable-port-xhdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/android/app/src/main/res/drawable-port-xxhdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/android/app/src/main/res/drawable-port-xxxhdpi/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/android/app/src/main/res/drawable/ic_launcher_background.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/drawable/splash.png b/android/android/app/src/main/res/drawable/splash.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/layout/activity_main.xml b/android/android/app/src/main/res/layout/activity_main.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/values/ic_launcher_background.xml b/android/android/app/src/main/res/values/ic_launcher_background.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/values/strings.xml b/android/android/app/src/main/res/values/strings.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/values/styles.xml b/android/android/app/src/main/res/values/styles.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/main/res/xml/file_paths.xml b/android/android/app/src/main/res/xml/file_paths.xml
old mode 100644
new mode 100755
diff --git a/android/android/app/src/test/java/de/yourpart/app/ExampleUnitTest.java b/android/android/app/src/test/java/de/yourpart/app/ExampleUnitTest.java
old mode 100644
new mode 100755
diff --git a/android/android/build.gradle b/android/android/build.gradle
old mode 100644
new mode 100755
diff --git a/android/android/capacitor.settings.gradle b/android/android/capacitor.settings.gradle
old mode 100644
new mode 100755
diff --git a/android/android/gradle.properties b/android/android/gradle.properties
old mode 100644
new mode 100755
diff --git a/android/android/gradle/wrapper/gradle-wrapper.jar b/android/android/gradle/wrapper/gradle-wrapper.jar
old mode 100644
new mode 100755
diff --git a/android/android/gradle/wrapper/gradle-wrapper.properties b/android/android/gradle/wrapper/gradle-wrapper.properties
old mode 100644
new mode 100755
diff --git a/android/android/gradlew.bat b/android/android/gradlew.bat
old mode 100644
new mode 100755
diff --git a/android/android/settings.gradle b/android/android/settings.gradle
old mode 100644
new mode 100755
diff --git a/android/android/variables.gradle b/android/android/variables.gradle
old mode 100644
new mode 100755
diff --git a/android/capacitor.config.ts b/android/capacitor.config.ts
old mode 100644
new mode 100755
diff --git a/android/native/.idea/AndroidProjectSystem.xml b/android/native/.idea/AndroidProjectSystem.xml
new file mode 100755
index 0000000..4a53bee
--- /dev/null
+++ b/android/native/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/caches/deviceStreaming.xml b/android/native/.idea/caches/deviceStreaming.xml
new file mode 100755
index 0000000..e70eb94
--- /dev/null
+++ b/android/native/.idea/caches/deviceStreaming.xml
@@ -0,0 +1,2001 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/deploymentTargetSelector.xml b/android/native/.idea/deploymentTargetSelector.xml
new file mode 100755
index 0000000..0e8629b
--- /dev/null
+++ b/android/native/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/deviceManager.xml b/android/native/.idea/deviceManager.xml
new file mode 100755
index 0000000..91f9558
--- /dev/null
+++ b/android/native/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/gradle.xml b/android/native/.idea/gradle.xml
new file mode 100755
index 0000000..844d25a
--- /dev/null
+++ b/android/native/.idea/gradle.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/markdown.xml b/android/native/.idea/markdown.xml
new file mode 100755
index 0000000..c61ea33
--- /dev/null
+++ b/android/native/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/migrations.xml b/android/native/.idea/migrations.xml
new file mode 100755
index 0000000..d033e77
--- /dev/null
+++ b/android/native/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/misc.xml b/android/native/.idea/misc.xml
new file mode 100755
index 0000000..b2c751a
--- /dev/null
+++ b/android/native/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/runConfigurations.xml b/android/native/.idea/runConfigurations.xml
new file mode 100755
index 0000000..16660f1
--- /dev/null
+++ b/android/native/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.idea/vcs.xml b/android/native/.idea/vcs.xml
new file mode 100755
index 0000000..d462174
--- /dev/null
+++ b/android/native/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/native/.kotlin/errors/errors-1783688579715.log b/android/native/.kotlin/errors/errors-1783688579715.log
new file mode 100755
index 0000000..250126e
--- /dev/null
+++ b/android/native/.kotlin/errors/errors-1783688579715.log
@@ -0,0 +1,76 @@
+kotlin version: 2.0.21
+error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /mnt/share/torsten/Programs/YourPart3/android/native/app/src/main/java/de/yourpart/nativeapp/MainActivity.kt:24:5: java.lang.IllegalArgumentException: source must not be null
+ at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
+ at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
+ at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
+ at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
+ at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
+ at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
+ at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
+ at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
+ at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
+ at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
+ at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
+ at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
+ at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
+ at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
+ at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
+ at java.base/java.lang.reflect.Method.invoke(Unknown Source)
+ at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+ at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
+ at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
+ at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
+ at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
+ at java.base/java.lang.Thread.run(Unknown Source)
+Caused by: java.lang.IllegalArgumentException: source must not be null
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
+ at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
+ at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
+ at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
+ at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
+ at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
+ at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
+ ... 33 more
+
+
diff --git a/android/native/.kotlin/errors/errors-1783688926385.log b/android/native/.kotlin/errors/errors-1783688926385.log
new file mode 100755
index 0000000..250126e
--- /dev/null
+++ b/android/native/.kotlin/errors/errors-1783688926385.log
@@ -0,0 +1,76 @@
+kotlin version: 2.0.21
+error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /mnt/share/torsten/Programs/YourPart3/android/native/app/src/main/java/de/yourpart/nativeapp/MainActivity.kt:24:5: java.lang.IllegalArgumentException: source must not be null
+ at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
+ at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
+ at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
+ at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
+ at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
+ at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
+ at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
+ at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
+ at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
+ at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
+ at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
+ at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
+ at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
+ at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
+ at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
+ at java.base/java.lang.reflect.Method.invoke(Unknown Source)
+ at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+ at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
+ at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
+ at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
+ at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
+ at java.base/java.lang.Thread.run(Unknown Source)
+Caused by: java.lang.IllegalArgumentException: source must not be null
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
+ at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
+ at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
+ at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
+ at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
+ at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
+ at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
+ ... 33 more
+
+
diff --git a/android/native/.kotlin/errors/errors-1783689039380.log b/android/native/.kotlin/errors/errors-1783689039380.log
new file mode 100755
index 0000000..250126e
--- /dev/null
+++ b/android/native/.kotlin/errors/errors-1783689039380.log
@@ -0,0 +1,76 @@
+kotlin version: 2.0.21
+error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /mnt/share/torsten/Programs/YourPart3/android/native/app/src/main/java/de/yourpart/nativeapp/MainActivity.kt:24:5: java.lang.IllegalArgumentException: source must not be null
+ at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
+ at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
+ at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
+ at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
+ at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
+ at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
+ at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
+ at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
+ at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
+ at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
+ at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
+ at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
+ at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
+ at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
+ at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
+ at java.base/java.lang.reflect.Method.invoke(Unknown Source)
+ at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+ at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+ at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
+ at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
+ at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
+ at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
+ at java.base/java.lang.Thread.run(Unknown Source)
+Caused by: java.lang.IllegalArgumentException: source must not be null
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
+ at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
+ at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
+ at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
+ at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
+ at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
+ at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
+ at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
+ at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
+ at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
+ at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
+ at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
+ ... 33 more
+
+
diff --git a/android/native/.kotlin/errors/errors-1784193658070.log b/android/native/.kotlin/errors/errors-1784193658070.log
new file mode 100644
index 0000000..dab4a64
--- /dev/null
+++ b/android/native/.kotlin/errors/errors-1784193658070.log
@@ -0,0 +1,149 @@
+kotlin version: 2.2.21
+error message: Daemon compilation failed: null
+java.lang.Exception
+ at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
+ at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:236)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:155)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:110)
+ at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
+ at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
+ at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
+ at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
+ at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
+ at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
+ at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176)
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
+ at org.gradle.internal.Factories$1.create(Factories.java:31)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
+ at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
+ at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
+ at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
+ at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
+ at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
+ at java.base/java.lang.Thread.run(Thread.java:1583)
+Caused by: java.lang.AssertionError: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin: class-attributes.tab, source-to-classes.tab, internal-name-to-source.tab
+ at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:218)
+ at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55)
+ at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:293)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
+ at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
+ at java.base/java.lang.reflect.Method.invoke(Method.java:580)
+ at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
+ at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
+ at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
+ at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
+ ... 3 more
+Caused by: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin: class-attributes.tab, source-to-classes.tab, internal-name-to-source.tab
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205)
+ ... 22 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 43 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.AppendableSetBasicMap.close(BasicMap.kt:157)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 44 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 44 more
+
+
diff --git a/android/native/.kotlin/errors/errors-1784193658098.log b/android/native/.kotlin/errors/errors-1784193658098.log
new file mode 100644
index 0000000..9c601a1
--- /dev/null
+++ b/android/native/.kotlin/errors/errors-1784193658098.log
@@ -0,0 +1,150 @@
+kotlin version: 2.2.21
+error message: Daemon compilation failed: null
+java.lang.Exception
+ at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
+ at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:236)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:155)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:110)
+ at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
+ at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
+ at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
+ at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
+ at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
+ at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
+ at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176)
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
+ at org.gradle.internal.Factories$1.create(Factories.java:31)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
+ at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
+ at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
+ at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
+ at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
+ at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
+ at java.base/java.lang.Thread.run(Thread.java:1583)
+Caused by: java.lang.AssertionError: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin: class-attributes.tab, proto.tab
+ at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:218)
+ at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55)
+ at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:293)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
+ at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
+ at java.base/java.lang.reflect.Method.invoke(Method.java:580)
+ at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
+ at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
+ at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
+ at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
+ ... 3 more
+Caused by: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin: class-attributes.tab, proto.tab
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205)
+ ... 22 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 43 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 43 more
+ Suppressed: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/inputs: source-to-output.tab
+ ... 25 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.AppendableSetBasicMap.close(BasicMap.kt:157)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 44 more
+
+
diff --git a/android/native/.kotlin/errors/errors-1784193702998.log b/android/native/.kotlin/errors/errors-1784193702998.log
new file mode 100644
index 0000000..25eba86
--- /dev/null
+++ b/android/native/.kotlin/errors/errors-1784193702998.log
@@ -0,0 +1,153 @@
+kotlin version: 2.2.21
+error message: Daemon compilation failed: null
+java.lang.Exception
+ at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
+ at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:236)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:155)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:110)
+ at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
+ at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
+ at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
+ at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
+ at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
+ at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
+ at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176)
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
+ at org.gradle.internal.Factories$1.create(Factories.java:31)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
+ at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
+ at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
+ at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
+ at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
+ at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
+ at java.base/java.lang.Thread.run(Thread.java:1583)
+Caused by: java.lang.AssertionError: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin: class-fq-name-to-source.tab, proto.tab
+ at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:218)
+ at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55)
+ at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:293)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
+ at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
+ at java.base/java.lang.reflect.Method.invoke(Method.java:580)
+ at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
+ at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
+ at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
+ at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
+ ... 3 more
+Caused by: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin: class-fq-name-to-source.tab, proto.tab
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205)
+ ... 22 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 43 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 43 more
+ Suppressed: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/lookups: lookups.tab
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.LookupStorage.close(LookupStorage.kt:155)
+ ... 23 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.AppendableSetBasicMap.close(BasicMap.kt:157)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 25 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/compileLocalDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 45 more
+
+
diff --git a/android/native/.kotlin/errors/errors-1784193703057.log b/android/native/.kotlin/errors/errors-1784193703057.log
new file mode 100644
index 0000000..771b5b4
--- /dev/null
+++ b/android/native/.kotlin/errors/errors-1784193703057.log
@@ -0,0 +1,128 @@
+kotlin version: 2.2.21
+error message: Daemon compilation failed: null
+java.lang.Exception
+ at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:69)
+ at org.jetbrains.kotlin.daemon.common.CompileService$CallResult$Error.get(CompileService.kt:65)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:236)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:155)
+ at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:110)
+ at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
+ at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
+ at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
+ at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
+ at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
+ at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
+ at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
+ at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
+ at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176)
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
+ at org.gradle.internal.Factories$1.create(Factories.java:31)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
+ at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
+ at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
+ at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
+ at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
+ at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
+ at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
+ at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
+ at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
+ at java.base/java.lang.Thread.run(Thread.java:1583)
+Caused by: java.lang.AssertionError: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin: class-fq-name-to-source.tab
+ at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:218)
+ at org.jetbrains.kotlin.incremental.IncrementalCachesManager.close(IncrementalCachesManager.kt:55)
+ at kotlin.io.CloseableKt.closeFinally(Closeable.kt:46)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:293)
+ at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:128)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
+ at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
+ at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
+ at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
+ at java.base/java.lang.reflect.Method.invoke(Method.java:580)
+ at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
+ at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
+ at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
+ at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
+ at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
+ at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
+ ... 3 more
+Caused by: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin: class-fq-name-to-source.tab
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.com.google.common.io.Closer.close(Closer.java:205)
+ ... 22 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.close(PersistentStorage.kt:124)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 24 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 43 more
+ Suppressed: java.lang.Exception: Could not close incremental caches in /mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/lookups: lookups.tab
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:95)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.close(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.LookupStorage.close(LookupStorage.kt:155)
+ ... 23 more
+ Suppressed: java.lang.IllegalStateException: Storage for [/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab] is already registered
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
+ at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.(PagedFileStorage.java:72)
+ at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.(ResizeableMappedFile.java:68)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.(PersistentBTreeEnumerator.java:128)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:165)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.(PersistentMapImpl.java:140)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:46)
+ at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.(PersistentHashMap.java:72)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
+ at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.applyChanges(InMemoryStorage.kt:108)
+ at org.jetbrains.kotlin.incremental.storage.AppendableInMemoryStorage.applyChanges(InMemoryStorage.kt:179)
+ at org.jetbrains.kotlin.incremental.storage.InMemoryStorage.close(InMemoryStorage.kt:136)
+ at org.jetbrains.kotlin.incremental.storage.AppendableSetBasicMap.close(BasicMap.kt:157)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner$close$1.invoke(BasicMapsOwner.kt:53)
+ at org.jetbrains.kotlin.incremental.storage.BasicMapsOwner.forEachMapSafe(BasicMapsOwner.kt:87)
+ ... 25 more
+ Suppressed: java.lang.Exception: Storage[/mnt/share/torsten/Programs/YourPart3/android/native/app/build/kotlin/kaptGenerateStubsLocalDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab] registration stack trace
+ at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
+ ... 45 more
+
+
diff --git a/android/native/README.md b/android/native/README.md
old mode 100644
new mode 100755
index 707b2d4..477faf7
--- a/android/native/README.md
+++ b/android/native/README.md
@@ -13,6 +13,7 @@ Native Android-App fuer YourPart als paralleler Aufbau zur bestehenden Capacitor
- compileSdk/targetSdk: 36
- Flavors: `local`, `staging`, `production`
- Build Types: `debug`, `release`
+- Feature Flags im Build: `FEATURE_ADMIN`, `FEATURE_ADULT`, `FEATURE_3D`, `FEATURE_MINIGAMES`, `FEATURE_PUSH`
## Kommandos
@@ -32,3 +33,26 @@ Der `local` Flavor nutzt:
- API: `http://10.0.2.2:2020`
- Socket.IO: `http://10.0.2.2:2020`
- Daemon: `ws://10.0.2.2:2021`
+
+## Security
+
+- Debug erlaubt Cleartext nur fuer `10.0.2.2` und `localhost`.
+- Release verbietet Cleartext komplett.
+- Release-Builds pruefen automatisch, dass keine lokalen oder unverschluesselten URLs in `staging` oder `production` verwendet werden.
+
+## Firebase Push
+
+1. Die registrierte Produktions-App `de.yourpart.nativeapp` als
+ `app/google-services.json` ablegen. Die Beispieldatei
+ `app/google-services.json.example` ist kein gültiger Ersatz.
+2. Push ist ausschließlich in `productionRelease` aktiviert. `local` und `staging`
+ verwenden abweichende Paketnamen und bleiben absichtlich pushfrei, bis dafür eigene
+ Firebase-Apps registriert sind.
+3. Im Backend eine Firebase-Service-Account-Datei außerhalb des Repositories bereitstellen
+ und `FIREBASE_SERVICE_ACCOUNT_PATH=/sicherer/pfad/firebase-service-account.json`
+ setzen. Alternativ kann `FIREBASE_SERVICE_ACCOUNT_JSON` mit dem JSON-Inhalt gesetzt
+ werden. Ohne eine dieser Variablen werden keine Pushes versendet, die App und die API
+ bleiben aber funktionsfähig.
+
+Das Google-Services-Plugin wird nur für `productionRelease` mit vorhandener echter
+Konfigurationsdatei angewendet; lokale Builds bleiben dadurch reproduzierbar.
diff --git a/android/native/app/build.gradle.kts b/android/native/app/build.gradle.kts
old mode 100644
new mode 100755
index 9c14e76..50ba87e
--- a/android/native/app/build.gradle.kts
+++ b/android/native/app/build.gradle.kts
@@ -4,10 +4,20 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
+ id("org.jetbrains.kotlin.plugin.serialization")
id("com.google.dagger.hilt.android")
kotlin("kapt")
}
+// Only the production release uses the Firebase app registered as de.yourpart.nativeapp.
+// Local/staging builds have different application IDs and deliberately stay push-free.
+val isProductionReleaseBuild = gradle.startParameter.taskNames.any {
+ it.contains("productionrelease", ignoreCase = true)
+}
+if (isProductionReleaseBuild && file("google-services.json").isFile) {
+ apply(plugin = "com.google.gms.google-services")
+}
+
val gitShaProvider = providers.exec {
workingDir = rootDir.resolve("../..")
commandLine("git", "rev-parse", "--short=12", "HEAD")
@@ -15,6 +25,34 @@ val gitShaProvider = providers.exec {
val gitSha: String = runCatching { gitShaProvider.get() }.getOrDefault("dev")
+data class EnvironmentConfig(
+ val apiBaseUrl: String,
+ val socketIoUrl: String,
+ val daemonSocketUrl: String,
+ val cleartextAllowed: Boolean,
+)
+
+val environmentConfigs = mapOf(
+ "local" to EnvironmentConfig(
+ apiBaseUrl = "http://10.0.2.2:2020",
+ socketIoUrl = "http://10.0.2.2:2020",
+ daemonSocketUrl = "ws://10.0.2.2:2021",
+ cleartextAllowed = true,
+ ),
+ "staging" to EnvironmentConfig(
+ apiBaseUrl = "https://staging.your-part.de",
+ socketIoUrl = "https://staging.your-part.de",
+ daemonSocketUrl = "wss://staging.your-part.de",
+ cleartextAllowed = false,
+ ),
+ "production" to EnvironmentConfig(
+ apiBaseUrl = "https://www.your-part.de",
+ socketIoUrl = "https://www.your-part.de",
+ daemonSocketUrl = "wss://www.your-part.de",
+ cleartextAllowed = false,
+ ),
+)
+
android {
namespace = "de.yourpart.nativeapp"
compileSdk = 36
@@ -35,23 +73,32 @@ android {
dimension = "environment"
applicationIdSuffix = ".local"
versionNameSuffix = "-local"
- buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:2020\"")
- buildConfigField("String", "SOCKET_IO_URL", "\"http://10.0.2.2:2020\"")
- buildConfigField("String", "DAEMON_SOCKET_URL", "\"ws://10.0.2.2:2021\"")
+ val cfg = environmentConfigs.getValue(name)
+ buildConfigField("String", "ENVIRONMENT", "\"$name\"")
+ buildConfigField("String", "API_BASE_URL", "\"${cfg.apiBaseUrl}\"")
+ buildConfigField("String", "SOCKET_IO_URL", "\"${cfg.socketIoUrl}\"")
+ buildConfigField("String", "DAEMON_SOCKET_URL", "\"${cfg.daemonSocketUrl}\"")
+ buildConfigField("boolean", "ALLOW_CLEARTEXT", cfg.cleartextAllowed.toString())
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
- buildConfigField("String", "API_BASE_URL", "\"https://staging.your-part.de\"")
- buildConfigField("String", "SOCKET_IO_URL", "\"https://staging.your-part.de\"")
- buildConfigField("String", "DAEMON_SOCKET_URL", "\"wss://staging.your-part.de\"")
+ val cfg = environmentConfigs.getValue(name)
+ buildConfigField("String", "ENVIRONMENT", "\"$name\"")
+ buildConfigField("String", "API_BASE_URL", "\"${cfg.apiBaseUrl}\"")
+ buildConfigField("String", "SOCKET_IO_URL", "\"${cfg.socketIoUrl}\"")
+ buildConfigField("String", "DAEMON_SOCKET_URL", "\"${cfg.daemonSocketUrl}\"")
+ buildConfigField("boolean", "ALLOW_CLEARTEXT", cfg.cleartextAllowed.toString())
}
create("production") {
dimension = "environment"
- buildConfigField("String", "API_BASE_URL", "\"https://www.your-part.de\"")
- buildConfigField("String", "SOCKET_IO_URL", "\"https://www.your-part.de\"")
- buildConfigField("String", "DAEMON_SOCKET_URL", "\"wss://www.your-part.de\"")
+ val cfg = environmentConfigs.getValue(name)
+ buildConfigField("String", "ENVIRONMENT", "\"$name\"")
+ buildConfigField("String", "API_BASE_URL", "\"${cfg.apiBaseUrl}\"")
+ buildConfigField("String", "SOCKET_IO_URL", "\"${cfg.socketIoUrl}\"")
+ buildConfigField("String", "DAEMON_SOCKET_URL", "\"${cfg.daemonSocketUrl}\"")
+ buildConfigField("boolean", "ALLOW_CLEARTEXT", cfg.cleartextAllowed.toString())
}
}
@@ -59,9 +106,21 @@ android {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
+ buildConfigField("boolean", "IS_RELEASE_BUILD", "false")
+ buildConfigField("boolean", "FEATURE_ADMIN", "false")
+ buildConfigField("boolean", "FEATURE_ADULT", "true")
+ buildConfigField("boolean", "FEATURE_3D", "false")
+ buildConfigField("boolean", "FEATURE_MINIGAMES", "false")
+ buildConfigField("boolean", "FEATURE_PUSH", "false")
}
release {
isMinifyEnabled = false
+ buildConfigField("boolean", "IS_RELEASE_BUILD", "true")
+ buildConfigField("boolean", "FEATURE_ADMIN", "false")
+ buildConfigField("boolean", "FEATURE_ADULT", "false")
+ buildConfigField("boolean", "FEATURE_3D", "false")
+ buildConfigField("boolean", "FEATURE_MINIGAMES", "false")
+ buildConfigField("boolean", "FEATURE_PUSH", "true")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
@@ -74,6 +133,29 @@ android {
compose = true
}
+ packaging {
+ resources {
+ excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
+ }
+ }
+
+ testOptions {
+ managedDevices {
+ devices {
+ create("mediumPhone") {
+ device = "Medium Phone"
+ apiLevel = 34
+ systemImageSource = "google"
+ }
+ create("mediumTablet") {
+ device = "Medium Tablet"
+ apiLevel = 34
+ systemImageSource = "google"
+ }
+ }
+ }
+ }
+
lint {
abortOnError = true
checkReleaseBuilds = true
@@ -91,13 +173,51 @@ android {
}
}
+kapt {
+ arguments {
+ arg("room.schemaLocation", "$projectDir/schemas")
+ }
+}
+
+val validateReleaseConfig by tasks.registering {
+ doLast {
+ environmentConfigs
+ .filterKeys { it != "local" }
+ .forEach { (name, cfg) ->
+ require(cfg.apiBaseUrl.startsWith("https://")) {
+ "Release flavor '$name' must use HTTPS API URL, got ${cfg.apiBaseUrl}"
+ }
+ require(cfg.socketIoUrl.startsWith("https://")) {
+ "Release flavor '$name' must use HTTPS Socket.IO URL, got ${cfg.socketIoUrl}"
+ }
+ require(cfg.daemonSocketUrl.startsWith("wss://")) {
+ "Release flavor '$name' must use WSS daemon URL, got ${cfg.daemonSocketUrl}"
+ }
+ require(!cfg.apiBaseUrl.contains("10.0.2.2") && !cfg.apiBaseUrl.contains("localhost")) {
+ "Release flavor '$name' must not use local API URL, got ${cfg.apiBaseUrl}"
+ }
+ }
+ }
+}
+
+tasks.matching { task ->
+ task.name.startsWith("assemble") && task.name.endsWith("Release")
+}.configureEach {
+ dependsOn(validateReleaseConfig)
+}
+
dependencies {
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.activity:activity-compose:1.13.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
+ implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.navigation:navigation-compose:2.9.6")
+ implementation("androidx.browser:browser:1.9.0")
+ implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
implementation("androidx.compose.material3:material3:1.4.0")
+ implementation("androidx.compose.material:material-icons-extended:1.4.1")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
@@ -106,17 +226,27 @@ dependencies {
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
implementation("com.squareup.okhttp3:logging-interceptor:5.3.2")
+ implementation("io.socket:socket.io-client:2.1.0")
implementation("com.google.dagger:hilt-android:2.57.2")
kapt("com.google.dagger:hilt-android-compiler:2.57.2")
- implementation("androidx.room:room-runtime:2.6.1")
- implementation("androidx.room:room-ktx:2.6.1")
- kapt("androidx.room:room-compiler:2.6.1")
+ implementation("androidx.room:room-runtime:2.8.0")
+ implementation("androidx.room:room-ktx:2.8.0")
+ kapt("androidx.room:room-compiler:2.8.0")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("io.coil-kt:coil-compose:2.6.0")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("com.google.firebase:firebase-messaging:24.1.2")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
testImplementation("junit:junit:4.13.2")
+ testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.9.4")
+ debugImplementation("androidx.compose.ui:ui-test-manifest:1.9.4")
+ androidTestImplementation("androidx.room:room-testing:2.8.0")
+ androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
}
diff --git a/android/native/app/google-services.json b/android/native/app/google-services.json
new file mode 100644
index 0000000..b310d44
--- /dev/null
+++ b/android/native/app/google-services.json
@@ -0,0 +1,29 @@
+{
+ "project_info": {
+ "project_number": "872182891503",
+ "project_id": "yourpart-2f689",
+ "storage_bucket": "yourpart-2f689.firebasestorage.app"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:872182891503:android:6004ef4c46ce27e26eec1a",
+ "android_client_info": {
+ "package_name": "de.yourpart.nativeapp"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyC28yw1StDE15tdHKDQEXqEC_cBNXG9xkM"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/android/native/app/google-services.json.example b/android/native/app/google-services.json.example
new file mode 100644
index 0000000..52df3aa
--- /dev/null
+++ b/android/native/app/google-services.json.example
@@ -0,0 +1,17 @@
+{
+ "project_info": {
+ "project_number": "REPLACE_WITH_FIREBASE_PROJECT_NUMBER",
+ "project_id": "REPLACE_WITH_FIREBASE_PROJECT_ID",
+ "storage_bucket": "REPLACE_WITH_FIREBASE_STORAGE_BUCKET"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "REPLACE_WITH_ANDROID_APP_ID",
+ "android_client_info": { "package_name": "de.yourpart.nativeapp" }
+ },
+ "api_key": [{ "current_key": "REPLACE_WITH_FIREBASE_API_KEY"}]
+ }
+ ],
+ "configuration_version": "1"
+}
diff --git a/android/native/app/proguard-rules.pro b/android/native/app/proguard-rules.pro
old mode 100644
new mode 100755
diff --git a/android/native/app/schemas/de.yourpart.nativeapp.core.persistence.AppCacheDatabase/2.json b/android/native/app/schemas/de.yourpart.nativeapp.core.persistence.AppCacheDatabase/2.json
new file mode 100644
index 0000000..2893cf6
--- /dev/null
+++ b/android/native/app/schemas/de.yourpart.nativeapp.core.persistence.AppCacheDatabase/2.json
@@ -0,0 +1,232 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "771f218fbabd59ebba146d6688176afa",
+ "entities": [
+ {
+ "tableName": "user_profile_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT, `avatarUrl` TEXT, `profileJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`userId`))",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "avatarUrl",
+ "columnName": "avatarUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "profileJson",
+ "columnName": "profileJson",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAtMillis",
+ "columnName": "updatedAtMillis",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "userId"
+ ]
+ }
+ },
+ {
+ "tableName": "friend_search_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, `resultsJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`query`))",
+ "fields": [
+ {
+ "fieldPath": "query",
+ "columnName": "query",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "resultsJson",
+ "columnName": "resultsJson",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAtMillis",
+ "columnName": "updatedAtMillis",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "query"
+ ]
+ }
+ },
+ {
+ "tableName": "falukant_status_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cacheKey` TEXT NOT NULL, `statusJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`cacheKey`))",
+ "fields": [
+ {
+ "fieldPath": "cacheKey",
+ "columnName": "cacheKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusJson",
+ "columnName": "statusJson",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAtMillis",
+ "columnName": "updatedAtMillis",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "cacheKey"
+ ]
+ }
+ },
+ {
+ "tableName": "vocab_course_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` TEXT NOT NULL, `title` TEXT NOT NULL, `summary` TEXT, `courseJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`courseId`))",
+ "fields": [
+ {
+ "fieldPath": "courseId",
+ "columnName": "courseId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "summary",
+ "columnName": "summary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "courseJson",
+ "columnName": "courseJson",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAtMillis",
+ "columnName": "updatedAtMillis",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "courseId"
+ ]
+ }
+ },
+ {
+ "tableName": "vocab_lesson_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`lessonId` TEXT NOT NULL, `courseId` TEXT NOT NULL, `title` TEXT NOT NULL, `lessonJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`lessonId`))",
+ "fields": [
+ {
+ "fieldPath": "lessonId",
+ "columnName": "lessonId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "courseId",
+ "columnName": "courseId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lessonJson",
+ "columnName": "lessonJson",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAtMillis",
+ "columnName": "updatedAtMillis",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "lessonId"
+ ]
+ }
+ },
+ {
+ "tableName": "vocab_review_outbox",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`requestId` TEXT NOT NULL, `payloadJson` TEXT NOT NULL, `createdAtMillis` INTEGER NOT NULL, PRIMARY KEY(`requestId`))",
+ "fields": [
+ {
+ "fieldPath": "requestId",
+ "columnName": "requestId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "payloadJson",
+ "columnName": "payloadJson",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAtMillis",
+ "columnName": "createdAtMillis",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "requestId"
+ ]
+ }
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '771f218fbabd59ebba146d6688176afa')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/android/native/app/src/androidTest/java/de/yourpart/nativeapp/core/auth/storage/SessionStoreResumeTest.kt b/android/native/app/src/androidTest/java/de/yourpart/nativeapp/core/auth/storage/SessionStoreResumeTest.kt
new file mode 100644
index 0000000..63316fc
--- /dev/null
+++ b/android/native/app/src/androidTest/java/de/yourpart/nativeapp/core/auth/storage/SessionStoreResumeTest.kt
@@ -0,0 +1,25 @@
+package de.yourpart.nativeapp.core.auth.storage
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import de.yourpart.nativeapp.core.auth.model.AuthSession
+import de.yourpart.nativeapp.core.auth.model.UserDto
+import kotlinx.serialization.json.Json
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SessionStoreResumeTest {
+ @Test fun savedSessionSurvivesStoreRecreation() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val firstStore = SessionStore(context, Json { ignoreUnknownKeys = true })
+ firstStore.clearSession()
+ firstStore.saveSession(AuthSession(UserDto("resume-user", "alice", true, authCode = "resume-code"), true, 1L))
+
+ val resumedStore = SessionStore(context, Json { ignoreUnknownKeys = true })
+
+ assertEquals("resume-user", resumedStore.currentSession?.user?.id)
+ resumedStore.clearSession()
+ }
+}
diff --git a/android/native/app/src/androidTest/java/de/yourpart/nativeapp/core/persistence/AppCacheMigrationTest.kt b/android/native/app/src/androidTest/java/de/yourpart/nativeapp/core/persistence/AppCacheMigrationTest.kt
new file mode 100644
index 0000000..71ba5b5
--- /dev/null
+++ b/android/native/app/src/androidTest/java/de/yourpart/nativeapp/core/persistence/AppCacheMigrationTest.kt
@@ -0,0 +1,42 @@
+package de.yourpart.nativeapp.core.persistence
+
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AppCacheMigrationTest {
+ @get:Rule
+ val helper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ AppCacheDatabase::class.java,
+ )
+
+ @Test
+ fun migrationFromVersion1PreservesExistingCacheAndCreatesReviewOutbox() {
+ helper.createDatabase(DB_NAME, 1).apply {
+ execSQL("CREATE TABLE IF NOT EXISTS `user_profile_cache` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT, `avatarUrl` TEXT, `profileJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`userId`))")
+ execSQL("CREATE TABLE IF NOT EXISTS `friend_search_cache` (`query` TEXT NOT NULL, `resultsJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`query`))")
+ execSQL("CREATE TABLE IF NOT EXISTS `falukant_status_cache` (`cacheKey` TEXT NOT NULL, `statusJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`cacheKey`))")
+ execSQL("CREATE TABLE IF NOT EXISTS `vocab_course_cache` (`courseId` TEXT NOT NULL, `title` TEXT NOT NULL, `summary` TEXT, `courseJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`courseId`))")
+ execSQL("CREATE TABLE IF NOT EXISTS `vocab_lesson_cache` (`lessonId` TEXT NOT NULL, `courseId` TEXT NOT NULL, `title` TEXT NOT NULL, `lessonJson` TEXT NOT NULL, `updatedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`lessonId`))")
+ execSQL("INSERT INTO `vocab_lesson_cache` VALUES ('lesson-1', 'course-1', 'Titel', '{}', 1)")
+ close()
+ }
+
+ helper.runMigrationsAndValidate(DB_NAME, 2, true, AppCacheMigrations.MIGRATION_1_2).use { database ->
+ database.query("SELECT lessonId FROM vocab_lesson_cache").use { cursor ->
+ assertEquals(1, cursor.count)
+ }
+ database.query("SELECT name FROM sqlite_master WHERE type='table' AND name='vocab_review_outbox'").use { cursor ->
+ assertEquals(1, cursor.count)
+ }
+ }
+ }
+
+ private companion object { const val DB_NAME = "cache-migration-test" }
+}
diff --git a/android/native/app/src/androidTest/java/de/yourpart/nativeapp/ui/NativeScreenComposeTest.kt b/android/native/app/src/androidTest/java/de/yourpart/nativeapp/ui/NativeScreenComposeTest.kt
new file mode 100644
index 0000000..f252ac5
--- /dev/null
+++ b/android/native/app/src/androidTest/java/de/yourpart/nativeapp/ui/NativeScreenComposeTest.kt
@@ -0,0 +1,83 @@
+package de.yourpart.nativeapp.ui
+
+import androidx.activity.ComponentActivity
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import de.yourpart.nativeapp.feature.auth.AuthScreen
+import de.yourpart.nativeapp.feature.auth.LoginUiState
+import de.yourpart.nativeapp.feature.home.HomeScreen
+import de.yourpart.nativeapp.feature.home.HomeUiState
+import de.yourpart.nativeapp.feature.vocab.VocabScreen
+import de.yourpart.nativeapp.feature.vocab.VocabUiState
+import de.yourpart.nativeapp.ui.theme.YourPartTheme
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+class NativeScreenComposeTest {
+ @get:Rule val composeRule = createAndroidComposeRule()
+
+ @Test fun loginShowsValidationAndPrimaryAction() {
+ composeRule.setContent {
+ YourPartTheme {
+ AuthScreen(
+ uiState = LoginUiState(errorMessage = "Ungültige Anmeldung"),
+ oauthProviders = emptyList(),
+ onUsernameChange = {}, onPasswordChange = {}, onRememberMeChange = {}, onLogin = {},
+ onRegisterClick = {}, onForgotPasswordClick = {}, onOAuthLogin = {},
+ )
+ }
+ }
+ composeRule.onNodeWithText("Anmelden").assertIsDisplayed()
+ composeRule.onNodeWithText("Ungültige Anmeldung").assertIsDisplayed()
+ }
+
+ @Test fun navigationMovesToDestination() {
+ composeRule.setContent {
+ YourPartTheme {
+ val navController = rememberNavController()
+ LaunchedEffect(Unit) { navController.navigate("target") }
+ NavHost(navController = navController, startDestination = "start") {
+ composable("start") { Text("Start") }
+ composable("target") { Text("Ziel") }
+ }
+ }
+ }
+ composeRule.onNodeWithText("Ziel").assertIsDisplayed()
+ }
+
+ @Test fun homeRendersDashboardAndInvokesRefresh() {
+ var refreshes = 0
+ composeRule.setContent { YourPartTheme { HomeScreen(HomeUiState(isLoading = false), onRefresh = { refreshes += 1 }) } }
+ composeRule.onNodeWithText("Dashboard").assertIsDisplayed()
+ composeRule.onNodeWithText("Aktualisieren").performClick()
+ composeRule.runOnIdle { assertEquals(1, refreshes) }
+ }
+
+ @Test fun vocabRendersLoadedLesson() {
+ val lesson = buildJsonObject { put("title", "Lektion eins") }
+ composeRule.setContent {
+ YourPartTheme {
+ VocabScreen(
+ state = VocabUiState(lesson = lesson),
+ onCourses = {}, onCourseId = {}, onLoadCourse = {}, onLessonId = {}, onLoadLesson = {},
+ onLanguageId = {}, onLanguageName = {}, onCreateLanguage = {}, onShareCode = {}, onSubscribe = {},
+ onChapters = {}, onChapterId = {}, onChapterVocabs = {}, onDictionaryQuery = {}, onSearchDictionary = {},
+ onPractice = {}, onStartPractice = {}, onCheckPractice = {}, onLearning = {}, onReference = {},
+ onToggleCorrect = {}, onAnswer = {}, onReview = {}, onDictionary = {}, onProgress = {},
+ )
+ }
+ }
+ composeRule.onNodeWithText("Lektion").assertIsDisplayed()
+ composeRule.onNodeWithText("Lektion eins").assertIsDisplayed()
+ }
+}
diff --git a/android/native/app/src/debug/res/xml/network_security_config.xml b/android/native/app/src/debug/res/xml/network_security_config.xml
new file mode 100755
index 0000000..3b7adb7
--- /dev/null
+++ b/android/native/app/src/debug/res/xml/network_security_config.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ 10.0.2.2
+ localhost
+
+
diff --git a/android/native/app/src/main/AndroidManifest.xml b/android/native/app/src/main/AndroidManifest.xml
old mode 100644
new mode 100755
index fa9f522..817f845
--- a/android/native/app/src/main/AndroidManifest.xml
+++ b/android/native/app/src/main/AndroidManifest.xml
@@ -1,15 +1,24 @@
+
+
+
+
+
+
@@ -17,6 +26,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/MainActivity.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/MainActivity.kt
old mode 100644
new mode 100755
index 27c76ac..af0339c
--- a/android/native/app/src/main/java/de/yourpart/nativeapp/MainActivity.kt
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/MainActivity.kt
@@ -1,22 +1,70 @@
package de.yourpart.nativeapp
import android.os.Bundle
+import android.content.Intent
+import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import de.yourpart.nativeapp.core.realtime.RealtimeManager
+import de.yourpart.nativeapp.core.realtime.RealtimeCacheInvalidator
+import de.yourpart.nativeapp.core.realtime.RealtimeEventCenter
+import de.yourpart.nativeapp.feature.auth.AuthViewModel
+import de.yourpart.nativeapp.core.navigation.AppLink
+import de.yourpart.nativeapp.core.navigation.AppLinkParser
import de.yourpart.nativeapp.ui.YourPartNativeApp
import de.yourpart.nativeapp.ui.theme.YourPartTheme
import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
+ private val authViewModel: AuthViewModel by viewModels()
+ @Inject lateinit var realtimeManager: RealtimeManager
+ @Inject lateinit var realtimeEventCenter: RealtimeEventCenter
+ @Inject lateinit var realtimeCacheInvalidator: RealtimeCacheInvalidator
+ private var incomingAppLink by mutableStateOf(null)
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ incomingAppLink = AppLinkParser.parse(intent) ?: intent.notificationRoute()
enableEdgeToEdge()
setContent {
YourPartTheme {
- YourPartNativeApp()
+ YourPartNativeApp(
+ authViewModel = authViewModel,
+ realtimeManager = realtimeManager,
+ realtimeEventCenter = realtimeEventCenter,
+ realtimeCacheInvalidator = realtimeCacheInvalidator,
+ incomingAppLink = incomingAppLink,
+ onSensitiveContentVisible = ::setSensitiveContentProtection,
+ )
}
}
}
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ setIntent(intent)
+ incomingAppLink = AppLinkParser.parse(intent) ?: intent.notificationRoute()
+ }
+
+ private fun Intent.notificationRoute(): AppLink.Route? = getStringExtra(EXTRA_NOTIFICATION_ROUTE)
+ ?.let { AppLink.Route(route = it, requiresAuth = true) }
+
+ private fun setSensitiveContentProtection(enabled: Boolean) {
+ if (enabled) {
+ window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ }
+ }
+
+ companion object {
+ const val EXTRA_NOTIFICATION_ROUTE = "de.yourpart.nativeapp.NOTIFICATION_ROUTE"
+ }
}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/YourPartNativeApplication.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/YourPartNativeApplication.kt
old mode 100644
new mode 100755
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/model/AuthModels.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/model/AuthModels.kt
new file mode 100755
index 0000000..6e34612
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/model/AuthModels.kt
@@ -0,0 +1,58 @@
+package de.yourpart.nativeapp.core.auth.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class UserParamDto(
+ val name: String,
+ val value: String? = null,
+)
+
+@Serializable
+data class UserDto(
+ val id: String,
+ val username: String,
+ val active: Boolean,
+ val param: List = emptyList(),
+ val authCode: String,
+)
+
+@Serializable
+data class LoginRequest(
+ val username: String,
+ val password: String,
+)
+
+@Serializable
+data class RegisterRequest(
+ val email: String,
+ val username: String,
+ val password: String,
+ val language: String,
+)
+
+@Serializable
+data class ForgotPasswordRequest(
+ val email: String,
+)
+
+@Serializable
+data class OAuthProviderDto(
+ val slug: String,
+ val label: String,
+ val issuer: String,
+ val scope: String,
+ val configured: Boolean = false,
+)
+
+@Serializable
+data class ErrorResponse(
+ val error: String? = null,
+)
+
+@Serializable
+data class AuthSession(
+ val user: UserDto,
+ val rememberMe: Boolean,
+ val createdAt: Long,
+)
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/storage/AuthSessionStore.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/storage/AuthSessionStore.kt
new file mode 100755
index 0000000..344cf55
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/storage/AuthSessionStore.kt
@@ -0,0 +1,9 @@
+package de.yourpart.nativeapp.core.auth.storage
+
+import de.yourpart.nativeapp.core.auth.model.AuthSession
+
+interface AuthSessionStore {
+ val currentSession: AuthSession?
+ fun saveSession(session: AuthSession)
+ fun clearSession(notifyExpired: Boolean = false)
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/storage/SessionStore.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/storage/SessionStore.kt
new file mode 100755
index 0000000..1eb27c5
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/auth/storage/SessionStore.kt
@@ -0,0 +1,66 @@
+package de.yourpart.nativeapp.core.auth.storage
+
+import android.content.Context
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import dagger.hilt.android.qualifiers.ApplicationContext
+import de.yourpart.nativeapp.core.auth.model.AuthSession
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class SessionStore @Inject constructor(
+ @ApplicationContext context: Context,
+ private val json: Json,
+) : AuthSessionStore {
+ private val prefs = EncryptedSharedPreferences.create(
+ context,
+ PREFS_NAME,
+ MasterKey.Builder(context)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build(),
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
+ )
+
+ private val _session = MutableStateFlow(loadSession())
+ private val _sessionExpiredMessage = MutableStateFlow(null)
+
+ val session: StateFlow = _session.asStateFlow()
+ val sessionExpiredMessage: StateFlow = _sessionExpiredMessage.asStateFlow()
+
+ override val currentSession: AuthSession?
+ get() = _session.value
+
+ override fun saveSession(session: AuthSession) {
+ prefs.edit().putString(KEY_SESSION, json.encodeToString(session)).apply()
+ _session.value = session
+ }
+
+ override fun clearSession(notifyExpired: Boolean) {
+ prefs.edit().remove(KEY_SESSION).apply()
+ _session.value = null
+ if (notifyExpired) {
+ _sessionExpiredMessage.value = "Deine Sitzung ist abgelaufen."
+ }
+ }
+
+ fun consumeSessionExpiredMessage() {
+ _sessionExpiredMessage.value = null
+ }
+
+ private fun loadSession(): AuthSession? {
+ val raw = prefs.getString(KEY_SESSION, null) ?: return null
+ return runCatching { json.decodeFromString(raw) }.getOrNull()
+ }
+
+ private companion object {
+ const val PREFS_NAME = "yourpart_native_secure_auth"
+ const val KEY_SESSION = "auth_session"
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/config/AppConfig.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/config/AppConfig.kt
new file mode 100755
index 0000000..8ac93fc
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/config/AppConfig.kt
@@ -0,0 +1,35 @@
+package de.yourpart.nativeapp.core.config
+
+import de.yourpart.nativeapp.BuildConfig
+
+data class AppConfig(
+ val environment: String,
+ val apiBaseUrl: String,
+ val socketIoUrl: String,
+ val daemonSocketUrl: String,
+ val allowCleartext: Boolean,
+ val isReleaseBuild: Boolean,
+ val featureAdmin: Boolean,
+ val featureAdult: Boolean,
+ val feature3d: Boolean,
+ val featureMinigames: Boolean,
+ val featurePush: Boolean,
+ val gitSha: String,
+) {
+ companion object {
+ fun fromBuildConfig(): AppConfig = AppConfig(
+ environment = BuildConfig.ENVIRONMENT,
+ apiBaseUrl = BuildConfig.API_BASE_URL,
+ socketIoUrl = BuildConfig.SOCKET_IO_URL,
+ daemonSocketUrl = BuildConfig.DAEMON_SOCKET_URL,
+ allowCleartext = BuildConfig.ALLOW_CLEARTEXT,
+ isReleaseBuild = BuildConfig.IS_RELEASE_BUILD,
+ featureAdmin = BuildConfig.FEATURE_ADMIN,
+ featureAdult = BuildConfig.FEATURE_ADULT,
+ feature3d = BuildConfig.FEATURE_3D,
+ featureMinigames = BuildConfig.FEATURE_MINIGAMES,
+ featurePush = BuildConfig.FEATURE_PUSH,
+ gitSha = BuildConfig.GIT_SHA,
+ )
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/AppModule.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/AppModule.kt
new file mode 100755
index 0000000..f712072
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/AppModule.kt
@@ -0,0 +1,60 @@
+package de.yourpart.nativeapp.core.di
+
+import de.yourpart.nativeapp.BuildConfig
+import de.yourpart.nativeapp.core.config.AppConfig
+import de.yourpart.nativeapp.core.network.AuthHeaderInterceptor
+import de.yourpart.nativeapp.core.network.NetworkPolicy
+import de.yourpart.nativeapp.core.realtime.DefaultRealtimeManager
+import de.yourpart.nativeapp.core.realtime.RealtimeManager
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.serialization.json.Json
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import javax.inject.Singleton
+import java.util.concurrent.TimeUnit
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppModule {
+ @Provides
+ @Singleton
+ fun provideAppConfig(): AppConfig = AppConfig.fromBuildConfig()
+
+ @Provides
+ @Singleton
+ fun provideJson(): Json = Json {
+ ignoreUnknownKeys = true
+ encodeDefaults = true
+ }
+
+ @Provides
+ @Singleton
+ fun provideOkHttpClient(
+ authHeaderInterceptor: AuthHeaderInterceptor,
+ ): OkHttpClient {
+ val builder = OkHttpClient.Builder()
+ .addInterceptor(authHeaderInterceptor)
+ .connectTimeout(NetworkPolicy.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
+ .readTimeout(NetworkPolicy.readTimeoutMillis(), TimeUnit.MILLISECONDS)
+ .writeTimeout(NetworkPolicy.writeTimeoutMillis(), TimeUnit.MILLISECONDS)
+ .callTimeout(NetworkPolicy.callTimeoutMillis(), TimeUnit.MILLISECONDS)
+ .retryOnConnectionFailure(true)
+
+ if (!BuildConfig.IS_RELEASE_BUILD) {
+ builder.addInterceptor(
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ },
+ )
+ }
+
+ return builder.build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideRealtimeManager(impl: DefaultRealtimeManager): RealtimeManager = impl
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/PersistenceModule.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/PersistenceModule.kt
new file mode 100755
index 0000000..6f199af
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/PersistenceModule.kt
@@ -0,0 +1,38 @@
+package de.yourpart.nativeapp.core.di
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.preferencesDataStoreFile
+import androidx.room.Room
+import de.yourpart.nativeapp.core.persistence.AppCacheDatabase
+import de.yourpart.nativeapp.core.persistence.AppCacheMigrations
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object PersistenceModule {
+ @Provides
+ @Singleton
+ fun providePreferencesDataStore(
+ @ApplicationContext context: Context,
+ ): DataStore = PreferenceDataStoreFactory.create(
+ produceFile = { context.preferencesDataStoreFile("yourpart_native_user_prefs.preferences_pb") },
+ )
+
+ @Provides
+ @Singleton
+ fun provideAppCacheDatabase(
+ @ApplicationContext context: Context,
+ ): AppCacheDatabase = Room.databaseBuilder(
+ context,
+ AppCacheDatabase::class.java,
+ "yourpart_native_cache.db",
+ ).addMigrations(AppCacheMigrations.MIGRATION_1_2).build()
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/StorageModule.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/StorageModule.kt
new file mode 100755
index 0000000..f72b661
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/di/StorageModule.kt
@@ -0,0 +1,16 @@
+package de.yourpart.nativeapp.core.di
+
+import de.yourpart.nativeapp.core.auth.storage.AuthSessionStore
+import de.yourpart.nativeapp.core.auth.storage.SessionStore
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class StorageModule {
+ @Binds
+ abstract fun bindAuthSessionStore(impl: SessionStore): AuthSessionStore
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/navigation/AppLink.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/navigation/AppLink.kt
new file mode 100644
index 0000000..456ee92
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/navigation/AppLink.kt
@@ -0,0 +1,58 @@
+package de.yourpart.nativeapp.core.navigation
+
+import android.content.Intent
+import android.net.Uri
+import de.yourpart.nativeapp.ui.navigation.BlogDestination
+import de.yourpart.nativeapp.ui.navigation.FalukantDestination
+import de.yourpart.nativeapp.ui.navigation.ForumTopicDestination
+import de.yourpart.nativeapp.ui.navigation.ForumTopicsDestination
+import de.yourpart.nativeapp.ui.navigation.GalleryDestination
+import de.yourpart.nativeapp.ui.navigation.GuideDestination
+import de.yourpart.nativeapp.ui.navigation.HomeDestination
+import de.yourpart.nativeapp.ui.navigation.SettingsDestination
+import de.yourpart.nativeapp.ui.navigation.SocialDestination
+import de.yourpart.nativeapp.ui.navigation.SocialProfileDestination
+import de.yourpart.nativeapp.ui.navigation.SocialSearchDestination
+import de.yourpart.nativeapp.ui.navigation.VocabDestination
+
+sealed interface AppLink {
+ data class Route(val route: String, val requiresAuth: Boolean) : AppLink
+ data class OAuthCallback(val code: String?, val state: String?, val issuer: String?, val error: String?) : AppLink
+}
+
+object AppLinkParser {
+ private const val HOST = "www.your-part.de"
+
+ fun parse(intent: Intent?): AppLink? = intent?.data?.let(::parse)
+
+ fun parse(uri: Uri): AppLink? {
+ if (uri.scheme != "https" || uri.host != HOST) return null
+ val segments = uri.pathSegments
+ if (segments == listOf("android", "oauth", "callback")) {
+ return AppLink.OAuthCallback(
+ code = uri.getQueryParameter("code"),
+ state = uri.getQueryParameter("state"),
+ issuer = uri.getQueryParameter("iss"),
+ error = uri.getQueryParameter("error"),
+ )
+ }
+ return when {
+ segments.isEmpty() -> AppLink.Route(HomeDestination.route, requiresAuth = true)
+ segments == listOf("socialnetwork", "search") -> AppLink.Route(SocialSearchDestination.route, true)
+ segments == listOf("socialnetwork", "gallery") -> AppLink.Route(GalleryDestination.route, true)
+ segments.getOrNull(0) == "socialnetwork" && segments.getOrNull(1) == "forum" ->
+ segments.getOrNull(2)?.toLongOrNull()?.let { AppLink.Route(ForumTopicsDestination.createRoute(it), true) }
+ segments.getOrNull(0) == "socialnetwork" && segments.getOrNull(1) == "forumtopic" ->
+ segments.getOrNull(2)?.toLongOrNull()?.let { AppLink.Route(ForumTopicDestination.createRoute(it), true) }
+ segments.getOrNull(0) == "socialnetwork" && segments.getOrNull(1) == "vocab" -> AppLink.Route(VocabDestination.route, true)
+ segments.getOrNull(0) == "socialnetwork" && segments.getOrNull(1) == "profile" ->
+ segments.getOrNull(2)?.takeIf { it.isNotBlank() }?.let { AppLink.Route(SocialProfileDestination.createRoute(it), true) }
+ segments.firstOrNull() == "falukant" -> AppLink.Route(FalukantDestination.route, true)
+ segments.firstOrNull() == "settings" -> AppLink.Route(SettingsDestination.route, true)
+ segments.firstOrNull() == "blogs" -> AppLink.Route(BlogDestination.route, requiresAuth = false)
+ segments.firstOrNull() == "guides" -> AppLink.Route(GuideDestination.route, requiresAuth = false)
+ segments.firstOrNull() == "socialnetwork" -> AppLink.Route(SocialDestination.route, true)
+ else -> null
+ }
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/ApiResult.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/ApiResult.kt
new file mode 100755
index 0000000..8af2263
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/ApiResult.kt
@@ -0,0 +1,7 @@
+package de.yourpart.nativeapp.core.network
+
+sealed interface ApiResult {
+ data class Success(val value: T) : ApiResult
+ data class Failure(val error: NetworkError) : ApiResult
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/AuthHeaderInterceptor.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/AuthHeaderInterceptor.kt
new file mode 100755
index 0000000..f37c399
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/AuthHeaderInterceptor.kt
@@ -0,0 +1,28 @@
+package de.yourpart.nativeapp.core.network
+
+import de.yourpart.nativeapp.core.auth.storage.AuthSessionStore
+import okhttp3.Interceptor
+import okhttp3.Response
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AuthHeaderInterceptor @Inject constructor(
+ private val sessionStore: AuthSessionStore,
+) : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val session = sessionStore.currentSession
+ val requestBuilder = chain.request().newBuilder()
+
+ session?.user?.let { user ->
+ requestBuilder.header("userid", user.id)
+ requestBuilder.header("authcode", user.authCode)
+ }
+
+ val response = chain.proceed(requestBuilder.build())
+ if (response.code == 401 && session != null) {
+ sessionStore.clearSession(notifyExpired = true)
+ }
+ return response
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/BackendErrorParser.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/BackendErrorParser.kt
new file mode 100755
index 0000000..aff33c5
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/BackendErrorParser.kt
@@ -0,0 +1,25 @@
+package de.yourpart.nativeapp.core.network
+
+import de.yourpart.nativeapp.core.auth.model.ErrorResponse
+import kotlinx.serialization.json.Json
+
+object BackendErrorParser {
+ fun parseBackendCode(rawBody: String?, json: Json): String? {
+ if (rawBody.isNullOrBlank()) return null
+ return runCatching {
+ json.decodeFromString(rawBody).error
+ }.getOrNull()
+ }
+
+ fun toNetworkError(
+ statusCode: Int,
+ rawBody: String?,
+ json: Json,
+ ): NetworkError {
+ return HttpNetworkError(
+ statusCode = statusCode,
+ backendCode = parseBackendCode(rawBody, json),
+ rawBody = rawBody,
+ )
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/DownloadHelper.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/DownloadHelper.kt
new file mode 100755
index 0000000..d1773b3
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/DownloadHelper.kt
@@ -0,0 +1,35 @@
+package de.yourpart.nativeapp.core.network
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.io.File
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class DownloadHelper @Inject constructor(
+ private val okHttpClient: OkHttpClient,
+) {
+ suspend fun downloadToFile(
+ url: String,
+ destination: File,
+ ): Result = withContext(Dispatchers.IO) {
+ runCatching {
+ val request = Request.Builder().url(url).get().build()
+ okHttpClient.newCall(request).execute().use { response ->
+ if (!response.isSuccessful) {
+ throw IllegalStateException("Download failed with HTTP ${response.code}")
+ }
+
+ val body = response.body ?: throw IllegalStateException("Download response body missing")
+ destination.outputStream().use { output ->
+ body.byteStream().use { input -> input.copyTo(output) }
+ }
+ }
+ destination
+ }
+ }
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/MultipartUploadHelper.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/MultipartUploadHelper.kt
new file mode 100755
index 0000000..add7c6f
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/MultipartUploadHelper.kt
@@ -0,0 +1,28 @@
+package de.yourpart.nativeapp.core.network
+
+import android.content.ContentResolver
+import android.net.Uri
+import androidx.annotation.WorkerThread
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.toRequestBody
+
+class MultipartUploadHelper(
+ private val contentResolver: ContentResolver,
+) {
+ @WorkerThread
+ fun toMultipartPart(
+ uri: Uri,
+ formField: String,
+ fileName: String = "upload.bin",
+ ): MultipartBody.Part {
+ val mimeType = contentResolver.getType(uri) ?: "application/octet-stream"
+ val bytes = requireNotNull(contentResolver.openInputStream(uri)?.use { it.readBytes() }) {
+ "Unable to read content URI: $uri"
+ }
+
+ val requestBody = bytes.toRequestBody(mimeType.toMediaType())
+ return MultipartBody.Part.createFormData(formField, fileName, requestBody)
+ }
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkError.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkError.kt
new file mode 100755
index 0000000..988263f
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkError.kt
@@ -0,0 +1,35 @@
+package de.yourpart.nativeapp.core.network
+
+sealed interface NetworkError {
+ val message: String
+}
+
+data class HttpNetworkError(
+ val statusCode: Int,
+ val backendCode: String? = null,
+ val rawBody: String? = null,
+ override val message: String = when (backendCode) {
+ "credentialsinvalid" -> "Benutzername oder Passwort ist falsch."
+ "userblocked" -> "Dieses Konto ist deaktiviert."
+ "emailinuse" -> "Diese E-Mail-Adresse wird bereits verwendet."
+ "languagenotfound" -> "Die Sprachkonfiguration konnte nicht geladen werden."
+ else -> "Die Anfrage wurde vom Server abgelehnt."
+ },
+) : NetworkError
+
+data object NoNetworkError : NetworkError {
+ override val message: String = "Keine Netzwerkverbindung."
+}
+
+data object TimeoutNetworkError : NetworkError {
+ override val message: String = "Die Anfrage hat zu lange gedauert."
+}
+
+data class SerializationNetworkError(
+ override val message: String = "Antwort konnte nicht verarbeitet werden.",
+) : NetworkError
+
+data class UnknownNetworkError(
+ override val message: String = "Unbekannter Netzwerkfehler.",
+) : NetworkError
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkPolicy.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkPolicy.kt
new file mode 100755
index 0000000..2e2412f
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkPolicy.kt
@@ -0,0 +1,17 @@
+package de.yourpart.nativeapp.core.network
+
+import java.util.concurrent.TimeUnit
+
+object NetworkPolicy {
+ const val CONNECT_TIMEOUT_SECONDS = 15L
+ const val READ_TIMEOUT_SECONDS = 30L
+ const val WRITE_TIMEOUT_SECONDS = 30L
+ const val CALL_TIMEOUT_SECONDS = 60L
+ const val MAX_RETRY_ATTEMPTS = 2
+
+ fun connectTimeoutMillis(): Long = TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SECONDS)
+ fun readTimeoutMillis(): Long = TimeUnit.SECONDS.toMillis(READ_TIMEOUT_SECONDS)
+ fun writeTimeoutMillis(): Long = TimeUnit.SECONDS.toMillis(WRITE_TIMEOUT_SECONDS)
+ fun callTimeoutMillis(): Long = TimeUnit.SECONDS.toMillis(CALL_TIMEOUT_SECONDS)
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkRequestExecutor.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkRequestExecutor.kt
new file mode 100755
index 0000000..8ed6fd5
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/NetworkRequestExecutor.kt
@@ -0,0 +1,39 @@
+package de.yourpart.nativeapp.core.network
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.io.IOException
+import java.net.SocketTimeoutException
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class NetworkRequestExecutor @Inject constructor(
+ private val okHttpClient: OkHttpClient,
+ private val json: Json,
+) {
+ suspend fun execute(request: Request): ApiResult = withContext(Dispatchers.IO) {
+ runCatching {
+ okHttpClient.newCall(request).execute().use { response ->
+ val payload = response.body?.string().orEmpty()
+ if (response.isSuccessful) {
+ ApiResult.Success(payload)
+ } else {
+ ApiResult.Failure(BackendErrorParser.toNetworkError(response.code, payload, json))
+ }
+ }
+ }.getOrElse { throwable ->
+ ApiResult.Failure(throwable.toNetworkError())
+ }
+ }
+
+ private fun Throwable.toNetworkError(): NetworkError = when (this) {
+ is SocketTimeoutException -> TimeoutNetworkError
+ is IOException -> NoNetworkError
+ else -> UnknownNetworkError(message ?: UnknownNetworkError().message)
+ }
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/PaginationModels.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/PaginationModels.kt
new file mode 100755
index 0000000..bdd9522
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/network/PaginationModels.kt
@@ -0,0 +1,20 @@
+package de.yourpart.nativeapp.core.network
+
+data class PageRequest(
+ val page: Int = 1,
+ val pageSize: Int = 30,
+)
+
+data class PageResponse(
+ val items: List,
+ val page: Int,
+ val pageSize: Int,
+ val hasMore: Boolean,
+)
+
+sealed interface RefreshState {
+ data object Idle : RefreshState
+ data object Loading : RefreshState
+ data object Error : RefreshState
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/AppCacheDatabase.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/AppCacheDatabase.kt
new file mode 100755
index 0000000..c444b63
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/AppCacheDatabase.kt
@@ -0,0 +1,25 @@
+package de.yourpart.nativeapp.core.persistence
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+
+@Database(
+ entities = [
+ UserProfileCacheEntity::class,
+ FriendSearchCacheEntity::class,
+ FalukantStatusCacheEntity::class,
+ VocabCourseCacheEntity::class,
+ VocabLessonCacheEntity::class,
+ VocabReviewOutboxEntity::class,
+ ],
+ version = 2,
+ exportSchema = true,
+)
+abstract class AppCacheDatabase : RoomDatabase() {
+ abstract fun userProfileCacheDao(): UserProfileCacheDao
+ abstract fun friendSearchCacheDao(): FriendSearchCacheDao
+ abstract fun falukantStatusCacheDao(): FalukantStatusCacheDao
+ abstract fun vocabCourseCacheDao(): VocabCourseCacheDao
+ abstract fun vocabLessonCacheDao(): VocabLessonCacheDao
+ abstract fun vocabReviewOutboxDao(): VocabReviewOutboxDao
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/AppCacheMigrations.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/AppCacheMigrations.kt
new file mode 100644
index 0000000..2f90401
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/AppCacheMigrations.kt
@@ -0,0 +1,21 @@
+package de.yourpart.nativeapp.core.persistence
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+object AppCacheMigrations {
+ val MIGRATION_1_2 = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL(
+ """
+ CREATE TABLE IF NOT EXISTS `vocab_review_outbox` (
+ `requestId` TEXT NOT NULL,
+ `payloadJson` TEXT NOT NULL,
+ `createdAtMillis` INTEGER NOT NULL,
+ PRIMARY KEY(`requestId`)
+ )
+ """.trimIndent(),
+ )
+ }
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CacheDaos.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CacheDaos.kt
new file mode 100755
index 0000000..3545fbd
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CacheDaos.kt
@@ -0,0 +1,82 @@
+package de.yourpart.nativeapp.core.persistence
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface UserProfileCacheDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(entity: UserProfileCacheEntity)
+
+ @Query("SELECT * FROM user_profile_cache WHERE userId = :userId LIMIT 1")
+ fun observe(userId: String): Flow
+
+ @Query("DELETE FROM user_profile_cache")
+ suspend fun clear()
+}
+
+@Dao
+interface FriendSearchCacheDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(entity: FriendSearchCacheEntity)
+
+ @Query("SELECT * FROM friend_search_cache WHERE query = :query LIMIT 1")
+ fun observe(query: String): Flow
+
+ @Query("DELETE FROM friend_search_cache")
+ suspend fun clear()
+}
+
+@Dao
+interface FalukantStatusCacheDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(entity: FalukantStatusCacheEntity)
+
+ @Query("SELECT * FROM falukant_status_cache WHERE cacheKey = 'current' LIMIT 1")
+ fun observe(): Flow
+
+ @Query("DELETE FROM falukant_status_cache")
+ suspend fun clear()
+}
+
+@Dao
+interface VocabCourseCacheDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(entity: VocabCourseCacheEntity)
+
+ @Query("SELECT * FROM vocab_course_cache WHERE courseId = :courseId LIMIT 1")
+ fun observe(courseId: String): Flow
+
+ @Query("DELETE FROM vocab_course_cache")
+ suspend fun clear()
+}
+
+@Dao
+interface VocabLessonCacheDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(entity: VocabLessonCacheEntity)
+
+ @Query("SELECT * FROM vocab_lesson_cache WHERE lessonId = :lessonId LIMIT 1")
+ fun observe(lessonId: String): Flow
+
+ @Query("SELECT * FROM vocab_lesson_cache WHERE lessonId = :lessonId LIMIT 1")
+ suspend fun get(lessonId: String): VocabLessonCacheEntity?
+
+ @Query("DELETE FROM vocab_lesson_cache WHERE courseId = :courseId")
+ suspend fun clearForCourse(courseId: String)
+}
+
+@Dao
+interface VocabReviewOutboxDao {
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun enqueue(entity: VocabReviewOutboxEntity)
+
+ @Query("SELECT * FROM vocab_review_outbox ORDER BY createdAtMillis ASC")
+ suspend fun pending(): List
+
+ @Query("DELETE FROM vocab_review_outbox WHERE requestId = :requestId")
+ suspend fun delete(requestId: String)
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CacheEntities.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CacheEntities.kt
new file mode 100755
index 0000000..e297189
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CacheEntities.kt
@@ -0,0 +1,54 @@
+package de.yourpart.nativeapp.core.persistence
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "user_profile_cache")
+data class UserProfileCacheEntity(
+ @PrimaryKey val userId: String,
+ val username: String,
+ val displayName: String? = null,
+ val avatarUrl: String? = null,
+ val profileJson: String,
+ val updatedAtMillis: Long,
+)
+
+@Entity(tableName = "friend_search_cache")
+data class FriendSearchCacheEntity(
+ @PrimaryKey val query: String,
+ val resultsJson: String,
+ val updatedAtMillis: Long,
+)
+
+@Entity(tableName = "falukant_status_cache")
+data class FalukantStatusCacheEntity(
+ @PrimaryKey val cacheKey: String = "current",
+ val statusJson: String,
+ val updatedAtMillis: Long,
+)
+
+@Entity(tableName = "vocab_course_cache")
+data class VocabCourseCacheEntity(
+ @PrimaryKey val courseId: String,
+ val title: String,
+ val summary: String? = null,
+ val courseJson: String,
+ val updatedAtMillis: Long,
+)
+
+@Entity(tableName = "vocab_lesson_cache")
+data class VocabLessonCacheEntity(
+ @PrimaryKey val lessonId: String,
+ val courseId: String,
+ val title: String,
+ val lessonJson: String,
+ val updatedAtMillis: Long,
+)
+
+/** A review is kept locally until the server acknowledges it. */
+@Entity(tableName = "vocab_review_outbox")
+data class VocabReviewOutboxEntity(
+ @PrimaryKey val requestId: String,
+ val payloadJson: String,
+ val createdAtMillis: Long,
+)
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CachePolicy.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CachePolicy.kt
new file mode 100755
index 0000000..b86066b
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/CachePolicy.kt
@@ -0,0 +1,18 @@
+package de.yourpart.nativeapp.core.persistence
+
+import java.util.concurrent.TimeUnit
+
+object CachePolicy {
+ const val USER_PROFILE_TTL_MILLIS = 24L * 60L * 60L * 1000L
+ const val FRIEND_SEARCH_TTL_MILLIS = 15L * 60L * 1000L
+ const val FALUKANT_STATUS_TTL_MILLIS = 5L * 60L * 1000L
+ const val VOCAB_COURSE_TTL_MILLIS = 12L * 60L * 60L * 1000L
+ const val VOCAB_LESSON_TTL_MILLIS = 12L * 60L * 60L * 1000L
+
+ fun isExpired(updatedAtMillis: Long, ttlMillis: Long, nowMillis: Long = System.currentTimeMillis()): Boolean {
+ return nowMillis - updatedAtMillis > ttlMillis
+ }
+
+ fun ttlHours(hours: Long): Long = TimeUnit.HOURS.toMillis(hours)
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/FeatureFlagDefaults.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/FeatureFlagDefaults.kt
new file mode 100755
index 0000000..9604433
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/FeatureFlagDefaults.kt
@@ -0,0 +1,10 @@
+package de.yourpart.nativeapp.core.persistence
+
+data class FeatureFlagState(
+ val adminEnabled: Boolean = false,
+ val adultEnabled: Boolean = false,
+ val threeDEnabled: Boolean = false,
+ val minigamesEnabled: Boolean = false,
+ val pushEnabled: Boolean = false,
+)
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/UserPreferencesStore.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/UserPreferencesStore.kt
new file mode 100755
index 0000000..2f6c5de
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/persistence/UserPreferencesStore.kt
@@ -0,0 +1,60 @@
+package de.yourpart.nativeapp.core.persistence
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class UserPreferencesStore @Inject constructor(
+ private val dataStore: DataStore,
+) {
+ val language: Flow = dataStore.data.map { prefs ->
+ prefs[Keys.LANGUAGE] ?: DEFAULT_LANGUAGE
+ }
+
+ val featureFlags: Flow = dataStore.data.map { prefs ->
+ FeatureFlagState(
+ adminEnabled = prefs[Keys.FEATURE_ADMIN] ?: false,
+ adultEnabled = prefs[Keys.FEATURE_ADULT] ?: false,
+ threeDEnabled = prefs[Keys.FEATURE_3D] ?: false,
+ minigamesEnabled = prefs[Keys.FEATURE_MINIGAMES] ?: false,
+ pushEnabled = prefs[Keys.FEATURE_PUSH] ?: false,
+ )
+ }
+
+ suspend fun setLanguage(language: String) {
+ dataStore.edit { prefs ->
+ prefs[Keys.LANGUAGE] = language.trim().ifBlank { DEFAULT_LANGUAGE }
+ }
+ }
+
+ suspend fun setFeatureFlags(state: FeatureFlagState) {
+ dataStore.edit { prefs ->
+ prefs[Keys.FEATURE_ADMIN] = state.adminEnabled
+ prefs[Keys.FEATURE_ADULT] = state.adultEnabled
+ prefs[Keys.FEATURE_3D] = state.threeDEnabled
+ prefs[Keys.FEATURE_MINIGAMES] = state.minigamesEnabled
+ prefs[Keys.FEATURE_PUSH] = state.pushEnabled
+ }
+ }
+
+ private object Keys {
+ val LANGUAGE = stringPreferencesKey("ui_language")
+ val FEATURE_ADMIN = booleanPreferencesKey("feature_admin")
+ val FEATURE_ADULT = booleanPreferencesKey("feature_adult")
+ val FEATURE_3D = booleanPreferencesKey("feature_3d")
+ val FEATURE_MINIGAMES = booleanPreferencesKey("feature_minigames")
+ val FEATURE_PUSH = booleanPreferencesKey("feature_push")
+ }
+
+ private companion object {
+ const val DEFAULT_LANGUAGE = "de"
+ }
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/DaemonMessageParser.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/DaemonMessageParser.kt
new file mode 100755
index 0000000..6aed5c6
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/DaemonMessageParser.kt
@@ -0,0 +1,22 @@
+package de.yourpart.nativeapp.core.realtime
+
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonObject
+
+object DaemonMessageParser {
+ fun parse(rawMessage: String, json: Json): DaemonEnvelope? {
+ val trimmed = rawMessage.trim()
+ if (trimmed.isEmpty() || trimmed == "ping" || trimmed == "pong") {
+ return null
+ }
+
+ val element = runCatching { json.parseToJsonElement(trimmed) }.getOrNull() ?: return null
+ val obj = element as? JsonObject ?: return null
+ val event = obj["event"]?.toString()?.trim('"')
+ ?.takeIf { it.isNotBlank() }
+ ?: return null
+ val payload = obj["data"]?.toString()
+ return DaemonEnvelope(event = event, payloadRaw = payload)
+ }
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/DefaultRealtimeManager.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/DefaultRealtimeManager.kt
new file mode 100755
index 0000000..d67ab94
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/DefaultRealtimeManager.kt
@@ -0,0 +1,250 @@
+package de.yourpart.nativeapp.core.realtime
+
+import de.yourpart.nativeapp.core.auth.model.AuthSession
+import de.yourpart.nativeapp.core.config.AppConfig
+import io.socket.client.IO
+import io.socket.client.Socket
+import io.socket.emitter.Emitter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.serialization.json.Json
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.WebSocket
+import okhttp3.WebSocketListener
+import org.json.JSONObject
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+@Singleton
+class DefaultRealtimeManager @Inject constructor(
+ private val appConfig: AppConfig,
+ private val json: Json,
+ private val okHttpClient: OkHttpClient,
+) : RealtimeManager {
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+ private val _connectionSnapshot = MutableStateFlow(RealtimeConnectionSnapshot())
+ override val connectionSnapshot: StateFlow = _connectionSnapshot.asStateFlow()
+ private val _events = MutableSharedFlow(extraBufferCapacity = 64)
+ override val events: SharedFlow = _events.asSharedFlow()
+
+ @Volatile
+ private var session: AuthSession? = null
+
+ @Volatile
+ private var isForeground: Boolean = false
+
+ private var backendSocket: Socket? = null
+ private var daemonWebSocket: WebSocket? = null
+ private val reconnectingDaemon = AtomicBoolean(false)
+
+ override fun sync(context: RealtimeSessionContext) {
+ session = context.session
+ isForeground = context.isForeground
+ reconnectingDaemon.set(false)
+ scope.launch { reconcileConnections() }
+ }
+
+ override fun onAppForeground() {
+ isForeground = true
+ scope.launch { reconcileConnections() }
+ }
+
+ override fun onAppBackground() {
+ isForeground = false
+ scope.launch { disconnectAll() }
+ }
+
+ private suspend fun reconcileConnections() {
+ if (session == null || !isForeground) {
+ disconnectAll()
+ return
+ }
+
+ connectBackend(session)
+ connectDaemon(session)
+ }
+
+ private suspend fun connectBackend(session: AuthSession?) {
+ if (backendSocket?.connected() == true) {
+ backendSocket?.emit("setUserId", session?.user?.id)
+ return
+ }
+
+ _connectionSnapshot.value = _connectionSnapshot.value.copy(backend = RealtimeConnectionStatus.Connecting)
+ val options = IO.Options().apply {
+ forceNew = true
+ reconnection = true
+ reconnectionAttempts = 5
+ reconnectionDelay = 1_000
+ timeout = 10_000
+ transports = arrayOf("websocket")
+ query = "client=native"
+ }
+
+ val socket = IO.socket(appConfig.socketIoUrl, options)
+ backendSocket = socket
+
+ socket.on(Socket.EVENT_CONNECT) {
+ _connectionSnapshot.value = _connectionSnapshot.value.copy(backend = RealtimeConnectionStatus.Connected)
+ session?.user?.id?.let { userId ->
+ socket.emit("setUserId", userId)
+ }
+ }
+ socket.on(Socket.EVENT_DISCONNECT) {
+ _connectionSnapshot.value = _connectionSnapshot.value.copy(backend = RealtimeConnectionStatus.Disconnected)
+ }
+ socket.on(Socket.EVENT_CONNECT_ERROR) {
+ _connectionSnapshot.value = _connectionSnapshot.value.copy(backend = RealtimeConnectionStatus.Error)
+ }
+
+ registerSocketEventHandlers(socket)
+ socket.connect()
+ }
+
+ private fun registerSocketEventHandlers(socket: Socket) {
+ val eventNames = listOf(
+ "forumschanged",
+ "topicschanged",
+ "messageschanged",
+ "friendloginchanged",
+ "reloadmenu",
+ "adultVerificationChanged",
+ "moderationReportChanged",
+ "userAccessChanged",
+ "falukantUpdateStatus",
+ "falukantUpdateFamily",
+ "falukantUpdateChurch",
+ "falukantUpdateDebt",
+ "children_update",
+ "falukantUpdateProductionCertificate",
+ "falukantBranchUpdate",
+ "stock_change",
+ "familychanged",
+ )
+
+ eventNames.forEach { eventName ->
+ socket.on(eventName, socketEventListener(eventName))
+ }
+ }
+
+ private fun socketEventListener(eventName: String): Emitter.Listener = Emitter.Listener { args ->
+ val payloadRaw = args.firstOrNull()?.let { toRawJson(it) }
+ _events.tryEmit(
+ RealtimeSocketEvent(
+ source = RealtimeSource.BackendSocket,
+ eventName = eventName,
+ payloadRaw = payloadRaw,
+ ),
+ )
+ }
+
+ private suspend fun connectDaemon(session: AuthSession?) {
+ if (daemonWebSocket != null) return
+
+ _connectionSnapshot.value = _connectionSnapshot.value.copy(daemon = RealtimeConnectionStatus.Connecting)
+ val request = Request.Builder()
+ .url(appConfig.daemonSocketUrl)
+ .build()
+
+ daemonWebSocket = suspendCancellableCoroutine { continuation ->
+ val listener = object : WebSocketListener() {
+ override fun onOpen(webSocket: WebSocket, response: Response) {
+ _connectionSnapshot.value = _connectionSnapshot.value.copy(daemon = RealtimeConnectionStatus.Connected)
+ session?.user?.id?.let { userId ->
+ webSocket.send(
+ """{"event":"setUserId","data":{"userId":"$userId"}}""",
+ )
+ }
+ continuation.resume(webSocket)
+ }
+
+ override fun onMessage(webSocket: WebSocket, text: String) {
+ if (text == "ping") {
+ webSocket.send("pong")
+ return
+ }
+
+ val envelope = DaemonMessageParser.parse(text, json) ?: return
+ _events.tryEmit(
+ RealtimeSocketEvent(
+ source = RealtimeSource.DaemonWebSocket,
+ eventName = envelope.event,
+ payloadRaw = envelope.payloadRaw,
+ ),
+ )
+ }
+
+ override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
+ _connectionSnapshot.value = _connectionSnapshot.value.copy(daemon = RealtimeConnectionStatus.Disconnected)
+ if (shouldReconnect()) {
+ scheduleDaemonReconnect()
+ }
+ }
+
+ override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
+ _connectionSnapshot.value = _connectionSnapshot.value.copy(daemon = RealtimeConnectionStatus.Error)
+ if (shouldReconnect()) {
+ scheduleDaemonReconnect()
+ }
+ if (!continuation.isCompleted) {
+ continuation.resumeWithException(t)
+ }
+ }
+ }
+ val webSocket = okHttpClient.newWebSocket(request, listener)
+ continuation.invokeOnCancellation {
+ webSocket.close(1000, "cancelled")
+ }
+ }
+ }
+
+ private fun scheduleDaemonReconnect() {
+ if (!reconnectingDaemon.compareAndSet(false, true)) return
+ scope.launch {
+ delay(2_000)
+ reconnectingDaemon.set(false)
+ if (shouldReconnect()) {
+ daemonWebSocket = null
+ connectDaemon(session)
+ }
+ }
+ }
+
+ private fun shouldReconnect(): Boolean = isForeground && session != null
+
+ private suspend fun disconnectAll() {
+ backendSocket?.let { socket ->
+ socket.off()
+ socket.disconnect()
+ socket.close()
+ }
+ backendSocket = null
+
+ daemonWebSocket?.close(1000, "background")
+ daemonWebSocket = null
+
+ _connectionSnapshot.value = RealtimeConnectionSnapshot()
+ }
+
+ private fun toRawJson(value: Any): String? = when (value) {
+ is JSONObject -> value.toString()
+ is String -> value
+ is Number, is Boolean -> value.toString()
+ else -> null
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeCacheInvalidator.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeCacheInvalidator.kt
new file mode 100755
index 0000000..ac4b5b0
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeCacheInvalidator.kt
@@ -0,0 +1,28 @@
+package de.yourpart.nativeapp.core.realtime
+
+import de.yourpart.nativeapp.core.persistence.AppCacheDatabase
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class RealtimeCacheInvalidator @Inject constructor(
+ private val cacheDatabase: AppCacheDatabase,
+) {
+ suspend fun handle(event: RealtimeSocketEvent) {
+ when (event.eventName) {
+ "reloadmenu" -> {
+ // Menu ist aus API-Policy abgeleitet; kein lokaler Cache zu leeren.
+ }
+ "friendloginchanged", "forumschanged" -> {
+ cacheDatabase.friendSearchCacheDao().clear()
+ }
+ "adultVerificationChanged", "moderationReportChanged", "userAccessChanged" -> {
+ cacheDatabase.userProfileCacheDao().clear()
+ }
+ "falukantUpdateStatus", "falukantUpdateFamily", "falukantUpdateChurch", "falukantUpdateDebt",
+ "children_update", "falukantUpdateProductionCertificate", "falukantBranchUpdate", "stock_change", "familychanged" -> {
+ cacheDatabase.falukantStatusCacheDao().clear()
+ }
+ }
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeEventCenter.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeEventCenter.kt
new file mode 100755
index 0000000..fe0d993
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeEventCenter.kt
@@ -0,0 +1,42 @@
+package de.yourpart.nativeapp.core.realtime
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+data class RealtimeDebugState(
+ val recentEvents: List = emptyList(),
+ val invalidationTags: Set = emptySet(),
+)
+
+@Singleton
+class RealtimeEventCenter @Inject constructor() {
+ private val _debugState = MutableStateFlow(RealtimeDebugState())
+ val debugState: StateFlow = _debugState.asStateFlow()
+
+ fun handle(event: RealtimeSocketEvent) {
+ val updatedTags = when (event.eventName) {
+ "reloadmenu" -> setOf("menu", "dashboard")
+ "friendloginchanged", "forumschanged", "topicschanged", "messageschanged" -> setOf("social", "community", "forum")
+ "adultVerificationChanged", "moderationReportChanged", "userAccessChanged" -> setOf("account", "access")
+ "falukantUpdateStatus", "falukantUpdateFamily", "falukantUpdateChurch", "falukantUpdateDebt",
+ "children_update", "falukantUpdateProductionCertificate", "falukantBranchUpdate", "stock_change", "familychanged" -> setOf("falukant")
+ else -> emptySet()
+ }
+
+ _debugState.value = _debugState.value.copy(
+ recentEvents = (_debugState.value.recentEvents + event).takeLast(20),
+ invalidationTags = _debugState.value.invalidationTags + updatedTags,
+ )
+ }
+
+ fun acknowledgeInvalidations() {
+ _debugState.value = _debugState.value.copy(invalidationTags = emptySet())
+ }
+
+ fun clearHistory() {
+ _debugState.value = RealtimeDebugState()
+ }
+}
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeManager.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeManager.kt
new file mode 100755
index 0000000..ef1b71a
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeManager.kt
@@ -0,0 +1,14 @@
+package de.yourpart.nativeapp.core.realtime
+
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+
+interface RealtimeManager {
+ val connectionSnapshot: StateFlow
+ val events: SharedFlow
+
+ fun sync(context: RealtimeSessionContext)
+ fun onAppForeground()
+ fun onAppBackground()
+}
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeModels.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeModels.kt
new file mode 100755
index 0000000..468d13e
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/core/realtime/RealtimeModels.kt
@@ -0,0 +1,38 @@
+package de.yourpart.nativeapp.core.realtime
+
+import de.yourpart.nativeapp.core.auth.model.AuthSession
+
+enum class RealtimeSource {
+ BackendSocket,
+ DaemonWebSocket,
+}
+
+enum class RealtimeConnectionStatus {
+ Disconnected,
+ Connecting,
+ Connected,
+ Reconnecting,
+ Error,
+}
+
+data class RealtimeSocketEvent(
+ val source: RealtimeSource,
+ val eventName: String,
+ val payloadRaw: String? = null,
+)
+
+data class RealtimeConnectionSnapshot(
+ val backend: RealtimeConnectionStatus = RealtimeConnectionStatus.Disconnected,
+ val daemon: RealtimeConnectionStatus = RealtimeConnectionStatus.Disconnected,
+)
+
+data class DaemonEnvelope(
+ val event: String,
+ val payloadRaw: String? = null,
+)
+
+data class RealtimeSessionContext(
+ val session: AuthSession?,
+ val isForeground: Boolean,
+)
+
diff --git a/android/native/app/src/main/java/de/yourpart/nativeapp/feature/admin/AdminAccessRepository.kt b/android/native/app/src/main/java/de/yourpart/nativeapp/feature/admin/AdminAccessRepository.kt
new file mode 100644
index 0000000..397d2ff
--- /dev/null
+++ b/android/native/app/src/main/java/de/yourpart/nativeapp/feature/admin/AdminAccessRepository.kt
@@ -0,0 +1,46 @@
+package de.yourpart.nativeapp.feature.admin
+
+import de.yourpart.nativeapp.core.config.AppConfig
+import de.yourpart.nativeapp.core.network.ApiResult
+import de.yourpart.nativeapp.core.network.NetworkRequestExecutor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import okhttp3.Request
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Serializable
+data class NavigationNodeDto(
+ val path: String? = null,
+ val children: Map = emptyMap(),
+)
+
+@Singleton
+class AdminAccessRepository @Inject constructor(
+ private val requestExecutor: NetworkRequestExecutor,
+ private val json: Json,
+ private val appConfig: AppConfig,
+) {
+ suspend fun loadAdminPaths(userId: String): Result> = withContext(Dispatchers.IO) {
+ runCatching {
+ val request = Request.Builder()
+ .url("${appConfig.apiBaseUrl}/api/navigation/$userId")
+ .get()
+ .build()
+ when (val result = requestExecutor.execute(request)) {
+ is ApiResult.Success -> {
+ val menu = json.decodeFromString