Your music app needs to keep playing when the user switches to another app. Your fitness app needs to track location while the phone is in the user’s pocket. A download manager needs to stay alive until the file is complete.

These are foreground services. They run long-running operations that the user is aware of, shown with a persistent notification.

In this tutorial, you will learn how to build foreground services — service types, lifecycle, communication with the UI, and practical examples for music playback and location tracking.

Prerequisites: You should understand notifications first. Read Android Tutorial #11: Notifications before this tutorial.


When to Use Foreground Services

Android gives you several options for background work:

ToolUse CaseSurvives App Kill?
CoroutinesQuick async work (API calls)No
WorkManagerDeferrable tasks (sync, upload)Yes
Foreground ServiceUser-visible ongoing workYes

Use a foreground service when:

  • The user expects the task to continue (music, navigation, recording)
  • The task takes more than a few minutes
  • You need to show ongoing progress

Don’t use foreground services for things WorkManager can handle. WorkManager is better for tasks that can be deferred (like syncing data or uploading logs). See Android Tutorial #9: WorkManager.


Foreground Service Types

Since Android 14 (API 34), you must declare the type of your foreground service. This tells the system and the user what the service is doing.

Add the type to your AndroidManifest.xml:

<service
    android:name=".service.MusicService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="false" />

<service
    android:name=".service.LocationService"
    android:foregroundServiceType="location"
    android:exported="false" />

Common foreground service types:

TypePermission RequiredUse Case
mediaPlaybackNoneMusic, podcasts, audio books
locationACCESS_FINE_LOCATIONGPS tracking, navigation
cameraCAMERAVideo recording
microphoneRECORD_AUDIOVoice recording
dataSyncNoneLarge file sync
healthBODY_SENSORSFitness tracking
connectedDeviceVariesBluetooth, USB devices

Building a Basic Foreground Service

Step 1: Create the Service

class MusicService : Service() {

    companion object {
        const val ACTION_START = "action_start"
        const val ACTION_STOP = "action_stop"
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_START -> start()
            ACTION_STOP -> stop()
        }
        return START_STICKY
    }

    private fun start() {
        val notification = NotificationCompat.Builder(this, NotificationChannels.MUSIC)
            .setSmallIcon(R.drawable.ic_music)
            .setContentTitle("Now Playing")
            .setContentText("Song Title — Artist Name")
            .setOngoing(true)
            .build()

        ServiceCompat.startForeground(
            this,
            1,
            notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
        )
    }

    private fun stop() {
        stopSelf()
    }

    override fun onDestroy() {
        super.onDestroy()
        // Clean up resources
    }
}

Important: Use ServiceCompat.startForeground() instead of the plain startForeground(). It handles backward compatibility for the service type parameter.

Step 2: Declare in Manifest

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

<service
    android:name=".service.MusicService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="false" />

Step 3: Start from Compose

@Composable
fun MusicPlayerScreen() {
    val context = LocalContext.current

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Button(onClick = {
            val intent = Intent(context, MusicService::class.java).apply {
                action = MusicService.ACTION_START
            }
            ContextCompat.startForegroundService(context, intent)
        }) {
            Text("Start Playing")
        }

        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = {
            val intent = Intent(context, MusicService::class.java).apply {
                action = MusicService.ACTION_STOP
            }
            context.startService(intent)
        }) {
            Text("Stop Playing")
        }
    }
}

Use ContextCompat.startForegroundService() to start the service. This works on all Android versions.


Music Player with MediaSession

A real music player needs media controls in the notification and lock screen. MediaSession handles this.

Setting Up MediaSession

class MusicService : Service() {

    private var mediaSession: MediaSessionCompat? = null
    private var mediaPlayer: MediaPlayer? = null

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onCreate() {
        super.onCreate()
        setupMediaSession()
    }

