209 lines
8.1 KiB
Dart
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).
|
|
}
|
|
}
|