geolocation: Implement fallback mechanism when Wi-Fi is Disabled
This change implements a fallback mechanism from `CoreLocationProvider`
to the `NetworkLocationProvider` when Wi-Fi is disabled. The goal is to
continue providing location information even when the
`CoreLocationProvider` fails due to Wi-Fi unavailability.
Key Changes:
- New error code: Adds a new `kWifiDisabled` error code to
`GeopositionErrorCode` to signal the fallback scenario.
- NetworkChangeObserver: Implement
`net::NetworkChangeNotifier::NetworkChangeObserver` in
`SystemGeolocationSourceApple`. Ensure fallback is started after
network status is settled.
- Thread safety: Add thread check in `SystemGeolocationSourceApple` to
ensure critical position methods are running on main thread so all
access to `network_changed_timer_` is safe.
- Code organization: Renanmed from
'GeolocationSystemPermissionManagerDelegate` to
`LocationManagerDelegate` for better readability.
Bug: 346842084
Change-Id: If6368a222785a0f61876bf2d7b0ffaaa2a856aaa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5767326
Reviewed-by: Adam Langley <agl@chromium.org>
Reviewed-by: Alex Gough <ajgo@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Reviewed-by: Nico Weber <thakis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1350850}
diff --git a/services/device/BUILD.gn b/services/device/BUILD.gn
index 45791b6e..59b9abb 100644
--- a/services/device/BUILD.gn
+++ b/services/device/BUILD.gn
@@ -369,7 +369,19 @@
}
if (is_apple) {
- sources += [ "geolocation/core_location_provider_unittest.cc" ]
+ sources += [
+ "geolocation/core_location_provider_unittest.cc",
+ "public/cpp/geolocation/system_geolocation_source_apple_unittest.mm",
+ ]
+ deps += [
+ "//services/device/public/cpp/geolocation",
+ "//third_party/ocmock",
+ ]
+ frameworks = [
+ "CoreLocation.framework",
+ "CoreWLAN.framework",
+ "Foundation.framework",
+ ]
}
# UsbContext is a libusb-specific object.
diff --git a/services/device/DEPS b/services/device/DEPS
index 8b7abc8c..7cd4479 100644
--- a/services/device/DEPS
+++ b/services/device/DEPS
@@ -4,10 +4,12 @@
"+components/system_cpu",
"+components/variations",
"+device",
+ "+net/base",
"+services/device/usb/jni_headers",
"+services/network/public/cpp",
"+services/network/test",
"+testing/gmock/include/gmock/gmock.h",
+ "+third_party/ocmock",
"+ui/gfx/native_widget_types.h",
]
diff --git a/services/device/geolocation/location_provider_manager.cc b/services/device/geolocation/location_provider_manager.cc
index 8543c48a..4e4cee4d 100644
--- a/services/device/geolocation/location_provider_manager.cc
+++ b/services/device/geolocation/location_provider_manager.cc
@@ -122,6 +122,15 @@
platform_location_provider_.reset();
custom_location_provider_.reset();
is_running_ = false;
+
+ // Disable any fallback mechanisms by resetting `provider_manager_mode_ ` when
+ // stopping the provider. This allows new location requests to attempt using
+ // the preferred provider (configured by Finch or Chrome flags). Currently
+ // implemented only for macOS; add other platforms here if they support
+ // fallback.
+#if BUILDFLAG(IS_MAC)
+ provider_manager_mode_ = features::kLocationProviderManagerParam.Get();
+#endif // BUILDFLAG(IS_MAC)
}
void LocationProviderManager::RegisterProvider(LocationProvider& provider) {
@@ -184,9 +193,24 @@
switch (provider_manager_mode_) {
case kHybridPlatform:
- // TODO(crbug.com/346842084): kHybridPlatform mode currently behaves the
- // same as kPlatformOnly. fallback mechanism will not be added until
- // platform provider is fully evaluated.
+ platform_location_provider_result_ = new_result.Clone();
+ // Currently, the fallback mechanism only triggers on macOS when Wi-Fi is
+ // disabled (either before or during position watching).
+ if (new_result->is_error() &&
+ new_result->get_error()->error_code ==
+ device::mojom::GeopositionErrorCode::kWifiDisabled) {
+ provider_manager_mode_ = kHybridFallbackNetwork;
+ platform_location_provider_->StopProvider();
+ platform_location_provider_.reset();
+ network_location_provider_ =
+ NewNetworkLocationProvider(url_loader_factory_, api_key_);
+ RegisterProvider(*network_location_provider_.get());
+ network_location_provider_->StartProvider(enable_high_accuracy_);
+ // Skip location update and wait for the network location provider to
+ // provide an update.
+ return;
+ }
+ break;
case kPlatformOnly:
platform_location_provider_result_ = new_result.Clone();
break;
diff --git a/services/device/geolocation/location_provider_manager.h b/services/device/geolocation/location_provider_manager.h
index f88c746..c60f573 100644
--- a/services/device/geolocation/location_provider_manager.h
+++ b/services/device/geolocation/location_provider_manager.h
@@ -73,7 +73,9 @@
private:
friend class TestingLocationProviderManager;
-
+ friend class GeolocationLocationProviderManagerTest;
+ FRIEND_TEST_ALL_PREFIXES(GeolocationLocationProviderManagerTest,
+ HybridPlatformFallback);
void RegisterProvider(LocationProvider& provider);
// Initializes the appropriate LocationProvider based on the configured mode
diff --git a/services/device/geolocation/location_provider_manager_unittest.cc b/services/device/geolocation/location_provider_manager_unittest.cc
index e93a95d2..684d8a5 100644
--- a/services/device/geolocation/location_provider_manager_unittest.cc
+++ b/services/device/geolocation/location_provider_manager_unittest.cc
@@ -105,14 +105,11 @@
std::unique_ptr<LocationProvider> NewNetworkLocationProvider(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& api_key) override {
- auto provider = std::make_unique<FakeLocationProvider>();
- network_location_provider_ = provider->GetWeakPtr();
- return provider;
+ return std::make_unique<FakeLocationProvider>();
}
std::unique_ptr<LocationProvider> NewSystemLocationProvider() override {
- system_location_provider_ = new FakeLocationProvider;
- return base::WrapUnique(system_location_provider_.get());
+ return std::make_unique<FakeLocationProvider>();
}
mojom::GeolocationDiagnostics::ProviderState state() {
@@ -120,9 +117,6 @@
FillDiagnostics(diagnostics);
return diagnostics.provider_state;
}
-
- base::WeakPtr<FakeLocationProvider> network_location_provider_;
- raw_ptr<FakeLocationProvider> system_location_provider_ = nullptr;
};
class GeolocationLocationProviderManagerTest : public testing::Test {
@@ -169,11 +163,13 @@
}
FakeLocationProvider* network_location_provider() {
- return location_provider_manager_->network_location_provider_.get();
+ return static_cast<FakeLocationProvider*>(
+ location_provider_manager_->network_location_provider_.get());
}
- FakeLocationProvider* system_location_provider() {
- return location_provider_manager_->system_location_provider_;
+ FakeLocationProvider* platform_location_provider() {
+ return static_cast<FakeLocationProvider*>(
+ location_provider_manager_->platform_location_provider_.get());
}
// Configure the `kLocationProviderManager` feature for testing with a
@@ -232,7 +228,7 @@
// Can't check the provider has been notified without going through the
// motions to create the provider (see next test).
EXPECT_FALSE(network_location_provider());
- EXPECT_FALSE(system_location_provider());
+ EXPECT_FALSE(platform_location_provider());
}
#if !BUILDFLAG(IS_ANDROID)
@@ -244,11 +240,11 @@
ASSERT_TRUE(location_provider_manager_);
EXPECT_FALSE(network_location_provider());
- EXPECT_FALSE(system_location_provider());
+ EXPECT_FALSE(platform_location_provider());
location_provider_manager_->StartProvider(false);
ASSERT_TRUE(network_location_provider());
- EXPECT_FALSE(system_location_provider());
+ EXPECT_FALSE(platform_location_provider());
EXPECT_EQ(mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy,
network_location_provider()->state());
EXPECT_FALSE(observer_->last_result());
@@ -290,37 +286,37 @@
ASSERT_TRUE(location_provider_manager_);
EXPECT_FALSE(network_location_provider());
- EXPECT_FALSE(system_location_provider());
+ EXPECT_FALSE(platform_location_provider());
location_provider_manager_->StartProvider(false);
EXPECT_FALSE(network_location_provider());
- ASSERT_TRUE(system_location_provider());
+ ASSERT_TRUE(platform_location_provider());
EXPECT_EQ(mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy,
- system_location_provider()->state());
+ platform_location_provider()->state());
EXPECT_FALSE(observer_->last_result());
- SetReferencePosition(system_location_provider());
+ SetReferencePosition(platform_location_provider());
ASSERT_TRUE(observer_->last_result());
if (observer_->last_result()->is_position()) {
- ASSERT_TRUE(system_location_provider()->GetPosition());
+ ASSERT_TRUE(platform_location_provider()->GetPosition());
EXPECT_EQ(
- system_location_provider()->GetPosition()->get_position()->latitude,
+ platform_location_provider()->GetPosition()->get_position()->latitude,
observer_->last_result()->get_position()->latitude);
}
- EXPECT_FALSE(system_location_provider()->is_permission_granted());
+ EXPECT_FALSE(platform_location_provider()->is_permission_granted());
EXPECT_FALSE(location_provider_manager_->HasPermissionBeenGrantedForTest());
location_provider_manager_->OnPermissionGranted();
EXPECT_TRUE(location_provider_manager_->HasPermissionBeenGrantedForTest());
- EXPECT_TRUE(system_location_provider()->is_permission_granted());
+ EXPECT_TRUE(platform_location_provider()->is_permission_granted());
// In `kPlatformOnly` mode, an error position is reported directly.
- SetErrorPosition(system_location_provider(),
+ SetErrorPosition(platform_location_provider(),
mojom::GeopositionErrorCode::kPositionUnavailable);
- EXPECT_TRUE(system_location_provider()->GetPosition()->is_error());
+ EXPECT_TRUE(platform_location_provider()->GetPosition()->is_error());
EXPECT_TRUE(observer_->last_result()->is_error());
- EXPECT_EQ(system_location_provider()->GetPosition()->get_error(),
+ EXPECT_EQ(platform_location_provider()->GetPosition()->get_error(),
observer_->last_result()->get_error());
}
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
@@ -339,11 +335,11 @@
ASSERT_TRUE(location_provider_manager_);
EXPECT_FALSE(network_location_provider());
- EXPECT_FALSE(system_location_provider());
+ EXPECT_FALSE(platform_location_provider());
location_provider_manager_->StartProvider(false);
EXPECT_FALSE(network_location_provider());
- EXPECT_FALSE(system_location_provider());
+ EXPECT_FALSE(platform_location_provider());
EXPECT_EQ(mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy,
fake_location_provider->state());
EXPECT_FALSE(observer_->last_result());
@@ -372,7 +368,7 @@
url_loader_factory_);
location_provider_manager_->StartProvider(false);
ASSERT_TRUE(network_location_provider());
- EXPECT_FALSE(system_location_provider());
+ EXPECT_FALSE(platform_location_provider());
EXPECT_EQ(mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy,
network_location_provider()->state());
SetReferencePosition(network_location_provider());
@@ -384,10 +380,10 @@
}
#endif // !BUILDFLAG(IS_ANDROID)
-#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
-// TODO(crbug.com/346842084): kHybridPlatform mode currently behaves the
-// same as kPlatformOnly. fallback mechanism will not be added until platform
-// provider is fully evaluated.
+#if BUILDFLAG(IS_MAC)
+// This test fallback mechanism by simulating a `kWifiDisabled` error code
+// reported from platform location provider. Fallback is currently only
+// supported on macOS.
TEST_F(GeolocationLocationProviderManagerTest, HybridPlatformFallback) {
ASSERT_TRUE(
SetExperimentMode(mojom::LocationProviderManagerMode::kHybridPlatform));
@@ -396,37 +392,33 @@
ASSERT_TRUE(location_provider_manager_);
EXPECT_FALSE(network_location_provider());
- EXPECT_FALSE(system_location_provider());
+ EXPECT_FALSE(platform_location_provider());
location_provider_manager_->StartProvider(false);
EXPECT_FALSE(network_location_provider());
- ASSERT_TRUE(system_location_provider());
+ ASSERT_TRUE(platform_location_provider());
EXPECT_EQ(mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy,
- system_location_provider()->state());
+ platform_location_provider()->state());
EXPECT_FALSE(observer_->last_result());
- SetReferencePosition(system_location_provider());
+ // Simulate a `kWifiDisabled` error which will initiate fallback mode.
+ SetErrorPosition(platform_location_provider(),
+ mojom::GeopositionErrorCode::kWifiDisabled);
- ASSERT_TRUE(observer_->last_result());
- if (observer_->last_result()->is_position()) {
- ASSERT_TRUE(system_location_provider()->GetPosition());
- EXPECT_EQ(
- system_location_provider()->GetPosition()->get_position()->latitude,
- observer_->last_result()->get_position()->latitude);
- }
+ EXPECT_EQ(mojom::LocationProviderManagerMode::kHybridFallbackNetwork,
+ location_provider_manager_->provider_manager_mode_);
+ ASSERT_FALSE(observer_->last_result());
+ EXPECT_FALSE(platform_location_provider());
+ EXPECT_TRUE(network_location_provider());
+ EXPECT_EQ(mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy,
+ network_location_provider()->state());
- SetErrorPosition(system_location_provider(),
- mojom::GeopositionErrorCode::kPositionUnavailable);
- EXPECT_TRUE(system_location_provider()->GetPosition()->is_error());
- EXPECT_TRUE(observer_->last_result()->is_error());
- EXPECT_EQ(system_location_provider()->GetPosition()->get_error(),
- observer_->last_result()->get_error());
-
- // Ensure that network location provider is not created since fallback
- // mechanism isn't implemented yet.
- EXPECT_FALSE(network_location_provider());
+ // Stop provider and ensure that provider manager mode is reset.
+ location_provider_manager_->StopProvider();
+ EXPECT_EQ(mojom::LocationProviderManagerMode::kHybridPlatform,
+ location_provider_manager_->provider_manager_mode_);
}
-#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
+#endif // BUILDFLAG(IS_MAC)
} // namespace device
diff --git a/services/device/public/cpp/geolocation/BUILD.gn b/services/device/public/cpp/geolocation/BUILD.gn
index 4d5f706..e3e34f7 100644
--- a/services/device/public/cpp/geolocation/BUILD.gn
+++ b/services/device/public/cpp/geolocation/BUILD.gn
@@ -28,11 +28,14 @@
}
if (is_apple) {
sources += [
+ "location_manager_delegate.h",
+ "location_manager_delegate.mm",
"system_geolocation_source_apple.h",
"system_geolocation_source_apple.mm",
]
frameworks = [
"CoreLocation.framework",
+ "CoreWLAN.framework",
"Foundation.framework",
]
}
diff --git a/services/device/public/cpp/geolocation/location_manager_delegate.h b/services/device/public/cpp/geolocation/location_manager_delegate.h
new file mode 100644
index 0000000..7c7fbef
--- /dev/null
+++ b/services/device/public/cpp/geolocation/location_manager_delegate.h
@@ -0,0 +1,40 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_DEVICE_PUBLIC_CPP_GEOLOCATION_LOCATION_MANAGER_DELEGATE_H_
+#define SERVICES_DEVICE_PUBLIC_CPP_GEOLOCATION_LOCATION_MANAGER_DELEGATE_H_
+
+#import <CoreLocation/CoreLocation.h>
+#import <Foundation/Foundation.h>
+
+#include "base/memory/weak_ptr.h"
+
+namespace device {
+
+class SystemGeolocationSourceApple;
+
+} // namespace device
+
+@interface LocationManagerDelegate : NSObject <CLLocationManagerDelegate> {
+ BOOL _permissionInitialized;
+ BOOL _hasPermission;
+ base::WeakPtr<device::SystemGeolocationSourceApple> _manager;
+}
+
+- (instancetype)initWithManager:
+ (base::WeakPtr<device::SystemGeolocationSourceApple>)manager;
+
+// CLLocationManagerDelegate.
+- (void)locationManager:(CLLocationManager*)manager
+ didUpdateLocations:(NSArray*)locations;
+- (void)locationManager:(CLLocationManager*)manager
+ didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
+- (void)locationManager:(CLLocationManager*)manager
+ didFailWithError:(NSError*)error;
+
+- (BOOL)hasPermission;
+- (BOOL)permissionInitialized;
+@end
+
+#endif // SERVICES_DEVICE_PUBLIC_CPP_GEOLOCATION_LOCATION_MANAGER_DELEGATE_H_
diff --git a/services/device/public/cpp/geolocation/location_manager_delegate.mm b/services/device/public/cpp/geolocation/location_manager_delegate.mm
new file mode 100644
index 0000000..0004e8c
--- /dev/null
+++ b/services/device/public/cpp/geolocation/location_manager_delegate.mm
@@ -0,0 +1,139 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/device/public/cpp/geolocation/location_manager_delegate.h"
+
+#include "base/metrics/histogram_functions.h"
+#include "services/device/public/cpp/geolocation/system_geolocation_source_apple.h"
+
+@implementation LocationManagerDelegate
+
+- (instancetype)initWithManager:
+ (base::WeakPtr<device::SystemGeolocationSourceApple>)manager {
+ if ((self = [super init])) {
+ _permissionInitialized = NO;
+ _hasPermission = NO;
+ _manager = manager;
+ }
+ return self;
+}
+
+- (void)locationManager:(CLLocationManager*)manager
+ didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
+ if (status == kCLAuthorizationStatusNotDetermined) {
+ _permissionInitialized = NO;
+ return;
+ }
+ _permissionInitialized = YES;
+ if (status == kCLAuthorizationStatusAuthorizedAlways) {
+ _hasPermission = YES;
+ } else {
+ _hasPermission = NO;
+ }
+
+#if BUILDFLAG(IS_IOS)
+ if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
+ _hasPermission = YES;
+ }
+#endif
+
+ _manager->PermissionUpdated();
+}
+
+- (BOOL)hasPermission {
+ return _hasPermission;
+}
+
+- (BOOL)permissionInitialized {
+ return _permissionInitialized;
+}
+
+- (void)locationManager:(CLLocationManager*)manager
+ didUpdateLocations:(NSArray*)locations {
+ CLLocation* location = [locations lastObject];
+ device::mojom::Geoposition position;
+ position.latitude = location.coordinate.latitude;
+ position.longitude = location.coordinate.longitude;
+ position.timestamp = base::Time::FromSecondsSinceUnixEpoch(
+ location.timestamp.timeIntervalSince1970);
+ position.altitude = location.altitude;
+ position.accuracy = location.horizontalAccuracy;
+ position.altitude_accuracy = location.verticalAccuracy;
+ position.speed = location.speed;
+ position.heading = location.course;
+
+ _manager->PositionUpdated(position);
+}
+
+- (void)locationManager:(CLLocationManager*)manager
+ didFailWithError:(NSError*)error {
+ base::UmaHistogramSparse("Geolocation.CoreLocationProvider.ErrorCode",
+ static_cast<int>(error.code));
+ device::mojom::GeopositionError position_error;
+ switch (error.code) {
+ case kCLErrorDenied:
+ position_error.error_code =
+ device::mojom::GeopositionErrorCode::kPermissionDenied;
+ position_error.error_message =
+ device::mojom::kGeoPermissionDeniedErrorMessage;
+ position_error.error_technical =
+ "CoreLocationProvider: CoreLocation framework reported a "
+ "kCLErrorDenied failure.";
+ break;
+ case kCLErrorPromptDeclined:
+ position_error.error_code =
+ device::mojom::GeopositionErrorCode::kPermissionDenied;
+ position_error.error_message =
+ device::mojom::kGeoPermissionDeniedErrorMessage;
+ position_error.error_technical =
+ "CoreLocationProvider: CoreLocation framework reported a "
+ "kCLErrorPromptDeclined failure.";
+ break;
+ case kCLErrorLocationUnknown: {
+ if (!_manager->WasWifiEnabled()) {
+ // If Wi-Fi was already disabled when `StartWatchingPosition` was
+ // called, we can immediately trigger the fallback mechanism by
+ // reporting a `kWifiDisabled` error.
+ position_error.error_code =
+ device::mojom::GeopositionErrorCode::kWifiDisabled;
+ } else if (!_manager->IsWifiEnabled()) {
+ // If Wi-Fi was enabled but is now disabled when
+ // `kCLErrorLocationUnknown` is reported, initiate the fallback
+ // mechanism after the network changed event fires. This ensures the
+ // initial network request doesn't fail due to an unsettled network
+ // state.
+ _manager->StartNetworkChangedTimer();
+ return;
+ } else {
+ // In all other cases, let the `kPositionUnavailable` error propagate.
+ position_error.error_code =
+ device::mojom::GeopositionErrorCode::kPositionUnavailable;
+ }
+ position_error.error_message =
+ device::mojom::kGeoPositionUnavailableErrorMessage;
+ position_error.error_technical =
+ "CoreLocationProvider: CoreLocation framework reported a "
+ "kCLErrorLocationUnknown failure.";
+ break;
+ }
+ case kCLErrorNetwork:
+ position_error.error_code =
+ device::mojom::GeopositionErrorCode::kPositionUnavailable;
+ position_error.error_message =
+ device::mojom::kGeoPositionUnavailableErrorMessage;
+ position_error.error_technical =
+ "CoreLocationProvider: CoreLocation framework reported a "
+ "kCLErrorNetwork failure.";
+ break;
+ default:
+ // For non-critical errors (heading, ranging, or region monitoring),
+ // return immediately without setting the position_error, as they may not
+ // be relevant to the caller.
+ return;
+ }
+
+ _manager->PositionError(position_error);
+}
+
+@end
diff --git a/services/device/public/cpp/geolocation/system_geolocation_source_apple.h b/services/device/public/cpp/geolocation/system_geolocation_source_apple.h
index df337c1..18950015 100644
--- a/services/device/public/cpp/geolocation/system_geolocation_source_apple.h
+++ b/services/device/public/cpp/geolocation/system_geolocation_source_apple.h
@@ -6,20 +6,35 @@
#define SERVICES_DEVICE_PUBLIC_CPP_GEOLOCATION_SYSTEM_GEOLOCATION_SOURCE_APPLE_H_
#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "net/base/network_change_notifier.h"
#include "services/device/public/cpp/geolocation/geolocation_system_permission_manager.h"
#include "services/device/public/cpp/geolocation/system_geolocation_source.h"
-@class GeolocationSystemPermissionManagerDelegate;
@class CLLocationManager;
+@class LocationManagerDelegate;
namespace device {
class COMPONENT_EXPORT(GEOLOCATION) SystemGeolocationSourceApple
- : public SystemGeolocationSource {
+ : public SystemGeolocationSource,
+ public net::NetworkChangeNotifier::NetworkChangeObserver {
public:
static std::unique_ptr<GeolocationSystemPermissionManager>
CreateGeolocationSystemPermissionManager();
+ // Checks if Wi-Fi is currently enabled on the device.
+ static bool IsWifiEnabled();
+
+ // Sets the `mock_wifi_status_` for testing purposes.
+ static void SetWifiStatusForTesting(bool mock_wifi_status) {
+ mock_wifi_status_ = mock_wifi_status;
+ }
+
+ // The maximum time to wait for a network status change event before
+ // triggering a timeout.
+ static constexpr unsigned int kNetworkChangeTimeoutMs = 1000;
+
SystemGeolocationSourceApple();
~SystemGeolocationSourceApple() override;
@@ -38,19 +53,62 @@
void AddPositionUpdateObserver(PositionObserver* observer) override;
void RemovePositionUpdateObserver(PositionObserver* observer) override;
+
+ // `StartWatchingPosition` and `StopWatchingPosition` will be called from
+ // `CoreLocationProvider` on geolocation thread to start / stop watching
+ // position.
void StartWatchingPosition(bool high_accuracy) override;
void StopWatchingPosition() override;
+
void RequestPermission() override;
void OpenSystemPermissionSetting() override;
+ // Actual implementations for Start/Stop-WatchingPosition.
+ // These methods are called on the main thread to ensure thread-safety.
+ void StartWatchingPositionInternal(bool high_accuracy);
+ void StopWatchingPositionInternal();
+
+ // net::NetworkChangeNotifier::NetworkChangeObserver implementation.
+ void OnNetworkChanged(
+ net::NetworkChangeNotifier::ConnectionType type) override;
+
+ // Indicates whether Wi-Fi was enabled when the last position watch was
+ // started.
+ bool WasWifiEnabled() { return was_wifi_enabled_; }
+
+ // Start the timer that wait for network status change event with a connection
+ // type other than `net::NetworkChangeNotifier::CONNECTION_NONE`.
+ void StartNetworkChangedTimer();
+
+ // Invoked when the `network_changed_timer_` expires, indicating a timeout
+ // while waiting for a network status change event.
+ void OnNetworkChangedTimeout();
+
+ // Sets the `location_manager_` for testing purposes.
+ void SetLocationManagerForTesting(CLLocationManager* manager) {
+ location_manager_ = manager;
+ }
+
+ // Gets the location manager delegate for testing.
+ LocationManagerDelegate* GetDelegateForTesting() { return delegate_; }
+
private:
+ friend class SystemGeolocationSourceAppleTest;
+
+ // Mock wifi status only used for testing.
+ static std::optional<bool> mock_wifi_status_;
LocationSystemPermissionStatus GetSystemPermission() const;
- GeolocationSystemPermissionManagerDelegate* __strong delegate_;
+ LocationManagerDelegate* __strong delegate_;
CLLocationManager* __strong location_manager_;
- SEQUENCE_CHECKER(sequence_checker_);
PermissionUpdateCallback permission_update_callback_;
scoped_refptr<PositionObserverList> position_observers_;
+ // A reusable timer to detect timeouts when waiting for network status change
+ // events. When the timer expires, `OnNetworkChangedTimeout` is invoked.
+ base::RetainingOneShotTimer network_changed_timer_;
+ const scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
+ // Caches the initial Wi-Fi status at the beginning of watching position.
+ bool was_wifi_enabled_ = false;
base::WeakPtrFactory<SystemGeolocationSourceApple> weak_ptr_factory_{this};
};
diff --git a/services/device/public/cpp/geolocation/system_geolocation_source_apple.mm b/services/device/public/cpp/geolocation/system_geolocation_source_apple.mm
index 1082bb5..da992f34 100644
--- a/services/device/public/cpp/geolocation/system_geolocation_source_apple.mm
+++ b/services/device/public/cpp/geolocation/system_geolocation_source_apple.mm
@@ -4,45 +4,36 @@
#include "services/device/public/cpp/geolocation/system_geolocation_source_apple.h"
-#import <CoreLocation/CoreLocation.h>
+#import <CoreWLAN/CoreWLAN.h>
#include <memory>
#include "base/functional/callback_helpers.h"
#include "base/mac/mac_util.h"
#include "base/metrics/histogram_functions.h"
-#include "base/sequence_checker.h"
#include "build/build_config.h"
-
-@interface GeolocationSystemPermissionManagerDelegate
- : NSObject <CLLocationManagerDelegate> {
- BOOL _permissionInitialized;
- BOOL _hasPermission;
- base::WeakPtr<device::SystemGeolocationSourceApple> _manager;
-}
-
-- (instancetype)initWithManager:
- (base::WeakPtr<device::SystemGeolocationSourceApple>)manager;
-
-// CLLocationManagerDelegate
-- (void)locationManager:(CLLocationManager*)manager
- didUpdateLocations:(NSArray*)locations;
-- (void)locationManager:(CLLocationManager*)manager
- didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
-- (void)locationManager:(CLLocationManager*)manager
- didFailWithError:(NSError*)error;
-
-- (BOOL)hasPermission;
-- (BOOL)permissionInitialized;
-@end
+#include "services/device/public/cpp/geolocation/location_manager_delegate.h"
namespace device {
+std::optional<bool> SystemGeolocationSourceApple::mock_wifi_status_;
+
+// static
+bool SystemGeolocationSourceApple::IsWifiEnabled() {
+ if (mock_wifi_status_.has_value()) {
+ return *mock_wifi_status_;
+ }
+ CWWiFiClient* wifi_client = [CWWiFiClient sharedWiFiClient];
+ CWInterface* interface = wifi_client.interface;
+ return (interface && interface.powerOn);
+}
+
SystemGeolocationSourceApple::SystemGeolocationSourceApple()
: location_manager_([[CLLocationManager alloc] init]),
permission_update_callback_(base::DoNothing()),
- position_observers_(base::MakeRefCounted<PositionObserverList>()) {
- delegate_ = [[GeolocationSystemPermissionManagerDelegate alloc]
+ position_observers_(base::MakeRefCounted<PositionObserverList>()),
+ main_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) {
+ delegate_ = [[LocationManagerDelegate alloc]
initWithManager:weak_ptr_factory_.GetWeakPtr()];
location_manager_.delegate = delegate_;
}
@@ -68,17 +59,28 @@
void SystemGeolocationSourceApple::PositionUpdated(
const mojom::Geoposition& position) {
+ CHECK(main_task_runner_->BelongsToCurrentThread());
position_observers_->Notify(FROM_HERE, &PositionObserver::OnPositionUpdated,
position);
}
void SystemGeolocationSourceApple::PositionError(
const mojom::GeopositionError& error) {
+ CHECK(main_task_runner_->BelongsToCurrentThread());
+ // If an error reported from `LocationManagerDelegate` when
+ // network change timer is running. Stop the timer (which also cancel the
+ // pending fallback) and report that error.
+ if (network_changed_timer_.IsRunning()) {
+ network_changed_timer_.Stop();
+ net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
+ }
position_observers_->Notify(FROM_HERE, &PositionObserver::OnPositionError,
error);
}
-void SystemGeolocationSourceApple::StartWatchingPosition(bool high_accuracy) {
+void SystemGeolocationSourceApple::StartWatchingPositionInternal(
+ bool high_accuracy) {
+ CHECK(main_task_runner_->BelongsToCurrentThread());
if (high_accuracy) {
location_manager_.desiredAccuracy = kCLLocationAccuracyBest;
} else {
@@ -86,15 +88,39 @@
location_manager_.desiredAccuracy = kCLLocationAccuracyHundredMeters;
}
[location_manager_ startUpdatingLocation];
+ was_wifi_enabled_ = IsWifiEnabled();
+}
+
+void SystemGeolocationSourceApple::StartWatchingPosition(bool high_accuracy) {
+ main_task_runner_->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ &SystemGeolocationSourceApple::StartWatchingPositionInternal,
+ weak_ptr_factory_.GetWeakPtr(), high_accuracy));
+}
+
+void SystemGeolocationSourceApple::StopWatchingPositionInternal() {
+ CHECK(main_task_runner_->BelongsToCurrentThread());
+ [location_manager_ stopUpdatingLocation];
+ // If `StopWatchingPosition` is called for any reason, stop the network status
+ // event timer (which also cancel the pending fallback).
+ if (network_changed_timer_.IsRunning()) {
+ network_changed_timer_.Stop();
+ net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
+ }
}
void SystemGeolocationSourceApple::StopWatchingPosition() {
- [location_manager_ stopUpdatingLocation];
+ main_task_runner_->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ &SystemGeolocationSourceApple::StopWatchingPositionInternal,
+ weak_ptr_factory_.GetWeakPtr()));
}
-LocationSystemPermissionStatus SystemGeolocationSourceApple::GetSystemPermission()
- const {
- DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+LocationSystemPermissionStatus
+SystemGeolocationSourceApple::GetSystemPermission() const {
+ CHECK(main_task_runner_->BelongsToCurrentThread());
if (![delegate_ permissionInitialized]) {
return LocationSystemPermissionStatus::kNotDetermined;
}
@@ -127,118 +153,61 @@
position_observers_->RemoveObserver(observer);
}
-} // namespace device
+void SystemGeolocationSourceApple::OnNetworkChanged(
+ net::NetworkChangeNotifier::ConnectionType type) {
+ CHECK(main_task_runner_->BelongsToCurrentThread());
+ // When Wi-Fi is disabled, we initially receive a
+ // `net::NetworkChangeNotifier::CONNECTION_NONE` event. Subsequently, after
+ // the network stabilizes, another network change event will be triggered
+ // (potentially any connection type except
+ // `net::NetworkChangeNotifier::CONNECTION_NONE`).
+ if (type != net::NetworkChangeNotifier::CONNECTION_NONE &&
+ network_changed_timer_.IsRunning()) {
+ network_changed_timer_.Stop();
+ net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
-@implementation GeolocationSystemPermissionManagerDelegate
-
-- (instancetype)initWithManager:
- (base::WeakPtr<device::SystemGeolocationSourceApple>)manager {
- if ((self = [super init])) {
- _permissionInitialized = NO;
- _hasPermission = NO;
- _manager = manager;
+ device::mojom::GeopositionError position_error;
+ position_error.error_code =
+ device::mojom::GeopositionErrorCode::kWifiDisabled;
+ position_error.error_message =
+ device::mojom::kGeoPositionUnavailableErrorMessage;
+ position_error.error_technical =
+ "CoreLocationProvider: CoreLocation framework reported a "
+ "kCLErrorLocationUnknown failure.";
+ PositionError(position_error);
}
- return self;
}
-- (void)locationManager:(CLLocationManager*)manager
- didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
- if (status == kCLAuthorizationStatusNotDetermined) {
- _permissionInitialized = NO;
+void SystemGeolocationSourceApple::StartNetworkChangedTimer() {
+ CHECK(main_task_runner_->BelongsToCurrentThread());
+ if (network_changed_timer_.IsRunning()) {
return;
}
- _permissionInitialized = YES;
- if (status == kCLAuthorizationStatusAuthorizedAlways) {
- _hasPermission = YES;
- } else {
- _hasPermission = NO;
- }
-#if BUILDFLAG(IS_IOS)
- if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
- _hasPermission = YES;
- }
-#endif
+ net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
- _manager->PermissionUpdated();
+ // This timer prevents premature fallback initiation if the network state is
+ // unstable. Empirically, network change events occur ~600-700ms after Wi-Fi
+ // is disabled. A 1-second timeout accommodates potential variations across
+ // different machines.
+ network_changed_timer_.Start(
+ FROM_HERE, base::Milliseconds(kNetworkChangeTimeoutMs), this,
+ &SystemGeolocationSourceApple::OnNetworkChangedTimeout);
}
-- (BOOL)hasPermission {
- return _hasPermission;
-}
+void SystemGeolocationSourceApple::OnNetworkChangedTimeout() {
+ CHECK(main_task_runner_->BelongsToCurrentThread());
+ net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
-- (BOOL)permissionInitialized {
- return _permissionInitialized;
-}
-
-- (void)locationManager:(CLLocationManager*)manager
- didUpdateLocations:(NSArray*)locations {
- CLLocation* location = [locations lastObject];
- device::mojom::Geoposition position;
- position.latitude = location.coordinate.latitude;
- position.longitude = location.coordinate.longitude;
- position.timestamp = base::Time::FromSecondsSinceUnixEpoch(
- location.timestamp.timeIntervalSince1970);
- position.altitude = location.altitude;
- position.accuracy = location.horizontalAccuracy;
- position.altitude_accuracy = location.verticalAccuracy;
- position.speed = location.speed;
- position.heading = location.course;
-
- _manager->PositionUpdated(position);
-}
-
-- (void)locationManager:(CLLocationManager*)manager
- didFailWithError:(NSError*)error {
- base::UmaHistogramSparse("Geolocation.CoreLocationProvider.ErrorCode",
- static_cast<int>(error.code));
device::mojom::GeopositionError position_error;
-
- switch (error.code) {
- case kCLErrorDenied:
- position_error.error_code =
- device::mojom::GeopositionErrorCode::kPermissionDenied;
- position_error.error_message =
- device::mojom::kGeoPermissionDeniedErrorMessage;
- position_error.error_technical =
- "CoreLocationProvider: CoreLocation framework reported a "
- "kCLErrorDenied failure.";
- break;
- case kCLErrorPromptDeclined:
- position_error.error_code =
- device::mojom::GeopositionErrorCode::kPermissionDenied;
- position_error.error_message =
- device::mojom::kGeoPermissionDeniedErrorMessage;
- position_error.error_technical =
- "CoreLocationProvider: CoreLocation framework reported a "
- "kCLErrorPromptDeclined failure.";
- break;
- case kCLErrorLocationUnknown:
- position_error.error_code =
- device::mojom::GeopositionErrorCode::kPositionUnavailable;
- position_error.error_message =
- device::mojom::kGeoPositionUnavailableErrorMessage;
- position_error.error_technical =
- "CoreLocationProvider: CoreLocation framework reported a "
- "kCLErrorLocationUnknown failure.";
- break;
- case kCLErrorNetwork:
- position_error.error_code =
- device::mojom::GeopositionErrorCode::kPositionUnavailable;
- position_error.error_message =
- device::mojom::kGeoPositionUnavailableErrorMessage;
- position_error.error_technical =
- "CoreLocationProvider: CoreLocation framework reported a "
- "kCLErrorNetwork failure.";
- break;
- default:
- // For non-critical errors (heading, ranging, or region monitoring),
- // return immediately without setting the position_error, as they may not
- // be relevant to the caller.
- return;
- }
-
- _manager->PositionError(position_error);
+ position_error.error_code =
+ device::mojom::GeopositionErrorCode::kPositionUnavailable;
+ position_error.error_message =
+ device::mojom::kGeoPositionUnavailableErrorMessage;
+ position_error.error_technical =
+ "CoreLocationProvider: CoreLocation framework reported a "
+ "kCLErrorLocationUnknown failure.";
+ PositionError(position_error);
}
-@end
+} // namespace device
diff --git a/services/device/public/cpp/geolocation/system_geolocation_source_apple_unittest.mm b/services/device/public/cpp/geolocation/system_geolocation_source_apple_unittest.mm
new file mode 100644
index 0000000..25f16b9c
--- /dev/null
+++ b/services/device/public/cpp/geolocation/system_geolocation_source_apple_unittest.mm
@@ -0,0 +1,325 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/device/public/cpp/geolocation/system_geolocation_source_apple.h"
+
+#include <memory>
+
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "net/base/mock_network_change_notifier.h"
+#include "services/device/public/cpp/geolocation/location_manager_delegate.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/ocmock/OCMock/OCMock.h"
+#include "third_party/ocmock/gtest_support.h"
+
+namespace device {
+
+namespace {
+
+class MockObserver : public device::SystemGeolocationSource::PositionObserver {
+ public:
+ MOCK_METHOD(void,
+ OnPositionUpdated,
+ (const device::mojom::Geoposition&),
+ (override));
+ MOCK_METHOD(void,
+ OnPositionError,
+ (const device::mojom::GeopositionError&),
+ (override));
+};
+
+} // namespace
+
+class SystemGeolocationSourceAppleTest : public testing::Test {
+ public:
+ void SetUp() override {
+ // Create `SystemGeolocationSourceApple` for testing.
+ source_ = std::make_unique<device::SystemGeolocationSourceApple>();
+ delegate_ = source_->GetDelegateForTesting();
+
+ // Create mocks.
+ mock_core_location_manager_ = OCMClassMock([CLLocationManager class]);
+ source_->SetLocationManagerForTesting(mock_core_location_manager_);
+ mock_network_notifier_ = net::test::MockNetworkChangeNotifier::Create();
+ mock_position_observer_ =
+ std::make_unique<::testing::StrictMock<MockObserver>>();
+ }
+
+ void TearDown() override { task_environment_.RunUntilIdle(); }
+
+ // Simulate CoreLocationProvider::StartProvider call.
+ void SimulateStartProvider() {
+ source_->AddPositionUpdateObserver(mock_position_observer_.get());
+ source_->StartWatchingPosition(/*high_accuracy*/ true);
+ base::RunLoop().RunUntilIdle();
+ }
+
+ // Simulate CoreLocationProvider::StopProvider call.
+ void SimulateStopProvider() {
+ source_->RemovePositionUpdateObserver(mock_position_observer_.get());
+ source_->StopWatchingPosition();
+ base::RunLoop().RunUntilIdle();
+ }
+
+ // Simulate a NetworkChange event with specified connection type.
+ void SetConnectionType(net::NetworkChangeNotifier::ConnectionType type) {
+ mock_network_notifier_->SetConnectionType(type);
+ mock_network_notifier_->NotifyObserversOfNetworkChangeForTests(
+ mock_network_notifier_->GetConnectionType());
+ }
+
+ void SimulateDidFailWithError(CLError error_code) {
+ NSError* ns_error = [NSError errorWithDomain:kCLErrorDomain
+ code:error_code
+ userInfo:nil];
+ [delegate_ locationManager:nil didFailWithError:ns_error];
+ }
+
+ protected:
+ base::test::SingleThreadTaskEnvironment task_environment_;
+
+ // Real objects for testing.
+ std::unique_ptr<device::SystemGeolocationSourceApple> source_;
+ LocationManagerDelegate* delegate_;
+
+ // Mocks.
+ std::unique_ptr<MockObserver> mock_position_observer_;
+ CLLocationManager* mock_core_location_manager_;
+ std::unique_ptr<net::test::MockNetworkChangeNotifier> mock_network_notifier_;
+};
+
+// This test verifies that when Wi-Fi is always on and `kCLErrorLocationUnknown`
+// is received, the error is propagated directly without fallback.
+TEST_F(SystemGeolocationSourceAppleTest, ErrorLocationUnknownWhenWifiAlwaysOn) {
+ // Simulate the Wi-Fi is enabled all the time.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(true);
+
+ OCMExpect([mock_core_location_manager_ startUpdatingLocation]);
+ SimulateStartProvider();
+ EXPECT_OCMOCK_VERIFY((id)mock_core_location_manager_);
+
+ SimulateDidFailWithError(kCLErrorLocationUnknown);
+
+ // Expect that non-fallback-able error code `kPositionUnavailable` is
+ // notified.
+ base::test::TestFuture<const device::mojom::GeopositionError&> error_future;
+ EXPECT_CALL(*mock_position_observer_, OnPositionError)
+ .WillOnce(base::test::InvokeFuture(error_future));
+ // Wait for position observers to be notified.
+ EXPECT_EQ(error_future.Get().error_code,
+ device::mojom::GeopositionErrorCode::kPositionUnavailable);
+}
+
+// This test verifies that when Wi-Fi is disabled initially, the fallback
+// mechanism is initiated directly.
+TEST_F(SystemGeolocationSourceAppleTest, FallbackWhenWifiDisabledInitially) {
+ // Simulate the Wi-Fi is disabled before starting position watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(false);
+
+ OCMExpect([mock_core_location_manager_ startUpdatingLocation]);
+ SimulateStartProvider();
+ EXPECT_OCMOCK_VERIFY((id)mock_core_location_manager_);
+
+ SimulateDidFailWithError(kCLErrorLocationUnknown);
+
+ // Expect that fallback-able error code `kWifiDisabled` is notified.
+ base::test::TestFuture<const device::mojom::GeopositionError&> error_future;
+ EXPECT_CALL(*mock_position_observer_, OnPositionError)
+ .WillOnce(base::test::InvokeFuture(error_future));
+ // Wait for position observers to be notified.
+ EXPECT_EQ(error_future.Get().error_code,
+ device::mojom::GeopositionErrorCode::kWifiDisabled);
+}
+
+// This test verifies that when Wi-Fi is enabled and then disabled, the fallback
+// mechanism is triggered by a network change event.
+TEST_F(SystemGeolocationSourceAppleTest, FallbackTriggeredByNetworkChanged) {
+ // Simulate the Wi-Fi is enabled before start watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(true);
+
+ OCMExpect([mock_core_location_manager_ startUpdatingLocation]);
+ SimulateStartProvider();
+ EXPECT_OCMOCK_VERIFY((id)mock_core_location_manager_);
+
+ // Simulate the Wi-Fi is disabled during position watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(false);
+
+ SimulateDidFailWithError(kCLErrorLocationUnknown);
+
+ // Expect that fallback-able error code `kWifiDisabled` is notified.
+ base::test::TestFuture<const device::mojom::GeopositionError&> error_future;
+ EXPECT_CALL(*mock_position_observer_, OnPositionError)
+ .WillOnce(base::test::InvokeFuture(error_future));
+
+ // Simulate a network change event sequence: first with `CONNECTION_NONE` to
+ // represent Wi-Fi being disabled, then with `CONNECTION_UNKNOWN` to indicate
+ // a settled network state.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_NONE);
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_UNKNOWN);
+
+ // Wait for position observers to be notified.
+ EXPECT_EQ(error_future.Get().error_code,
+ device::mojom::GeopositionErrorCode::kWifiDisabled);
+}
+
+// This test verifies that if the fallback mechanism times out while waiting
+// for a network change event, a `kPositionUnavailable` error is propagated.
+TEST_F(SystemGeolocationSourceAppleTest, OnNetworkChangedTimeout) {
+ // Simulate the Wi-Fi is enabled before start watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(true);
+
+ OCMExpect([mock_core_location_manager_ startUpdatingLocation]);
+ SimulateStartProvider();
+ EXPECT_OCMOCK_VERIFY((id)mock_core_location_manager_);
+
+ // Simulate the Wi-Fi is disabled during position watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(false);
+
+ SimulateDidFailWithError(kCLErrorLocationUnknown);
+
+ // Simulate a single OnNetworkChanged event with `CONNECTION_NONE` to mimic a
+ // network change without a subsequent settled state.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_NONE);
+
+ // This waits for the network change event timer to expire.
+ // `kNetworkChangeTimeoutMs` is currently set to 1000 milliseconds, so we wait
+ // for 1200 milliseconds to ensure the timer has definitely expired.
+ base::PlatformThreadBase::Sleep(
+ base::Milliseconds(source_->kNetworkChangeTimeoutMs + 200));
+
+ // Expect that `OnPositionError` is called with `kPositionUnavailable` because
+ // the network event timer timeouted.
+ base::test::TestFuture<const device::mojom::GeopositionError&> error_future;
+ EXPECT_CALL(*mock_position_observer_, OnPositionError)
+ .WillOnce(base::test::InvokeFuture(error_future));
+
+ // Wait for timeout handler to be finished.
+ EXPECT_EQ(error_future.Get().error_code,
+ device::mojom::GeopositionErrorCode::kPositionUnavailable);
+
+ // Simulate the second OnNetworkChanged event with `CONNECTION_UNKNOWN` to
+ // mimic a network change settle, but this should be no-op since the timeout
+ // handler has been invoked.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_UNKNOWN);
+
+ // Wait to ensure position observers is not notified by a OnNetworkChanged
+ // event arrived after timeout.
+ base::RunLoop().RunUntilIdle();
+}
+
+// This test verifies that if the fallback timer is canceled by
+// `StopWatchingPosition` call while waiting for a network change event.
+TEST_F(SystemGeolocationSourceAppleTest, FallbackCanceledByStopWatching) {
+ // Simulate the Wi-Fi is enabled before start watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(true);
+
+ OCMExpect([mock_core_location_manager_ startUpdatingLocation]);
+ SimulateStartProvider();
+ EXPECT_OCMOCK_VERIFY((id)mock_core_location_manager_);
+
+ // Simulate the Wi-Fi is disabled during position watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(false);
+
+ SimulateDidFailWithError(kCLErrorLocationUnknown);
+
+ // Simulate first OnNetworkChanged event with `CONNECTION_NONE` when Wi-Fi is
+ // just disabled.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_NONE);
+
+ SimulateStopProvider();
+
+ // Simulate the second OnNetworkChanged event with `CONNECTION_UNKNOWN` to
+ // mimic a network change settle, but this should be no-op since watching
+ // position has been stopped.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_UNKNOWN);
+
+ // Expect that `OnPositionError` is not called because location provider is
+ // already stopped.
+ EXPECT_CALL(*mock_position_observer_, OnPositionError).Times(0);
+
+ // Wait to ensure that `OnPosisionError` is not invoked because provider has
+ // been stopped.
+ base::RunLoop().RunUntilIdle();
+}
+
+// This test verifies that if the fallback is canceled by `didFailWithError`
+// call while waiting for a network change event.
+TEST_F(SystemGeolocationSourceAppleTest, FallbackCanceledByDidFailWithError) {
+ // Simulate the Wi-Fi is enabled before start watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(true);
+
+ OCMExpect([mock_core_location_manager_ startUpdatingLocation]);
+ SimulateStartProvider();
+ EXPECT_OCMOCK_VERIFY((id)mock_core_location_manager_);
+
+ // Simulate the Wi-Fi is disabled during position watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(false);
+
+ // Simulate first NSError wihch is kCLErrorLocationUnknown + Wi-Fi disabled so
+ // it can be fallbacked.
+ SimulateDidFailWithError(kCLErrorLocationUnknown);
+
+ // Simulate first OnNetworkChanged event with `CONNECTION_NONE` when Wi-Fi is
+ // just disabled.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_NONE);
+
+ // Simulate second NSError wihch is critical and can not be fallbacked.
+ SimulateDidFailWithError(kCLErrorDenied);
+
+ // Simulate second OnNetworkChanged event with `CONNECTION_UNKNOWN` when
+ // network change settle, but this should be no-op since the timer has been
+ // canceld.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_UNKNOWN);
+
+ // Expected that `OnPositionError` is only called once with `kPermissionDenie`
+ // instead of `kWifiDisabled`.
+ base::test::TestFuture<const device::mojom::GeopositionError&> error_future;
+ EXPECT_CALL(*mock_position_observer_, OnPositionError)
+ .WillOnce(base::test::InvokeFuture(error_future));
+ // Wait for position observers to be notified.
+ EXPECT_EQ(error_future.Get().error_code,
+ device::mojom::GeopositionErrorCode::kPermissionDenied);
+}
+
+// This test verifies that if the fallback error code is only sent once.
+TEST_F(SystemGeolocationSourceAppleTest, FallbackTriggeredOnlyOnce) {
+ // Simulate the Wi-Fi is enabled before start watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(true);
+
+ OCMExpect([mock_core_location_manager_ startUpdatingLocation]);
+ SimulateStartProvider();
+ EXPECT_OCMOCK_VERIFY((id)mock_core_location_manager_);
+
+ // Simulate the Wi-Fi is disabled during position watching.
+ SystemGeolocationSourceApple::SetWifiStatusForTesting(false);
+
+ SimulateDidFailWithError(kCLErrorLocationUnknown);
+
+ // Simulate first OnNetworkChanged event with `CONNECTION_NONE` when Wi-Fi is
+ // just disabled.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_NONE);
+
+ // Simulate second NSError with same `kCLErrorLocationUnknown` error code.
+ SimulateDidFailWithError(kCLErrorLocationUnknown);
+
+ // Simulate second OnNetworkChanged event with `CONNECTION_UNKNOWN` when
+ // network change settle, but this should be no-op since the timer has been
+ // canceld.
+ SetConnectionType(net::NetworkChangeNotifier::CONNECTION_UNKNOWN);
+
+ // Expected that `OnPositionError` is only called once with `kWifiDisabled`
+ // even with two `didFailWithError` called.
+ base::test::TestFuture<const device::mojom::GeopositionError&> error_future;
+ EXPECT_CALL(*mock_position_observer_, OnPositionError)
+ .WillOnce(base::test::InvokeFuture(error_future));
+
+ // Wait for position observers to be notified.
+ EXPECT_EQ(error_future.Get().error_code,
+ device::mojom::GeopositionErrorCode::kWifiDisabled);
+}
+
+} // namespace device
diff --git a/services/device/public/mojom/geoposition.mojom b/services/device/public/mojom/geoposition.mojom
index a9ada67..e746c46d 100644
--- a/services/device/public/mojom/geoposition.mojom
+++ b/services/device/public/mojom/geoposition.mojom
@@ -49,11 +49,19 @@
mojo_base.mojom.Time timestamp;
};
-// These values follow the W3C geolocation specification and can be returned
-// to JavaScript without the need for a conversion.
+// Error codes shared between W3C geolocation specification and platform
+// specific implementations. W3C codes can be returned to JavaScript without the
+// need for a conversion.
enum GeopositionErrorCode {
+ // W3C geolocation specification defined error codes.
kPermissionDenied = 1,
- kPositionUnavailable = 2
+ kPositionUnavailable = 2,
+ // macOS-specific error codes.
+ // When `LocationProviderManager` received `kWifiDisabled` from
+ // `CoreLocationProvider`, that means Wi-Fi is disabled but user's machine
+ // still has avalailable network (everything other than Wi-Fi). It will then
+ // initiate fallback mechanism and start `NetworkLocationProvider`.
+ kWifiDisabled = 3
};
// A GeopositionError communicates the reason why the Geolocation service could
diff --git a/third_party/blink/renderer/modules/geolocation/geolocation.cc b/third_party/blink/renderer/modules/geolocation/geolocation.cc
index 263add8a8..223049e 100644
--- a/third_party/blink/renderer/modules/geolocation/geolocation.cc
+++ b/third_party/blink/renderer/modules/geolocation/geolocation.cc
@@ -29,6 +29,7 @@
#include <optional>
+#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "services/device/public/mojom/geoposition.mojom-blink.h"
#include "third_party/blink/public/mojom/permissions_policy/permissions_policy.mojom-blink.h"
@@ -88,6 +89,11 @@
case device::mojom::blink::GeopositionErrorCode::kPositionUnavailable:
error_code = GeolocationPositionError::kPositionUnavailable;
break;
+ default:
+ // On Blink side, it should only handles W3C defined error codes.
+ // If it reaches here that means an unexpected error type being propagated
+ // to Blink. This should never happen.
+ NOTREACHED_NORETURN();
}
return MakeGarbageCollected<GeolocationPositionError>(error_code,
error.error_message);