    private fun setupMediaSession() {
        mediaSession = MediaSessionCompat(this, "MusicService").apply {
            setCallback(object : MediaSessionCompat.Callback() {
                override fun onPlay() {
                    mediaPlayer?.start()
                    updateNotification(isPlaying = true)
                }

                override fun onPause() {
                    mediaPlayer?.pause()
                    updateNotification(isPlaying = false)
                }

                override fun onStop() {
                    mediaPlayer?.stop()
                    stopSelf()
                }

                override fun onSkipToNext() {
                    // Load next track
                }

                override fun onSkipToPrevious() {
                    // Load previous track
                }
            })
            isActive = true
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_START -> startPlayback(intent)
            ACTION_STOP -> stopSelf()
        }
        return START_STICKY
    }

    private fun startPlayback(intent: Intent) {
        val songUri = intent.getStringExtra("song_uri") ?: return

        mediaPlayer?.release()
        mediaPlayer = MediaPlayer().apply {
            setDataSource(songUri)
            prepare()
            start()
        }

        val notification = buildMusicNotification(isPlaying = true)

        ServiceCompat.startForeground(
            this, 1, notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
        )
    }

    private fun buildMusicNotification(isPlaying: Boolean): Notification {
        val playPauseAction = if (isPlaying) {
            NotificationCompat.Action.Builder(
                R.drawable.ic_pause, "Pause",
                buildMediaAction(PlaybackStateCompat.ACTION_PAUSE)
            ).build()
        } else {
            NotificationCompat.Action.Builder(
                R.drawable.ic_play, "Play",
                buildMediaAction(PlaybackStateCompat.ACTION_PLAY)
            ).build()
        }

        val stopAction = NotificationCompat.Action.Builder(
            R.drawable.ic_stop, "Stop",
            buildMediaAction(PlaybackStateCompat.ACTION_STOP)
        ).build()

        return NotificationCompat.Builder(this, NotificationChannels.MUSIC)
            .setSmallIcon(R.drawable.ic_music)
            .setContentTitle("Now Playing")
            .setContentText("Song Title — Artist Name")
            .addAction(playPauseAction)
            .addAction(stopAction)
            .setStyle(
                androidx.media.app.NotificationCompat.MediaStyle()
                    .setMediaSession(mediaSession?.sessionToken)
                    .setShowActionsInCompactView(0, 1)
            )
            .setOngoing(isPlaying)
            .build()
    }

    private fun buildMediaAction(action: Long): PendingIntent {
        return MediaButtonReceiver.buildMediaButtonPendingIntent(
            this,
            action
        )
    }

    private fun updateNotification(isPlaying: Boolean) {
        val notification = buildMusicNotification(isPlaying)
        NotificationManagerCompat.from(this).notify(1, notification)
    }

    override fun onDestroy() {
        super.onDestroy()
        mediaPlayer?.release()
        mediaPlayer = null
        mediaSession?.release()
        mediaSession = null
    }

    companion object {
        const val ACTION_START = "action_start"
        const val ACTION_STOP = "action_stop"
    }
}

The MediaStyle notification shows playback controls that work from the notification shade and lock screen.


Location Tracking Service

Location tracking is one of the most common uses for foreground services. The FusedLocationProviderClient gives you efficient, battery-friendly location updates.

Permissions

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- Required if your app starts location tracking while in the background -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

Android 14+ note: If you start a location foreground service while your app is in the background, you must have ACCESS_BACKGROUND_LOCATION granted first. Without it, calling ContextCompat.startForegroundService() from a background context throws a SecurityException. Always check location permissions and grant status before starting the service.

The Service

class LocationService : Service() {

    private lateinit var fusedClient: FusedLocationProviderClient
    private lateinit var locationCallback: LocationCallback

    private val _locationFlow = MutableStateFlow<Location?>(null)

    override fun onBind(intent: Intent?): IBinder = LocationBinder()

    inner class LocationBinder : Binder() {
        val locationFlow: StateFlow<Location?> = _locationFlow.asStateFlow()
    }

