wifi connection established by app, process socket binding, primary os connection untouched

This commit is contained in:
Constantin Leue
2026-07-31 15:25:45 +02:00
parent 222d8743fb
commit 3b3451b8a2
16 changed files with 1188 additions and 89 deletions
+5 -1
View File
@@ -19,7 +19,11 @@ android {
applicationId = "com.dmc.dmc_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
// minSdk 29 (statt Flutter-Default 24): WifiNetworkSpecifier/
// ConnectivityManager.requestNetwork() fuer den app-initiierten
// mLRS-Verbindungsaufbau (Doku Kommunikationsschicht v2, Abschnitt 4)
// gibt es erst ab Android 10.
minSdk = 29
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
@@ -5,6 +5,14 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!-- Fuer WifiNetworkSpecifier/ConnectivityManager.requestNetwork() (Doku
Kommunikationsschicht v2, Abschnitt 4/5) - die App baut die
WLAN-Verbindung zur mLRS-Bruecke selbst auf, statt sich auf eine
manuelle Verbindung ueber die Systemeinstellungen zu verlassen (das
wuerde das mLRS-Netz zur Standardroute machen und Kartenkacheln/
Wetter kappen). Fehlt Mission Planner, weshalb dieser Weg dort nicht
moeglich ist. -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!-- Haelt das Geraet waehrend des Flugs wach, sonst schlaeft es ein und
die Telemetrie reisst ab (Doku Kommunikationsschicht Abschnitt 3).
CHANGE_WIFI_MULTICAST_STATE erst ergaenzen, wenn sich zeigt, dass die
@@ -1,5 +1,22 @@
package com.dmc.dmc_app
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val plugin = MlrsNetworkPlugin(applicationContext)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
MlrsNetworkPlugin.METHOD_CHANNEL,
).setMethodCallHandler(plugin)
EventChannel(
flutterEngine.dartExecutor.binaryMessenger,
MlrsNetworkPlugin.EVENT_CHANNEL,
).setStreamHandler(plugin)
}
}
@@ -0,0 +1,164 @@
package com.dmc.dmc_app
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSpecifier
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PatternMatcher
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
/**
* Verbindungsaufbau zur mLRS-WLAN-Bruecke ausschliesslich aus der App heraus
* (Doku Kommunikationsschicht v2, Abschnitt 4): das WLAN darf nicht die
* Standardroute des Geraets werden (sonst fallen Kartenkacheln/Wetter aus),
* daher `WifiNetworkSpecifier` statt Verbindung ueber die Systemeinstellungen.
*
* Ablauf: [requestNetwork] loest `ConnectivityManager.requestNetwork()` aus,
* das System zeigt dabei automatisch einen Bestaetigungsdialog fuer die
* passende SSID. `onAvailable`/`onUnavailable`/`onLost` werden ueber den
* EventChannel nach Dart gemeldet, das den restlichen Ablauf (Socket binden,
* lauschen, Fehleranzeige) uebernimmt.
*
* [bindProcessToNetwork]/[unbindProcessFromNetwork] setzen/loeschen die
* Prozessbindung fuer das kurze Fenster, in dem Dart den UDP-Socket erzeugt -
* siehe UdpTransport.connect() fuer die Gegenseite dieses Musters.
*/
class MlrsNetworkPlugin(
private val context: Context,
) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler {
companion object {
const val METHOD_CHANNEL = "com.dmc.dmc_app/mlrs_network"
const val EVENT_CHANNEL = "com.dmc.dmc_app/mlrs_network_events"
}
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
// NetworkCallback-Methoden laufen ohne expliziten Handler auf einem
// internen ConnectivityThread, nicht auf dem Main-Thread - Flutters
// EventChannel.EventSink.success() verlangt aber @UiThread (schlaegt
// sonst mit "Methods marked with @UiThread must be executed on the main
// thread" fehl). Deshalb Callback-Dispatch explizit auf den Main-Looper
// legen.
private val mainHandler = Handler(Looper.getMainLooper())
private var eventSink: EventChannel.EventSink? = null
private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var currentNetwork: Network? = null
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
eventSink = sink
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"requestNetwork" -> {
val ssidPrefix = call.argument<String>("ssidPrefix")
if (ssidPrefix.isNullOrEmpty()) {
result.error("invalid_argument", "ssidPrefix darf nicht leer sein", null)
return
}
requestNetwork(ssidPrefix)
result.success(null)
}
"releaseNetwork" -> {
releaseNetwork()
result.success(null)
}
"bindProcessToNetwork" -> {
val network = currentNetwork
if (network == null) {
result.success(false)
} else {
result.success(connectivityManager.bindProcessToNetwork(network))
}
}
"unbindProcessFromNetwork" -> {
connectivityManager.bindProcessToNetwork(null)
result.success(null)
}
"isStaConcurrencySupported" -> {
result.success(isStaConcurrencySupported())
}
else -> result.notImplemented()
}
}
private fun requestNetwork(ssidPrefix: String) {
// Vorherigen Callback zuerst abmelden - sonst haeuften sich bei
// mehrfachem Connect-Versuch mehrere registrierte Callbacks an.
releaseNetwork()
val specifier = WifiNetworkSpecifier.Builder()
.setSsidPattern(PatternMatcher(ssidPrefix, PatternMatcher.PATTERN_PREFIX))
.build()
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(specifier)
.build()
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
currentNetwork = network
val gateway = gatewayAddressOf(connectivityManager.getLinkProperties(network))
eventSink?.success(
mapOf("type" to "available", "gatewayAddress" to gateway),
)
}
override fun onUnavailable() {
currentNetwork = null
eventSink?.success(mapOf("type" to "unavailable"))
}
override fun onLost(network: Network) {
currentNetwork = null
eventSink?.success(mapOf("type" to "lost"))
}
}
networkCallback = callback
connectivityManager.requestNetwork(request, callback, mainHandler)
}
private fun releaseNetwork() {
networkCallback?.let {
try {
connectivityManager.unregisterNetworkCallback(it)
} catch (_: IllegalArgumentException) {
// Callback war bereits abgemeldet (z.B. durch onUnavailable
// gefolgt von einem expliziten releaseNetwork()) - unkritisch.
}
}
networkCallback = null
currentNetwork = null
}
private fun gatewayAddressOf(linkProperties: LinkProperties?): String? {
val route = linkProperties?.routes?.firstOrNull { it.isDefaultRoute }
return route?.gateway?.hostAddress
}
private fun isStaConcurrencySupported(): Boolean? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
?: return null
return wifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported
}
}