Skip to content
All posts
June 13, 20266 min read

WorkManager + ForegroundService: The Only Compliant Path for Background Location on Android

Android's background process restrictions exist to protect battery life and user privacy. They also make background location collection significantly harder than it looks. Here is what actually works, why, and what the alternatives get wrong.

AndroidEngineeringArchitecture
Share:

Background location collection on Android is harder than it looks. There are multiple approaches that appear to work in development and fail in production. The failure is always the same: the OS kills the process, location updates stop, and the user has no idea until they check the app and find it hasn't updated in an hour.

The only approach that is both compliant with Android OS restrictions and reliable in production is WorkManager with a ForegroundService. Here is why everything else fails, and how the compliant pattern works.


Why background processes get killed

Android's OS enforces battery and memory constraints by killing background processes. The targeting of specific processes depends on several factors: battery optimization settings, device manufacturer customizations (which vary significantly), app standby buckets, and the process priority at the time the OS needs to reclaim resources.

Location collection is expensive. GPS hardware, network location providers, and Fused Location Provider all consume battery. The OS is aware of this. An app running background location collection that isn't showing the user a persistent notification is treated as a candidate for termination under any resource pressure.

The Doze mode and App Standby restrictions compound this. When the device is idle, network access, wake locks, and alarm managers are restricted. Even if your background service survives the initial kill protection, it may be unable to collect or transmit location data when the device has been idle.


What doesn't work

Plain coroutines in a Service. A

code
Service
without
code
startForeground()
is a background service. Android 8.0+ restricts background services — they can be created, but the OS can stop them at any time. Coroutines launched in the service scope die when the service is stopped.

kotlin
// This will be killed
class LocationService : Service() {
    private val scope = CoroutineScope(Dispatchers.IO)

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        scope.launch {
            while (true) {
                collectLocation() // won't run reliably
                delay(10_000)
            }
        }
        return START_STICKY
    }
}

code
START_STICKY
causes the service to restart after being killed, but the restart doesn't happen immediately and the location data during the killed period is lost.

AlarmManager for periodic location. AlarmManager can schedule periodic work, but it doesn't hold a process alive between alarms. The OS can defer inexact alarms (which is the only kind available in background). Under Doze mode, even exact alarms are deferred. AlarmManager-based location collection will produce gaps.

JobScheduler directly. JobScheduler is the right primitive for deferred background work, but it doesn't provide a mechanism for running continuously with user-facing notification. It's appropriate for periodic sync operations, not continuous location streaming.


What works: WorkManager + ForegroundService

WorkManager is the recommended API for background work in Android. It uses JobScheduler internally on API 23+, handles rescheduling after process death, and respects OS battery restrictions while still guaranteeing eventual execution.

For continuous location collection that needs to run while the app is in the background, the correct pattern is a WorkManager

code
CoroutineWorker
that calls
code
setForeground()
to promote itself to a foreground service:

kotlin
class LocationWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        // Promote to foreground — shows persistent notification, prevents killing
        setForeground(createForegroundInfo())

        val locationClient = FusedLocationProviderClient(applicationContext)
        val locationRequest = LocationRequest.Builder(
            Priority.PRIORITY_HIGH_ACCURACY,
            10_000L // 10 second interval
        ).build()

        return try {
            collectLocationUpdates(locationClient, locationRequest)
            Result.success()
        } catch (e: CancellationException) {
            Result.success() // Worker was cancelled cleanly
        } catch (e: Exception) {
            Result.retry()
        }
    }

    private suspend fun collectLocationUpdates(
        client: FusedLocationProviderClient,
        request: LocationRequest
    ) {
        client.locationFlow(request).collect { location ->
            // Write to Firebase Realtime DB
            saveLocationToDatabase(location)
        }
    }

    private fun createForegroundInfo(): ForegroundInfo {
        val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
            .setContentTitle("Location sharing active")
            .setContentText("Family members can see your location")
            .setSmallIcon(R.drawable.ic_location)
            .setOngoing(true)
            .build()

        return ForegroundInfo(NOTIFICATION_ID, notification)
    }
}

The

code
setForeground()
call is the critical step. It tells the OS that this worker has user-visible activity — the persistent notification — and should be treated as a foreground service with the associated protections against termination.


The location Flow extension

code
FusedLocationProviderClient
doesn't expose a Flow directly. Wrapping the callback-based API in a callbackFlow makes it composable with coroutines:

kotlin
@SuppressLint("MissingPermission")
fun FusedLocationProviderClient.locationFlow(
    request: LocationRequest
): Flow<Location> = callbackFlow {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) {
            result.locations.forEach { location ->
                trySend(location)
            }
        }
    }

    requestLocationUpdates(request, callback, Looper.getMainLooper())
        .addOnFailureListener { e -> close(e) }

    awaitClose {
        removeLocationUpdates(callback)
    }
}

code
awaitClose
ensures
code
removeLocationUpdates
is called when the flow is cancelled — either because the worker was stopped or because the coroutine scope was cancelled. This prevents location callback leaks.


Scheduling the worker

WorkManager's

code
OneTimeWorkRequest
with the worker calling
code
setForeground()
is the correct scheduling approach. The worker runs until it returns a result or is cancelled:

kotlin
val locationWork = OneTimeWorkRequestBuilder<LocationWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

WorkManager.getInstance(context).enqueueUniqueWork(
    "location_tracking",
    ExistingWorkPolicy.KEEP, // don't restart if already running
    locationWork
)

code
ExistingWorkPolicy.KEEP
prevents duplicate workers from being scheduled if
code
startLocationTracking()
is called multiple times.

To stop tracking:

kotlin
WorkManager.getInstance(context).cancelUniqueWork("location_tracking")

Cancellation is clean — the

code
awaitClose
block in the Flow runs,
code
removeLocationUpdates
is called, and the persistent notification is removed.


OEM behavior differences

The compliant pattern described above works on stock Android. OEM behavior is the part of the documentation that doesn't exist but matters most in practice.

Samsung, Xiaomi, Huawei, and OnePlus all have battery optimization layers that operate outside the Android OS specification. These layers can kill foreground services with persistent notifications, defer WorkManager execution, and restrict background network access — even when the app has all required permissions and is following the documented pattern.

The mitigations are imperfect:

  • Request the user to disable battery optimization for the app (
    code
    ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
    )
  • Use
    code
    PRIORITY_HIGH_ACCURACY
    for the location request to signal that the collection is active and intentional
  • Use
    code
    setExpedited()
    on the WorkRequest to request elevated priority

None of these guarantees protection against aggressive OEM kill layers. The most reliable mitigation is user education — if the app's core function is background location sharing, the onboarding flow should walk the user through the battery optimization exemption process for their specific device.

This is an unsatisfying answer. It reflects the actual state of Android background process reliability across the device ecosystem.


The permissions required

Background location collection requires the following permissions:

xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />

code
ACCESS_BACKGROUND_LOCATION
must be granted separately from
code
ACCESS_FINE_LOCATION
. On Android 11+, the system shows a separate permission dialog for background location, and the user must explicitly choose "Allow all the time" rather than "Allow only while using the app."

Play Store requires a privacy policy URL and a Data Safety declaration that accurately describes background location collection. The declaration must specify what data is collected, how it is used, whether it is shared, and whether it can be deleted.

This is the complete picture of what it takes to do background location collection correctly on Android. The technical implementation is one part. The permissions model, OEM compatibility, and compliance requirements are the rest.

Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus