wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.) als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# Push-Benachrichtigungen & E2EE im Hintergrund
|
||||
|
||||
Stand: 2026-06-04. Dieses Dokument beschreibt, wie Pyramid auf Android
|
||||
verschlüsselte Push-Benachrichtigungen verarbeitet — inklusive Entschlüsselung
|
||||
und verschlüsseltem Antworten **bei komplett gekillter App**, ohne dass sich die
|
||||
App öffnet.
|
||||
|
||||
## Überblick der Kette
|
||||
|
||||
```
|
||||
Matrix-Homeserver (Continuwuity)
|
||||
│ (event_id_only Push)
|
||||
▼
|
||||
Sygnal (self-hosted: push.steggi-matrix.work)
|
||||
▼
|
||||
Firebase Cloud Messaging (FCM)
|
||||
▼
|
||||
PushService.kt (FirebaseMessagingService, nativ)
|
||||
├─ App lebt im Vordergrund → App zeigt selbst, Push wird übersprungen
|
||||
├─ App lebt im Hintergrund → Haupt-Engine entschlüsselt (warm, schnell)
|
||||
└─ App gekillt → BgEngine bootet ein headless Flutter-Isolate
|
||||
```
|
||||
|
||||
Der Flutter-eigene FCM-Dienst (`FlutterFirebaseMessagingService`) ist im
|
||||
`AndroidManifest.xml` **deaktiviert** — alle Pushes laufen über `PushService.kt`.
|
||||
Push-Format ist `event_id_only` (nur die Event-ID wird übertragen, nicht der
|
||||
Inhalt — datensparsam).
|
||||
|
||||
## Warum dieser Aufbau?
|
||||
|
||||
Die E2EE-Schlüssel (olm/megolm) liegen **nur** im Matrix-Client (verschlüsselte
|
||||
SQLite-DB), nicht im nativen Kotlin-Code. Kotlin kann daher Sender + Raumname
|
||||
(unverschlüsselte Room-States) per HTTP holen, aber **nicht** den Nachrichtentext
|
||||
entschlüsseln. Dafür braucht es einen laufenden Matrix-Client.
|
||||
|
||||
**Sicherheitsregel:** Niemals zwei Matrix-Clients gleichzeitig auf derselben
|
||||
olm-DB — das korrumpiert den Ratchet-State und führt zu dauerhaften
|
||||
„Unable to decrypt"-Fehlern. Deshalb läuft der Hintergrund-Client (`BgEngine`)
|
||||
**nur**, wenn die Haupt-App nachweislich tot ist (`FlutterEngineCache.get("main")
|
||||
== null`).
|
||||
|
||||
## Die drei Zustände
|
||||
|
||||
| Zustand | Erkennung | Pfad |
|
||||
|---|---|---|
|
||||
| Vordergrund | Heartbeat < 20s (`flutter.notif_app_heartbeat`) | Push übersprungen, App zeigt selbst |
|
||||
| Hintergrund (lebt) | `FlutterEngineCache.get("main") != null` | Platzhalter sofort → Haupt-Engine entschlüsselt + aktualisiert |
|
||||
| Gekillt | keine Haupt-Engine | `BgEngine` bootet headless Isolate, **decrypt-first** |
|
||||
|
||||
## Gekillter Zustand im Detail (decrypt-first)
|
||||
|
||||
1. `PushService.onMessageReceived` holt Titel (Sender · Raum) per HTTP.
|
||||
2. Statt sofort einen Platzhalter zu zeigen:
|
||||
- `BgEngine.scheduleFallback(…, 6s)` plant einen „Neue Nachricht"-Platzhalter,
|
||||
der **nur** erscheint, falls die Entschlüsselung > 6s dauert oder scheitert.
|
||||
- `BgEngine.run("decryptAndShow", …)` bootet das Isolate.
|
||||
3. `BgEngine` (BgEngine.kt) startet eine headless `FlutterEngine` über den
|
||||
gespeicherten Callback-Handle (`flutter.bg_engine_handle`) und führt das
|
||||
Dart-Entrypoint `notificationEngineMain` (background_push.dart) aus.
|
||||
4. Dart `_bgDecryptAndShow`:
|
||||
- `_buildClient()` öffnet **dieselbe** DB (`pyramid.sqlite`,
|
||||
`MatrixSdkDatabase.init('pyramid')`, `NativeImplementationsDummy`).
|
||||
- `client.getEventByPushNotification(...)` entschlüsselt mit den lokal
|
||||
vorhandenen megolm-Schlüsseln (oneShotSync nur als Fallback).
|
||||
- Gibt Titel + Text per MethodChannel `showDecrypted` an Native zurück.
|
||||
5. `BgEngine` zeigt via `NotificationHelper.show` die echte Nachricht und
|
||||
bricht den Fallback-Platzhalter ab.
|
||||
|
||||
Ergebnis: Die Benachrichtigung erscheint ~4 s nach der Nachricht **direkt mit
|
||||
echtem Text** (kein „Neue Nachricht"-Zwischenschritt). Die ~4 s sind der
|
||||
Engine-Bootstrap (~1,7 s) + Client-Init/Entschlüsselung (~2,3 s).
|
||||
|
||||
## Verschlüsselt antworten (gekillt, vollständig still)
|
||||
|
||||
- Die Reply-Action ist ein **BroadcastReceiver** (`getBroadcast` → `ReplyReceiver`),
|
||||
**nicht** MainActivity — die App öffnet sich also nicht.
|
||||
- `ReplyReceiver`:
|
||||
- App lebt → MethodChannel `replyFromNotification` an die Haupt-Engine.
|
||||
- Gekillt → `BgEngine.run("sendReply", …)` → Dart `_bgSendReply` → oneShotSync +
|
||||
`room.sendTextEvent` (verschlüsselt). `goAsync()` + finish nach 15 s hält den
|
||||
Receiver-Prozess am Leben, bis der Versand durch ist.
|
||||
- RemoteInput-Extraktion hat mehrere Key-Fallbacks (OEM-Eigenheiten) und
|
||||
funktioniert auf Samsung S23 trotz der alten „getBroadcast verschluckt
|
||||
RemoteInput"-Warnung.
|
||||
|
||||
## Gelöste Fallstricke (NICHT wieder einbauen)
|
||||
|
||||
1. **Loader-Reihenfolge:** `FlutterCallbackInformation.lookupCallbackInformation`
|
||||
erst **nach** `flutterLoader.startInitialization` + `ensureInitializationComplete`
|
||||
aufrufen — sonst `UnsatisfiedLinkError` (native Methode, libflutter.so noch
|
||||
nicht geladen).
|
||||
2. **vodozemac doppelt:** Wenn Decrypt- und Reply-Task dasselbe wiederverwendete
|
||||
Isolate nutzen, wirft das zweite `vod.init()` „already initialized". In
|
||||
`_buildClient` in eigenem try/catch schlucken — sonst gibt `_buildClient`
|
||||
still `null` zurück und die Antwort geht nicht raus.
|
||||
3. **`database_closed`-Rauschen:** Vor `client.dispose()` 2 s warten, damit der
|
||||
asynchrone Key-Backup-Upload fertig wird.
|
||||
|
||||
## Relevante Dateien
|
||||
|
||||
| Datei | Rolle |
|
||||
|---|---|
|
||||
| `android/.../PushService.kt` | FCM-Empfang, Heartbeat-Check, decrypt-first-Routing |
|
||||
| `android/.../BgEngine.kt` | Headless-Engine-Bootstrap, Fallback-Platzhalter |
|
||||
| `android/.../ReplyReceiver.kt` | Stilles verschlüsseltes Antworten (Broadcast) |
|
||||
| `android/.../NotificationHelper.kt` | Native Benachrichtigung (Reply/Tap/Dismiss, setOnlyAlertOnce) |
|
||||
| `android/.../MainActivity.kt` | MethodChannel `install`, Cold-Start-Reply-Fallback |
|
||||
| `lib/core/background_push.dart` | Dart-Hintergrund-Isolate: decrypt + reply |
|
||||
| `lib/core/fcm_push_service.dart` | Pusher-Registrierung bei Sygnal |
|
||||
| `lib/core/notification_service.dart` | Vordergrund/Hintergrund-Notifs, Heartbeat |
|
||||
| `lib/main.dart` | `registerBgEngineHandle()` beim Start |
|
||||
|
||||
## Debugging
|
||||
|
||||
`flutter run` verliert die Verbindung, sobald die App gekillt wird — daher direkt
|
||||
`adb logcat` nutzen:
|
||||
|
||||
```
|
||||
adb logcat -v time PYRAMID-PUSH:* PYRAMID-BGENGINE:* PYRAMID-REPLY:* PYRAMID-INTENT:* flutter:* AndroidRuntime:E *:S
|
||||
```
|
||||
|
||||
Erwartete Log-Sequenz (gekillt, Erfolg):
|
||||
```
|
||||
PYRAMID-PUSH: decrypt-first via background engine for <room>
|
||||
PYRAMID-BGENGINE: background engine started
|
||||
PYRAMID-BGENGINE: Dart background engine ready — flushing 1 task(s)
|
||||
PYRAMID-BGENGINE: showDecrypted → shown decrypted notification for <room>
|
||||
```
|
||||
Reply (gekillt):
|
||||
```
|
||||
PYRAMID-REPLY: extracted replyText=… (remoteInput=true)
|
||||
PYRAMID-REPLY: Flutter engine not running → background engine send
|
||||
PYRAMID-BGENGINE: (sendReply → sendTextEvent OK)
|
||||
```
|
||||
|
||||
Hinweis: Kotlin lässt sich nur auf einem Rechner mit JAVA_HOME/Android-SDK bauen.
|
||||
Reference in New Issue
Block a user