wifi connection established by app, process socket binding, primary os connection untouched
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user