    override fun onCreate() {
        super.onCreate()
        fusedClient = LocationServices.getFusedLocationProviderClient(this)

        locationCallback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                result.lastLocation?.let { location ->
                    _locationFlow.value = location
                    updateNotification(location)
                }
            }
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_START -> startTracking()
            ACTION_STOP -> stopTracking()
        }
        return START_STICKY
    }

    private fun startTracking() {
        val notification = buildLocationNotification("Starting location tracking...")

        ServiceCompat.startForeground(
            this, 2, notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
        )

        val locationRequest = LocationRequest.Builder(
            Priority.PRIORITY_HIGH_ACCURACY,
            5000L // Update every 5 seconds
        )
            .setMinUpdateIntervalMillis(2000L)
            .build()

        try {
            fusedClient.requestLocationUpdates(
                locationRequest,
                locationCallback,
                Looper.getMainLooper()
            )
        } catch (e: SecurityException) {
            stopSelf()
        }
    }

    private fun stopTracking() {
        fusedClient.removeLocationUpdates(locationCallback)
        stopSelf()
    }

    private fun buildLocationNotification(text: String): Notification {
        return NotificationCompat.Builder(this, NotificationChannels.LOCATION)
            .setSmallIcon(R.drawable.ic_location)
            .setContentTitle("Location Tracking")
            .setContentText(text)
            .setOngoing(true)
            .build()
    }

    private fun updateNotification(location: Location) {
        val text = "Lat: %.4f, Lng: %.4f".format(location.latitude, location.longitude)
        val notification = buildLocationNotification(text)
        NotificationManagerCompat.from(this).notify(2, notification)
    }

    override fun onDestroy() {
        super.onDestroy()
        fusedClient.removeLocationUpdates(locationCallback)
    }

    companion object {
        const val ACTION_START = "action_start"
        const val ACTION_STOP = "action_stop"
    }
}

Communicating Between Service and UI

There are three ways to get data from a service to your Compose UI.

Option 1: Bound Service with StateFlow

The cleanest approach. Bind to the service and collect its StateFlow:

@Composable
fun LocationTrackingScreen() {
    val context = LocalContext.current
    var location by remember { mutableStateOf<Location?>(null) }
    var isBound by remember { mutableStateOf(false) }

    val connection = remember {
        object : ServiceConnection {
            var binder: LocationService.LocationBinder? = null

            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
                binder = service as? LocationService.LocationBinder
                isBound = true
            }

            override fun onServiceDisconnected(name: ComponentName?) {
                binder = null
                isBound = false
            }
        }
    }

    // Bind to service
    DisposableEffect(Unit) {
        val intent = Intent(context, LocationService::class.java)
        context.bindService(intent, connection, Context.BIND_AUTO_CREATE)

        onDispose {
            context.unbindService(connection)
        }
    }

    // Collect location updates
    LaunchedEffect(isBound) {
        connection.binder?.locationFlow?.collect { loc ->
            location = loc
        }
    }

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = location?.let {
                "Lat: %.4f\nLng: %.4f".format(it.latitude, it.longitude)
            } ?: "Waiting for location...",
            style = MaterialTheme.typography.headlineMedium
        )

        Spacer(modifier = Modifier.height(24.dp))

        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = {
                Intent(context, LocationService::class.java).apply {
                    action = LocationService.ACTION_START
                }.also { ContextCompat.startForegroundService(context, it) }
            }) {
                Text("Start Tracking")
            }

            Button(onClick = {
                Intent(context, LocationService::class.java).apply {
                    action = LocationService.ACTION_STOP
                }.also { context.startService(it) }
            }) {
                Text("Stop Tracking")
            }
        }
    }
}

Option 2: Shared Repository with Flow

Use a Hilt-injected repository with a SharedFlow that both the service and UI observe:

@Singleton
class LocationRepository @Inject constructor() {
    private val _locations = MutableSharedFlow<Location>(replay = 1)
    val locations: SharedFlow<Location> = _locations.asSharedFlow()

