Skip to content

Commit 90aaca8

Browse files
author
Chris Yang
authored
[Connectivity] add a method to request location on iOS (for iOS 13) (flutter#1999)
1 parent cd40169 commit 90aaca8

12 files changed

+392
-12
lines changed

packages/connectivity/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.4.4
2+
3+
* Add `requestLocationServiceAuthorization` to request location authorization on iOS.
4+
* Add `getLocationServiceAuthorization` to get location authorization status on iOS.
5+
* Update README: add more information on iOS 13 updates with CNCopyCurrentNetworkInfo.
6+
17
## 0.4.3+7
28

39
* Update README with the updated information about CNCopyCurrentNetworkInfo on iOS 13.

packages/connectivity/README.md

+30-8
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ dispose() {
5050
}
5151
```
5252

53-
You can get WIFI related information using:
53+
You can get wi-fi related information using:
5454

5555
```dart
5656
import 'package:connectivity/connectivity.dart';
@@ -60,17 +60,39 @@ var wifiIP = await (Connectivity().getWifiIP());network
6060
var wifiName = await (Connectivity().getWifiName());wifi network
6161
```
6262

63-
### Known Issues
63+
### iOS 12
6464

65-
#### iOS 13
65+
To use `.getWifiBSSID()` and `.getWifiName()` on iOS >= 12, the `Access WiFi information capability` in XCode must be enabled. Otherwise, both methods will return null.
6666

67-
The methods `.getWifiBSSID()` and `.getWifiName()` utilize the [CNCopyCurrentNetworkInfo](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) function on iOS.
67+
### iOS 13
6868

69-
As of iOS 13, Apple announced that these APIs will no longer return valid information by default and will instead return the following:
70-
> SSID: "Wi-Fi" or "WLAN" ("WLAN" will be returned for the China SKU)
71-
> BSSID: "00:00:00:00:00:00"
69+
The methods `.getWifiBSSID()` and `.getWifiName()` utilize the [`CNCopyCurrentNetworkInfo`](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) function on iOS.
7270

73-
You can follow issue [#37804](https://github.com/flutter/flutter/issues/37804) for the changes required to return valid SSID and BSSID values with iOS 13.
71+
As of iOS 13, Apple announced that these APIs will no longer return valid information.
72+
An app linked against iOS 12 or earlier receives pseudo-values such as:
73+
74+
* SSID: "Wi-Fi" or "WLAN" ("WLAN" will be returned for the China SKU).
75+
76+
* BSSID: "00:00:00:00:00:00"
77+
78+
An app linked against iOS 13 or later receives `null`.
79+
80+
The `CNCopyCurrentNetworkInfo` will work for Apps that:
81+
82+
* The app uses Core Location, and has the user’s authorization to use location information.
83+
84+
* The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network.
85+
86+
* The app has active VPN configurations installed.
87+
88+
If your app falls into the last two categories, it will work as it is. If your app doesn't fall into the last two categories,
89+
and you still need to access the wifi information, you should request user's authorization to use location information.
90+
91+
There is a helper method provided in this plugin to request the location authorization: `requestLocationServiceAuthorization`.
92+
To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`:
93+
94+
* `NSLocationAlwaysAndWhenInUseUsageDescription` - describe why the app needs access to the user’s location information all the time (foreground and background). This is called _Privacy - Location Always and When In Use Usage Description_ in the visual editor.
95+
* `NSLocationWhenInUseUsageDescription` - describe why the app needs access to the user’s location information when the app is running in the foreground. This is called _Privacy - Location When In Use Usage Description_ in the visual editor.
7496

7597
## Getting Started
7698

packages/connectivity/example/ios/Runner/Info.plist

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<string>1</string>
2323
<key>LSRequiresIPhoneOS</key>
2424
<true/>
25+
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
26+
<string>This app requires accessing your location information all the time to get wi-fi information.</string>
27+
<key>NSLocationWhenInUseUsageDescription</key>
28+
<string>This app requires accessing your location information when the app is in foreground to get wi-fi information.</string>
2529
<key>UILaunchStoryboardName</key>
2630
<string>LaunchScreen</string>
2731
<key>UIMainStoryboardFile</key>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.developer.networking.wifi-info</key>
6+
<true/>
7+
</dict>
8+
</plist>

packages/connectivity/example/lib/main.dart

+33-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:io';
67

78
import 'package:flutter/material.dart';
89
import 'package:flutter/services.dart';
@@ -90,14 +91,44 @@ class _MyHomePageState extends State<MyHomePage> {
9091
String wifiName, wifiBSSID, wifiIP;
9192

9293
try {
93-
wifiName = await _connectivity.getWifiName();
94+
if (Platform.isIOS) {
95+
LocationAuthorizationStatus status =
96+
await _connectivity.getLocationServiceAuthorization();
97+
if (status == LocationAuthorizationStatus.notDetermined) {
98+
status =
99+
await _connectivity.requestLocationServiceAuthorization();
100+
}
101+
if (status == LocationAuthorizationStatus.authorizedAlways ||
102+
status == LocationAuthorizationStatus.authorizedWhenInUse) {
103+
wifiName = await _connectivity.getWifiName();
104+
} else {
105+
wifiName = await _connectivity.getWifiName();
106+
}
107+
} else {
108+
wifiName = await _connectivity.getWifiName();
109+
}
94110
} on PlatformException catch (e) {
95111
print(e.toString());
96112
wifiName = "Failed to get Wifi Name";
97113
}
98114

99115
try {
100-
wifiBSSID = await _connectivity.getWifiBSSID();
116+
if (Platform.isIOS) {
117+
LocationAuthorizationStatus status =
118+
await _connectivity.getLocationServiceAuthorization();
119+
if (status == LocationAuthorizationStatus.notDetermined) {
120+
status =
121+
await _connectivity.requestLocationServiceAuthorization();
122+
}
123+
if (status == LocationAuthorizationStatus.authorizedAlways ||
124+
status == LocationAuthorizationStatus.authorizedWhenInUse) {
125+
wifiBSSID = await _connectivity.getWifiBSSID();
126+
} else {
127+
wifiBSSID = await _connectivity.getWifiBSSID();
128+
}
129+
} else {
130+
wifiBSSID = await _connectivity.getWifiBSSID();
131+
}
101132
} on PlatformException catch (e) {
102133
print(e.toString());
103134
wifiBSSID = "Failed to get Wifi BSSID";

packages/connectivity/example/test_driver/connectivity.dart

+9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:io';
23
import 'package:flutter_driver/driver_extension.dart';
34
import 'package:flutter_test/flutter_test.dart';
45
import 'package:connectivity/connectivity.dart';
@@ -28,5 +29,13 @@ void main() {
2829
break;
2930
}
3031
});
32+
33+
test('test location methods, iOS only', () async {
34+
print(Platform.isIOS);
35+
if (Platform.isIOS) {
36+
expect((await _connectivity.getLocationServiceAuthorization()),
37+
LocationAuthorizationStatus.notDetermined);
38+
}
39+
});
3140
});
3241
}

