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:
| Tool | Use Case | Survives App Kill? |
|---|---|---|
| Coroutines | Quick async work (API calls) | No |
| WorkManager | Deferrable tasks (sync, upload) | Yes |
| Foreground Service | User-visible ongoing work | Yes |
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:
| Type | Permission Required | Use Case |
|---|---|---|
mediaPlayback | None | Music, podcasts, audio books |
location | ACCESS_FINE_LOCATION | GPS tracking, navigation |
camera | CAMERA | Video recording |
microphone | RECORD_AUDIO | Voice recording |
dataSync | None | Large file sync |
health | BODY_SENSORS | Fitness tracking |
connectedDevice | Varies | Bluetooth, 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
locationforeground service while your app is in the background, you must haveACCESS_BACKGROUND_LOCATIONgranted first. Without it, callingContextCompat.startForegroundService()from a background context throws aSecurityException. 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
dataSynctype 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
dataSyncforeground 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_STICKYmakes 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_ACCURACYfor 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.
Related Articles
- Android Tutorial #11: Notifications — notification channels and styles
- Android Tutorial #9: WorkManager — background tasks without services
- Kotlin Tutorial: Coroutines — async programming
- Kotlin Tutorial: Flow — reactive data streams
This is part 12 of the Android Development Tutorial series.