From dbb7ace395b1bce9968ccdc2ee5337037d25d284 Mon Sep 17 00:00:00 2001 From: RandomModderJDK <62660085+RandomModderJDK@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:57:23 +0200 Subject: [PATCH 1/5] Added PROXY_CHANGED_RECEIVER -> call callback through the method channel. Opt-in feature. The only way to get a proxy specified by PAC --- .../FlutterProxyPlugin.kt | 78 ++++++++++++++++++- example/lib/main.dart | 1 + lib/src/native_proxy_reader.dart | 15 +++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt b/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt index 467536c..0f61ab2 100644 --- a/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt +++ b/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt @@ -1,16 +1,30 @@ package com.victorblaess.native_flutter_proxy +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Proxy +import android.net.ProxyInfo +import android.net.Uri +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat.getSystemService import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import java.util.LinkedHashMap -class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler { + +class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler, BroadcastReceiver() { private var methodChannel: MethodChannel? = null + private var context: Context? = null private fun setupChannel(messenger: BinaryMessenger) { methodChannel = MethodChannel(messenger, "native_flutter_proxy") @@ -19,16 +33,30 @@ class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler { override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { setupChannel(binding.binaryMessenger) + context = binding.applicationContext } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodChannel?.setMethodCallHandler(null) methodChannel = null + unregisterReceiver() + } + + private fun unregisterReceiver() { + ContextWrapper(context).unregisterReceiver(this) } override fun onMethodCall(call: MethodCall, result: Result) { if (call.method == "getProxySetting") { result.success(getProxySetting()) + } else if (call.method == "setProxyChangeListenerEnabled") { + if (call.arguments>()?.get(0) == true) { + Log.d("ProxyChangeReceiver", "Enabled receiver") + context!!.registerReceiver(this, IntentFilter(Proxy.PROXY_CHANGE_ACTION)) + } else { + Log.d("ProxyChangeReceiver", "Disabled receiver") + unregisterReceiver() + } } else { result.notImplemented() } @@ -38,6 +66,52 @@ class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler { val map = LinkedHashMap() map["host"] = System.getProperty("http.proxyHost") map["port"] = System.getProperty("http.proxyPort") + Log.d("ProxyChangeReceiver", "Properties: ${map["host"]}:${map["port"]}") + val pi = extractProxyInfo(null) // this does not look into PAC + Log.d("ProxyChangeReceiver", "ProxyInfo without intent: ${pi?.host}:${pi?.port}") return map } + + override fun onReceive(context: Context, intent: Intent) { + if (Proxy.PROXY_CHANGE_ACTION == intent.action) { + // Handle the proxy change here + Log.d("ProxyChangeReceiver", "Proxy settings changed") + val pi = extractProxyInfo(intent) + Log.d("ProxyChangeReceiver", "ProxyInfo: ${pi?.host}:${pi?.port}") + methodChannel!!.invokeMethod("proxyChangedCallback", null) + } + } + + + private fun extractProxyInfo(intent: Intent?): ProxyInfo? { + val connectivityManager = getSystemService(context!!,ConnectivityManager::class.java) + var info: ProxyInfo? = connectivityManager!!.defaultProxy + if (info == null) { + return null + } + + // If a proxy is configured using the PAC file use + // Android's injected localhost HTTP proxy. + // + // Android's injected localhost proxy can be accessed using a proxy host + // equal to `localhost` and a proxy port retrieved from intent's 'extras'. + // We cannot take a proxy port from the ProxyInfo object that's exposed by + // the connectivity manager as it's always equal to -1 for cases when PAC + // proxy is configured. + if (info.pacFileUrl != null && info.pacFileUrl !== Uri.EMPTY) { + if (intent == null) { + // PAC proxies are supported only when Intent is present + return null + } + + val extras = intent.extras + if (extras == null) { + return null + } + + info = extras.get("android.intent.extra.PROXY_INFO") as ProxyInfo? + } + + return info + } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 5df3952..3d1f29b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -14,6 +14,7 @@ void main() async { try { final settings = await NativeProxyReader.proxySetting; + NativeProxyReader.setProxyChangedCallback((a) async => debugPrint('Callback for proxy change')); enabled = settings.enabled; host = settings.host; port = settings.port; diff --git a/lib/src/native_proxy_reader.dart b/lib/src/native_proxy_reader.dart index 042cc00..5c7c771 100644 --- a/lib/src/native_proxy_reader.dart +++ b/lib/src/native_proxy_reader.dart @@ -18,13 +18,26 @@ import 'package:flutter/services.dart'; /// ``` /// {@endtemplate} abstract final class NativeProxyReader { - /// Method channel for native platform communication.ƒ + /// Method channel for native platform communication. static const _channel = MethodChannel('native_flutter_proxy'); /// Get the proxy settings from the native platform. static Future get proxySetting async { return _channel.invokeMapMethod('getProxySetting').then(ProxySetting._fromMap); } + + /// Register callback, when the system proxy is changed. + /// This is the only way to use a proxy specified by PAC. + static void setProxyChangedCallback(Future Function(ProxySetting)? handler) { + _channel..invokeMethod('setProxyChangeListenerEnabled', [handler != null]) + ..setMethodCallHandler((call) async { + switch (call.method) { + case 'proxyChangedCallback': + if (handler != null) await handler(await proxySetting); + default: throw MissingPluginException('notImplemented'); + } + }); + } } /// {@template proxy_setting} From a1e3994feeda1057615d859ffe1d6d110f77c8e3 Mon Sep 17 00:00:00 2001 From: RandomModderJDK <62660085+RandomModderJDK@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:19:08 +0200 Subject: [PATCH 2/5] Discard the old property approach. Cache the proxyinfo in-between fetching. Made broadcast receiver THE backbone (always registered). --- .../FlutterProxyPlugin.kt | 56 +++++++++---------- lib/src/native_proxy_reader.dart | 12 ++-- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt b/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt index 0f61ab2..e834d0b 100644 --- a/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt +++ b/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt @@ -9,9 +9,7 @@ import android.net.ConnectivityManager import android.net.Proxy import android.net.ProxyInfo import android.net.Uri -import android.os.Build import android.util.Log -import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat.getSystemService import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger @@ -34,59 +32,51 @@ class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler, BroadcastReceiver() override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { setupChannel(binding.binaryMessenger) context = binding.applicationContext + + // initial default proxy, that could not be captured beforehand + val pi = refreshProxyInfo(null) // this does not look consider proxies from PAC + Log.d("ProxyChangeReceiver", "Properties: ${System.getProperty("http.proxyHost")}:${System.getProperty("http.proxyPort")}") + Log.d("ProxyChangeReceiver", "ProxyInfo without intent: ${pi?.host}:${pi?.port}") + + context!!.registerReceiver(this, IntentFilter(Proxy.PROXY_CHANGE_ACTION)) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodChannel?.setMethodCallHandler(null) methodChannel = null - unregisterReceiver() - } - - private fun unregisterReceiver() { - ContextWrapper(context).unregisterReceiver(this) + context!!.unregisterReceiver(this) + context = null } override fun onMethodCall(call: MethodCall, result: Result) { if (call.method == "getProxySetting") { - result.success(getProxySetting()) - } else if (call.method == "setProxyChangeListenerEnabled") { - if (call.arguments>()?.get(0) == true) { - Log.d("ProxyChangeReceiver", "Enabled receiver") - context!!.registerReceiver(this, IntentFilter(Proxy.PROXY_CHANGE_ACTION)) - } else { - Log.d("ProxyChangeReceiver", "Disabled receiver") - unregisterReceiver() - } + result.success(proxySetting) } else { result.notImplemented() } } - private fun getProxySetting(): Any? { - val map = LinkedHashMap() - map["host"] = System.getProperty("http.proxyHost") - map["port"] = System.getProperty("http.proxyPort") - Log.d("ProxyChangeReceiver", "Properties: ${map["host"]}:${map["port"]}") - val pi = extractProxyInfo(null) // this does not look into PAC - Log.d("ProxyChangeReceiver", "ProxyInfo without intent: ${pi?.host}:${pi?.port}") - return map - } + private val proxySetting: LinkedHashMap = LinkedHashMap() override fun onReceive(context: Context, intent: Intent) { if (Proxy.PROXY_CHANGE_ACTION == intent.action) { // Handle the proxy change here Log.d("ProxyChangeReceiver", "Proxy settings changed") - val pi = extractProxyInfo(intent) + val pi = refreshProxyInfo(intent) Log.d("ProxyChangeReceiver", "ProxyInfo: ${pi?.host}:${pi?.port}") - methodChannel!!.invokeMethod("proxyChangedCallback", null) + methodChannel!!.invokeMethod("proxyChangedCallback", proxySetting) } } - - private fun extractProxyInfo(intent: Intent?): ProxyInfo? { + /** + * Get system proxy and update cache with optional intent argument needed for PAC. + */ + private fun refreshProxyInfo(intent: Intent?): ProxyInfo? { val connectivityManager = getSystemService(context!!,ConnectivityManager::class.java) var info: ProxyInfo? = connectivityManager!!.defaultProxy if (info == null) { + proxySetting["host"] = null + proxySetting["port"] = null return null } @@ -100,18 +90,24 @@ class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler, BroadcastReceiver() // proxy is configured. if (info.pacFileUrl != null && info.pacFileUrl !== Uri.EMPTY) { if (intent == null) { + proxySetting["host"] = null + proxySetting["port"] = null // PAC proxies are supported only when Intent is present return null } val extras = intent.extras if (extras == null) { + proxySetting["host"] = null + proxySetting["port"] = null return null } - info = extras.get("android.intent.extra.PROXY_INFO") as ProxyInfo? + info = extras.getParcelable("android.intent.extra.PROXY_INFO", ProxyInfo::class.java) } + proxySetting["host"] = info!!.host + proxySetting["port"] = info.port return info } } diff --git a/lib/src/native_proxy_reader.dart b/lib/src/native_proxy_reader.dart index 5c7c771..4b6799f 100644 --- a/lib/src/native_proxy_reader.dart +++ b/lib/src/native_proxy_reader.dart @@ -29,12 +29,12 @@ abstract final class NativeProxyReader { /// Register callback, when the system proxy is changed. /// This is the only way to use a proxy specified by PAC. static void setProxyChangedCallback(Future Function(ProxySetting)? handler) { - _channel..invokeMethod('setProxyChangeListenerEnabled', [handler != null]) - ..setMethodCallHandler((call) async { - switch (call.method) { - case 'proxyChangedCallback': - if (handler != null) await handler(await proxySetting); - default: throw MissingPluginException('notImplemented'); + _channel.setMethodCallHandler((call) async { + if (handler != null && call.method == 'proxyChangedCallback' && call.arguments is Map) { + final map = (call.arguments as Map).cast(); + await handler(ProxySetting._fromMap(map)); + } else { + throw MissingPluginException('notImplemented'); } }); } From 0b32ff900c53245ed6c792f8adf73b332a0377b8 Mon Sep 17 00:00:00 2001 From: Shakle Date: Wed, 24 Dec 2025 21:22:32 +0100 Subject: [PATCH 3/5] merge RandomModderJDK changes --- example/lib/main.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index bb7aada..ceaff39 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -12,9 +12,11 @@ void main() async { String? error; try { - final settings = await NativeProxyReader.proxySetting; - NativeProxyReader.setProxyChangedCallback((a) async => debugPrint('Callback for proxy change')); ProxySetting settings = await NativeProxyReader.proxySetting; + NativeProxyReader.setProxyChangedCallback((_) async { + debugPrint('Callback for proxy change'); + }); + enabled = settings.enabled; host = settings.host; port = settings.port; From 0bd4f5ab3f60f283337a6930931f3deac5098264 Mon Sep 17 00:00:00 2001 From: Shakle Date: Wed, 24 Dec 2025 21:54:56 +0100 Subject: [PATCH 4/5] add auto-update channel --- CHANGELOG.md | 1 + .../FlutterProxyPlugin.kt | 8 +- example/lib/app.dart | 12 +- example/lib/main.dart | 90 +++++--- ios/Classes/SwiftFlutterProxyPlugin.swift | 205 +++++++++++++++++- ios/native_flutter_proxy.podspec | 2 + 6 files changed, 279 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f501e2..47514ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.3.0 * Support of latest Android and iOS * Improvements on example app +* Updates from RandomModderJDK ## 0.2.3 diff --git a/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt b/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt index e834d0b..03d550d 100644 --- a/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt +++ b/android/src/main/kotlin/com/victorblaess/native_flutter_proxy/FlutterProxyPlugin.kt @@ -9,6 +9,7 @@ import android.net.ConnectivityManager import android.net.Proxy import android.net.ProxyInfo import android.net.Uri +import android.os.Build import android.util.Log import androidx.core.content.ContextCompat.getSystemService import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -103,7 +104,12 @@ class FlutterProxyPlugin : FlutterPlugin, MethodCallHandler, BroadcastReceiver() return null } - info = extras.getParcelable("android.intent.extra.PROXY_INFO", ProxyInfo::class.java) + info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + extras.getParcelable("android.intent.extra.PROXY_INFO", ProxyInfo::class.java) + } else { + @Suppress("DEPRECATION") + extras.getParcelable("android.intent.extra.PROXY_INFO") as? ProxyInfo + } } proxySetting["host"] = info!!.host diff --git a/example/lib/app.dart b/example/lib/app.dart index 16af527..43cb23e 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'data/models/proxy_info.dart'; @@ -6,9 +7,9 @@ import 'view/screens/proxy_screen.dart'; export 'data/models/proxy_info.dart'; class App extends StatelessWidget { - const App({super.key, required this.proxyInfo}); + const App({super.key, required this.proxyInfoListenable}); - final ProxyInfo proxyInfo; + final ValueListenable proxyInfoListenable; @override Widget build(BuildContext context) { @@ -60,7 +61,12 @@ class App extends StatelessWidget { scaffoldBackgroundColor: const Color(0xFFF7EFE6), textTheme: textTheme, ), - home: ProxyScreen(info: proxyInfo), + home: ValueListenableBuilder( + valueListenable: proxyInfoListenable, + builder: (context, info, _) { + return ProxyScreen(info: info); + }, + ), ); } } diff --git a/example/lib/main.dart b/example/lib/main.dart index ceaff39..8ced0c0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,48 +1,70 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:native_flutter_proxy/native_flutter_proxy.dart'; + import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - bool enabled = false; - String? host; - int? port; - bool applied = false; - String? error; + final proxyInfoNotifier = ValueNotifier( + const ProxyInfo( + enabled: false, + applied: false, + ), + ); + + Future updateProxyInfo(ProxySetting settings) async { + final enabled = settings.enabled; + final host = settings.host; + final port = settings.port; + var applied = false; + + if (enabled && host != null) { + final proxy = CustomProxy(ipAddress: host, port: port); + proxy.enable(); + applied = true; + debugPrint('====\nProxy enabled\n===='); + } else { + HttpOverrides.global = null; + debugPrint('====\nProxy disabled\n===='); + } + + proxyInfoNotifier.value = ProxyInfo( + enabled: enabled, + applied: applied, + host: host, + port: port, + ); + } + + Future handleError(Object error) async { + HttpOverrides.global = null; + final message = error.toString(); + debugPrint(message); + proxyInfoNotifier.value = ProxyInfo( + enabled: false, + applied: false, + error: message, + ); + } try { - ProxySetting settings = await NativeProxyReader.proxySetting; - NativeProxyReader.setProxyChangedCallback((_) async { - debugPrint('Callback for proxy change'); - }); - - enabled = settings.enabled; - host = settings.host; - port = settings.port; + final settings = await NativeProxyReader.proxySetting; + await updateProxyInfo(settings); } catch (e) { - error = e.toString(); - debugPrint(error); + await handleError(e); } - if (enabled && host != null) { - final proxy = CustomProxy(ipAddress: host, port: port); - proxy.enable(); - applied = true; - debugPrint("====\nProxy enabled\n===="); - } else { - debugPrint("====\nProxy disabled\n===="); - } + NativeProxyReader.setProxyChangedCallback((settings) async { + debugPrint('Callback for proxy change was used'); + try { + await updateProxyInfo(settings); + } catch (e) { + await handleError(e); + } + }); - runApp( - App( - proxyInfo: ProxyInfo( - enabled: enabled, - applied: applied, - host: host, - port: port, - error: error, - ), - ), - ); + runApp(App(proxyInfoListenable: proxyInfoNotifier)); } diff --git a/ios/Classes/SwiftFlutterProxyPlugin.swift b/ios/Classes/SwiftFlutterProxyPlugin.swift index 360c8a2..4e55ee4 100644 --- a/ios/Classes/SwiftFlutterProxyPlugin.swift +++ b/ios/Classes/SwiftFlutterProxyPlugin.swift @@ -1,4 +1,6 @@ import Flutter +import Network +import SystemConfiguration import UIKit /** @@ -9,6 +11,16 @@ import UIKit * from Dart code. It provides functionality to retrieve the system proxy settings. */ public class SwiftFlutterProxyPlugin: NSObject, FlutterPlugin { + private var channel: FlutterMethodChannel? + @available(iOS 12.0, *) + private var pathMonitor: NWPathMonitor? + private var pathMonitorQueue: DispatchQueue? + private var reachability: SCNetworkReachability? + private var reachabilityQueue: DispatchQueue? + private var appActiveObserver: NSObjectProtocol? + private var proxyPollTimer: DispatchSourceTimer? + private let proxyStateLock = NSLock() + private var lastProxyKey: String? /** * Registers the plugin with the Flutter plugin registrar. @@ -18,6 +30,8 @@ public class SwiftFlutterProxyPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "native_flutter_proxy", binaryMessenger: registrar.messenger()) let instance = SwiftFlutterProxyPlugin() + instance.channel = channel + instance.startProxyChangeObserver() registrar.addMethodCallDelegate(instance, channel: channel) } @@ -38,6 +52,195 @@ public class SwiftFlutterProxyPlugin: NSObject, FlutterPlugin { } } + deinit { + stopProxyChangeObserver() + } + + private func startProxyChangeObserver() { + if appActiveObserver == nil { + appActiveObserver = NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.notifyProxyChanged() + } + } + + seedProxyKey() + startProxyPolling() + + if #available(iOS 12.0, *) { + guard pathMonitor == nil else { + return + } + + let monitor = NWPathMonitor() + pathMonitor = monitor + let queue = DispatchQueue(label: "native_flutter_proxy.pathMonitor") + pathMonitorQueue = queue + monitor.pathUpdateHandler = { [weak self] _ in + self?.notifyProxyChanged() + } + monitor.start(queue: queue) + return + } + + startReachabilityObserver() + } + + private func startReachabilityObserver() { + guard reachability == nil else { + return + } + + guard let reachability = makeReachability() else { + return + } + + self.reachability = reachability + reachabilityQueue = DispatchQueue(label: "native_flutter_proxy.reachability") + + var context = SCNetworkReachabilityContext( + version: 0, + info: Unmanaged.passUnretained(self).toOpaque(), + retain: nil, + release: nil, + copyDescription: nil + ) + + let callback: SCNetworkReachabilityCallBack = { _, _, info in + guard let info = info else { + return + } + let plugin = Unmanaged + .fromOpaque(info) + .takeUnretainedValue() + plugin.notifyProxyChanged() + } + + if !SCNetworkReachabilitySetCallback(reachability, callback, &context) { + self.reachability = nil + reachabilityQueue = nil + return + } + + if let queue = reachabilityQueue, + !SCNetworkReachabilitySetDispatchQueue(reachability, queue) { + SCNetworkReachabilitySetCallback(reachability, nil, nil) + self.reachability = nil + reachabilityQueue = nil + } + } + + private func stopProxyChangeObserver() { + if let appActiveObserver { + NotificationCenter.default.removeObserver(appActiveObserver) + self.appActiveObserver = nil + } + + if #available(iOS 12.0, *) { + if let pathMonitor { + pathMonitor.cancel() + self.pathMonitor = nil + } + pathMonitorQueue = nil + } + + stopProxyPolling() + stopReachabilityObserver() + } + + private func stopReachabilityObserver() { + if let reachability { + SCNetworkReachabilitySetDispatchQueue(reachability, nil) + self.reachability = nil + } + reachabilityQueue = nil + } + + private func makeReachability() -> SCNetworkReachability? { + var address = sockaddr_in() + address.sin_len = UInt8(MemoryLayout.size) + address.sin_family = sa_family_t(AF_INET) + + return withUnsafePointer(to: &address) { pointer in + pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { addrPointer in + SCNetworkReachabilityCreateWithAddress(nil, addrPointer) + } + } + } + + private func startProxyPolling() { + guard proxyPollTimer == nil else { + return + } + + let queue = DispatchQueue(label: "native_flutter_proxy.proxyPoll") + let timer = DispatchSource.makeTimerSource(queue: queue) + timer.schedule(deadline: .now(), repeating: 5.0) + timer.setEventHandler { [weak self] in + self?.notifyProxyChanged() + } + timer.resume() + proxyPollTimer = timer + } + + private func stopProxyPolling() { + if let proxyPollTimer { + proxyPollTimer.cancel() + self.proxyPollTimer = nil + } + } + + private func seedProxyKey() { + let snapshot = proxySettingSnapshot() + proxyStateLock.lock() + lastProxyKey = snapshot.key + proxyStateLock.unlock() + } + + private func notifyProxyChanged() { + let snapshot = proxySettingSnapshot() + proxyStateLock.lock() + let shouldNotify = snapshot.key != lastProxyKey + if shouldNotify { + lastProxyKey = snapshot.key + } + proxyStateLock.unlock() + + guard shouldNotify else { + return + } + DispatchQueue.main.async { [weak self] in + self?.channel?.invokeMethod("proxyChangedCallback", arguments: snapshot.payload) + } + } + + private func proxySettingSnapshot() -> (payload: [String: Any], key: String) { + guard let setting = getProxySetting() as? [String: Any] else { + let payload: [String: Any] = ["host": NSNull(), "port": NSNull()] + return (payload, "|") + } + + let hostValue = setting["host"] as? String + let portValue = setting["port"] + let hostPayload: Any = hostValue ?? NSNull() + let portPayload: Any = portValue ?? NSNull() + let portKey: String + if let portNumber = portValue as? NSNumber { + portKey = portNumber.stringValue + } else if let portInt = portValue as? Int { + portKey = "\(portInt)" + } else if let portString = portValue as? String { + portKey = portString + } else { + portKey = "" + } + let key = "\(hostValue ?? "")|\(portKey)" + return (["host": hostPayload, "port": portPayload], key) + } + /** * Retrieves the system proxy settings. * @@ -59,4 +262,4 @@ public class SwiftFlutterProxyPlugin: NSObject, FlutterPlugin { } return nil } -} \ No newline at end of file +} diff --git a/ios/native_flutter_proxy.podspec b/ios/native_flutter_proxy.podspec index 7095a28..3878c4c 100644 --- a/ios/native_flutter_proxy.podspec +++ b/ios/native_flutter_proxy.podspec @@ -15,6 +15,8 @@ device proxy info. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' + s.frameworks = 'SystemConfiguration' + s.weak_frameworks = 'Network' s.ios.deployment_target = '8.0' # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. From 03e1c8e8ec68025623026505037f931c5aa37a97 Mon Sep 17 00:00:00 2001 From: Shakle Date: Wed, 24 Dec 2025 21:56:34 +0100 Subject: [PATCH 5/5] format files --- example/lib/main.dart | 5 +---- lib/src/native_proxy_reader.dart | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 8ced0c0..a348c04 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,10 +9,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); final proxyInfoNotifier = ValueNotifier( - const ProxyInfo( - enabled: false, - applied: false, - ), + const ProxyInfo(enabled: false, applied: false), ); Future updateProxyInfo(ProxySetting settings) async { diff --git a/lib/src/native_proxy_reader.dart b/lib/src/native_proxy_reader.dart index d0f078b..a79a187 100644 --- a/lib/src/native_proxy_reader.dart +++ b/lib/src/native_proxy_reader.dart @@ -30,7 +30,9 @@ abstract final class NativeProxyReader { /// This is the only way to use a proxy specified by PAC. static void setProxyChangedCallback(Future Function(ProxySetting)? handler) { _channel.setMethodCallHandler((call) async { - if (handler != null && call.method == 'proxyChangedCallback' && call.arguments is Map) { + if (handler != null && + call.method == 'proxyChangedCallback' && + call.arguments is Map) { final map = (call.arguments as Map).cast(); await handler(ProxySetting._fromMap(map)); } else {