packages/connectivity/ios/Classes/ConnectivityPlugin.m

+46-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66

77
#import "Reachability/Reachability.h"
88

9+
#import <CoreLocation/CoreLocation.h>
10+
#import "FLTConnectivityLocationHandler.h"
911
#import "SystemConfiguration/CaptiveNetwork.h"
1012

1113
#include <ifaddrs.h>
1214

1315
#include <arpa/inet.h>
1416

15-
@interface FLTConnectivityPlugin () <FlutterStreamHandler>
17+
@interface FLTConnectivityPlugin () <FlutterStreamHandler, CLLocationManagerDelegate>
18+
19+
@property(strong, nonatomic) FLTConnectivityLocationHandler* locationHandler;
20+
1621
@end
1722

1823
@implementation FLTConnectivityPlugin {
@@ -111,6 +116,18 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
111116
result([self getBSSID]);
112117
} else if ([call.method isEqualToString:@"wifiIPAddress"]) {
113118
result([self getWifiIP]);
119+
} else if ([call.method isEqualToString:@"getLocationServiceAuthorization"]) {
120+
result([self convertCLAuthorizationStatusToString:[FLTConnectivityLocationHandler
121+
locationAuthorizationStatus]]);
122+
} else if ([call.method isEqualToString:@"requestLocationServiceAuthorization"]) {
123+
NSArray* arguments = call.arguments;
124+
BOOL always = [arguments.firstObject boolValue];
125+
__weak typeof(self) weakSelf = self;
126+
[self.locationHandler
127+
requestLocationAuthorization:always
128+
completion:^(CLAuthorizationStatus status) {
129+
result([weakSelf convertCLAuthorizationStatusToString:status]);
130+
}];
114131
} else {
115132
result(FlutterMethodNotImplemented);
116133
}
@@ -121,6 +138,34 @@ - (void)onReachabilityDidChange:(NSNotification*)notification {
121138
_eventSink([self statusFromReachability:curReach]);
122139
}
123140

