import 'package:flutter/services.dart'; /// Ereignis der per [MlrsNetworkController.requestNetwork] angeforderten /// mLRS-WLAN-Verbindung (Doku Kommunikationsschicht v2, Abschnitt 4). sealed class MlrsNetworkEvent { const MlrsNetworkEvent(); } /// Das Netz ist verfuegbar (Nutzer hat den Systemdialog bestaetigt, oder /// Android hat eine zuvor bewilligte exakte SSID still erneut verbunden). /// [gatewayAddress] ist die Gateway-Adresse des Netzes, falls ermittelbar - /// dient als Vorbelegung der Zieladresse, bevor eine Gegenstelle aus dem /// Verkehr gelernt wurde (Doku Abschnitt 3). [broadcastAddress] ist die aus /// der zugewiesenen IPv4-Adresse + Praefixlaenge berechnete Subnetz- /// Broadcast-Adresse (z.B. 192.168.4.255) - robusterer Bootstrap-Fallback /// als eine geratene feste Standard-IP, falls die mLRS-Bruecke kein /// initiales Paket sendet, aus dem die App ihre Adresse lernen koennte: eine /// an die Broadcast-Adresse gesendete Anfrage erreicht die Bruecke /// unabhaengig von ihrer konkreten IP. [ssid] ist die tatsaechliche SSID des /// verbundenen Netzes, falls ermittelbar - wird gemerkt, damit kuenftige /// Verbindungen exakt (statt per Praefix) angefragt werden koennen (siehe /// [MlrsNetworkController.requestNetwork]). class MlrsNetworkAvailable extends MlrsNetworkEvent { const MlrsNetworkAvailable({this.gatewayAddress, this.broadcastAddress, this.ssid}); final String? gatewayAddress; final String? broadcastAddress; final String? ssid; } /// Die Anfrage konnte nicht erfuellt werden (Nutzer hat den Systemdialog /// abgelehnt, keine passende SSID in Reichweite, oder Systemtimeout). class MlrsNetworkUnavailable extends MlrsNetworkEvent { const MlrsNetworkUnavailable(); } /// Ein zuvor verfuegbares Netz wurde waehrend einer bestehenden Verbindung /// getrennt (Doku Abschnitt 8: "Bindung haftet am Socket ... hoeren bewusst /// auf zu funktionieren"). class MlrsNetworkLost extends MlrsNetworkEvent { const MlrsNetworkLost(); } /// Die SSID des verbundenen Netzes wurde (nachtraeglich) ermittelt. Android /// liefert `WifiInfo`/SSID bei `WifiNetworkSpecifier`-Netzen haeufig noch /// nicht synchron im `onAvailable`-Callback, sondern erst kurz danach ueber /// `NetworkCallback.onCapabilitiesChanged` (bekannter Android-Stolperstein, /// siehe MlrsNetworkPlugin.kt) - deshalb ein eigenes, spaeter eintreffendes /// Ereignis statt eines Felds auf [MlrsNetworkAvailable]. class MlrsNetworkSsidUpdated extends MlrsNetworkEvent { const MlrsNetworkSsidUpdated(this.ssid); final String ssid; } /// Ein per [MlrsNetworkController.openSocket] nativ gebundener UDP-Socket /// erfolgreich geoeffnet. [boundPort] ist der tatsaechlich gebundene Port; /// [fallbackFromPort] ist gesetzt, wenn der urspruenglich gewuenschte Port /// belegt war und auf einen fluechtigen ausgewichen wurde (Doku 3/9). class UdpSocketBindResult { const UdpSocketBindResult({required this.boundPort, this.fallbackFromPort}); final int boundPort; final int? fallbackFromPort; } /// Ein vom nativen Socket empfangenes UDP-Paket. class MlrsSocketPacket { const MlrsSocketPacket({ required this.address, required this.port, required this.data, }); final String address; final int port; final Uint8List data; } /// Plattform-Bruecke zum app-initiierten WLAN-Verbindungsaufbau /// (`WifiNetworkSpecifier`/`ConnectivityManager`) UND zum darauf gebundenen /// UDP-Socket (Doku Kommunikationsschicht v2 Abschnitt 4) - kapselt den /// einzigen Platform-Channel-Zugriff dieses Features. [UdpTransport] /// programmiert ausschliesslich gegen dieses Interface, damit es in Tests /// durch eine Fake-Implementierung ersetzt werden kann, ohne einen echten /// Android-Kanal zu brauchen. /// /// Der UDP-Socket wird bewusst nativ gehalten und per `Network.bindSocket()` /// **socket-genau** an das mLRS-Netz gebunden statt per /// `ConnectivityManager.bindProcessToNetwork()` den gesamten Prozess zu /// binden (Doku Abschnitt 4, Stufe 3) - Letzteres hat in der Praxis dazu /// gefuehrt, dass gleichzeitige Kartenkacheln-/Wetter-HTTP-Requests, die /// waehrend des kurzen Bindungsfensters neue Verbindungen aufbauten, auf dem /// internetlosen mLRS-Netz haengen blieben (besonders auf Geraeten ohne /// STA-Concurrency). Socket-genaue Bindung ist Androids eigene Empfehlung /// fuer Apps, die gleichzeitig ein spezielles lokales Netz UND normales /// Internet brauchen. abstract class MlrsNetworkController { /// Ergebnisse zur zuletzt per [requestNetwork] gestellten Anfrage - /// broadcast, damit sowohl [UdpTransport.connect] (wartet auf das erste /// Ereignis) als auch ein laufender Verbindungsueberwacher (reagiert auf /// ein spaeteres [MlrsNetworkLost]) gleichzeitig zuhoeren koennen. Stream get events; /// Loest `ConnectivityManager.requestNetwork()` mit einem /// `WifiNetworkSpecifier` aus. Ist [preferredSsid] gesetzt (zuvor /// erfolgreich verbundene, gemerkte SSID), wird exakt danach gefragt - /// Android kann das dann ohne erneuten Systemdialog automatisch bewilligen /// (Doku Abschnitt 8: "Verbindung automatisch herstellen"). Sonst wird /// [ssidPrefix] als Praefix-Muster verwendet (zeigt immer den Dialog). /// Das Ergebnis kommt asynchron ueber [events] (available/unavailable). Future requestNetwork(String ssidPrefix, {String? preferredSsid}); /// Meldet den Callback ab und geht damit von der aktuellen Anfrage /// zurueck - keine weiteren [events] danach, bis erneut angefragt wird. Future releaseNetwork(); /// Oeffnet einen nativ erzeugten UDP-Socket auf [port] und bindet ihn per /// `Network.bindSocket()` an das zuletzt verfuegbare Netz. Weicht bei /// belegtem Port auf einen fluechtigen aus (siehe [UdpSocketBindResult]). /// Setzt voraus, dass zuvor ein [MlrsNetworkAvailable]-Ereignis empfangen /// wurde. Future openSocket(int port); /// Schliesst den nativen Socket und beendet dessen Empfangsschleife. Future closeSocket(); /// Sendet [data] ueber den nativen Socket an [host]:[port]. Future send(Uint8List data, String host, int port); /// Eingehende UDP-Pakete vom nativen Socket. Stream get incomingPackets; /// Ergebnis von /// `WifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()` (Doku /// Abschnitt 4/8) - null falls nicht ermittelbar (API < 30). Nur zur /// Anzeige/zum Verbindungsprotokoll, nicht handlungsrelevant. Future isStaConcurrencySupported(); void dispose(); } /// Echte Implementierung ueber MethodChannel/EventChannel gegen /// `MlrsNetworkPlugin.kt` (android/app/.../MlrsNetworkPlugin.kt). class MethodChannelMlrsNetworkController implements MlrsNetworkController { MethodChannelMlrsNetworkController() : _methodChannel = const MethodChannel('com.dmc.dmc_app/mlrs_network'), _eventChannel = const EventChannel('com.dmc.dmc_app/mlrs_network_events'), _socketEventChannel = const EventChannel('com.dmc.dmc_app/mlrs_socket_events'); final MethodChannel _methodChannel; final EventChannel _eventChannel; final EventChannel _socketEventChannel; @override late final Stream events = _eventChannel.receiveBroadcastStream().map(_toEvent); @override late final Stream incomingPackets = _socketEventChannel.receiveBroadcastStream().map(_toPacket); MlrsNetworkEvent _toEvent(dynamic raw) { final map = Map.from(raw as Map); return switch (map['type']) { 'available' => MlrsNetworkAvailable( gatewayAddress: map['gatewayAddress'] as String?, broadcastAddress: map['broadcastAddress'] as String?, ssid: map['ssid'] as String?, ), 'ssid' => MlrsNetworkSsidUpdated(map['ssid']! as String), 'lost' => const MlrsNetworkLost(), _ => const MlrsNetworkUnavailable(), }; } MlrsSocketPacket _toPacket(dynamic raw) { final map = Map.from(raw as Map); return MlrsSocketPacket( address: map['address'] as String, port: map['port'] as int, data: map['data'] as Uint8List, ); } @override Future requestNetwork(String ssidPrefix, {String? preferredSsid}) => _methodChannel.invokeMethod('requestNetwork', { 'ssidPrefix': ssidPrefix, 'exactSsid': preferredSsid, }); @override Future releaseNetwork() => _methodChannel.invokeMethod('releaseNetwork'); @override Future openSocket(int port) async { final result = await _methodChannel .invokeMapMethod('openSocket', {'port': port}); final map = result!; return UdpSocketBindResult( boundPort: map['boundPort'] as int, fallbackFromPort: map['fallbackFromPort'] as int?, ); } @override Future closeSocket() => _methodChannel.invokeMethod('closeSocket'); @override Future send(Uint8List data, String host, int port) => _methodChannel.invokeMethod('sendPacket', { 'data': data, 'host': host, 'port': port, }); @override Future isStaConcurrencySupported() => _methodChannel.invokeMethod('isStaConcurrencySupported'); @override void dispose() { // Keine eigenen Ressourcen - Abonnenten von [events]/[incomingPackets] // verwalten ihre StreamSubscription selbst (siehe UdpTransport.dispose). } }