[geolocation] Add a Core Location backend behind a flag

This CL Adds a new Location Provider that uses the Core Location
API to determine Location.

Bug: 1035290
Change-Id: I7cff4285ba10163b7fcc6657c1c8705872526606
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2152105
Commit-Queue: James Hollyer <[email protected]>
Reviewed-by: Ovidio de Jesús Ruiz-Henríquez <[email protected]>
Reviewed-by: Reilly Grant <[email protected]>
Cr-Commit-Position: refs/heads/master@{#765416}
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index e16b34a..3529ea26 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -5134,6 +5134,13 @@
      FEATURE_VALUE_TYPE(features::kWinrtGeolocationImplementation)},
 #endif
 
+#if defined(OS_MACOSX)
+    {"enable-core-location-implementation",
+     flag_descriptions::kMacCoreLocationImplementationName,
+     flag_descriptions::kMacCoreLocationImplementationDescription, kOsMac,
+     FEATURE_VALUE_TYPE(features::kMacCoreLocationImplementation)},
+#endif
+
 #if defined(OS_CHROMEOS)
     {"exo-pointer-lock", flag_descriptions::kExoPointerLockName,
      flag_descriptions::kExoPointerLockDescription, kOsCrOS,
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 8964b2c..6998c69 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -1288,6 +1288,11 @@
     "expiry_milestone": 85
   },
   {
+    "name": "enable-core-location-implementation",
+    "owners": [ "[email protected]" ],
+    "expiry_milestone": 86
+  },
+  {
     "name": "enable-credit-card-assist",
     "owners": [ "ftirelo", "gogerald" ],
     "expiry_milestone": 76
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 12a7413..52369712 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -639,6 +639,11 @@
     "This option enables a post-quantum (i.e. resistent to quantum computers) "
     "key exchange algorithm in TLS (CECPQ2).";
 
+const char kMacCoreLocationImplementationName[] =
+    "Core Location Implementation";
+const char kMacCoreLocationImplementationDescription[] =
+    "Enables usage of the Core Location APIs on macOS for geolocation";
+
 const char kWinrtGeolocationImplementationName[] =
     "WinRT Geolocation Implementation";
 const char kWinrtGeolocationImplementationDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 8673531..f587821 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -387,6 +387,9 @@
 extern const char kPostQuantumCECPQ2Name[];
 extern const char kPostQuantumCECPQ2Description[];
 
+extern const char kMacCoreLocationImplementationName[];
+extern const char kMacCoreLocationImplementationDescription[];
+
 extern const char kWinrtGeolocationImplementationName[];
 extern const char kWinrtGeolocationImplementationDescription[];
 
diff --git a/services/device/BUILD.gn b/services/device/BUILD.gn
index 8dd1125..d478ddab 100644
--- a/services/device/BUILD.gn
+++ b/services/device/BUILD.gn
@@ -133,6 +133,7 @@
     "generic_sensor/platform_sensor_util_unittest.cc",
     "generic_sensor/relative_orientation_euler_angles_fusion_algorithm_using_accelerometer_and_gyroscope_unittest.cc",
     "generic_sensor/relative_orientation_euler_angles_fusion_algorithm_using_accelerometer_unittest.cc",
+    "geolocation/core_location_provider_unittest.mm",
     "geolocation/geolocation_provider_impl_unittest.cc",
     "geolocation/geolocation_service_unittest.cc",
     "geolocation/location_arbitrator_unittest.cc",
diff --git a/services/device/geolocation/BUILD.gn b/services/device/geolocation/BUILD.gn
index f1a8863..45e6296 100644
--- a/services/device/geolocation/BUILD.gn
+++ b/services/device/geolocation/BUILD.gn
@@ -119,7 +119,14 @@
     libs = [
       "CoreWLAN.framework",
       "Foundation.framework",
+      "CoreLocation.framework",
     ]
+    sources += [
+      "core_location_provider.h",
+      "core_location_provider.mm",
+    ]
+
+    deps += [ "//services/device/public/cpp:device_features" ]
   }
 
   if (is_win) {
diff --git a/services/device/geolocation/DEPS b/services/device/geolocation/DEPS
index 9175131..2109181 100644
--- a/services/device/geolocation/DEPS
+++ b/services/device/geolocation/DEPS
@@ -5,6 +5,7 @@
   "+net/base",
   "+net/traffic_annotation",
   "+services/network/public/cpp",
+  "+services/device/public/cpp/device_features.h",
   "+services/network/test",
   "+third_party/cros_system_api/dbus",
 ]
