Files
dmc/app/lib/transport/udp/mlrs_network_controller.dart
T

209 lines
8.1 KiB
Dart

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). [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.ssid});
final String? gatewayAddress;
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();
}
/// 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<MlrsNetworkEvent> 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<void> 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<void> 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<UdpSocketBindResult> openSocket(int port);
/// Schliesst den nativen Socket und beendet dessen Empfangsschleife.
Future<void> closeSocket();
/// Sendet [data] ueber den nativen Socket an [host]:[port].
Future<void> send(Uint8List data, String host, int port);
/// Eingehende UDP-Pakete vom nativen Socket.
Stream<MlrsSocketPacket> get incomingPackets;
/// Ergebnis von
/// `WifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()` (Doku
/// Abschnitt 4/8) - null falls nicht ermittelbar (API < 30). Nur zur
/// Anzeige/zum Verbindungsprotokoll, nicht handlungsrelevant.
Future<bool?> 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<MlrsNetworkEvent> events =
_eventChannel.receiveBroadcastStream().map(_toEvent);
@override
late final Stream<MlrsSocketPacket> incomingPackets =
_socketEventChannel.receiveBroadcastStream().map(_toPacket);
MlrsNetworkEvent _toEvent(dynamic raw) {
final map = Map<Object?, Object?>.from(raw as Map);
return switch (map['type']) {
'available' => MlrsNetworkAvailable(
gatewayAddress: map['gatewayAddress'] as String?,
ssid: map['ssid'] as String?,
),
'lost' => const MlrsNetworkLost(),
_ => const MlrsNetworkUnavailable(),
};
}
MlrsSocketPacket _toPacket(dynamic raw) {
final map = Map<Object?, Object?>.from(raw as Map);
return MlrsSocketPacket(
address: map['address'] as String,
port: map['port'] as int,
data: map['data'] as Uint8List,
);
}
@override
Future<void> requestNetwork(String ssidPrefix, {String? preferredSsid}) =>
_methodChannel.invokeMethod('requestNetwork', {
'ssidPrefix': ssidPrefix,
'exactSsid': preferredSsid,
});
@override
Future<void> releaseNetwork() => _methodChannel.invokeMethod('releaseNetwork');
@override
Future<UdpSocketBindResult> openSocket(int port) async {
final result = await _methodChannel
.invokeMapMethod<String, Object?>('openSocket', {'port': port});
final map = result!;
return UdpSocketBindResult(
boundPort: map['boundPort'] as int,
fallbackFromPort: map['fallbackFromPort'] as int?,
);
}
@override
Future<void> closeSocket() => _methodChannel.invokeMethod('closeSocket');
@override
Future<void> send(Uint8List data, String host, int port) =>
_methodChannel.invokeMethod('sendPacket', {
'data': data,
'host': host,
'port': port,
});
@override
Future<bool?> isStaConcurrencySupported() =>
_methodChannel.invokeMethod<bool>('isStaConcurrencySupported');
@override
void dispose() {
// Keine eigenen Ressourcen - Abonnenten von [events]/[incomingPackets]
// verwalten ihre StreamSubscription selbst (siehe UdpTransport.dispose).
}
}