# Architektur- & Implementierungsplan: Emergency App Status: **entscheidungsreif** (alle offenen Fragen aus der Architektur-Review geklärt am 2026-07-24) Basis: [`../anforderung.md`](../anforderung.md) ## 1. Kontext Flutter-App (Android + iOS) mit sicherheitskritischer Kernfunktion: **Notruf auslösen**. Die App ermittelt das aktuelle Land, zeigt einen oder mehrere große Buttons mit den landesspezifischen Notrufnummern (Allgemein / Feuerwehr / Polizei / Rettung), ein Tap öffnet die native Telefon-App mit vorgewählter Nummer. **Grundsatz:** Diese App ist ein Sicherheitswerkzeug. Sie muss **offline funktionieren**, **schnell starten**, und darf **niemals** in einem Zustand landen, in dem keine Notrufnummer erreichbar ist. Jede Architekturentscheidung ordnet sich diesem Grundsatz unter. ## 2. Getroffene Entscheidungen | Thema | Entscheidung | Begründung | |---|---|---| | Persistenz | `shared_preferences` (kein Hive) | Nur 1-2 Strings zu cachen (letzter Ländercode, manuelle Auswahl); Notrufnummern sind read-only JSON-Asset. Kein build_runner/TypeAdapter-Overhead nötig. | | Ländererkennung | **SIM/Netz-Ländercode zuerst**, GPS+Reverse-Geocoding als Fallback | Offline verfügbar; zeigt an, in welchem Mobilfunknetz man eingebucht ist – genau dieses Netz wickelt den Notruf ab. Bei Konflikt (Reisender mit Heimat-SIM) gewinnt automatisch die SIM-Quelle, kein manueller Auswahldialog nötig. | | Notruf-Auslösung | `tel:`-Scheme öffnet Wähler, **kein Auto-Dial** | Sicherer gegen Fehlauslösung; identisches Verhalten auf Android und iOS (Auto-Dial auf iOS technisch ohnehin nicht möglich, auf Android nur mit heikler `CALL_PHONE`-Permission). | | Panikmodus | Globaler 112/911-Button **immer sichtbar**, auch während Ländererkennung läuft | Kein Ladezustand darf ohne funktionierenden Notruf-Button sein. | | Lokalisierung | Sprache an erkanntes Land gekoppelt | Automatische UI-Sprache passend zum Land statt nur DE/EN. Mehr ARB-Dateien/Übersetzungsaufwand bereits in v1 eingeplant. | | Disclaimer | Permanent sichtbar/abrufbar (z. B. Info-/Einstellungsbereich) | Haftungsrisiko bei falschen/veralteten Nummern; Hinweis „im Zweifel 112 wählen". | | Datenpflege | Statisches JSON-Asset, **kein** Remote-Update-Mechanismus | Passt zum Offline-Grundsatz; Aktualisierung nur über App-Updates. | ## 3. Projekt-Setup ``` flutter create --org co.selfhost.hhml --platforms=android,ios emergency ``` Danach `git init`. ### Packages (`pubspec.yaml`) | Package | Zweck | |---|---| | `url_launcher` | `tel:`-Scheme → native Telefon-App | | `geolocator` | GPS-Position (Fallback-Quelle) | | `geocoding` | Reverse-Geocoding lat/lng → ISO-Ländercode (braucht ggf. Netz) | | Platform-Channel bzw. Package für SIM-/Netz-Ländercode (Android: `TelephonyManager.getNetworkCountryIso()`; iOS: CoreTelephony, ab iOS 16 eingeschränkt zuverlässig – **Rechercheposten in AP4**) | Primärquelle Ländererkennung | | `permission_handler` | Standort-Berechtigungs-UX | | `provider` | State Management (ChangeNotifier) | | `shared_preferences` | Cache: letzter Ländercode, manuelle Auswahl | | `flutter_localizations` / `intl` | Lokalisierung, gekoppelt an erkanntes Land | ## 4. Datenmodell & Datenhaltung **Quelle:** Wikipedia „List of emergency telephone numbers" als Ausgangsbasis, Gegenprüfung via ITU-T/offizielle Behördenseiten. Einmalig aufbereitet, als JSON versioniert, kein Live-Fetch. **Format** (`assets/data/emergency_numbers.json`), ISO 3166-1 alpha-2 als Schlüssel: ```jsonc { "DE": { "name_en": "Germany", "name_de": "Deutschland", "flag": "🇩🇪", "numbers": [ { "type": "general", "number": "112", "label_de": "Notruf", "label_en": "Emergency" }, { "type": "police", "number": "110", "label_de": "Polizei", "label_en": "Police" } ] } } ``` `type`-Enum: `general | police | fire | ambulance | sea_rescue | mountain_rescue` (für typspezifische Icons/Farben). **Dart-Modelle:** `EmergencyNumber`, `Country` (mit `fromJson`). **Pflege:** JSON im Repo, reviewbar per Diff. Optionales Hilfsskript (`tool/`) zur Generierung aus CSV, kein App-Bestandteil. ## 5. App-Architektur **State Management:** Ein `EmergencyProvider` (ChangeNotifier). ``` EmergencyProvider ├─ state: EmergencyState { status, country, numbers, errorType } │ status ∈ { initializing, locating, ready, permissionDenied, │ countryUnknown, error } ├─ init() → startet Ermittlungskette ├─ retry() └─ setCountryManually() ``` ### Ordnerstruktur ``` lib/ ├─ main.dart ├─ app.dart ├─ models/ │ ├─ country.dart │ ├─ emergency_number.dart │ └─ emergency_type.dart ├─ repositories/ │ └─ emergency_number_repository.dart ├─ services/ │ ├─ location_service.dart # Ermittlungskette │ ├─ country_code_sources/ # SIM, GPS, cached, manual │ └─ dialer_service.dart ├─ providers/ │ └─ emergency_provider.dart ├─ features/ │ └─ home/ │ ├─ home_screen.dart │ └─ widgets/ │ ├─ emergency_button.dart # inkl. globaler Panik-Button │ ├─ country_header.dart │ └─ status_banner.dart ├─ l10n/ │ └─ app_.arb # pro unterstütztem Land/Sprache └─ core/ ├─ constants.dart # Fallback-Nummern, Default-Land └─ result.dart assets/data/emergency_numbers.json ``` **Schichtentrennung:** UI kennt nur den Provider; Provider orchestriert Services + Repository ohne Flutter-Context; Services/Repository sind plattformnah bzw. datennah und gut testbar. ## 6. Ablauf ``` App-Start → EmergencyProvider.init() → globaler 112/911-Button sofort sichtbar (Panikmodus) → LocationService.resolveCountryCode(): 1. SIM/Netz-Ländercode (offline, primär) 2. GPS + Reverse-Geocoding (Fallback, ggf. Netz nötig) 3. zuletzt gespeicherter Code (shared_preferences) 4. sonst → status = countryUnknown → Repository.lookup(isoCode) → Country + numbers → Cache: isoCode in shared_preferences speichern → UI-Sprache an erkanntes Land koppeln → status = ready → HomeScreen rendert länderspezifische EmergencyButtons zusätzlich zum Panik-Button → Tap → DialerService.call(number) → url_launcher launchUrl(Uri(scheme:'tel', path:number)) → Telefon-App öffnet sich mit vorgewählter Nummer, Nutzer bestätigt selbst ``` ## 7. Fehlerfälle / Edge Cases | Fall | Verhalten | |---|---| | Standort-Berechtigung verweigert | App bleibt voll nutzbar; SIM-Ländercode/Cache/manuelle Auswahl, nicht-blockierender Banner. | | GPS aus / kein Fix | Kette fällt auf SIM/Cache/manuell zurück. | | Kein Internet | Kernfunktion voll erhalten (JSON-Asset lokal, SIM-Code offline, Wähler braucht kein Netz); nur Reverse-Geocoding kann ausfallen. | | Land nicht in Datenbasis | Fallback auf 112 (GSM-weit) und 911, mit Hinweis. | | Land nicht ermittelbar | `countryUnknown`-Screen: Panik-Button + Länder-Suchfeld. | | Mehrere Nummern pro Land | Liste von Buttons, `general` zuoberst/am größten. | | `tel:` nicht öffnbar (z. B. Tablet) | `canLaunchUrl`-Check; sonst Nummer groß anzeigen + Copy-Button. | | Ladezustand | Panik-Button von Anfang an sichtbar (siehe Ablauf). | ### Plattformspezifika - **Android:** `ACCESS_FINE/COARSE_LOCATION`-Permissions; ``-Eintrag für `tel:`-Scheme (Android 11+ Package Visibility). - **iOS:** `NSLocationWhenInUseUsageDescription`; `LSApplicationQueriesSchemes` mit `tel`. SIM-Ländercode über CoreTelephony ab iOS 16 unzuverlässig → GPS-Fallback wichtiger auf iOS. - Kein Web/Desktop (Scope laut Anforderung nur Android/iOS). ## 8. Schritt-für-Schritt-Implementierungsplan 1. **AP0 – Setup:** `flutter create`, Git-Init, Packages, Ordnerstruktur, l10n-Grundgerüst. 2. **AP1 – Datenbasis:** Notrufnummern recherchieren/aufbereiten → `emergency_numbers.json`; Modelle; `EmergencyNumberRepository` + Unit-Tests. 3. **AP2 – Dialer:** `DialerService` mit `url_launcher`, `canLaunchUrl`-Guard; Manifest/Plist-Einträge. 4. **AP3 – Statisches UI:** `HomeScreen` + `EmergencyButton` inkl. Panik-Button, zunächst mit hartkodiertem Land, damit der Notruf-Flow früh end-to-end testbar ist. 5. **AP4 – Ländererkennung:** `LocationService` mit priorisierter Kette (SIM → GPS/Geocoding → Cache → unknown). **Rechercheposten:** verlässliches Package/Platform-Channel für SIM-Ländercode je Plattform, insbesondere iOS-16-Einschränkung. 6. **AP5 – State-Integration:** `EmergencyProvider`, Verdrahtung Service↔Repository↔UI, alle `status`-Zustände. 7. **AP6 – Fallbacks & manuelle Auswahl:** `CountryPickerScreen`, Cache via `shared_preferences`, globale 112/911-Fallbacks. 8. **AP7 – Lokalisierung:** UI-Sprache an erkanntes Land koppeln, ARB-Dateien für Zielsprachen. 9. **AP8 – Disclaimer & Feinschliff:** Permanent abrufbarer Haftungsausschluss, Barrierefreiheit (große Touch-Ziele, hoher Kontrast, Screenreader-Labels), Datenstand-Hinweis, App-Icon/Name. 10. **AP9 – Tests & Härtung:** Unit-Tests Repository/Service-Kette, Widget-Tests HomeScreen-Zustände, manuelle Tests auf realen Geräten (Flugmodus, ohne SIM, Ausland-Simulation). 11. **AP10 – Store-Vorbereitung:** Datenschutzerklärung (Standortnutzung), Store-Texte, Screenshots. ## 9. Verbleibendes Risiko - **SIM-Ländercode auf iOS 16+:** `CTCarrier.isoCountryCode` ist deprecated und liefert teils unzuverlässige Werte. Konkrete Lösung (Package-Wahl oder Eigenimplementierung via Platform-Channel) muss in AP4 recherchiert/prototypisch verifiziert werden. - **Rechtliche Absicherung des Disclaimers:** Formulierung sollte vor Store-Veröffentlichung juristisch geprüft werden.