diff --git a/services/device/geolocation/core_location_provider.h b/services/device/geolocation/core_location_provider.h
new file mode 100644
index 0000000..d6fd2bfc
--- /dev/null
+++ b/services/device/geolocation/core_location_provider.h
@@ -0,0 +1,45 @@
+// Copyright 2020 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.
+
+#ifndef SERVICES_DEVICE_GEOLOCATION_CORE_LOCATION_PROVIDER_H_
+#define SERVICES_DEVICE_GEOLOCATION_CORE_LOCATION_PROVIDER_H_
+
+#import <CoreLocation/CoreLocation.h>
+
+#include "base/mac/scoped_nsobject.h"
+#include "services/device/public/cpp/geolocation/location_provider.h"
+#include "services/device/public/mojom/geoposition.mojom.h"
+
+@class LocationDelegate;
+
+namespace device {
+
+// Location provider for macOS using the platform's Core Location API.
+class CoreLocationProvider : public LocationProvider {
+ public:
+  CoreLocationProvider();
+  ~CoreLocationProvider() override;
+
+  // LocationProvider implementation.
+  void SetUpdateCallback(
+      const LocationProviderUpdateCallback& callback) override;
+  void StartProvider(bool high_accuracy) override;
+  void StopProvider() override;
+  const mojom::Geoposition& GetPosition() override;
+  void OnPermissionGranted() override;
+
+  void DidUpdatePosition(CLLocation* location);
+  void SetManagerForTesting(CLLocationManager* location_manager);
+
+ private:
+  base::scoped_nsobject<CLLocationManager> location_manager_;
+  base::scoped_nsobject<LocationDelegate> delegate_;
+  mojom::Geoposition last_position_;
+  LocationProviderUpdateCallback callback_;
+  base::WeakPtrFactory<CoreLocationProvider> weak_ptr_factory_{this};
+};
+
+}  // namespace device
+
+#endif  // SERVICES_DEVICE_GEOLOCATION_LOCATION_PROVIDER_MAC_H_
diff --git a/services/device/geolocation/core_location_provider.mm b/services/device/geolocation/core_location_provider.mm
new file mode 100644
index 0000000..a2065e2
--- /dev/null
+++ b/services/device/geolocation/core_location_provider.mm
@@ -0,0 +1,120 @@
+// Copyright 2020 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.
+
+#include "services/device/geolocation/core_location_provider.h"
+#include "services/device/public/cpp/device_features.h"
+
+@interface LocationDelegate : NSObject <CLLocationManagerDelegate> {
+  base::WeakPtr<device::CoreLocationProvider> provider_;
+}
+
+- (id)initWithProvider:(base::WeakPtr<device::CoreLocationProvider>)provider;
+
+- (void)locationManager:(CLLocationManager*)manager
+     didUpdateLocations:(NSArray*)locations;
+- (void)locationManager:(CLLocationManager*)manager
+    didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
+
+@end
+
+@implementation LocationDelegate
+
+- (id)initWithProvider:(base::WeakPtr<device::CoreLocationProvider>)provider {
+  self = [super init];
+  provider_ = provider;
+  return self;
+}
+
+- (void)locationManager:(CLLocationManager*)manager
+    didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
+  if (!provider_)
+    return;
+  if (@available(macOS 10.12.0, *)) {
+    if (status == kCLAuthorizationStatusAuthorizedAlways)
+      provider_->OnPermissionGranted();
+  } else {
+    if (status == kCLAuthorizationStatusAuthorized)
+      provider_->OnPermissionGranted();
+  }
+}
+
+- (void)locationManager:(CLLocationManager*)manager
+     didUpdateLocations:(NSArray*)locations {
+  if (provider_)
+    provider_->DidUpdatePosition([locations lastObject]);
+}
+
+@end
+
+namespace device {
+
+CoreLocationProvider::CoreLocationProvider() {
+  location_manager_.reset([[CLLocationManager alloc] init]);
+  delegate_.reset([[LocationDelegate alloc]
+      initWithProvider:weak_ptr_factory_.GetWeakPtr()]);
+  location_manager_.get().delegate = delegate_;
+}
+
+CoreLocationProvider::~CoreLocationProvider() {
+  StopProvider();
+}
+
+void CoreLocationProvider::SetUpdateCallback(
+    const LocationProviderUpdateCallback& callback) {
+  callback_ = callback;
+}
+
+void CoreLocationProvider::StartProvider(bool high_accuracy) {
+  if (high_accuracy) {
+    location_manager_.get().desiredAccuracy = kCLLocationAccuracyBest;
+  } else {
+    // Using kCLLocationAccuracyHundredMeters for consistency with Android.
+    location_manager_.get().desiredAccuracy = kCLLocationAccuracyHundredMeters;
+  }
+
+  [location_manager_ startUpdatingLocation];
+}
+
+void CoreLocationProvider::StopProvider() {
+  [location_manager_ stopUpdatingLocation];
+}
+
+const mojom::Geoposition& CoreLocationProvider::GetPosition() {
+  return last_position_;
+}
+
+void CoreLocationProvider::OnPermissionGranted() {
+  // Nothing to do here.
+}
+
+void CoreLocationProvider::DidUpdatePosition(CLLocation* location) {
+  // The error values in CLLocation correlate exactly to our error values.
+  last_position_.latitude = location.coordinate.latitude;
+  last_position_.longitude = location.coordinate.longitude;
+  last_position_.timestamp =
+      base::Time::FromDoubleT(location.timestamp.timeIntervalSince1970);
+  last_position_.altitude = location.altitude;
+  last_position_.accuracy = location.horizontalAccuracy;
+  last_position_.altitude_accuracy = location.verticalAccuracy;
+  last_position_.speed = location.speed;
+  last_position_.heading = location.course;
+
+  callback_.Run(this, last_position_);
+}
+
+void CoreLocationProvider::SetManagerForTesting(
+    CLLocationManager* location_manager) {
+  location_manager_.reset(location_manager);
+  location_manager_.get().delegate = delegate_;
+}
+
+// static
+std::unique_ptr<LocationProvider> NewSystemLocationProvider() {
+  if (!base::FeatureList::IsEnabled(features::kMacCoreLocationImplementation))
+    return nullptr;
+
+  return std::make_unique<CoreLocationProvider>();
+}
+
+}  // namespace device
diff --git a/services/device/geolocation/core_location_provider_unittest.mm b/services/device/geolocation/core_location_provider_unittest.mm
new file mode 100644
index 0000000..4f3d4ce
--- /dev/null
+++ b/services/device/geolocation/core_location_provider_unittest.mm
@@ -0,0 +1,153 @@
+// Copyright 2020 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.
+
+#include "services/device/geolocation/core_location_provider.h"
+
+#include "base/run_loop.h"
+#include "base/test/bind_test_util.h"
+#include "base/test/task_environment.h"
+#include "services/device/public/cpp/geolocation/geoposition.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+@interface FakeCLLocationManager : NSObject {
+  CLLocationAccuracy _desiredAccuracy;
+  id<CLLocationManagerDelegate> _delegate;
+  bool _updating;
+}
+@property(assign, nonatomic) CLLocationAccuracy desiredAccuracy;
+@property(weak, nonatomic) id<CLLocationManagerDelegate> delegate;
+// CLLocationManager implementation.
+- (void)stopUpdatingLocation;
+- (void)startUpdatingLocation;
+- (bool)updating;
+
+// Utility functions.
+- (void)fakeUpdatePosition:(CLLocation*)test_location;
+@end
+
+@implementation FakeCLLocationManager
+@synthesize desiredAccuracy = _desiredAccuracy;
+@synthesize delegate = _delegate;
+- (instancetype)init {
+  self = [super init];
+  _updating = false;
+  return self;
+}
+
+- (void)stopUpdatingLocation {
+  _updating = false;
+}
+
+- (void)startUpdatingLocation {
+  _updating = true;
+}
+
+- (bool)updating {
+  return _updating;
+}
+
+- (void)fakeUpdatePosition:(CLLocation*)testLocation {
+  [_delegate locationManager:(id)self didUpdateLocations:@[ testLocation ]];
+}
+
+@end
+
+namespace device {
+
+class CoreLocationProviderTest : public testing::Test {
+ public:
+  std::unique_ptr<CoreLocationProvider> provider_;
+
+ protected:
+  CoreLocationProviderTest() {}
+
+  void InitializeProvider() {
+    fake_location_manager_ = [[FakeCLLocationManager alloc] init];
+    provider_ = std::make_unique<CoreLocationProvider>();
+    provider_->SetManagerForTesting((id)fake_location_manager_);
+  }
+
+  bool IsUpdating() { return [fake_location_manager_ updating]; }
+
+  // updates the position synchronously
+  void FakeUpdatePosition(CLLocation* location) {
+    [fake_location_manager_ fakeUpdatePosition:location];
+  }
+
+  const mojom::Geoposition& GetLatestPosition() {
+    return provider_->GetPosition();
+  }
+
+  base::test::TaskEnvironment task_environment_;
+  const LocationProvider::LocationProviderUpdateCallback callback_;
+  FakeCLLocationManager* fake_location_manager_;
+};
+
+TEST_F(CoreLocationProviderTest, CreateDestroy) {
+  InitializeProvider();
+  EXPECT_TRUE(provider_);
+  provider_.reset();
+}
+
+TEST_F(CoreLocationProviderTest, StartAndStopUpdating) {
+  InitializeProvider();
+  provider_->StartProvider(/*high_accuracy=*/true);
+  EXPECT_TRUE(IsUpdating());
+  EXPECT_EQ([fake_location_manager_ desiredAccuracy], kCLLocationAccuracyBest);
+  provider_->StopProvider();
+  EXPECT_FALSE(IsUpdating());
+  provider_.reset();
+}
+
+TEST_F(CoreLocationProviderTest, GetPositionUpdates) {
+  InitializeProvider();
+  provider_->StartProvider(/*high_accuracy=*/true);
+  EXPECT_TRUE(IsUpdating());
+  EXPECT_EQ([fake_location_manager_ desiredAccuracy], kCLLocationAccuracyBest);
+
+  // test info
+  double latitude = 147.147;
+  double longitude = 101.101;
+  double altitude = 417.417;
+  double accuracy = 10.5;
+  double altitude_accuracy = 15.5;
+  NSDate* mac_timestamp = [NSDate date];
+
+  CLLocationCoordinate2D coors;
+  coors.latitude = latitude;
+  coors.longitude = longitude;
+  CLLocation* test_mac_location =
+      [[CLLocation alloc] initWithCoordinate:coors
+                                    altitude:altitude
+                          horizontalAccuracy:accuracy
+                            verticalAccuracy:altitude_accuracy
+                                   timestamp:mac_timestamp];
+  mojom::Geoposition test_position;
+  test_position.latitude = latitude;
+  test_position.longitude = longitude;
+  test_position.altitude = altitude;
+  test_position.accuracy = accuracy;
+  test_position.altitude_accuracy = altitude_accuracy;
+  test_position.timestamp =
+      base::Time::FromDoubleT(mac_timestamp.timeIntervalSince1970);
+
+  bool update_callback_called = false;
+  provider_->SetUpdateCallback(
+      base::BindLambdaForTesting([&](const LocationProvider* provider,
+                                     const mojom::Geoposition& position) {
+        update_callback_called = true;
+        EXPECT_TRUE(test_position.Equals(position));
+      }));
+
+  FakeUpdatePosition(test_mac_location);
+
+  EXPECT_TRUE(update_callback_called);
+  EXPECT_TRUE(GetLatestPosition().Equals(test_position));
+
+  provider_->StopProvider();
+  EXPECT_FALSE(IsUpdating());
+  provider_.reset();
+}
+
+}  // namespace device
diff --git a/services/device/geolocation/geolocation_provider_impl.cc b/services/device/geolocation/geolocation_provider_impl.cc
index 829b7caa..26edf4f 100644
--- a/services/device/geolocation/geolocation_provider_impl.cc
+++ b/services/device/geolocation/geolocation_provider_impl.cc
@@ -171,7 +171,11 @@
                           base::Unretained(this));
   } else {
     if (!IsRunning()) {
-      Start();
+      base::Thread::Options options;
+#if defined(OS_MACOSX)
+      options.message_pump_type = base::MessagePumpType::NS_RUNLOOP;
+#endif
+      StartWithOptions(options);
       if (user_did_opt_into_location_services_)
         InformProvidersPermissionGranted();
     }
diff --git a/services/device/geolocation/location_arbitrator.cc b/services/device/geolocation/location_arbitrator.cc
index d399293..394ec28b 100644
--- a/services/device/geolocation/location_arbitrator.cc
+++ b/services/device/geolocation/location_arbitrator.cc
@@ -113,9 +113,8 @@
     return;
   }
 
