#166·skills

[Feature] Add fused-location skill for foreground FLP, Android 12 permissions, and update lifecycle

Author: endlessYoungCreated Aug 29, 2026Updated Aug 29, 2026

[!NOTE] Feature requests are accepted only if the proposed skill fills a verified gap in state-of-the-art LLMs. Evaluations below used a fast model (Cursor Composer 2.5 Fast) and a thinking model (Cursor Grok 4.6). Gemini 3 Flash / Gemini 3.1 Pro were not available in this environment; please re-run the same prompts on those models if needed.

What is the gap in the LLM's knowledge?

android/skills has no location category. Current models already know to reach for FusedLocationProviderClient and they do not grab ACCESS_BACKGROUND_LOCATION for on-screen features. The remaining, repeatable gaps are the decision tree and Android 12 permission semantics, not "does FLP exist".

Four prompts, no web/docs/skills, code from memory only:

Prompt Fast (Composer 2.5) Thinking (Grok 4.6)
A. Compose screen, get location once, city-level, targetSdk 35 Pass: ACCESS_COARSE_LOCATION only + getCurrentLocation(PRIORITY_BALANCED_POWER_ACCURACY) Pass: same + CurrentLocationRequest / GRANULARITY_COARSE
B. Track while screen visible (street-level), must stop when leaving Pass: FINE+COARSE together, requestLocationUpdates, ON_START/ON_STOP Pass: same + location-enabled check
C. Vague: "home screen, show current lat/lng" Fail: requests FINE+COARSE and results.values.all { it }, so an Android 12 approximate-only grant is treated as denied. Also PRIORITY_HIGH_ACCURACY for a one-shot home screen. Fail: requests FINE+COARSE, then starts unbounded requestLocationUpdates in a ViewModel (not tied to ON_STOP) for a one-shot display. getCurrentLocation(..., null) (no CancellationToken).
D. "I need GPS coordinates… keep them updated as the user moves" Fail: FLP + HIGH_ACCURACY, but cleanup is only DisposableEffect.onDispose / ViewModel.onCleared. Home / recents does not stop updates (background leak without a background permission). Fail: same lifecycle hole (DisposableEffect(granted) only). Seeds UI from lastLocation with no age check. Starts PRIORITY_HIGH_ACCURACY even if the user granted only coarse.

When the prompt spelled out "coarse / once / stop on leave", both models complied. When a developer speaks naturally ("show my coordinates", "GPS", "keep updating"), they over-request fine, start a stream they do not stop on ON_STOP, and mishandle Android 12 approximate grants.

None of the eight runs used SettingsClient.checkLocationSettings. Permission granted was treated as "location is on".

These match documented platform rules:

Proposed Skill

Path: location/fused-location/SKILL.md (same layout as camera/camerax).

Scope: foreground only — runtime permissions (coarse vs fine, Android 12+), getCurrentLocation vs requestLocationUpdates, bind updates to ON_START/ON_STOP, location settings. Not Maps, geofencing, or background location.

Full draft:

---
name: fused-location
description: >
  Guide Android foreground location with FusedLocationProviderClient.
  Use when implementing current location, location updates, runtime location
  permissions, approximate vs precise location on Android 12+, or when an
  app would otherwise use LocationManager GPS_PROVIDER.
license: Complete terms in LICENSE.txt
metadata:
  author: community proposal
  last-updated: '2026-08-29'
  keywords:
  - recipe
  - Android
  - location
  - FusedLocationProvider
  - FusedLocationProviderClient
  - ACCESS_COARSE_LOCATION
  - ACCESS_FINE_LOCATION
  - getCurrentLocation
  - requestLocationUpdates
  - Compose
  - permissions
  - approximate location
  - precise location
---

