Shiny Locations
GPS tracking, geofence monitoring, and motion activity recognition for .NET MAUI, iOS, and Android applications with full foreground and background support.
When to Use This Skill
Use this skill when the user needs to:
- Track the device GPS position (foreground or background)
- Monitor geofence regions (enter/exit events)
- Calculate distances between geographic positions
- Request location permissions
- Get a single current position reading
- Implement background location tracking delegates
- Detect stationary vs. in-motion state
- Recognize motion activity (walking, running, cycling, automotive, stationary)
- Implement motion activity delegates for background activity processing
Library Overview
| Property |
Value |
| NuGet |
Shiny.Locations (MAUI), Shiny.Locations.Blazor (Blazor WASM) |
| Namespace |
Shiny.Locations |
| Platforms |
iOS, Android, Windows, Blazor WebAssembly (foreground GPS only). No tvOS target — CLMonitor, CLMonitorConfiguration and CLRegionState are absent on tvOS, so geofencing cannot be implemented there |
| DI Namespace |
Shiny (extension methods on IServiceCollection) |
| Support Library |
Shiny.Support.Locations (provides Position and Distance) |
Setup
GPS Registration
Register GPS in MauiProgram.cs:
// GPS without a background delegate (foreground only)
services.AddGps();
// GPS with a background delegate
services.AddGps<MyGpsDelegate>();
Blazor WebAssembly GPS Registration
Register GPS in Program.cs of a Blazor WebAssembly project. Only foreground GPS
is supported - the browser does not expose background location, geofencing, or
significant-location-change APIs. Background modes on a GpsRequest are silently
treated as foreground.
builder.Services.AddGps();
// or with a foreground-only delegate:
builder.Services.AddGps<MyGpsDelegate>();
Geofencing (AddGeofencing, AddGpsDirectGeofencing) is not available in
Shiny.Locations.Blazor. For region-entry behavior on the web, evaluate regions
server-side from GPS reports and notify the client via Shiny.Push.Blazor.
Geofence Registration
Register geofencing in MauiProgram.cs:
// Standard geofencing with a delegate
services.AddGeofencing<MyGeofenceDelegate>();
// GPS-direct geofencing (uses realtime GPS - battery intensive)
services.AddGpsDirectGeofencing<MyGeofenceDelegate>();
Motion Activity Registration
Register motion activity recognition in MauiProgram.cs:
// Motion activity without a background delegate
services.AddMotionActivity();
// Motion activity with a background delegate
services.AddMotionActivity<MyMotionActivityDelegate>();
Platform support: Motion activity is supported on iOS (CMMotionActivityManager) and Android (Google Play Services Activity Recognition). On Android, Google Play Services must be available — the registration silently no-ops if unavailable. Other platforms (Windows, Blazor) are no-ops.
Code Generation Instructions
When generating code for Shiny.Locations:
- Always request permissions before starting listeners. Call
RequestAccess and check the returned AccessState before calling StartListener or StartMonitoring.
- Use
GpsRequest factories or constructor based on the background mode needed:
GpsRequest.Foreground for foreground-only use (equivalent to new GpsRequest(GpsBackgroundMode.None))
new GpsRequest(GpsBackgroundMode.Standard) for standard background (iOS: significant location changes; Android: 3-4 updates/hour)
GpsRequest.Realtime(true) for background realtime with precise accuracy (iOS/Android: updates every 1 second)
- Inject
IGpsManager or IGeofenceManager via constructor injection. Never instantiate managers directly.
- Implement
IGpsDelegate for background GPS processing, or subclass the abstract GpsDelegate base class for built-in filtering by distance/time and stationary detection. The GpsDelegate supports minimum filters (MinimumDistance, MinimumTime) that use AND logic when both are set, and maximum filters (MaximumDistance, MaximumTime) that use OR logic and always override minimums when crossed.
- Implement
IGeofenceDelegate for geofence enter/exit events.
- Implement
IMotionActivityDelegate for background motion activity processing. The delegate receives MotionActivityReading with Activity (MotionActivityType), Confidence (MotionActivityConfidence), and Timestamp.
- Use
Position record with (latitude, longitude) -- latitude range is -90 to 90, longitude range is -180 to 180.
- Use
Distance factory methods -- Distance.FromMeters(), Distance.FromKilometers(), Distance.FromMiles(). Never construct Distance directly with kilometers unless intentional.
- Use extension methods for convenience:
GetCurrentPosition(), GetLastReadingOrCurrentPosition(), IsListening(), IsPositionInside(), IsInsideRegion().
- Subscribe to the
GpsReadingReceived C# event on IGpsManager (or MotionActivityReadingReceived on IMotionActivityManager) for foreground UI updates. Rx has been removed from Shiny.Locations — use event EventHandler<GpsReading> / event EventHandler<MotionActivityReading> and always unsubscribe on disappear/dispose to avoid leaks. Delegates remain the way to handle readings while the app is backgrounded.
- For
GeofenceRegion, always provide a unique Identifier string. The SingleUse parameter removes the region after the first trigger. To register a region idempotently, use the TryStartMonitoring(region, replaceIfExists) extension on IGeofenceManager — it only starts monitoring if a region with the same identifier isn't already being monitored, and (when replaceIfExists is true, the default) stops and restarts an existing region so changed position/notification settings take effect. It returns true when the region already existed, false when it was newly added.
- Inject
IMotionActivityManager via constructor injection for motion activity features. Call RequestAccess() before StartListener(), then subscribe to MotionActivityReadingReceived for foreground updates or register IMotionActivityDelegate for background processing.
Conventions
- All async operations return
Task or Task<T>.
- The convenience extension methods live on
Shiny.Locations.LocationExtensions (renamed from Extensions in 5.2.5 — a type named Shiny.Locations.Extensions collides with the Shiny.Locations.Extensions.AI namespace and produces CS0434 in consuming projects). They are extension methods, so call sites are unaffected.
- Foreground observation uses C#
event EventHandler<T> on the managers (GpsReadingReceived, MotionActivityReadingReceived) — Rx is no longer used in Shiny.Locations.
- The
GpsBackgroundMode enum controls background behavior: None (foreground), Standard (periodic), Realtime (continuous).
GeofenceState enum values: Unknown, Entered, Exited.
AccessState is from Shiny.Core and includes Available, Denied, Disabled, Restricted, NotSupported, Unknown.
Best Practices
- Always check
AccessState before starting GPS or geofence monitoring. Handle Denied and Restricted states gracefully with user-facing messaging.
- Prefer
GpsBackgroundMode.Standard over Realtime to conserve battery. Only use Realtime when continuous tracking is required.
- Stop listeners when they are no longer needed (
StopListener() / StopAllMonitoring()).
- Use the abstract
GpsDelegate base class instead of implementing IGpsDelegate directly. It provides MinimumDistance, MinimumTime (AND when both set), MaximumDistance, MaximumTime (OR, overrides minimums) filtering, and stationary detection out of the box.
- For single position reads, use the
GetCurrentPosition() extension method which handles starting/stopping the listener automatically.
- Unsubscribe from
GpsReadingReceived / MotionActivityReadingReceived when the view/page is no longer active (pair += with -= on disappear/dispose).
- On iOS, configure
NSLocationWhenInUseUsageDescription and NSLocationAlwaysAndWhenInUseUsageDescription in Info.plist. Word the "always" strings for what background access actually does — following the user with the app closed — rather than reusing the when-in-use sentence.
- On iOS, call
RequestAccess with the background request from the feature that needs it, not at launch. Authorization escalates (when-in-use first, always only as an upgrade) and the system presents the upgrade prompt once, so the moment the user turns the feature on is the only moment the dialog can be explained. Requires Shiny.Locations 5.5.0+ — earlier builds reused the when-in-use session for background requests and returned AccessState.Restricted without prompting.
- Treat
AccessState.Restricted as "granted, but not for what you asked for", not as a refusal. It is what AuthorizedWhenInUse reports as when a background request asks about it, and also what reduced accuracy reports as. A user-facing message should say which journeys/readings are lost and offer the upgrade, rather than reading like a blocked permission.
- On Android, configure
ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, and ACCESS_BACKGROUND_LOCATION permissions in AndroidManifest.xml.
- On iOS, add
NSMotionUsageDescription to Info.plist when using motion activity recognition.
- On Android, motion activity recognition requires
com.google.android.gms.permission.ACTIVITY_RECOGNITION permission and Google Play Services.
AI Tool Integration (Shiny.Locations.Extensions.AI)
The optional Shiny.Locations.Extensions.AI package exposes IGpsManager as read-only Microsoft.Extensions.AI tool functions (AIFunctions) for LLM agents — the agent can learn where the user is and reason about distance/time to a destination, but never writes location data. You opt-in via AddGps() (an allow-list you control on behalf of the agent — not an OS permission prompt; location permission must already be granted). AOT-compatible (hand-built schemas, JsonNode results — no reflection).
using Shiny.Locations;
using Shiny.Locations.Extensions.AI;
builder.Services.AddGps(); // registers IGpsManager
builder.Services.AddLocationAITool(); // read-only; there is no write capability for GPS
// resolve the bundle and pass the tools to any IChatClient
var tools = sp.GetRequiredService<LocationAITools>().Tools;
var response = await chatClient.GetResponseAsync(
messages,
new ChatOptions { Tools = [.. tools] }
);
Key types:
AddLocationAITool() — parameterless DI extension. GPS is read-only, so there is no builder or capability to opt-in to; the call registers all three location tools.
LocationAITools — resolve from DI; .Tools is IReadOnlyList<AITool>.
Generated tools: get_current_location (last cached fix — lat/lng, accuracy, altitude, speed, heading, timestamp), get_distance_to (great-circle distance + compass bearing to a destination lat/lng), estimate_travel_time (mode walking/cycling/transit/driving or a speedKmh override → straight-line ETA).
The tools read the last cached GPS reading; start a listener or ensure a recent fix exists first. Distances and travel times are great-circle (straight-line) estimates — not routed ETAs with roads/traffic — and the tool results say so. Location permission should already be granted before invoking the agent.
Reference Files
1---2name: shiny-locations3description: GPS tracking, geofence monitoring, and motion activity recognition for .NET MAUI, iOS, and Android using Shiny.Locations4---56# Shiny Locations78GPS tracking, geofence monitoring, and motion activity recognition for .NET MAUI, iOS, and Android applications with full foreground and background support.910## When to Use This Skill1112Use this skill when the user needs to:1314- Track the device GPS position (foreground or background)15- Monitor geofence regions (enter/exit events)16- Calculate distances between geographic positions17- Request location permissions18- Get a single current position reading19- Implement background location tracking delegates20- Detect stationary vs. in-motion state21- Recognize motion activity (walking, running, cycling, automotive, stationary)22- Implement motion activity delegates for background activity processing2324## Library Overview2526| Property | Value |27|------------|------------------------------|28| NuGet | `Shiny.Locations` (MAUI), `Shiny.Locations.Blazor` (Blazor WASM) |29| Namespace | `Shiny.Locations` |30| Platforms | iOS, Android, Windows, Blazor WebAssembly (foreground GPS only). **No tvOS target** — `CLMonitor`, `CLMonitorConfiguration` and `CLRegionState` are absent on tvOS, so geofencing cannot be implemented there |31| DI Namespace | `Shiny` (extension methods on `IServiceCollection`) |32| Support Library | `Shiny.Support.Locations` (provides `Position` and `Distance`) |3334## Setup3536### GPS Registration3738Register GPS in `MauiProgram.cs`:3940```csharp41// GPS without a background delegate (foreground only)42services.AddGps();4344// GPS with a background delegate45services.AddGps<MyGpsDelegate>();46```4748### Blazor WebAssembly GPS Registration4950Register GPS in `Program.cs` of a Blazor WebAssembly project. Only foreground GPS51is supported - the browser does not expose background location, geofencing, or52significant-location-change APIs. Background modes on a `GpsRequest` are silently53treated as foreground.5455```csharp56builder.Services.AddGps();57// or with a foreground-only delegate:58builder.Services.AddGps<MyGpsDelegate>();59```6061Geofencing (`AddGeofencing`, `AddGpsDirectGeofencing`) is **not** available in62`Shiny.Locations.Blazor`. For region-entry behavior on the web, evaluate regions63server-side from GPS reports and notify the client via `Shiny.Push.Blazor`.6465### Geofence Registration6667Register geofencing in `MauiProgram.cs`:6869```csharp70// Standard geofencing with a delegate71services.AddGeofencing<MyGeofenceDelegate>();7273// GPS-direct geofencing (uses realtime GPS - battery intensive)74services.AddGpsDirectGeofencing<MyGeofenceDelegate>();75```7677### Motion Activity Registration7879Register motion activity recognition in `MauiProgram.cs`:8081```csharp82// Motion activity without a background delegate83services.AddMotionActivity();8485// Motion activity with a background delegate86services.AddMotionActivity<MyMotionActivityDelegate>();87```8889> **Platform support:** Motion activity is supported on iOS (CMMotionActivityManager) and Android (Google Play Services Activity Recognition). On Android, Google Play Services must be available — the registration silently no-ops if unavailable. Other platforms (Windows, Blazor) are no-ops.9091## Code Generation Instructions9293When generating code for Shiny.Locations:94951. **Always request permissions before starting listeners.** Call `RequestAccess` and check the returned `AccessState` before calling `StartListener` or `StartMonitoring`.962. **Use `GpsRequest` factories or constructor** based on the background mode needed:97 - `GpsRequest.Foreground` for foreground-only use (equivalent to `new GpsRequest(GpsBackgroundMode.None)`)98 - `new GpsRequest(GpsBackgroundMode.Standard)` for standard background (iOS: significant location changes; Android: 3-4 updates/hour)99 - `GpsRequest.Realtime(true)` for background realtime with precise accuracy (iOS/Android: updates every 1 second)1003. **Inject `IGpsManager` or `IGeofenceManager`** via constructor injection. Never instantiate managers directly.1014. **Implement `IGpsDelegate`** for background GPS processing, or subclass the abstract `GpsDelegate` base class for built-in filtering by distance/time and stationary detection. The `GpsDelegate` supports minimum filters (`MinimumDistance`, `MinimumTime`) that use AND logic when both are set, and maximum filters (`MaximumDistance`, `MaximumTime`) that use OR logic and always override minimums when crossed.1025. **Implement `IGeofenceDelegate`** for geofence enter/exit events.1036. **Implement `IMotionActivityDelegate`** for background motion activity processing. The delegate receives `MotionActivityReading` with `Activity` (MotionActivityType), `Confidence` (MotionActivityConfidence), and `Timestamp`.1046. **Use `Position` record** with `(latitude, longitude)` -- latitude range is -90 to 90, longitude range is -180 to 180.1057. **Use `Distance` factory methods** -- `Distance.FromMeters()`, `Distance.FromKilometers()`, `Distance.FromMiles()`. Never construct `Distance` directly with kilometers unless intentional.1068. **Use extension methods** for convenience: `GetCurrentPosition()`, `GetLastReadingOrCurrentPosition()`, `IsListening()`, `IsPositionInside()`, `IsInsideRegion()`.1079. **Subscribe to the `GpsReadingReceived` C# event on `IGpsManager` (or `MotionActivityReadingReceived` on `IMotionActivityManager`) for foreground UI updates.** Rx has been removed from Shiny.Locations — use `event EventHandler<GpsReading>` / `event EventHandler<MotionActivityReading>` and always unsubscribe on disappear/dispose to avoid leaks. Delegates remain the way to handle readings while the app is backgrounded.10810. **For `GeofenceRegion`**, always provide a unique `Identifier` string. The `SingleUse` parameter removes the region after the first trigger. To register a region idempotently, use the `TryStartMonitoring(region, replaceIfExists)` extension on `IGeofenceManager` — it only starts monitoring if a region with the same identifier isn't already being monitored, and (when `replaceIfExists` is `true`, the default) stops and restarts an existing region so changed position/notification settings take effect. It returns `true` when the region already existed, `false` when it was newly added.10911. **Inject `IMotionActivityManager`** via constructor injection for motion activity features. Call `RequestAccess()` before `StartListener()`, then subscribe to `MotionActivityReadingReceived` for foreground updates or register `IMotionActivityDelegate` for background processing.110111## Conventions112113- All async operations return `Task` or `Task<T>`.114- The convenience extension methods live on `Shiny.Locations.LocationExtensions` (renamed from `Extensions` in 5.2.5 — a type named `Shiny.Locations.Extensions` collides with the `Shiny.Locations.Extensions.AI` namespace and produces CS0434 in consuming projects). They are extension methods, so call sites are unaffected.115- Foreground observation uses C# `event EventHandler<T>` on the managers (`GpsReadingReceived`, `MotionActivityReadingReceived`) — Rx is no longer used in Shiny.Locations.116- The `GpsBackgroundMode` enum controls background behavior: `None` (foreground), `Standard` (periodic), `Realtime` (continuous).117- `GeofenceState` enum values: `Unknown`, `Entered`, `Exited`.118- `AccessState` is from Shiny.Core and includes `Available`, `Denied`, `Disabled`, `Restricted`, `NotSupported`, `Unknown`.119120## Best Practices121122- Always check `AccessState` before starting GPS or geofence monitoring. Handle `Denied` and `Restricted` states gracefully with user-facing messaging.123- Prefer `GpsBackgroundMode.Standard` over `Realtime` to conserve battery. Only use `Realtime` when continuous tracking is required.124- Stop listeners when they are no longer needed (`StopListener()` / `StopAllMonitoring()`).125- Use the abstract `GpsDelegate` base class instead of implementing `IGpsDelegate` directly. It provides `MinimumDistance`, `MinimumTime` (AND when both set), `MaximumDistance`, `MaximumTime` (OR, overrides minimums) filtering, and stationary detection out of the box.126- For single position reads, use the `GetCurrentPosition()` extension method which handles starting/stopping the listener automatically.127- Unsubscribe from `GpsReadingReceived` / `MotionActivityReadingReceived` when the view/page is no longer active (pair `+=` with `-=` on disappear/dispose).128- On iOS, configure `NSLocationWhenInUseUsageDescription` and `NSLocationAlwaysAndWhenInUseUsageDescription` in `Info.plist`. Word the "always" strings for what background access actually does — following the user with the app closed — rather than reusing the when-in-use sentence.129- **On iOS, call `RequestAccess` with the background request from the feature that needs it, not at launch.** Authorization escalates (when-in-use first, always only as an upgrade) and the system presents the upgrade prompt once, so the moment the user turns the feature on is the only moment the dialog can be explained. Requires Shiny.Locations 5.5.0+ — earlier builds reused the when-in-use session for background requests and returned `AccessState.Restricted` without prompting.130- **Treat `AccessState.Restricted` as "granted, but not for what you asked for", not as a refusal.** It is what `AuthorizedWhenInUse` reports as when a background request asks about it, and also what reduced accuracy reports as. A user-facing message should say which journeys/readings are lost and offer the upgrade, rather than reading like a blocked permission.131- On Android, configure `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`, and `ACCESS_BACKGROUND_LOCATION` permissions in `AndroidManifest.xml`.132- On iOS, add `NSMotionUsageDescription` to `Info.plist` when using motion activity recognition.133- On Android, motion activity recognition requires `com.google.android.gms.permission.ACTIVITY_RECOGNITION` permission and Google Play Services.134135## AI Tool Integration (Shiny.Locations.Extensions.AI)136137The optional `Shiny.Locations.Extensions.AI` package exposes `IGpsManager` as **read-only** `Microsoft.Extensions.AI` tool functions (`AIFunction`s) for LLM agents — the agent can learn where the user is and reason about distance/time to a destination, but never writes location data. You opt-in via `AddGps()` (an allow-list you control on behalf of the agent — **not** an OS permission prompt; location permission must already be granted). AOT-compatible (hand-built schemas, `JsonNode` results — no reflection).138139```csharp140using Shiny.Locations;141using Shiny.Locations.Extensions.AI;142143builder.Services.AddGps(); // registers IGpsManager144builder.Services.AddLocationAITool(); // read-only; there is no write capability for GPS145146// resolve the bundle and pass the tools to any IChatClient147var tools = sp.GetRequiredService<LocationAITools>().Tools;148var response = await chatClient.GetResponseAsync(149 messages,150 new ChatOptions { Tools = [.. tools] }151);152```153154Key types:155- `AddLocationAITool()` — parameterless DI extension. GPS is read-only, so there is no builder or capability to opt-in to; the call registers all three location tools.156- `LocationAITools` — resolve from DI; `.Tools` is `IReadOnlyList<AITool>`.157158Generated tools: `get_current_location` (last cached fix — lat/lng, accuracy, altitude, speed, heading, timestamp), `get_distance_to` (great-circle distance + compass bearing to a destination lat/lng), `estimate_travel_time` (`mode` walking/cycling/transit/driving or a `speedKmh` override → straight-line ETA).159160> The tools read the **last cached GPS reading**; start a listener or ensure a recent fix exists first. Distances and travel times are **great-circle (straight-line) estimates** — not routed ETAs with roads/traffic — and the tool results say so. Location permission should already be granted before invoking the agent.161162## Reference Files163164- [API Reference](reference/api-reference.md)