Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Essentials/src/Types/Location.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public Location(Location point)
Longitude = point.Longitude;
Timestamp = DateTime.UtcNow;
Altitude = point.Altitude;
AltitudeReferenceSystem = point.AltitudeReferenceSystem;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion — release-note this behavior change.

Adding AltitudeReferenceSystem to the copy constructor is correct (the previous omission was a latent bug — copies were silently coerced to the default Unspecified). Worth calling out in release notes alongside the Android Ellipsoid → Unspecified change so consumers know to re-check any code that compared AltitudeReferenceSystem after copying a Location.

Accuracy = point.Accuracy;
VerticalAccuracy = point.VerticalAccuracy;
ReducedAccuracy = point.ReducedAccuracy;
Expand Down
43 changes: 35 additions & 8 deletions src/Essentials/src/Types/LocationExtensions.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ internal static Location ToLocation(this AndroidAddress address) =>
internal static IEnumerable<Location> ToLocations(this IEnumerable<AndroidAddress> 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, verticalAccuracy) = 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 =
OperatingSystem.IsAndroidVersionAtLeast(26) && location.HasVerticalAccuracy
? location.VerticalAccuracyMeters
: null,
VerticalAccuracy = verticalAccuracy,
ReducedAccuracy = false,
Course = location.HasBearing ? location.Bearing : default(double?),
Speed = location.HasSpeed ? location.Speed : default(double?),
Expand All @@ -40,8 +40,35 @@ 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.
// Altitude and its accuracy are selected together so they always come from
// the same reference system (MSL/geoid vs WGS84/ellipsoid).
static (double? Altitude, AltitudeReferenceSystem Reference, double? VerticalAccuracy) GetAltitude(AndroidLocation location)
{
if (OperatingSystem.IsAndroidVersionAtLeast(34) && location.HasMslAltitude)
{
double? mslAccuracy = location.HasMslAltitudeAccuracy
? location.MslAltitudeAccuracyMeters
: null;
return (location.MslAltitudeMeters, AltitudeReferenceSystem.Geoid, mslAccuracy);
}

if (location.HasAltitude)
{
double? ellipsoidAccuracy =
OperatingSystem.IsAndroidVersionAtLeast(26) && location.HasVerticalAccuracy
? location.VerticalAccuracyMeters
: null;
return (location.Altitude, AltitudeReferenceSystem.Ellipsoid, ellipsoidAccuracy);
}

return (null, AltitudeReferenceSystem.Unspecified, null);
}

static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

Expand Down
124 changes: 124 additions & 0 deletions src/Essentials/test/DeviceTests/Tests/Android/Geolocation_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using Microsoft.Maui.Devices.Sensors;
using Xunit;
using AndroidLocation = Android.Locations.Location;

namespace Microsoft.Maui.Essentials.DeviceTests
{
[Category("Geolocation")]
public class Android_Geolocation_Tests
{
[Fact]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error — [Category("Android Geolocation")] contains a space, which breaks Android device-test execution.

When Run-DeviceTests.ps1 (or XHarness directly) forwards the filter as --arg TestFilter=Category=Android Geolocation, the unquoted space causes Android's am instrument to tokenize the value: only Android reaches the TestFilter argument, and Geolocation becomes a stray positional that the runner interprets as the instrumentation name, producing:

Error: No instrumentation found for: Geolocation

This is precisely the failure the gate captured (exitCode 82, RETURN_CODE_NOT_SET). Although the existing Windows test files use space-bearing categories (e.g. "Windows ActiveWindowTracker"), Android+XHarness does not survive that quoting and all Android category names in this project are space-free (Geolocation, Vibration, Accelerometer, …).

Fix: Drop the space, e.g. [Category("AndroidGeolocation")]. The gate's TestFilter=Category=AndroidGeolocation would then survive tokenization and the new tests would actually be executed.

public void ToLocation_NoAltitude_UsesUnspecifiedReferenceSystem()
{
var androidLocation = new AndroidLocation("test");

var location = androidLocation.ToLocation();

Assert.Null(location.Altitude);
Assert.Equal(AltitudeReferenceSystem.Unspecified, location.AltitudeReferenceSystem);
Assert.Null(location.VerticalAccuracy);
}

[Fact]
public void ToLocation_EllipsoidalAltitude_UsesEllipsoidReferenceSystem()
{
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()
{
var androidLocation = new AndroidLocation("test")
{
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 used rather than the ellipsoidal one.
Assert.Equal(2.5, location.VerticalAccuracy);
}

[Fact]
public void ToLocation_MslAltitudeWithoutMslAccuracy_ReportsNullVerticalAccuracy()
{
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;

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

Assert.Equal(100.0, location.Altitude);
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);
}
}
}
Loading