141+
- (NSString*)convertCLAuthorizationStatusToString:(CLAuthorizationStatus)status {
142+
switch (status) {
143+
case kCLAuthorizationStatusNotDetermined: {
144+
return @"notDetermined";
145+
}
146+
case kCLAuthorizationStatusRestricted: {
147+
return @"restricted";
148+
}
149+
case kCLAuthorizationStatusDenied: {
150+
return @"denied";
151+
}
152+
case kCLAuthorizationStatusAuthorizedAlways: {
153+
return @"authorizedAlways";
154+
}
155+
case kCLAuthorizationStatusAuthorizedWhenInUse: {
156+
return @"authorizedWhenInUse";
157+
}
158+
default: { return @"unknown"; }
159+
}
160+
}
161+
162+
- (FLTConnectivityLocationHandler*)locationHandler {
163+
if (!_locationHandler) {
164+
_locationHandler = [FLTConnectivityLocationHandler new];
165+
}
166+
return _locationHandler;
167+
}
168+
124169
#pragma mark FlutterStreamHandler impl
125170

126171
- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
#import <CoreLocation/CoreLocation.h>
5+
#import <Foundation/Foundation.h>
6+
7+
NS_ASSUME_NONNULL_BEGIN
8+
9+
@class FLTConnectivityLocationDelegate;
10+
11+
typedef void (^FLTConnectivityLocationCompletion)(CLAuthorizationStatus);
12+
13+
@interface FLTConnectivityLocationHandler : NSObject
14+
15+
+ (CLAuthorizationStatus)locationAuthorizationStatus;
16+
17+
- (void)requestLocationAuthorization:(BOOL)always
18+
completion:(_Nonnull FLTConnectivityLocationCompletion)completionHnadler;
19+
20+
@end
21+
22+
NS_ASSUME_NONNULL_END
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#import "FLTConnectivityLocationHandler.h"
6+
7+
@interface FLTConnectivityLocationHandler () <CLLocationManagerDelegate>
8+
9+
@property(copy, nonatomic) FLTConnectivityLocationCompletion completion;
10+
@property(strong, nonatomic) CLLocationManager *locationManager;
11+
12+
@end
13+
14+
@implementation FLTConnectivityLocationHandler
15+
16+
+ (CLAuthorizationStatus)locationAuthorizationStatus {
17+
return CLLocationManager.authorizationStatus;
18+
}
19+
20+
- (void)requestLocationAuthorization:(BOOL)always
21+
completion:(FLTConnectivityLocationCompletion)completionHandler {
22+
CLAuthorizationStatus status = CLLocationManager.authorizationStatus;
23+
if (status != kCLAuthorizationStatusAuthorizedWhenInUse && always) {
24+
completionHandler(kCLAuthorizationStatusDenied);
25+
return;
26+
} else if (status != kCLAuthorizationStatusNotDetermined) {
27+
completionHandler(status);
28+
return;
29+
}
30+
31+
if (self.completion) {
32+
// If a request is still in process, immediately return.
33+
completionHandler(kCLAuthorizationStatusNotDetermined);
34+
return;
35+
}
36+
37+
self.completion = completionHandler;
38+
self.locationManager = [CLLocationManager new];
39+
self.locationManager.delegate = self;
40+
if (always) {
41+
[self.locationManager requestAlwaysAuthorization];
42+
} else {
43+
[self.locationManager requestWhenInUseAuthorization];
44+
}
45+
}
46+
47+
- (void)locationManager:(CLLocationManager *)manager
48+
didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
49+
if (status == kCLAuthorizationStatusNotDetermined) {
50+
return;
51+
}
52+
if (self.completion) {
53+
self.completion(status);
54+
self.completion = nil;
55+
}
56+
}
57+
58+
@end

0 commit comments

Comments
 (0)