diff --git a/README.md b/README.md
index 09a650a4e..79a8582e8 100644
--- a/README.md
+++ b/README.md
@@ -47,13 +47,28 @@ on Mobile:
### Android
+WiFi ssid policy
+
+```yaml
+proxy-groups:
+ - name: "🚀 节点选择"
+ type: select
+ proxies: [...]
+ ssid-policy:
+ "ip:192.168.10.": "home" # 匹配 192.168.10.x(家里)
+ "ip:10.10.": "office" # 匹配 10.10.x.x(公司)
+ "HomeWiFi": "DIRECT" # SSID
+ "cellular": "hk" # 移动网络
+ "default": "auto" # 默认规则
+```
+
Support the following actions
```bash
com.follow.clash.action.START
-
+
com.follow.clash.action.STOP
-
+
com.follow.clash.action.TOGGLE
```
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 4cef3e8ad..5b115c274 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -38,7 +38,7 @@ android {
defaultConfig {
applicationId = "com.follow.clash"
- minSdk = flutter.minSdkVersion
+ minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = flutter.versionCode
versionName = flutter.versionName
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0c4c9c85f..0b0d43639 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -16,6 +16,10 @@
+
+
+
+
applySsidPolicy(
+ String networkType,
+ String ssid,
+ String wifiIp,
+ ) async {
+ try {
+ // Check if proxy is running
+ if (globalState.isStart != true) {
+ return;
+ }
+
+ // Get the current running config from the core
+ final currentProfileId = _ref.read(currentProfileIdProvider);
+ if (currentProfileId == null) {
+ return;
+ }
+
+ // Get the config that's currently loaded (after scripts have processed it)
+ final configMap = await coreController.getConfig(currentProfileId);
+
+ // Extract proxy-groups from config
+ final proxyGroups = configMap['proxy-groups'] as List?;
+ if (proxyGroups == null || proxyGroups.isEmpty) {
+ return;
+ }
+
+ // Determine network key for policy lookup
+ final networkKey = networkType == 'wifi' ? ssid : networkType;
+
+ commonPrint.log(
+ 'Applying SSID policy for network: $networkKey${networkType == 'wifi' && wifiIp.isNotEmpty ? " (IP: $wifiIp)" : ""}',
+ );
+
+ // Get current proxy groups to validate target proxies exist
+ final currentGroups = groups;
+
+ // Apply policy for each proxy group that has ssid-policy defined
+ for (final groupData in proxyGroups) {
+ if (groupData is! Map) continue;
+
+ final groupName = groupData['name'] as String?;
+ final ssidPolicyData = groupData['ssid-policy'] as Map?;
+
+ if (groupName == null ||
+ ssidPolicyData == null ||
+ ssidPolicyData.isEmpty) {
+ continue;
+ }
+
+ // Convert ssidPolicyData to Map
+ final policy = ssidPolicyData.map(
+ (key, value) => MapEntry(key.toString(), value.toString()),
+ );
+
+ // Look up target proxy with priority: IP segment match > SSID/network > default
+ String? targetProxy;
+ String? matchedKey;
+
+ // For WiFi, try IP segment match first (e.g., 'ip:192.168.2' matches '192.168.2.100')
+ if (networkType == 'wifi' && wifiIp.isNotEmpty) {
+ for (final key in policy.keys) {
+ if (key.startsWith('ip:')) {
+ final ipSegment = key.substring(3); // Remove 'ip:' prefix
+ if (wifiIp.startsWith(ipSegment)) {
+ targetProxy = policy[key];
+ matchedKey = key;
+ break;
+ }
+ }
+ }
+ }
+
+ // If no IP match, try SSID/network match
+ if (targetProxy == null && policy.containsKey(networkKey)) {
+ targetProxy = policy[networkKey];
+ matchedKey = networkKey;
+ }
+
+ // Finally try default
+ if (targetProxy == null) {
+ targetProxy = policy['default'];
+ matchedKey = 'default';
+ }
+
+ if (targetProxy != null && targetProxy.isNotEmpty) {
+ // Find the group to validate the proxy exists
+ final group = currentGroups
+ .where((g) => g.name == groupName)
+ .firstOrNull;
+
+ if (group == null || group.type != GroupType.Selector) {
+ commonPrint.log(
+ 'SSID Policy: Group "$groupName" not found or not a Selector type, skipping',
+ );
+ continue;
+ }
+
+ // Check if target proxy exists in the group's proxy list
+ final proxyExists = group.all.any(
+ (proxy) => proxy.name == targetProxy,
+ );
+
+ if (!proxyExists) {
+ commonPrint.log(
+ 'SSID Policy: Proxy "$targetProxy" not found in group "$groupName", skipping',
+ );
+ continue;
+ }
+
+ commonPrint.log(
+ 'SSID Policy: Switching $groupName to $targetProxy (matched: $matchedKey)',
+ );
+ updateCurrentSelectedMap(groupName, targetProxy);
+ changeProxyDebounce(groupName, targetProxy);
+ }
+ }
+ } catch (e) {
+ commonPrint.log(
+ 'Error applying SSID policy: $e',
+ logLevel: LogLevel.warning,
+ );
+ }
+ }
}
extension SetupControllerExt on AppController {
@@ -565,6 +690,9 @@ extension SetupControllerExt on AppController {
}
await globalState.handleStart([updateRunTime, updateTraffic]);
applyProfileDebounce(force: true, silence: true);
+
+ // Start network monitoring and apply initial SSID policy
+ _startNetworkMonitoring();
} else {
globalState.needInitStatus = false;
await applyProfile(
@@ -573,6 +701,9 @@ extension SetupControllerExt on AppController {
await globalState.handleStart([updateRunTime, updateTraffic]);
},
);
+
+ // Start network monitoring and apply initial SSID policy
+ _startNetworkMonitoring();
}
} else {
await globalState.handleStop();
@@ -581,7 +712,32 @@ extension SetupControllerExt on AppController {
_ref.read(totalTrafficProvider.notifier).value = Traffic();
_ref.read(runTimeProvider.notifier).value = null;
addCheckIp();
+
+ // Stop network monitoring when proxy stops
+ _stopNetworkMonitoring();
+ }
+ }
+
+ void _startNetworkMonitoring() {
+ // Only support SSID policy on Android and macOS
+ if (!Platform.isAndroid && !Platform.isMacOS) {
+ return;
}
+
+ final monitor = NetworkStateMonitor();
+ monitor.onNetworkStateChanged = (networkType, ssid, wifiIp) {
+ applySsidPolicy(networkType, ssid, wifiIp);
+ };
+ monitor.startMonitoring();
+ }
+
+ void _stopNetworkMonitoring() {
+ // Only support SSID policy on Android and macOS
+ if (!Platform.isAndroid && !Platform.isMacOS) {
+ return;
+ }
+
+ NetworkStateMonitor().stopMonitoring();
}
Future needSetup() async {
diff --git a/lib/core/network_monitor.dart b/lib/core/network_monitor.dart
new file mode 100644
index 000000000..948a116e1
--- /dev/null
+++ b/lib/core/network_monitor.dart
@@ -0,0 +1,265 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:connectivity_plus/connectivity_plus.dart';
+import 'package:fl_clash/common/common.dart';
+import 'package:flutter/services.dart';
+import 'package:network_info_plus/network_info_plus.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+/// Network state monitor for SSID-based policy switching
+class NetworkStateMonitor {
+ static final NetworkStateMonitor _instance = NetworkStateMonitor._internal();
+ factory NetworkStateMonitor() => _instance;
+ NetworkStateMonitor._internal();
+
+ final _connectivity = Connectivity();
+ final _networkInfo = NetworkInfo();
+ StreamSubscription>? _subscription;
+ static const _macLocationChannel = MethodChannel(
+ 'com.flclash/location_permission',
+ );
+
+ String _currentNetworkType = '';
+ String _currentSsid = '';
+ String _currentWifiIp = '';
+ bool _hasLocationPermission = false;
+
+ Function(String networkType, String ssid, String wifiIp)?
+ onNetworkStateChanged;
+
+ /// Check and request location permission (required for getting WiFi SSID)
+ Future _checkAndRequestPermission() async {
+ try {
+ // On macOS Sonoma+, CWWiFiClient.ssid() requires CLLocationManager
+ // authorization. We use a native MethodChannel to request it.
+ if (Platform.isMacOS) {
+ return await _requestMacOSLocationPermission();
+ }
+
+ // Android permission flow
+ // First check locationAlways permission (includes locationWhenInUse)
+ var alwaysStatus = await Permission.locationAlways.status;
+
+ if (alwaysStatus.isGranted) {
+ _hasLocationPermission = true;
+ commonPrint.log(
+ 'NetworkStateMonitor: Location Always permission granted',
+ );
+ return true;
+ }
+
+ // Check if we have locationWhenInUse permission
+ var whenInUseStatus = await Permission.locationWhenInUse.status;
+
+ if (whenInUseStatus.isGranted) {
+ // We have When In Use, now request Always
+ commonPrint.log(
+ 'NetworkStateMonitor: Requesting location Always permission',
+ );
+ alwaysStatus = await Permission.locationAlways.request();
+
+ if (alwaysStatus.isGranted) {
+ _hasLocationPermission = true;
+ commonPrint.log(
+ 'NetworkStateMonitor: Location Always permission granted',
+ );
+ return true;
+ } else {
+ // Even if Always is denied, we still have When In Use
+ _hasLocationPermission = true;
+ commonPrint.log(
+ 'NetworkStateMonitor: Location When In Use permission granted (Always denied)',
+ );
+ return true;
+ }
+ }
+
+ // Request When In Use permission first
+ if (whenInUseStatus.isDenied) {
+ commonPrint.log(
+ 'NetworkStateMonitor: Requesting location When In Use permission',
+ );
+ whenInUseStatus = await Permission.locationWhenInUse.request();
+
+ if (whenInUseStatus.isGranted) {
+ _hasLocationPermission = true;
+ commonPrint.log(
+ 'NetworkStateMonitor: Location When In Use permission granted',
+ );
+
+ // Now request Always permission
+ commonPrint.log(
+ 'NetworkStateMonitor: Requesting location Always permission',
+ );
+ alwaysStatus = await Permission.locationAlways.request();
+
+ if (alwaysStatus.isGranted) {
+ commonPrint.log(
+ 'NetworkStateMonitor: Location Always permission granted',
+ );
+ } else {
+ commonPrint.log(
+ 'NetworkStateMonitor: Location Always permission denied, using When In Use',
+ );
+ }
+
+ return true;
+ } else if (whenInUseStatus.isPermanentlyDenied) {
+ commonPrint.log(
+ 'NetworkStateMonitor: Location permission permanently denied. '
+ 'Please enable it in Settings.',
+ );
+ return false;
+ } else {
+ commonPrint.log(
+ 'NetworkStateMonitor: Location When In Use permission denied',
+ );
+ return false;
+ }
+ }
+
+ if (whenInUseStatus.isPermanentlyDenied) {
+ commonPrint.log(
+ 'NetworkStateMonitor: Location permission permanently denied. '
+ 'Please enable it in Settings.',
+ );
+ return false;
+ }
+
+ return false;
+ } catch (e) {
+ commonPrint.log('NetworkStateMonitor: Error checking permission: $e');
+ return false;
+ }
+ }
+
+ /// Request location permission on macOS via native CLLocationManager
+ Future _requestMacOSLocationPermission() async {
+ try {
+ final status = await _macLocationChannel.invokeMethod(
+ 'requestLocationPermission',
+ );
+ commonPrint.log(
+ 'NetworkStateMonitor: macOS location permission status: $status',
+ );
+ _hasLocationPermission = status == 'granted';
+ if (!_hasLocationPermission) {
+ commonPrint.log(
+ 'NetworkStateMonitor: macOS location permission not granted ($status). '
+ 'WiFi SSID will not be available. '
+ 'Please grant location permission in System Settings > '
+ 'Privacy & Security > Location Services.',
+ );
+ }
+ return _hasLocationPermission;
+ } catch (e) {
+ commonPrint.log(
+ 'NetworkStateMonitor: Error requesting macOS location permission: $e',
+ );
+ return false;
+ }
+ }
+
+ /// Start monitoring network state changes
+ Future startMonitoring() async {
+ commonPrint.log('NetworkStateMonitor: Starting network monitoring');
+
+ // Check and request location permission first
+ await _checkAndRequestPermission();
+
+ // Get initial state
+ await _updateNetworkState();
+
+ // Listen to connectivity changes
+ _subscription = _connectivity.onConnectivityChanged.listen((results) {
+ commonPrint.log('NetworkStateMonitor: Connectivity changed: $results');
+ _updateNetworkState();
+ });
+ }
+
+ /// Stop monitoring
+ void stopMonitoring() {
+ commonPrint.log('NetworkStateMonitor: Stopping network monitoring');
+ _subscription?.cancel();
+ _subscription = null;
+ }
+
+ /// Update network state and notify if changed
+ Future _updateNetworkState() async {
+ try {
+ final results = await _connectivity.checkConnectivity();
+ final networkType = _getNetworkType(results);
+ final ssid = await _getSsid(results);
+ final wifiIp = await _getWifiIp(results);
+
+ if (networkType != _currentNetworkType ||
+ ssid != _currentSsid ||
+ wifiIp != _currentWifiIp) {
+ _currentNetworkType = networkType;
+ _currentSsid = ssid;
+ _currentWifiIp = wifiIp;
+ onNetworkStateChanged?.call(networkType, ssid, wifiIp);
+ }
+ } catch (e) {
+ commonPrint.log('NetworkStateMonitor: Error updating network state: $e');
+ }
+ }
+
+ /// Convert ConnectivityResult to network type string
+ String _getNetworkType(List results) {
+ if (results.isEmpty || results.contains(ConnectivityResult.none)) {
+ return 'none';
+ }
+ if (results.contains(ConnectivityResult.wifi)) {
+ return 'wifi';
+ }
+ if (results.contains(ConnectivityResult.mobile)) {
+ return 'cellular';
+ }
+ if (results.contains(ConnectivityResult.ethernet)) {
+ return 'ethernet';
+ }
+ return 'other';
+ }
+
+ /// Get SSID for WiFi networks
+ Future _getSsid(List results) async {
+ if (!results.contains(ConnectivityResult.wifi)) {
+ return '';
+ }
+
+ // On macOS, system handles permission automatically
+ // On Android/iOS, check if we have location permission
+ if (!Platform.isMacOS && !_hasLocationPermission) {
+ commonPrint.log(
+ 'NetworkStateMonitor: Cannot get SSID without location permission',
+ );
+ return '';
+ }
+
+ try {
+ final wifiName = await _networkInfo.getWifiName();
+ // Remove quotes if present (iOS returns SSID with quotes)
+ return wifiName?.replaceAll('"', '') ?? '';
+ } catch (e) {
+ commonPrint.log('NetworkStateMonitor: Error getting SSID: $e');
+ return '';
+ }
+ }
+
+ /// Get WiFi IP (local IP address)
+ Future _getWifiIp(List results) async {
+ if (!results.contains(ConnectivityResult.wifi)) {
+ return '';
+ }
+
+ try {
+ await Future.delayed(Duration(milliseconds: 360));
+ final wifiIp = await utils.getLocalIpAddress();
+ return wifiIp ?? '';
+ } catch (e) {
+ commonPrint.log('NetworkStateMonitor: Error getting WiFi IP: $e');
+ return '';
+ }
+ }
+}
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 4d3a298fd..78880bd9d 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -14,8 +14,8 @@ import file_selector_macos
import flutter_js
import hotkey_manager_macos
import mobile_scanner
+import network_info_plus
import package_info_plus
-import path_provider_foundation
import screen_retriever_macos
import shared_preferences_foundation
import sqflite_darwin
@@ -35,8 +35,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin"))
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
+ NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
- PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj
index 8cfb75eac..bb4918fd9 100644
--- a/macos/Runner.xcodeproj/project.pbxproj
+++ b/macos/Runner.xcodeproj/project.pbxproj
@@ -24,6 +24,7 @@
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ F5LOCPER2024A1A10003CAFE /* LocationPermissionPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5LOCPER2024A1A00003CAFE /* LocationPermissionPlugin.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
@@ -84,6 +85,7 @@
33CC10ED2044A3C60003C045 /* FlClash.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlClash.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ F5LOCPER2024A1A00003CAFE /* LocationPermissionPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPlugin.swift; sourceTree = ""; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
@@ -193,6 +195,7 @@
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ F5LOCPER2024A1A00003CAFE /* LocationPermissionPlugin.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
@@ -459,6 +462,7 @@
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ F5LOCPER2024A1A10003CAFE /* LocationPermissionPlugin.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements
index f8697e31c..3264fe0ae 100755
--- a/macos/Runner/DebugProfile.entitlements
+++ b/macos/Runner/DebugProfile.entitlements
@@ -6,5 +6,7 @@
com.apple.security.files.user-selected.read-write
+ com.apple.security.personal-information.location
+
diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist
index df57c8d84..24980b945 100755
--- a/macos/Runner/Info.plist
+++ b/macos/Runner/Info.plist
@@ -43,5 +43,9 @@
MainMenu
NSPrincipalClass
NSApplication
+ NSLocationWhenInUseUsageDescription
+ FlClash needs location access to detect WiFi SSID for automatic proxy switching based on network.
+ NSLocationUsageDescription
+ FlClash needs location access to detect WiFi SSID for automatic proxy switching based on network.
diff --git a/macos/Runner/LocationPermissionPlugin.swift b/macos/Runner/LocationPermissionPlugin.swift
new file mode 100644
index 000000000..131df8870
--- /dev/null
+++ b/macos/Runner/LocationPermissionPlugin.swift
@@ -0,0 +1,111 @@
+import Cocoa
+import FlutterMacOS
+import CoreLocation
+
+class LocationPermissionPlugin: NSObject, FlutterPlugin, CLLocationManagerDelegate {
+ private let locationManager = CLLocationManager()
+ private var pendingResult: FlutterResult?
+
+ static func register(with registrar: FlutterPluginRegistrar) {
+ let channel = FlutterMethodChannel(
+ name: "com.flclash/location_permission",
+ binaryMessenger: registrar.messenger
+ )
+ let instance = LocationPermissionPlugin()
+ registrar.addMethodCallDelegate(instance, channel: channel)
+ }
+
+ override init() {
+ super.init()
+ locationManager.delegate = self
+ }
+
+ func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ switch call.method {
+ case "requestLocationPermission":
+ requestLocationPermission(result: result)
+ case "checkLocationPermission":
+ checkLocationPermission(result: result)
+ default:
+ result(FlutterMethodNotImplemented)
+ }
+ }
+
+ private func requestLocationPermission(result: @escaping FlutterResult) {
+ let status: CLAuthorizationStatus
+ if #available(macOS 11.0, *) {
+ status = locationManager.authorizationStatus
+ } else {
+ status = CLLocationManager.authorizationStatus()
+ }
+
+ switch status {
+ case .authorizedAlways:
+ result("granted")
+ case .denied:
+ result("denied")
+ case .restricted:
+ result("restricted")
+ case .notDetermined:
+ // Store result to respond after authorization callback
+ pendingResult = result
+ if #available(macOS 10.15, *) {
+ locationManager.requestWhenInUseAuthorization()
+ } else {
+ // On older macOS, just start updating which triggers the prompt
+ locationManager.startUpdatingLocation()
+ locationManager.stopUpdatingLocation()
+ }
+ @unknown default:
+ result("unknown")
+ }
+ }
+
+ private func checkLocationPermission(result: @escaping FlutterResult) {
+ let status: CLAuthorizationStatus
+ if #available(macOS 11.0, *) {
+ status = locationManager.authorizationStatus
+ } else {
+ status = CLLocationManager.authorizationStatus()
+ }
+
+ switch status {
+ case .authorizedAlways:
+ result("granted")
+ case .denied:
+ result("denied")
+ case .restricted:
+ result("restricted")
+ case .notDetermined:
+ result("notDetermined")
+ @unknown default:
+ result("unknown")
+ }
+ }
+
+ // CLLocationManagerDelegate
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
+ guard let result = pendingResult else { return }
+ pendingResult = nil
+
+ let status: CLAuthorizationStatus
+ if #available(macOS 11.0, *) {
+ status = manager.authorizationStatus
+ } else {
+ status = CLLocationManager.authorizationStatus()
+ }
+
+ switch status {
+ case .authorizedAlways:
+ result("granted")
+ case .denied:
+ result("denied")
+ case .restricted:
+ result("restricted")
+ case .notDetermined:
+ result("notDetermined")
+ @unknown default:
+ result("unknown")
+ }
+ }
+}
diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift
index aee339ecf..b24132b77 100755
--- a/macos/Runner/MainFlutterWindow.swift
+++ b/macos/Runner/MainFlutterWindow.swift
@@ -9,7 +9,7 @@ class MainFlutterWindow: NSWindow {
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
-
+
FlutterMethodChannel(
name: "launch_at_startup", binaryMessenger: flutterViewController.engine.binaryMessenger
)
@@ -26,8 +26,11 @@ class MainFlutterWindow: NSWindow {
result(FlutterMethodNotImplemented)
}
}
-
+
RegisterGeneratedPlugins(registry: flutterViewController)
+ LocationPermissionPlugin.register(
+ with: flutterViewController.registrar(forPlugin: "LocationPermissionPlugin")
+ )
super.awakeFromNib()
}
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements
index 080e1e075..46ab511be 100755
--- a/macos/Runner/Release.entitlements
+++ b/macos/Runner/Release.entitlements
@@ -4,5 +4,7 @@
com.apple.security.files.user-selected.read-write
+ com.apple.security.personal-information.location
+
diff --git a/pubspec.yaml b/pubspec.yaml
index cca65ac8e..16fbf676e 100755
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -46,6 +46,8 @@ dependencies:
uni_platform: ^0.1.3
device_info_plus: ^12.2.0
connectivity_plus: ^7.0.0
+ network_info_plus: ^6.0.2
+ permission_handler: ^12.0.0
screen_retriever: ^0.2.0
defer_pointer: ^0.0.2
flutter_riverpod: ^3.0.0
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 3cac9d06f..32440743a 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -12,6 +12,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -33,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterJsPlugin"));
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
+ PermissionHandlerWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ProxyPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ProxyPluginCApi"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 7f1cdbd69..9eb6be912 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_js
hotkey_manager_windows
+ permission_handler_windows
proxy
screen_retriever_windows
sqlite3_flutter_libs