From 63444de469c65af5c9bbb331a7ba554d9cfdf5d5 Mon Sep 17 00:00:00 2001 From: KitKeen Date: Wed, 22 Apr 2026 22:54:21 +0300 Subject: [PATCH 1/7] [Essentials] Use mean sea level altitude on Android API 34+ Android API level 34 introduced Location.getMslAltitudeMeters(), which provides altitude measured against the geoid (mean sea level). The existing implementation always reads the WGS84 ellipsoidal altitude, which is reported inconsistently with iOS/Windows where altitude is typically expressed against the geoid. Prefer the MSL altitude when the device exposes it (API 34+ and HasMslAltitude is true) and report AltitudeReferenceSystem.Geoid in that case. Fall back to the existing WGS84 path on older devices. Also reports AltitudeReferenceSystem.Unspecified when no altitude is available, instead of hard-coding Ellipsoid for a null altitude. Fixes #27554 --- .../src/Types/LocationExtensions.android.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Essentials/src/Types/LocationExtensions.android.cs b/src/Essentials/src/Types/LocationExtensions.android.cs index 7c3d664b9449..9a75028ab4ac 100644 --- a/src/Essentials/src/Types/LocationExtensions.android.cs +++ b/src/Essentials/src/Types/LocationExtensions.android.cs @@ -19,12 +19,15 @@ internal static Location ToLocation(this AndroidAddress address) => internal static IEnumerable ToLocations(this IEnumerable addresses) => addresses?.Select(a => a.ToLocation()); - internal static Location ToLocation(this AndroidLocation location) => - new Location + internal static Location ToLocation(this AndroidLocation location) + { + var (altitude, altitudeReference) = GetAltitude(location); + + return new Location { Latitude = location.Latitude, Longitude = location.Longitude, - Altitude = location.HasAltitude ? location.Altitude : default(double?), + Altitude = altitude, Timestamp = location.GetTimestamp().ToUniversalTime(), Accuracy = location.HasAccuracy ? location.Accuracy : default(float?), VerticalAccuracy = @@ -40,8 +43,22 @@ internal static Location ToLocation(this AndroidLocation location) => #pragma warning disable CS0618 // Type or member is obsolete : location.IsFromMockProvider, #pragma warning restore CS0618 // Type or member is obsolete - AltitudeReferenceSystem = AltitudeReferenceSystem.Ellipsoid + AltitudeReferenceSystem = altitudeReference }; + } + + // Prefer mean sea level altitude (Android API 34+) when available so altitude + // values are consistent across platforms without manual geoid correction. + static (double? Altitude, AltitudeReferenceSystem Reference) GetAltitude(AndroidLocation location) + { + if (OperatingSystem.IsAndroidVersionAtLeast(34) && location.HasMslAltitude) + return (location.MslAltitudeMeters, AltitudeReferenceSystem.Geoid); + + if (location.HasAltitude) + return (location.Altitude, AltitudeReferenceSystem.Ellipsoid); + + return (null, AltitudeReferenceSystem.Unspecified); + } static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); From 0d468444f60c63731a3550b9653ab37873032184 Mon Sep 17 00:00:00 2001 From: KitKeen Date: Fri, 24 Apr 2026 01:01:04 +0300 Subject: [PATCH 2/7] Pair VerticalAccuracy with altitude reference system; add tests Address review feedback on #35097. Previously VerticalAccuracy was always read from Location.VerticalAccuracyMeters (ellipsoidal accuracy), regardless of whether Altitude came from the MSL/geoid or WGS84/ellipsoid path. On Android API 34+ with MSL altitude, this produced an altitude and a vertical accuracy in different reference systems. Select altitude and its accuracy together in GetAltitude so they can never diverge: - MSL path (API 34+ && HasMslAltitude): altitude from MslAltitudeMeters, accuracy from MslAltitudeAccuracyMeters when HasMslAltitudeAccuracy is true, else null. - Ellipsoid path (HasAltitude): altitude from Altitude, accuracy from VerticalAccuracyMeters when API 26+ and HasVerticalAccuracy. - No altitude: (null, Unspecified, null). Add device tests covering all three GetAltitude branches and the MSL-without-MSL-accuracy case. --- .../src/Types/LocationExtensions.android.cs | 28 ++++-- .../Tests/Android/Geolocation_Tests.cs | 89 +++++++++++++++++++ 2 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs diff --git a/src/Essentials/src/Types/LocationExtensions.android.cs b/src/Essentials/src/Types/LocationExtensions.android.cs index 9a75028ab4ac..ceb72cc9a9b9 100644 --- a/src/Essentials/src/Types/LocationExtensions.android.cs +++ b/src/Essentials/src/Types/LocationExtensions.android.cs @@ -21,7 +21,7 @@ internal static IEnumerable ToLocations(this IEnumerable Date: Fri, 24 Apr 2026 01:42:06 +0300 Subject: [PATCH 3/7] Add explicit coverage for API 34+ ellipsoid fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing TestLocation_EllipsoidalAltitude_UsesEllipsoidReferenceSystem test happened to exercise the fallback because MslAltitudeMeters was never set, but that was implicit. This adds a dedicated test that pins the invariant: on API 34+ where HasMslAltitude is false (e.g. the device did not report a reference-corrected altitude), ToLocation falls back to the ellipsoidal altitude and tags it Ellipsoid — not Geoid, not Unspecified. This is the second half of the IsAndroidVersionAtLeast(34) && HasMslAltitude guard added in the previous commit; without an explicit test, a refactor that dropped the HasMslAltitude check would still pass the suite. --- .../Tests/Android/Geolocation_Tests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs index afb850149713..70eb684b04da 100644 --- a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs @@ -42,6 +42,29 @@ public void ToLocation_EllipsoidalAltitude_UsesEllipsoidReferenceSystem() Assert.Null(location.VerticalAccuracy); } + [Fact] + public void ToLocation_Api34WithoutMslAltitude_FallsBackToEllipsoid() + { + // On API 34+ without a reported MSL altitude (HasMslAltitude == false), + // we must fall back to the ellipsoidal altitude rather than silently + // returning nothing or mis-labelling it as Geoid. + if (!OperatingSystem.IsAndroidVersionAtLeast(34)) + return; + + var androidLocation = new AndroidLocation("test") + { + Altitude = 123.45, + VerticalAccuracyMeters = 5.0f, + // MslAltitudeMeters intentionally not set → HasMslAltitude == false + }; + + var location = androidLocation.ToLocation(); + + Assert.Equal(123.45, location.Altitude); + Assert.Equal(AltitudeReferenceSystem.Ellipsoid, location.AltitudeReferenceSystem); + Assert.Equal(5.0, location.VerticalAccuracy); + } + [Fact] public void ToLocation_MslAltitude_UsesGeoidReferenceSystem() { From cd128086def6c8855fa3c354df437a89df71d1de Mon Sep 17 00:00:00 2001 From: KitKeen Date: Sat, 25 Apr 2026 01:28:59 +0300 Subject: [PATCH 4/7] Remove silent-pass risk in API 34+ gated geolocation tests Each test now runs at least one assertion on any API level before the API 34+ specific branch, so pre-34 runs exercise the fallback path (Ellipsoid or Unspecified) instead of returning early without asserting. This keeps the gate test meaningful on devices below API 34 and catches regressions in either code path. --- .../Tests/Android/Geolocation_Tests.cs | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs index 70eb684b04da..9742e83aad1f 100644 --- a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs @@ -43,64 +43,86 @@ public void ToLocation_EllipsoidalAltitude_UsesEllipsoidReferenceSystem() } [Fact] - public void ToLocation_Api34WithoutMslAltitude_FallsBackToEllipsoid() + public void ToLocation_HasAltitudeButNoMslAltitude_UsesEllipsoid() { - // On API 34+ without a reported MSL altitude (HasMslAltitude == false), - // we must fall back to the ellipsoidal altitude rather than silently - // returning nothing or mis-labelling it as Geoid. - if (!OperatingSystem.IsAndroidVersionAtLeast(34)) - return; - + // On every API level, a location that reports an ellipsoidal altitude but no + // MSL altitude must resolve to Ellipsoid. On pre-34 devices that is the only + // code path; on API 34+ devices this exercises the HasMslAltitude == false + // fallback branch. Either way this assertion runs, so the test cannot silently + // pass if the fallback regresses. var androidLocation = new AndroidLocation("test") { Altitude = 123.45, - VerticalAccuracyMeters = 5.0f, - // MslAltitudeMeters intentionally not set → HasMslAltitude == false }; + if (OperatingSystem.IsAndroidVersionAtLeast(26)) + androidLocation.VerticalAccuracyMeters = 5.0f; + var location = androidLocation.ToLocation(); Assert.Equal(123.45, location.Altitude); Assert.Equal(AltitudeReferenceSystem.Ellipsoid, location.AltitudeReferenceSystem); - Assert.Equal(5.0, location.VerticalAccuracy); + + if (OperatingSystem.IsAndroidVersionAtLeast(26)) + Assert.Equal(5.0, location.VerticalAccuracy); + else + Assert.Null(location.VerticalAccuracy); } [Fact] public void ToLocation_MslAltitude_UsesGeoidReferenceSystem() { - // MSL altitude is only available on Android API 34+ - if (!OperatingSystem.IsAndroidVersionAtLeast(34)) - return; - var androidLocation = new AndroidLocation("test") { - Altitude = 123.45, // ellipsoidal - MslAltitudeMeters = 100.0, // geoid - MslAltitudeAccuracyMeters = 2.5f, - VerticalAccuracyMeters = 5.0f, // ellipsoidal accuracy + Altitude = 123.45, }; + // Baseline: without MSL altitude set, we must get Ellipsoid on any API level. + // This guarantees an assertion runs even on pre-34 devices so the test cannot + // silently pass if the API-34 branch is accidentally taken or the fallback + // regresses. + var baseline = androidLocation.ToLocation(); + Assert.Equal(123.45, baseline.Altitude); + Assert.Equal(AltitudeReferenceSystem.Ellipsoid, baseline.AltitudeReferenceSystem); + + // The remaining assertions exercise the API 34+ MSL path. MslAltitudeMeters is + // only meaningful on API 34+, so below that we stop after validating the + // fallback above. + if (!OperatingSystem.IsAndroidVersionAtLeast(34)) + return; + + androidLocation.MslAltitudeMeters = 100.0; + androidLocation.MslAltitudeAccuracyMeters = 2.5f; + androidLocation.VerticalAccuracyMeters = 5.0f; // ellipsoidal accuracy, must NOT be surfaced + var location = androidLocation.ToLocation(); Assert.Equal(100.0, location.Altitude); Assert.Equal(AltitudeReferenceSystem.Geoid, location.AltitudeReferenceSystem); // VerticalAccuracy must be paired with the chosen altitude reference system, - // so the MSL accuracy is preferred over the ellipsoidal one. + // so the MSL accuracy is used rather than the ellipsoidal one. Assert.Equal(2.5, location.VerticalAccuracy); } [Fact] public void ToLocation_MslAltitudeWithoutMslAccuracy_ReportsNullVerticalAccuracy() { - // MSL altitude is only available on Android API 34+ + var androidLocation = new AndroidLocation("test"); + + // Baseline: a location with no altitude reports Unspecified on any API level. + // This keeps the test meaningful on pre-34 devices instead of silently passing. + var baseline = androidLocation.ToLocation(); + Assert.Null(baseline.Altitude); + Assert.Equal(AltitudeReferenceSystem.Unspecified, baseline.AltitudeReferenceSystem); + Assert.Null(baseline.VerticalAccuracy); + if (!OperatingSystem.IsAndroidVersionAtLeast(34)) return; - var androidLocation = new AndroidLocation("test") - { - MslAltitudeMeters = 100.0, - VerticalAccuracyMeters = 5.0f, // ellipsoidal accuracy, must NOT be surfaced alongside MSL altitude - }; + // On API 34+, an MSL altitude without an MSL accuracy must NOT surface the + // ellipsoidal VerticalAccuracyMeters — they describe a different reference system. + androidLocation.MslAltitudeMeters = 100.0; + androidLocation.VerticalAccuracyMeters = 5.0f; var location = androidLocation.ToLocation(); From 93c354b1c351a3f985e1c5f81259db48e579002d Mon Sep 17 00:00:00 2001 From: KitKeen Date: Sun, 3 May 2026 16:33:16 +0300 Subject: [PATCH 5/7] Fix copy constructor not preserving AltitudeReferenceSystem Add missing AltitudeReferenceSystem assignment in the Location copy constructor and add a unit test to cover the regression. Co-Authored-By: Claude Sonnet 4.6 --- src/Essentials/src/Types/Location.shared.cs | 1 + .../Tests/Android/Geolocation_Tests.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Essentials/src/Types/Location.shared.cs b/src/Essentials/src/Types/Location.shared.cs index 892cf582d4f3..2268f477cb77 100644 --- a/src/Essentials/src/Types/Location.shared.cs +++ b/src/Essentials/src/Types/Location.shared.cs @@ -103,6 +103,7 @@ public Location(Location point) Longitude = point.Longitude; Timestamp = DateTime.UtcNow; Altitude = point.Altitude; + AltitudeReferenceSystem = point.AltitudeReferenceSystem; Accuracy = point.Accuracy; VerticalAccuracy = point.VerticalAccuracy; ReducedAccuracy = point.ReducedAccuracy; diff --git a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs index 9742e83aad1f..f910c572e30b 100644 --- a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs @@ -104,6 +104,23 @@ public void ToLocation_MslAltitude_UsesGeoidReferenceSystem() Assert.Equal(2.5, location.VerticalAccuracy); } + [Fact] + public void LocationCopyConstructor_PreservesAltitudeReferenceSystem() + { + var original = new Location(51.5, -0.1) + { + Altitude = 100.0, + AltitudeReferenceSystem = AltitudeReferenceSystem.Geoid, + VerticalAccuracy = 2.5 + }; + + var copy = new Location(original); + + Assert.Equal(original.Altitude, copy.Altitude); + Assert.Equal(original.AltitudeReferenceSystem, copy.AltitudeReferenceSystem); + Assert.Equal(original.VerticalAccuracy, copy.VerticalAccuracy); + } + [Fact] public void ToLocation_MslAltitudeWithoutMslAccuracy_ReportsNullVerticalAccuracy() { From 1e76470e1074d96239c81f9202d116c3957b7f9b Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 12:03:30 +0200 Subject: [PATCH 6/7] Fix test category and namespace per review suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove space from Category attribute ('Android Geolocation' → 'Geolocation') to prevent XHarness/am instrument tokenization failure on Android device tests. - Fix namespace to 'Microsoft.Maui.Essentials.DeviceTests' matching the convention used by all other test files in this project. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/DeviceTests/Tests/Android/Geolocation_Tests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs index f910c572e30b..3b5f5b2c7a80 100644 --- a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs @@ -3,9 +3,9 @@ using Xunit; using AndroidLocation = Android.Locations.Location; -namespace Microsoft.Maui.Essentials.DeviceTests.Shared +namespace Microsoft.Maui.Essentials.DeviceTests { - [Category("Android Geolocation")] + [Category("Geolocation")] public class Android_Geolocation_Tests { [Fact] From d711d268e9b423dfd3d3b95558ef708763c38b0f Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:58:05 +0200 Subject: [PATCH 7/7] Fix test: remove duplicate, keep copy constructor test in Android file - Remove duplicate ToLocation_HasAltitudeButNoMslAltitude_UsesEllipsoid (identical to ToLocation_EllipsoidalAltitude_UsesEllipsoidReferenceSystem) - Keep LocationCopyConstructor_PreservesAltitudeReferenceSystem in the Android test file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/Android/Geolocation_Tests.cs | 61 ++++++------------- 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs index 3b5f5b2c7a80..40d921831ad6 100644 --- a/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs @@ -42,33 +42,6 @@ public void ToLocation_EllipsoidalAltitude_UsesEllipsoidReferenceSystem() Assert.Null(location.VerticalAccuracy); } - [Fact] - public void ToLocation_HasAltitudeButNoMslAltitude_UsesEllipsoid() - { - // On every API level, a location that reports an ellipsoidal altitude but no - // MSL altitude must resolve to Ellipsoid. On pre-34 devices that is the only - // code path; on API 34+ devices this exercises the HasMslAltitude == false - // fallback branch. Either way this assertion runs, so the test cannot silently - // pass if the fallback regresses. - var androidLocation = new AndroidLocation("test") - { - Altitude = 123.45, - }; - - if (OperatingSystem.IsAndroidVersionAtLeast(26)) - androidLocation.VerticalAccuracyMeters = 5.0f; - - var location = androidLocation.ToLocation(); - - Assert.Equal(123.45, location.Altitude); - Assert.Equal(AltitudeReferenceSystem.Ellipsoid, location.AltitudeReferenceSystem); - - if (OperatingSystem.IsAndroidVersionAtLeast(26)) - Assert.Equal(5.0, location.VerticalAccuracy); - else - Assert.Null(location.VerticalAccuracy); - } - [Fact] public void ToLocation_MslAltitude_UsesGeoidReferenceSystem() { @@ -104,23 +77,6 @@ public void ToLocation_MslAltitude_UsesGeoidReferenceSystem() Assert.Equal(2.5, location.VerticalAccuracy); } - [Fact] - public void LocationCopyConstructor_PreservesAltitudeReferenceSystem() - { - var original = new Location(51.5, -0.1) - { - Altitude = 100.0, - AltitudeReferenceSystem = AltitudeReferenceSystem.Geoid, - VerticalAccuracy = 2.5 - }; - - var copy = new Location(original); - - Assert.Equal(original.Altitude, copy.Altitude); - Assert.Equal(original.AltitudeReferenceSystem, copy.AltitudeReferenceSystem); - Assert.Equal(original.VerticalAccuracy, copy.VerticalAccuracy); - } - [Fact] public void ToLocation_MslAltitudeWithoutMslAccuracy_ReportsNullVerticalAccuracy() { @@ -147,5 +103,22 @@ public void ToLocation_MslAltitudeWithoutMslAccuracy_ReportsNullVerticalAccuracy Assert.Equal(AltitudeReferenceSystem.Geoid, location.AltitudeReferenceSystem); Assert.Null(location.VerticalAccuracy); } + + [Fact] + public void LocationCopyConstructor_PreservesAltitudeReferenceSystem() + { + var original = new Location(51.5, -0.1) + { + Altitude = 100.0, + AltitudeReferenceSystem = AltitudeReferenceSystem.Geoid, + VerticalAccuracy = 2.5 + }; + + var copy = new Location(original); + + Assert.Equal(original.Altitude, copy.Altitude); + Assert.Equal(original.AltitudeReferenceSystem, copy.AltitudeReferenceSystem); + Assert.Equal(original.VerticalAccuracy, copy.VerticalAccuracy); + } } }