diff --git a/packages/connectivity/CHANGELOG.md b/packages/connectivity/CHANGELOG.md index 4115a1be4aae..e88bc2b2d4df 100644 --- a/packages/connectivity/CHANGELOG.md +++ b/packages/connectivity/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.4.4 + +* Add `requestLocationServiceAuthorization` to request location authorization on iOS. +* Add `getLocationServiceAuthorization` to get location authorization status on iOS. +* Update README: add more information on iOS 13 updates with CNCopyCurrentNetworkInfo. + ## 0.4.3+7 * Update README with the updated information about CNCopyCurrentNetworkInfo on iOS 13. diff --git a/packages/connectivity/README.md b/packages/connectivity/README.md index 215f8991a756..c26def4e8ea4 100644 --- a/packages/connectivity/README.md +++ b/packages/connectivity/README.md @@ -50,7 +50,7 @@ dispose() { } ``` -You can get WIFI related information using: +You can get wi-fi related information using: ```dart import 'package:connectivity/connectivity.dart'; @@ -60,17 +60,39 @@ var wifiIP = await (Connectivity().getWifiIP());network var wifiName = await (Connectivity().getWifiName());wifi network ``` -### Known Issues +### iOS 12 -#### iOS 13 +To use `.getWifiBSSID()` and `.getWifiName()` on iOS >= 12, the `Access WiFi information capability` in XCode must be enabled. Otherwise, both methods will return null. -The methods `.getWifiBSSID()` and `.getWifiName()` utilize the [CNCopyCurrentNetworkInfo](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) function on iOS. +### iOS 13 -As of iOS 13, Apple announced that these APIs will no longer return valid information by default and will instead return the following: -> SSID: "Wi-Fi" or "WLAN" ("WLAN" will be returned for the China SKU) -> BSSID: "00:00:00:00:00:00" +The methods `.getWifiBSSID()` and `.getWifiName()` utilize the [`CNCopyCurrentNetworkInfo`](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) function on iOS. -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. +As of iOS 13, Apple announced that these APIs will no longer return valid information. +An app linked against iOS 12 or earlier receives pseudo-values such as: + + * SSID: "Wi-Fi" or "WLAN" ("WLAN" will be returned for the China SKU). + + * BSSID: "00:00:00:00:00:00" + +An app linked against iOS 13 or later receives `null`. + +The `CNCopyCurrentNetworkInfo` will work for Apps that: + + * The app uses Core Location, and has the user’s authorization to use location information. + + * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. + + * The app has active VPN configurations installed. + +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, +and you still need to access the wifi information, you should request user's authorization to use location information. + +There is a helper method provided in this plugin to request the location authorization: `requestLocationServiceAuthorization`. +To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: + +* `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. +* `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. ## Getting Started diff --git a/packages/connectivity/example/ios/Runner/Info.plist b/packages/connectivity/example/ios/Runner/Info.plist index d76382b40acf..babbd80f1619 100644 --- a/packages/connectivity/example/ios/Runner/Info.plist +++ b/packages/connectivity/example/ios/Runner/Info.plist @@ -22,6 +22,10 @@ 1 LSRequiresIPhoneOS + NSLocationAlwaysAndWhenInUseUsageDescription + This app requires accessing your location information all the time to get wi-fi information. + NSLocationWhenInUseUsageDescription + This app requires accessing your location information when the app is in foreground to get wi-fi information. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/packages/connectivity/example/ios/Runner/Runner.entitlements b/packages/connectivity/example/ios/Runner/Runner.entitlements new file mode 100644 index 000000000000..ba21fbdaf290 --- /dev/null +++ b/packages/connectivity/example/ios/Runner/Runner.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.developer.networking.wifi-info + + + diff --git a/packages/connectivity/example/lib/main.dart b/packages/connectivity/example/lib/main.dart index 3784a22fc241..c01a110efb60 100644 --- a/packages/connectivity/example/lib/main.dart +++ b/packages/connectivity/example/lib/main.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -90,14 +91,44 @@ class _MyHomePageState extends State { String wifiName, wifiBSSID, wifiIP; try { - wifiName = await _connectivity.getWifiName(); + if (Platform.isIOS) { + LocationAuthorizationStatus status = + await _connectivity.getLocationServiceAuthorization(); + if (status == LocationAuthorizationStatus.notDetermined) { + status = + await _connectivity.requestLocationServiceAuthorization(); + } + if (status == LocationAuthorizationStatus.authorizedAlways || + status == LocationAuthorizationStatus.authorizedWhenInUse) { + wifiName = await _connectivity.getWifiName(); + } else { + wifiName = await _connectivity.getWifiName(); + } + } else { + wifiName = await _connectivity.getWifiName(); + } } on PlatformException catch (e) { print(e.toString()); wifiName = "Failed to get Wifi Name"; } try { - wifiBSSID = await _connectivity.getWifiBSSID(); + if (Platform.isIOS) { + LocationAuthorizationStatus status = + await _connectivity.getLocationServiceAuthorization(); + if (status == LocationAuthorizationStatus.notDetermined) { + status = + await _connectivity.requestLocationServiceAuthorization(); + } + if (status == LocationAuthorizationStatus.authorizedAlways || + status == LocationAuthorizationStatus.authorizedWhenInUse) { + wifiBSSID = await _connectivity.getWifiBSSID(); + } else { + wifiBSSID = await _connectivity.getWifiBSSID(); + } + } else { + wifiBSSID = await _connectivity.getWifiBSSID(); + } } on PlatformException catch (e) { print(e.toString()); wifiBSSID = "Failed to get Wifi BSSID"; diff --git a/packages/connectivity/example/test_driver/connectivity.dart b/packages/connectivity/example/test_driver/connectivity.dart index 7efba5e4ab50..685f69efb1c8 100644 --- a/packages/connectivity/example/test_driver/connectivity.dart +++ b/packages/connectivity/example/test_driver/connectivity.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:connectivity/connectivity.dart'; @@ -28,5 +29,13 @@ void main() { break; } }); + + test('test location methods, iOS only', () async { + print(Platform.isIOS); + if (Platform.isIOS) { + expect((await _connectivity.getLocationServiceAuthorization()), + LocationAuthorizationStatus.notDetermined); + } + }); }); } diff --git a/packages/connectivity/ios/Classes/ConnectivityPlugin.m b/packages/connectivity/ios/Classes/ConnectivityPlugin.m index e92fff220a2e..c69871175b01 100644 --- a/packages/connectivity/ios/Classes/ConnectivityPlugin.m +++ b/packages/connectivity/ios/Classes/ConnectivityPlugin.m @@ -6,13 +6,18 @@ #import "Reachability/Reachability.h" +#import +#import "FLTConnectivityLocationHandler.h" #import "SystemConfiguration/CaptiveNetwork.h" #include #include -@interface FLTConnectivityPlugin () +@interface FLTConnectivityPlugin () + +@property(strong, nonatomic) FLTConnectivityLocationHandler* locationHandler; + @end @implementation FLTConnectivityPlugin { @@ -111,6 +116,18 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { result([self getBSSID]); } else if ([call.method isEqualToString:@"wifiIPAddress"]) { result([self getWifiIP]); + } else if ([call.method isEqualToString:@"getLocationServiceAuthorization"]) { + result([self convertCLAuthorizationStatusToString:[FLTConnectivityLocationHandler + locationAuthorizationStatus]]); + } else if ([call.method isEqualToString:@"requestLocationServiceAuthorization"]) { + NSArray* arguments = call.arguments; + BOOL always = [arguments.firstObject boolValue]; + __weak typeof(self) weakSelf = self; + [self.locationHandler + requestLocationAuthorization:always + completion:^(CLAuthorizationStatus status) { + result([weakSelf convertCLAuthorizationStatusToString:status]); + }]; } else { result(FlutterMethodNotImplemented); } @@ -121,6 +138,34 @@ - (void)onReachabilityDidChange:(NSNotification*)notification { _eventSink([self statusFromReachability:curReach]); } +- (NSString*)convertCLAuthorizationStatusToString:(CLAuthorizationStatus)status { + switch (status) { + case kCLAuthorizationStatusNotDetermined: { + return @"notDetermined"; + } + case kCLAuthorizationStatusRestricted: { + return @"restricted"; + } + case kCLAuthorizationStatusDenied: { + return @"denied"; + } + case kCLAuthorizationStatusAuthorizedAlways: { + return @"authorizedAlways"; + } + case kCLAuthorizationStatusAuthorizedWhenInUse: { + return @"authorizedWhenInUse"; + } + default: { return @"unknown"; } + } +} + +- (FLTConnectivityLocationHandler*)locationHandler { + if (!_locationHandler) { + _locationHandler = [FLTConnectivityLocationHandler new]; + } + return _locationHandler; +} + #pragma mark FlutterStreamHandler impl - (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { diff --git a/packages/connectivity/ios/Classes/FLTConnectivityLocationHandler.h b/packages/connectivity/ios/Classes/FLTConnectivityLocationHandler.h new file mode 100644 index 000000000000..1731d56fe782 --- /dev/null +++ b/packages/connectivity/ios/Classes/FLTConnectivityLocationHandler.h @@ -0,0 +1,22 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class FLTConnectivityLocationDelegate; + +typedef void (^FLTConnectivityLocationCompletion)(CLAuthorizationStatus); + +@interface FLTConnectivityLocationHandler : NSObject + ++ (CLAuthorizationStatus)locationAuthorizationStatus; + +- (void)requestLocationAuthorization:(BOOL)always + completion:(_Nonnull FLTConnectivityLocationCompletion)completionHnadler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/connectivity/ios/Classes/FLTConnectivityLocationHandler.m b/packages/connectivity/ios/Classes/FLTConnectivityLocationHandler.m new file mode 100644 index 000000000000..bbb93aea6a5b --- /dev/null +++ b/packages/connectivity/ios/Classes/FLTConnectivityLocationHandler.m @@ -0,0 +1,58 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTConnectivityLocationHandler.h" + +@interface FLTConnectivityLocationHandler () + +@property(copy, nonatomic) FLTConnectivityLocationCompletion completion; +@property(strong, nonatomic) CLLocationManager *locationManager; + +@end + +@implementation FLTConnectivityLocationHandler + ++ (CLAuthorizationStatus)locationAuthorizationStatus { + return CLLocationManager.authorizationStatus; +} + +- (void)requestLocationAuthorization:(BOOL)always + completion:(FLTConnectivityLocationCompletion)completionHandler { + CLAuthorizationStatus status = CLLocationManager.authorizationStatus; + if (status != kCLAuthorizationStatusAuthorizedWhenInUse && always) { + completionHandler(kCLAuthorizationStatusDenied); + return; + } else if (status != kCLAuthorizationStatusNotDetermined) { + completionHandler(status); + return; + } + + if (self.completion) { + // If a request is still in process, immediately return. + completionHandler(kCLAuthorizationStatusNotDetermined); + return; + } + + self.completion = completionHandler; + self.locationManager = [CLLocationManager new]; + self.locationManager.delegate = self; + if (always) { + [self.locationManager requestAlwaysAuthorization]; + } else { + [self.locationManager requestWhenInUseAuthorization]; + } +} + +- (void)locationManager:(CLLocationManager *)manager + didChangeAuthorizationStatus:(CLAuthorizationStatus)status { + if (status == kCLAuthorizationStatusNotDetermined) { + return; + } + if (self.completion) { + self.completion(status); + self.completion = nil; + } +} + +@end diff --git a/packages/connectivity/lib/connectivity.dart b/packages/connectivity/lib/connectivity.dart index a1fd21cb1668..03659f5455a9 100644 --- a/packages/connectivity/lib/connectivity.dart +++ b/packages/connectivity/lib/connectivity.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; @@ -93,6 +94,125 @@ class Connectivity { Future getWifiIP() async { return await methodChannel.invokeMethod('wifiIPAddress'); } + + /// Request to authorize the location service (Only on iOS). + /// + /// This method will throw a [PlatformException] on Android. + /// + /// Returns a [LocationAuthorizationStatus] after user authorized or denied the location on this request. + /// + /// If the location information needs to be accessible all the time, set `requestAlwaysLocationUsage` to true. If user has + /// already granted a [LocationAuthorizationStatus.authorizedWhenInUse] prior to requesting an "always" access, it will return [LocationAuthorizationStatus.denied]. + /// + /// If the location service authorization is not determined prior to making this call, a platform standard UI of requesting a location service will pop up. + /// This UI will only show once unless the user re-install the app to their phone which resets the location service authorization to not determined. + /// + /// This method is a helper to get the location authorization that is necessary for certain functionality of this plugin. + /// It can be replaced with other permission handling code/plugin if preferred. + /// To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: + /// * `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. + /// * `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. + /// + /// Starting from iOS 13, `getWifiBSSID` and `getWifiIP` will only work properly if: + /// + /// * The app uses Core Location, and has the user’s authorization to use location information. + /// * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. + /// * The app has active VPN configurations installed. + /// + /// If the app falls into the first category, call this method before calling `getWifiBSSID` or `getWifiIP`. + /// For example, + /// ```dart + /// if (Platform.isIOS) { + /// LocationAuthorizationStatus status = await _connectivity.getLocationServiceAuthorization(); + /// if (status == LocationAuthorizationStatus.notDetermined) { + /// status = await _connectivity.requestLocationServiceAuthorization(); + /// } + /// if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { + /// wifiBSSID = await _connectivity.getWifiName(); + /// } else { + /// print('location service is not authorized, the data might not be correct'); + /// wifiBSSID = await _connectivity.getWifiName(); + /// } + /// } else { + /// wifiBSSID = await _connectivity.getWifiName(); + /// } + /// ``` + /// + /// Ideally, a location service authorization should only be requested if the current authorization status is not determined. + /// + /// See also [getLocationServiceAuthorization] to obtain current location service status. + Future requestLocationServiceAuthorization( + {bool requestAlwaysLocationUsage = false}) async { + //Just `assert(Platform.isIOS)` will prevent us from doing dart side unit testing. + assert(!Platform.isAndroid); + final String result = await methodChannel.invokeMethod( + 'requestLocationServiceAuthorization', + [requestAlwaysLocationUsage]); + return _convertLocationStatusString(result); + } + + /// Get the current location service authorization (Only on iOS). + /// + /// This method will throw a [PlatformException] on Android. + /// + /// Returns a [LocationAuthorizationStatus]. + /// If the returned value is [LocationAuthorizationStatus.notDetermined], a subsequent [requestLocationServiceAuthorization] call + /// can request the authorization. + /// If the returned value is not [LocationAuthorizationStatus.notDetermined], a subsequent [requestLocationServiceAuthorization] + /// will not initiate another request. It will instead return the "determined" status. + /// + /// This method is a helper to get the location authorization that is necessary for certain functionality of this plugin. + /// It can be replaced with other permission handling code/plugin if preferred. + /// + /// Starting from iOS 13, `getWifiBSSID` and `getWifiIP` will only work properly if: + /// + /// * The app uses Core Location, and has the user’s authorization to use location information. + /// * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. + /// * The app has active VPN configurations installed. + /// + /// If the app falls into the first category, call this method before calling `getWifiBSSID` or `getWifiIP`. + /// For example, + /// ```dart + /// if (Platform.isIOS) { + /// LocationAuthorizationStatus status = await _connectivity.getLocationServiceAuthorization(); + /// if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { + /// wifiBSSID = await _connectivity.getWifiName(); + /// } else { + /// print('location service is not authorized, the data might not be correct'); + /// wifiBSSID = await _connectivity.getWifiName(); + /// } + /// } else { + /// wifiBSSID = await _connectivity.getWifiName(); + /// } + /// ``` + /// + /// See also [requestLocationServiceAuthorization] for requesting a location service authorization. + Future getLocationServiceAuthorization() async { + //Just `assert(Platform.isIOS)` will prevent us from doing dart side unit testing. + assert(!Platform.isAndroid); + final String result = await methodChannel + .invokeMethod('getLocationServiceAuthorization'); + return _convertLocationStatusString(result); + } + + LocationAuthorizationStatus _convertLocationStatusString(String result) { + switch (result) { + case 'notDetermined': + return LocationAuthorizationStatus.notDetermined; + case 'restricted': + return LocationAuthorizationStatus.restricted; + case 'denied': + return LocationAuthorizationStatus.denied; + case 'authorizedAlways': + return LocationAuthorizationStatus.authorizedAlways; + case 'authorizedWhenInUse': + return LocationAuthorizationStatus.authorizedWhenInUse; + default: + return LocationAuthorizationStatus.unknown; + } + } } ConnectivityResult _parseConnectivityResult(String state) { @@ -106,3 +226,24 @@ ConnectivityResult _parseConnectivityResult(String state) { return ConnectivityResult.none; } } + +/// The status of the location service authorization. +enum LocationAuthorizationStatus { + /// The authorization of the location service is not determined. + notDetermined, + + /// This app is not authorized to use location. + restricted, + + /// User explicitly denied the location service. + denied, + + /// User authorized the app to access the location at any time. + authorizedAlways, + + /// User authorized the app to access the location when the app is visible to them. + authorizedWhenInUse, + + /// Status unknown. + unknown +} diff --git a/packages/connectivity/pubspec.yaml b/packages/connectivity/pubspec.yaml index b91741f2f916..7ec5d31bcc42 100644 --- a/packages/connectivity/pubspec.yaml +++ b/packages/connectivity/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) connectivity on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity -version: 0.4.3+7 +version: 0.4.4 flutter: plugin: diff --git a/packages/connectivity/test/connectivity_test.dart b/packages/connectivity/test/connectivity_test.dart index 311926b8b963..892e7d0085c5 100644 --- a/packages/connectivity/test/connectivity_test.dart +++ b/packages/connectivity/test/connectivity_test.dart @@ -25,6 +25,10 @@ void main() { return 'c0:ff:33:c0:d3:55'; case 'wifiIPAddress': return '127.0.0.1'; + case 'requestLocationServiceAuthorization': + return 'authorizedAlways'; + case 'getLocationServiceAuthorization': + return 'authorizedAlways'; default: return null; } @@ -98,6 +102,36 @@ void main() { ); }); + test('requestLocationServiceAuthorization', () async { + final LocationAuthorizationStatus result = + await Connectivity().requestLocationServiceAuthorization(); + expect(result, LocationAuthorizationStatus.authorizedAlways); + expect( + log, + [ + isMethodCall( + 'requestLocationServiceAuthorization', + arguments: [false], + ), + ], + ); + }); + + test('getLocationServiceAuthorization', () async { + final LocationAuthorizationStatus result = + await Connectivity().getLocationServiceAuthorization(); + expect(result, LocationAuthorizationStatus.authorizedAlways); + expect( + log, + [ + isMethodCall( + 'getLocationServiceAuthorization', + arguments: null, + ), + ], + ); + }); + test('checkConnectivity', () async { final ConnectivityResult result = await Connectivity().checkConnectivity();