Fused Location Provider

Every Trail in TrailLog carries a latitude and longitude — the location where the hike happened. This lesson covers actually obtaining that location, using the Fused Location Provider, part of Google Play Services and the API Google recommends for location on Android.

Why "Fused"

Raw GPS alone is accurate but slow to get a first fix and drains the battery quickly. The Fused Location Provider instead intelligently combines multiple signals — GPS, WiFi network positioning, and cell tower signals — choosing whichever combination gives the best accuracy for the least battery cost at any given moment. For an app like TrailLog, which needs both a quick one-time location and sustained tracking during an outdoor hike, that trade-off management is exactly what you want handled for you rather than reimplemented by hand.

Adding the Dependency

dependencies {
    implementation("com.google.android.gms:play-services-location:21.3.0")
}

Requesting Location Permission First

No location API call works without a granted runtime permission, requested the same way as in Runtime Permissions. Android actually offers two related permissions: ACCESS_COARSE_LOCATION gives an approximate location (city-block accuracy), while ACCESS_FINE_LOCATION gives a precise GPS-level fix. TrailLog needs accurate trail routes, so it requests ACCESS_FINE_LOCATION:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Getting a One-Time Current Location

For a quick, single reading — say, tagging where the user is standing right now when they start logging a trail — getCurrentLocation() is the right call:

private val fusedLocationClient: FusedLocationProviderClient by lazy {
    LocationServices.getFusedLocationProviderClient(this)
}

@SuppressLint("MissingPermission") // permission already checked before calling
private fun fetchCurrentLocation() {
    fusedLocationClient.getCurrentLocation(
        Priority.PRIORITY_HIGH_ACCURACY,
        null
    ).addOnSuccessListener { location: Location? ->
        location?.let {
            // it.latitude, it.longitude — save onto the new Trail being created
        }
    }
}

Continuous Updates While Tracking a Hike

A one-time fix isn't enough once the hike is actively being tracked by TrailLog's HikeTrackingService Foreground Service from Services & Foreground Service — that service needs a steady stream of location updates to build up the full route. This uses LocationRequest and LocationCallback instead:

private val locationRequest = LocationRequest.Builder(
    Priority.PRIORITY_HIGH_ACCURACY,
    5000L // request an update roughly every 5 seconds
).build()

private val locationCallback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
        result.lastLocation?.let { location ->
            // append (location.latitude, location.longitude) to the in-progress route
        }
    }
}

@SuppressLint("MissingPermission")
fun startTrackingUpdates() {
    fusedLocationClient.requestLocationUpdates(
        locationRequest,
        locationCallback,
        Looper.getMainLooper()
    )
}

fun stopTrackingUpdates() {
    fusedLocationClient.removeLocationUpdates(locationCallback)
}

startTrackingUpdates() and stopTrackingUpdates() are called from HikeTrackingService's own start/stop points, so location updates run for exactly as long as the Foreground Service itself is alive — never longer, which matters both for battery life and for respecting the user's expectation that tracking actually stops when they end a hike.

Wrapping the Callback API in a Coroutine-Friendly Flow

LocationCallback is a classic callback-based API, which doesn't compose naturally with the coroutines and Flow-based code the rest of TrailLog is built around. The fix is the exact same callbackFlow { } technique used in WebSockets to adapt a callback-driven connection into a Flow — worth calling out again here because it's a genuinely reusable Kotlin pattern for adapting any callback-based Android API, not just this one:

@SuppressLint("MissingPermission")
fun locationUpdates(client: FusedLocationProviderClient): Flow<Location> = callbackFlow {
    val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L).build()

    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) {
            result.lastLocation?.let { trySend(it) }
        }
    }

    client.requestLocationUpdates(request, callback, Looper.getMainLooper())

    awaitClose { client.removeLocationUpdates(callback) }
}

The awaitClose { } block is the key piece: it guarantees removeLocationUpdates() runs the moment the collecting coroutine is cancelled, so location updates never keep running after whatever was collecting them has stopped — no manual bookkeeping required.

Note: This Kotlin code is correct, real Fused Location Provider usage, but this website's own project doesn't run a live Android Studio build, so nothing here executes on the page itself.

Accuracy vs. Battery: Choosing a Priority

Both APIs above take a Priority. PRIORITY_HIGH_ACCURACY favors GPS for the tightest possible fix — the right choice while actively tracking a hike's route. PRIORITY_BALANCED_POWER_ACCURACY favors WiFi/cell signals over GPS, trading some precision for noticeably better battery life — a reasonable choice for a coarser feature, like just showing roughly which nearby trails exist, where GPS-level precision isn't needed.

Up next: With real coordinates in hand, Google Maps Integration puts them on an actual map inside TrailLog.