Files
dmc/app/lib/ui/providers/wifi_connection_provider.dart
T
Constantin Leue 539a6e4171 Drop SSID display from settings, connection pill shows generic "Connected"
Real hardware testing (Pixel 10a) showed NetworkCapabilities.getTransportInfo()
consistently returns a redacted WifiInfo (SSID "<unknown ssid>", masked BSSID)
even for the app's own self-requested WifiNetworkSpecifier network, disproving
this codebase's prior assumption of a self-request exemption from
ACCESS_FINE_LOCATION for that API path (confirmed via native diagnostic
logging cross-checked against `adb shell dumpsys wifi`, which does show and
correctly attribute the true SSID at the OS level). Rather than add a location
permission with a runtime prompt purely for this cosmetic display, the
settings pill now just shows "Connected". Documented as decision 4.24.
2026-08-06 22:24:03 +02:00

193 lines
6.7 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../transport/link_transport.dart';
import '../../transport/udp/udp_transport.dart';
import 'database_provider.dart';
/// App-lebenslanger UDP-Transport (Doku Kommunikationsschicht Abschnitt 3),
/// verdrahtet gegen Host/Port aus den Einstellungen. Analog zu
/// [bluetoothTransportProvider] bewusst nicht autoDispose - siehe dortige
/// Begruendung.
final wifiTransportProvider = Provider<UdpTransport>((ref) {
final settingsRepo = ref.watch(appSettingsRepositoryProvider);
final transport = UdpTransport(
getHost: settingsRepo.loadConnectionWifiHost,
getPort: settingsRepo.loadConnectionWifiPort,
getSsidPrefix: settingsRepo.loadConnectionWifiSsidPrefix,
getRememberedSsid: settingsRepo.loadConnectionWifiLastSsid,
rememberSsid: settingsRepo.setConnectionWifiLastSsid,
);
ref.onDispose(transport.dispose);
return transport;
});
/// Reaktiver [LinkState] des WLAN-Transports fuer die Oberflaeche.
final wifiLinkStateProvider = StreamProvider<LinkState>((ref) {
final transport = ref.watch(wifiTransportProvider);
return transport.state;
});
/// Grund des zuletzt gemeldeten [LinkState.error] des WLAN-Transports.
final wifiLinkErrorReasonProvider = Provider<LinkErrorReason?>((ref) {
ref.watch(wifiLinkStateProvider);
return ref.watch(wifiTransportProvider).lastErrorReason;
});
/// Zeigt an, ob gerade UDP-Pakete ankommen ("receiving"/"not receiving"),
/// unabhaengig vom (binaeren) Verbindungszustand - der Socket kann offen
/// sein (LinkState.connected), ohne dass die Gegenstelle tatsaechlich
/// sendet. Faellt [_receivingTimeout] nach dem letzten Paket automatisch
/// wieder auf "not receiving" zurueck, dafuer muss ein Timer laufen statt
/// nur auf den incoming-Stream zu reagieren.
class WifiStreamActivityNotifier extends Notifier<bool> {
static const _receivingTimeout = Duration(seconds: 4);
StreamSubscription<void>? _dataSub;
StreamSubscription<LinkState>? _stateSub;
Timer? _timeoutTimer;
@override
bool build() {
final transport = ref.watch(wifiTransportProvider);
_dataSub?.cancel();
_stateSub?.cancel();
_timeoutTimer?.cancel();
_dataSub = transport.incoming.listen((_) => _onPacket());
_stateSub = transport.state.listen(_onStateChanged);
ref.onDispose(() {
_dataSub?.cancel();
_stateSub?.cancel();
_timeoutTimer?.cancel();
});
return false;
}
void _onPacket() {
state = true;
_timeoutTimer?.cancel();
_timeoutTimer = Timer(_receivingTimeout, () => state = false);
}
void _onStateChanged(LinkState linkState) {
if (linkState != LinkState.connected) {
_timeoutTimer?.cancel();
state = false;
}
}
}
final wifiStreamActivityProvider =
NotifierProvider<WifiStreamActivityNotifier, bool>(
WifiStreamActivityNotifier.new);
/// Reaktive Gesamtzahl empfangener UDP-Pakete seit Verbindungsaufbau - fuer
/// die "Receiving"-Pille und die Connection-Log-Ueberschrift, die beide bei
/// jedem Paket sofort aktualisiert werden sollen (anders als
/// [WifiConnectionLogNotifier], deren Log-Eintraege nur bei Zustands-
/// aenderungen bzw. neu gelernter Gegenstelle geschrieben werden).
class WifiPacketCountNotifier extends Notifier<int> {
StreamSubscription<void>? _dataSub;
@override
int build() {
final transport = ref.watch(wifiTransportProvider);
_dataSub?.cancel();
_dataSub = transport.incoming.listen((_) => state++);
ref.onDispose(() => _dataSub?.cancel());
return 0;
}
}
final wifiPacketCountProvider =
NotifierProvider<WifiPacketCountNotifier, int>(WifiPacketCountNotifier.new);
/// Ein Eintrag im WLAN-Verbindungsprotokoll (Doku 7).
class WifiConnectionLogEntry {
const WifiConnectionLogEntry(this.timestamp, this.message);
final DateTime timestamp;
final String message;
}
/// Verbindungsprotokoll fuer den WLAN-Transport, analog zu
/// [ConnectionLogNotifier] fuer Bluetooth. Der Paketzaehler lebt separat in
/// [wifiPacketCountProvider] (reaktiv pro Paket, hier nur protokolliert).
class WifiConnectionLogNotifier
extends Notifier<List<WifiConnectionLogEntry>> {
static const _maxEntries = 50;
StreamSubscription<LinkState>? _stateSub;
StreamSubscription<void>? _dataSub;
String? _lastLoggedPeer;
@override
List<WifiConnectionLogEntry> build() {
final transport = ref.watch(wifiTransportProvider);
_stateSub?.cancel();
_dataSub?.cancel();
_lastLoggedPeer = null;
_stateSub = transport.state.listen(_onStateChanged);
_dataSub = transport.incoming.listen((_) => _onPacket(transport));
ref.onDispose(() {
_stateSub?.cancel();
_dataSub?.cancel();
});
// Telemetrie-Start/-Stopp (ueber denselben 4s-Timeout wie die
// "Receiving"-Pille) als eigene Log-Zeile, statt jedes einzelne Paket zu
// protokollieren - sonst wuerde das Log bei ~7 Paketen/s sofort
// ueberlaufen.
ref.listen<bool>(wifiStreamActivityProvider, (previous, isReceiving) {
if (previous == isReceiving) return;
_append(isReceiving ? 'Receiving telemetry' : 'Telemetry stream stopped');
});
return const [];
}
void _onPacket(UdpTransport transport) {
// Gegenstelle wird aus dem ersten eingehenden Paket gelernt (Doku
// Kommunikationsschicht v2, Abschnitt 3/9) - nur beim ersten Mal bzw.
// bei Aenderung protokollieren, nicht pro Paket.
final peer = transport.peerDescription;
if (peer != null && peer != _lastLoggedPeer) {
_lastLoggedPeer = peer;
_append('Peer learned: $peer');
}
}
void _onStateChanged(LinkState linkState) {
final transport = ref.read(wifiTransportProvider);
final message = switch (linkState) {
LinkState.disconnected => 'Disconnected',
LinkState.connecting => 'Requesting mLRS network...',
LinkState.connected => 'Connected to '
'${transport.connectedSsid ?? 'network'}, listening on port '
'${transport.boundPort} (STA concurrency: '
'${_formatStaConcurrency(transport.staConcurrencySupported)})',
LinkState.error => 'Error: '
'${transport.lastErrorMessage ?? transport.lastErrorReason?.name ?? 'unknown'}',
};
_append(message);
}
String _formatStaConcurrency(bool? supported) => switch (supported) {
true => 'supported',
false => 'not supported',
null => 'unknown',
};
void _append(String message) {
final entry = WifiConnectionLogEntry(DateTime.now(), message);
final next = [...state, entry];
state = next.length > _maxEntries
? next.sublist(next.length - _maxEntries)
: next;
}
}
final wifiConnectionLogProvider =
NotifierProvider<WifiConnectionLogNotifier, List<WifiConnectionLogEntry>>(
WifiConnectionLogNotifier.new);