-  if (url_loader_factory_) {
+  if (url_loader_factory_)
     RegisterProvider(NewNetworkLocationProvider(url_loader_factory_, api_key_));
-  }
 }
 
 void LocationArbitrator::OnLocationUpdate(
@@ -157,7 +156,7 @@
 
 std::unique_ptr<LocationProvider>
 LocationArbitrator::NewSystemLocationProvider() {
-#if defined(OS_MACOSX) || defined(OS_LINUX) || defined(OS_FUCHSIA)
+#if defined(OS_LINUX) || defined(OS_FUCHSIA)
   return nullptr;
 #else
   return device::NewSystemLocationProvider();
diff --git a/services/device/public/cpp/device_features.cc b/services/device/public/cpp/device_features.cc
index 07072a57..c244cb6ab 100644
--- a/services/device/public/cpp/device_features.cc
+++ b/services/device/public/cpp/device_features.cc
@@ -18,5 +18,9 @@
 // LocationProvider instead of the NetworkLocationProvider on Windows.
 const base::Feature kWinrtGeolocationImplementation{
     "WinrtGeolocationImplementation", base::FEATURE_DISABLED_BY_DEFAULT};
+// Enables usage of the CoreLocation API for LocationProvider instead of
+// NetworkLocationProvider for macOS.
+const base::Feature kMacCoreLocationImplementation{
+    "kMacCoreLocationImplementation", base::FEATURE_DISABLED_BY_DEFAULT};
 
 }  // namespace features
diff --git a/services/device/public/cpp/device_features.h b/services/device/public/cpp/device_features.h
index de55d10..dca4398 100644
--- a/services/device/public/cpp/device_features.h
+++ b/services/device/public/cpp/device_features.h
@@ -19,6 +19,8 @@
 DEVICE_FEATURES_EXPORT extern const base::Feature kWinrtSensorsImplementation;
 DEVICE_FEATURES_EXPORT extern const base::Feature
     kWinrtGeolocationImplementation;
+DEVICE_FEATURES_EXPORT extern const base::Feature
+    kMacCoreLocationImplementation;
 
 }  // namespace features
 
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index b843f33..dd87274a 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -38649,6 +38649,7 @@
   <int value="-1344375439" label="ServiceWorkerPaymentApps:disabled"/>
   <int value="-1343259222" label="RegionalLocalesAsDisplayUI:disabled"/>
   <int value="-1342961844" label="InlineUpdateFlow:disabled"/>
+  <int value="-1342829138" label="kMacCoreLocationImplementation:enabled"/>
   <int value="-1342039126" label="AshNewSystemMenu:enabled"/>
   <int value="-1341685799" label="AutofillAssistantDirectActions:enabled"/>
   <int value="-1341092934" label="enable-accelerated-overflow-scroll"/>
@@ -40565,6 +40566,7 @@
   <int value="885971656" label="EnablePlayStoreAppSearch:enabled"/>
   <int value="886907524" label="autoplay-policy"/>
   <int value="887011602" label="enable-spelling-auto-correct"/>
+  <int value="890322192" label="kMacCoreLocationImplementation:disabled"/>
   <int value="892899792" label="MaterialDesignIncognitoNTP:disabled"/>
   <int value="898311758" label="ReaderMode:disabled"/>
   <int value="900614020" label="ContentSuggestionsShowSummary:disabled"/>