Procedural guidance for **foreground** location on Android using Play services
`FusedLocationProviderClient`. Grounded in
[Request location permissions](https://developer.android.com/develop/sensors-and-location/location/permissions)
and
[Get the last known location](https://developer.android.com/develop/sensors-and-location/location/retrieve-current).

**Out of scope:** Maps SDK, geofencing, activity recognition, and background
location (`ACCESS_BACKGROUND_LOCATION`, location foreground services).

## Step 1: pick accuracy and API

Ask what the feature actually needs. Do not default to GPS / fine / streaming.

| Need | Permission | API |
| --- | --- | --- |
| City / weather / "which area" **once** | `ACCESS_COARSE_LOCATION` only | `getCurrentLocation` + `PRIORITY_BALANCED_POWER_ACCURACY` or `PRIORITY_LOW_POWER` |
| Street-level **once** | `ACCESS_FINE_LOCATION` **and** `ACCESS_COARSE_LOCATION` | `getCurrentLocation` + `PRIORITY_HIGH_ACCURACY` |
| Street-level **while UI is visible** | Fine + coarse | `requestLocationUpdates` + `removeLocationUpdates` on `ON_STOP` |

`getLastLocation()` / `lastLocation` is a cache. It can be `null` or hours old.
Do **not** use it as the primary one-shot API.
[Prefer `getCurrentLocation()`](https://developer.android.com/develop/sensors-and-location/location/retrieve-current).

```kotlin
// WRONG — GPS provider, no fusion, easy to leak updates
locationManager.requestLocationUpdates(
    LocationManager.GPS_PROVIDER, 2000L, 0f, listener
)

// WRONG — stale cache as the only source
fusedClient.lastLocation.addOnSuccessListener { loc -> show(loc) }

// CORRECT — one-shot, fused, cancellable
val cts = CancellationTokenSource()
fusedClient.getCurrentLocation(
    Priority.PRIORITY_BALANCED_POWER_ACCURACY,
    cts.token,
)
```

Dependency: `com.google.android.gms:play-services-location`.

## Step 2: declare and request permissions

### Manifest

- City-level: declare **only** `ACCESS_COARSE_LOCATION`.
- Precise: declare **both** `ACCESS_FINE_LOCATION` and `ACCESS_COARSE_LOCATION`.
  On Android 12+ the system [ignores a fine-only request](https://developer.android.com/develop/sensors-and-location/location/permissions)
  (`ACCESS_FINE_LOCATION must be requested with ACCESS_COARSE_LOCATION`).
- Do **not** declare `ACCESS_BACKGROUND_LOCATION` for on-screen features.

### Runtime

Use `ActivityResultContracts.RequestPermission` (coarse only) or
`RequestMultiplePermissions` (fine + coarse in **one** call).

Android 12+ users can grant **approximate** even when the app asked for precise.
That is a valid grant, not a denial.

```kotlin
// WRONG — approximate-only looks like "denied"
val granted = results.values.all { it }

// CORRECT — any location grant is success; inspect fine separately
val hasCoarse = results[Manifest.permission.ACCESS_COARSE_LOCATION] == true ||
    context.hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
val hasFine = results[Manifest.permission.ACCESS_FINE_LOCATION] == true ||
    context.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
```

If the feature **requires** street-level accuracy and the user granted only
approximate, keep the feature working at coarse if possible; otherwise explain
why precise is needed and request `ACCESS_FINE_LOCATION` again (upgrade dialog).
Do not block the whole app on first approximate grant unless the UX truly cannot
function.

Do **not** treat the first denial as "permanently denied".
`shouldShowRequestPermissionRationale == false` right after the first dialog is
unreliable.

## Step 3: one-shot current location

```kotlin
@SuppressLint("MissingPermission")
suspend fun currentLocation(
    client: FusedLocationProviderClient,
    fineGranted: Boolean,
): Location? {
    val cts = CancellationTokenSource()
    val priority = if (fineGranted) {
        Priority.PRIORITY_HIGH_ACCURACY
    } else {
        Priority.PRIORITY_BALANCED_POWER_ACCURACY
    }
    return suspendCancellableCoroutine { cont ->
        cont.invokeOnCancellation { cts.cancel() }
        client.getCurrentLocation(priority, cts.token)
            .addOnSuccessListener { loc -> if (cont.isActive) cont.resume(loc) }
            .addOnFailureListener { e ->
                if (cont.isActive) cont.resumeWithException(e)
            }
    }
}
```

If the result is `null`, the usual causes are: permission missing, location
services off, or Play services unavailable — not "call `lastLocation` and hope".

## Step 4: updates only while the screen is visible

`ViewModel.onCleared()` and `DisposableEffect.onDispose` do **not** run when the
user presses Home or opens recents. Updates keep flowing in the background
without a background permission. Bind to `Lifecycle.Event.ON_START` / `ON_STOP`.

```kotlin
// WRONG — Home / recents leaves the callback registered
DisposableEffect(Unit) {
    client.requestLocationUpdates(request, callback, Looper.getMainLooper())
    onDispose { client.removeLocationUpdates(callback) }
}

// CORRECT
DisposableEffect(lifecycleOwner, client) {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) {
            result.lastLocation?.let { /* ui state */ }
        }
    }
    val observer = LifecycleEventObserver { _, event ->
        when (event) {
            Lifecycle.Event.ON_START -> client.requestLocationUpdates(
                request, callback, Looper.getMainLooper()
            )
            Lifecycle.Event.ON_STOP -> client.removeLocationUpdates(callback)
            else -> Unit
        }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
        lifecycleOwner.lifecycle.removeObserver(observer)
        client.removeLocationUpdates(callback)
    }
}
```

Use `LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, intervalMillis)`
(or balanced/low-power for coarse). Do not use the deprecated
`LocationRequest.create()`.

## Step 5: resolve location settings

Permission granted ≠ location enabled. If `getCurrentLocation` returns `null` or
updates never arrive, check settings with
[`SettingsClient.checkLocationSettings`](https://developers.google.com/android/reference/com/google/android/gms/location/SettingsClient)
and, on `ResolvableApiException`, launch the system resolution dialog. Also
handle Play services missing / updating.

## Common pitfalls

- Defaulting to `ACCESS_FINE_LOCATION` + `PRIORITY_HIGH_ACCURACY` for "show my
  coordinates" on a home screen.
- Starting `requestLocationUpdates` when `getCurrentLocation` is enough.
- `results.values.all { it }` after `RequestMultiplePermissions` (Android 12
  approximate grant).
- Requesting `ACCESS_FINE_LOCATION` **without** `ACCESS_COARSE_LOCATION`.
- Pairing `ACCESS_BACKGROUND_LOCATION` with the first foreground request.
- Using `LocationManager` / `GPS_PROVIDER` instead of fused location.
- Passing `null` as the `CancellationToken` to `getCurrentLocation` and never
  cancelling.
- Seeding UI from `lastLocation` with no age check.

## Official docs

- [Request location permissions](https://developer.android.com/develop/sensors-and-location/location/permissions)
- [Get current / last location](https://developer.android.com/develop/sensors-and-location/location/retrieve-current)
- [Request location updates](https://developer.android.com/develop/sensors-and-location/location/request-updates)
- [Approximate location (Android 12+)](https://developer.android.com/develop/sensors-and-location/location/permissions)

Additional Context

  • Repo currently has camera, identity, system, and other categories, but no location/ tree.
  • README says public PRs are not accepted, so this is an issue-only proposal rather than a PR.
  • The skill is intentionally narrower than "maps + geofence + background" so it stays in the "LLM underperforms" zone (decision tree + Android 12), not a restatement of FLP existence.
  • Happy to add Maps / background location as separate skills if this one lands.