    suspend fun emit(location: Location) {
        _locations.emit(location)
    }
}

Option 3: Shared Flow via Hilt

Inject a shared MutableSharedFlow into both the service and the ViewModel. This works well in multi-module apps where the service and UI are in different modules.


Foreground Service Restrictions

Android has increasingly restricted background execution. Know these rules:

Android 12 (API 31)

Apps cannot start foreground services from the background. You must start them from a visible activity or from a high-priority FCM message.

Exception: Use RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED when registering broadcast receivers that start services.

Android 14 (API 34)

  • Foreground service types are mandatory
  • Each type requires specific permissions
  • The dataSync type has a 6-hour time limit

Android 16 (API 36)

  • Stricter enforcement of service type declarations
  • The system may stop services that don’t match their declared type
  • dataSync foreground service type is further restricted — consider migrating long sync operations to WorkManager with user-initiated constraints

Service Lifecycle

Understanding the lifecycle prevents memory leaks and crashes:

onCreate() → onStartCommand() → [running] → onDestroy()
                              onBind() / onUnbind()

Key points:

  • START_STICKY — system restarts the service if killed (use for music, location)
  • START_NOT_STICKY — system does not restart (use for one-time tasks)
  • START_REDELIVER_INTENT — system restarts and redelivers the last intent

What Happens When the App is Killed?

If the user swipes away the app:

  • The foreground service keeps running (it has a notification)
  • The bound connection is lost — the service’s onUnbind() is called
  • When the user reopens the app, they can rebind

If the system kills the service for memory:

  • START_STICKY makes the system recreate it
  • The notification briefly disappears and reappears

Battery and Doze Mode

Foreground services are exempt from Doze mode. But they still drain battery if misused.

Best practices:

  • Use the lowest frequency that works (location every 30 seconds instead of every second)
  • Use Priority.PRIORITY_BALANCED_POWER_ACCURACY for location if you don’t need GPS precision
  • Stop the service as soon as the task is done
  • Let the user stop the service from the notification
// Add a stop action to the notification
val stopIntent = PendingIntent.getService(
    this, 0,
    Intent(this, LocationService::class.java).apply {
        action = ACTION_STOP
    },
    PendingIntent.FLAG_IMMUTABLE
)

val notification = NotificationCompat.Builder(this, NotificationChannels.LOCATION)
    .setSmallIcon(R.drawable.ic_location)
    .setContentTitle("Tracking Location")
    .setContentText("Tap Stop to end tracking")
    .addAction(R.drawable.ic_stop, "Stop", stopIntent)
    .setOngoing(true)
    .build()

Common Mistakes

1. Not Calling startForeground() Within 10 Seconds

After startForegroundService() is called, you must call startForeground() within 10 seconds. Otherwise, the system throws a ForegroundServiceDidNotStartInTimeException.

2. Forgetting Service Type Permissions

On Android 14+, you need both the general FOREGROUND_SERVICE permission and the type-specific permission (like FOREGROUND_SERVICE_LOCATION).

3. Leaking the Service Connection

Always unbind in onDispose (Compose) or onDestroy (Activity). A leaked connection prevents the service from being destroyed.

4. Using startService() Instead of startForegroundService()

If your service calls startForeground(), you must start it with startForegroundService(). Otherwise, it crashes on Android 8+.

5. Not Handling Background Start Restrictions

On Android 12+, you cannot start a foreground service from the background. Check if your app is in the foreground, or use an alternative like WorkManager.


What’s Next?

In this tutorial, you learned:

  • When to use foreground services vs WorkManager
  • Declaring foreground service types for Android 14+
  • Building a music player with MediaSession and notification controls
  • Building a location tracking service with live UI updates
  • Three ways to communicate between service and Compose UI
  • Android 12-16 foreground service restrictions
  • Battery optimization and Doze mode considerations

Next up: Android Tutorial #13: Content Providers and File System — where you will learn how to access photos, files, and share data between apps.



This is part 12 of the Android Development Tutorial series.