[Connectivity] add a method to request location on iOS (for iOS 13) (#1999)

diff --git a/packages/connectivity/CHANGELOG.md b/packages/connectivity/CHANGELOG.md
index 4115a1b..e88bc2b 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 215f899..c26def4 100644
--- a/packages/connectivity/README.md
+++ b/packages/connectivity/README.md
@@ -50,7 +50,7 @@
 }
 ```
 
-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 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 `<project root>/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 d76382b..babbd80 100644
--- a/packages/connectivity/example/ios/Runner/Info.plist
+++ b/packages/connectivity/example/ios/Runner/Info.plist
@@ -22,6 +22,10 @@
 	<string>1</string>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
+	<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
+	<string>This app requires accessing your location information all the time to get wi-fi information.</string>
+	<key>NSLocationWhenInUseUsageDescription</key>
+	<string>This app requires accessing your location information when the app is in foreground to get wi-fi information.</string>
 	<key>UILaunchStoryboardName</key>
 	<string>LaunchScreen</string>
 	<key>UIMainStoryboardFile</key>
diff --git a/packages/connectivity/example/ios/Runner/Runner.entitlements b/packages/connectivity/example/ios/Runner/Runner.entitlements
new file mode 100644
index 0000000..ba21fbd
--- /dev/null
+++ b/packages/connectivity/example/ios/Runner/Runner.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.developer.networking.wifi-info</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/connectivity/example/lib/main.dart b/packages/connectivity/example/lib/main.dart
index 3784a22..c01a110 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 @@
         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 7efba5e..685f69e 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 @@
           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 e92fff2..c698711 100644
--- a/packages/connectivity/ios/Classes/ConnectivityPlugin.m
+++ b/packages/connectivity/ios/Classes/ConnectivityPlugin.m
@@ -6,13 +6,18 @@
 
 #import "Reachability/Reachability.h"
 
+#import <CoreLocation/CoreLocation.h>
+#import "FLTConnectivityLocationHandler.h"
 #import "SystemConfiguration/CaptiveNetwork.h"
 
 #include <ifaddrs.h>
 
 #include <arpa/inet.h>
 
-@interface FLTConnectivityPlugin () <FlutterStreamHandler>
+@interface FLTConnectivityPlugin () <FlutterStreamHandler, CLLocationManagerDelegate>
+
+@property(strong, nonatomic) FLTConnectivityLocationHandler* locationHandler;
+
 @end
 
 @implementation FLTConnectivityPlugin {
@@ -111,6 +116,18 @@
     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 @@
   _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 0000000..1731d56
--- /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 <CoreLocation/CoreLocation.h>
+#import <Foundation/Foundation.h>
+
+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 0000000..bbb93ae
--- /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 () <CLLocationManagerDelegate>
+
+@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 a1fd21cb..03659f5 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 @@
   Future<String> getWifiIP() async {
     return await methodChannel.invokeMethod<String>('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 `<project root>/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<LocationAuthorizationStatus> 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<String>(
+        'requestLocationServiceAuthorization',
+        <bool>[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<LocationAuthorizationStatus> getLocationServiceAuthorization() async {
+    //Just `assert(Platform.isIOS)` will prevent us from doing dart side unit testing.
+    assert(!Platform.isAndroid);
+    final String result = await methodChannel
+        .invokeMethod<String>('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 @@
       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 b91741f..7ec5d31 100644
--- a/packages/connectivity/pubspec.yaml
+++ b/packages/connectivity/pubspec.yaml
@@ -3,7 +3,7 @@
   mobile/cellular) connectivity on Android and iOS.
 author: Flutter Team <flutter-dev@googlegroups.com>
 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 311926b..892e7d0 100644
--- a/packages/connectivity/test/connectivity_test.dart
+++ b/packages/connectivity/test/connectivity_test.dart
@@ -25,6 +25,10 @@
             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 @@
       );
     });
 
+    test('requestLocationServiceAuthorization', () async {
+      final LocationAuthorizationStatus result =
+          await Connectivity().requestLocationServiceAuthorization();
+      expect(result, LocationAuthorizationStatus.authorizedAlways);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'requestLocationServiceAuthorization',
+            arguments: <bool>[false],
+          ),
+        ],
+      );
+    });
+
+    test('getLocationServiceAuthorization', () async {
+      final LocationAuthorizationStatus result =
+          await Connectivity().getLocationServiceAuthorization();
+      expect(result, LocationAuthorizationStatus.authorizedAlways);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'getLocationServiceAuthorization',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
     test('checkConnectivity', () async {
       final ConnectivityResult result =
           await Connectivity().checkConnectivity();