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);