# Shiny Locations

> GPS tracking, geofence monitoring, and motion activity recognition for .NET MAUI, iOS, and Android using Shiny.Locations

- Skill: `shinyorg/shiny-locations` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add shinyorg/shiny-locations`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shinyorg/shiny-locations/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: shinyorg (https://skillmd.com/u/shinyorg)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/shinyorg/shiny-locations

---


# 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`:

```csharp
// 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.

```csharp
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`:

```csharp
// 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`:

```csharp
// 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:

1. **Always request permissions before starting listeners.** Call `RequestAccess` and check the returned `AccessState` before calling `StartListener` or `StartMonitoring`.
2. **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)
3. **Inject `IGpsManager` or `IGeofenceManager`** via constructor injection. Never instantiate managers directly.
4. **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.
5. **Implement `IGeofenceDelegate`** for geofence enter/exit events.
6. **Implement `IMotionActivityDelegate`** for background motion activity processing. The delegate receives `MotionActivityReading` with `Activity` (MotionActivityType), `Confidence` (MotionActivityConfidence), and `Timestamp`.
6. **Use `Position` record** with `(latitude, longitude)` -- latitude range is -90 to 90, longitude range is -180 to 180.
7. **Use `Distance` factory methods** -- `Distance.FromMeters()`, `Distance.FromKilometers()`, `Distance.FromMiles()`. Never construct `Distance` directly with kilometers unless intentional.
8. **Use extension methods** for convenience: `GetCurrentPosition()`, `GetLastReadingOrCurrentPosition()`, `IsListening()`, `IsPositionInside()`, `IsInsideRegion()`.
9. **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.
10. **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.
11. **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 (`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).

```csharp
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

- [API Reference](reference/api-reference.md)

