Open menu with table of contents Services and Broadcast Receivers
Logo of Stuttgart Media University for light theme Logo of Stuttgart Media University for dark theme
Android Development

Services and Broadcast Receivers

Stuttgart Media University

1 Agenda

  • Service
    • Service lifecycle, started vs. bound
    • When not to use a Service
    • Background execution limits
    • Foreground Services
    • A Service in Kotlin
    • System Services, WorkManager, bindService
  • BroadcastReceiver
    • System broadcasts
    • Manifest-registered receivers
    • Dynamic registration, also in Compose
    • Legacy: LocalBroadcastManager

2 Service

  • A Service is an app component without a UI. It provides two main features:
    • A facility for the application to tell the system about something it wants to be doing without the user interacting with it, e.g. playing music, tracking a workout, downloading a file
      • Started service: Context.startService() / startForegroundService()
    • A facility for an application to expose some of its functionality to other components or apps
      • Bound service: Context.bindService(), the client gets an IBinder to call methods
  • A Service is not a separate process: it runs in the main process of your app
  • A Service is not a thread: its callbacks run on the main thread. Long work inside a Service needs a coroutine or thread, otherwise the UI freezes (ANR)

2.1 Service Lifecycle

center

  • Started: onCreate() -> onStartCommand() (once per startService() call) -> runs until stopSelf()/stopService() -> onDestroy()
  • Bound: onCreate() -> onBind() -> runs while clients are bound -> onUnbind() -> onDestroy()

2.2 When Not to Use a Service

Most "background" work in an app is not a Service. Decision help (background work overview):

Need Use
Work while the user is on the screen: network call, DB query Coroutines in viewModelScope (lecture 10)
Work that must finish even if the user leaves the app, can be deferred: sync, upload, cleanup WorkManager (lecture 10)
Work the user is actively aware of and that must run right now, with a visible notification: music, navigation, workout, large download Foreground Service
Expose functionality to other apps/components with an API Bound Service
  • Rule of thumb: if you can not explain to the user why there is a notification, you do not need a Service
  • The old IntentService is deprecated since API 30, it does not work with the limits on the next slide

2.3 Background Execution Limits (Android 8.0+)

Since Android 8.0 (API 26) the system restricts what apps may do while they are in the background (background execution limits):

  • A few minutes after the app leaves the foreground it is considered idle: the system stops its background services as if stopSelf() had been called
  • An idle app calling startService() gets an IllegalStateException
  • startForegroundService() is allowed from the background; the service then has 5 seconds to call startForeground() with a notification, otherwise the system stops it and reports an ANR
  • Most implicit broadcasts can no longer be received through the manifest (see receiver slides)
  • Consequence: a "plain" background service only lives while your app is visible, which is exactly the case a coroutine handles better. Long-running work means Foreground Service or WorkManager

2.4 Foreground Services

A foreground service shows a persistent notification, so the user knows the app is working:

  • Permission FOREGROUND_SERVICE (normal, API 28) in the manifest
  • Since Android 14 (API 34, i.e. when targetSdk >= 34) every foreground service must declare a type that matches its use case, plus the matching FOREGROUND_SERVICE_<TYPE> permission (service types): dataSync, location, mediaPlayback, camera, microphone, health, shortService, ...
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application ...>
    <service
        android:name=".DownloadService"
        android:exported="false"
        android:foregroundServiceType="dataSync" />
</application>
  • POST_NOTIFICATIONS is a dangerous permission since Android 13: request it at runtime (lecture 06), otherwise the notification is silently dropped (the service still runs)

2.5 A Service in Kotlin

class DownloadService : Service() {
    // Own scope: the Service has no lifecycleScope. SupervisorJob: one failed download
    // does not cancel the others. Dispatchers.IO: never block the main thread
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        startForeground(NOTIFICATION_ID, buildNotification())   // within 5 seconds!
        val url = intent?.getStringExtra(EXTRA_URL) ?: return START_NOT_STICKY
        scope.launch {
            downloadFile(url)                                   // suspend function, does the work
            stopSelf(startId)                                   // done, remove the notification
        }
        return START_NOT_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = null       // started service only

    override fun onDestroy() {
        scope.cancel()                                          // stop all running coroutines
        super.onDestroy()
    }

    companion object {
        const val EXTRA_URL = "de.hdmstuttgart.movietracker.EXTRA_URL"
        const val EXTRA_STATUS = "de.hdmstuttgart.movietracker.EXTRA_STATUS"
        const val ACTION_DOWNLOAD_FINISHED = "de.hdmstuttgart.movietracker.DOWNLOAD_FINISHED"
        private const val NOTIFICATION_ID = 1
    }
}

2.6 The Notification

startForeground() needs a Notification; since Android 8.0 notifications need a channel:

private fun buildNotification(): Notification {
    val channel = NotificationChannel(CHANNEL_ID, "Downloads", NotificationManager.IMPORTANCE_LOW)
    getSystemService(NotificationManager::class.java).createNotificationChannel(channel)

    return NotificationCompat.Builder(this, CHANNEL_ID)
        .setContentTitle("Downloading movie poster")
        .setSmallIcon(R.drawable.ic_launcher_foreground)
        .setOngoing(true)
        .build()
}
  • NotificationCompat is in androidx.core:core-ktx, which every starter project already has
  • createNotificationChannel() is idempotent, calling it on every start is fine
  • Progress can be shown with setProgress(max, progress, false) and NotificationManager.notify()

2.7 Service Restart Options

The return value of onStartCommand() tells the system what to do if it kills the service process while the service is running:

Option Description
START_STICKY Service is restarted, but the Intent passed to onStartCommand() is null. Used for services which manage their own state and do not depend on the Intent data (e.g. a music player)
START_NOT_STICKY Service is not restarted, unless there are pending startService() calls. Used for work that can simply be triggered again (our download)
START_REDELIVER_INTENT Service is restarted and the original Intent is re-delivered to onStartCommand(). Used for work that must be completed (e.g. upload of a specific file)

2.8 Starting a Service

  • Use an explicit Intent, exactly like for an Activity (lecture 06)
  • startForegroundService() is required when the app might be in the background; it also works while the app is in the foreground, so always use it for foreground services
@Composable
fun DownloadButton(url: String) {
    val context = LocalContext.current

    Button(onClick = {
        val intent = Intent(context, DownloadService::class.java)
            .putExtra(DownloadService.EXTRA_URL, url)
        context.startForegroundService(intent)
    }) {
        Text(text = "Download")
    }
}
  • Stop it from outside with context.stopService(Intent(context, DownloadService::class.java)), or let the service call stopSelf() when its work is done
  • Every startForegroundService() call results in one onStartCommand() call, the service instance is created only once

2.9 System Services

  • Android provides a number of predefined system services
  • They are exposed via a specific Manager class and obtained with getSystemService(), e.g.
    • NotificationManager to show notifications (previous slides)
    • ConnectivityManager to check the network state
    • AlarmManager to fire an Intent at a certain time
    • LocationManager to receive location (e.g. GPS) updates (for projects: prefer the Fused Location Provider)
    • PowerManager, SensorManager, CameraManager, ...
val context = LocalContext.current
val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
val isOnline = connectivityManager.activeNetwork != null

For more services read the documentation of Context

2.10 WorkManager and Bound Services

  • WorkManager (androidx.work) is the recommended API for deferrable, guaranteed work: sync, upload, periodic cleanup. You describe the work as a CoroutineWorker, add constraints (network, charging) and the system runs it even after the app was closed or the device rebooted. Long work is promoted to a foreground service by WorkManager itself (setForeground()). Details and code in lecture 10
  • Bound services expose an API: onBind() returns an IBinder, the client calls bindService(intent, connection, Context.BIND_AUTO_CREATE) and gets the binder in ServiceConnection.onServiceConnected(). Within one process a Binder subclass with a getService() method is enough; across processes you need AIDL or a Messenger (bound services)
  • Typical bound service: a media playback service that the UI controls (play/pause) while the foreground notification keeps it alive

3 Broadcast Receiver

  • A BroadcastReceiver is a specialized event handler for Intents (like a message mailbox), following the publish/subscribe pattern
  • Subscription to specific action strings (possibly multiple) via an IntentFilter
    • Action strings are defined by the system or by developers
    • System broadcasts: e.g. power connected, timezone changed, airplane mode, boot completed
    • Application broadcasts: e.g. finished download, data is ready, ...
  • onReceive() runs on the main thread and must return quickly (about 10 seconds), afterwards the process may be killed: hand long work to WorkManager or a service

4 System Broadcast Examples

Event Description
Intent.ACTION_BOOT_COMPLETED Boot completed. Requires the android.permission.RECEIVE_BOOT_COMPLETED permission. One of the few implicit broadcasts a manifest receiver may still receive
Intent.ACTION_POWER_CONNECTED Power got connected to the device
Intent.ACTION_POWER_DISCONNECTED Power got disconnected from the device
Intent.ACTION_BATTERY_LOW Battery gets low, typically used to reduce activities in your app which consume power
Intent.ACTION_BATTERY_OKAY Battery status good again
Intent.ACTION_AIRPLANE_MODE_CHANGED Airplane mode was switched on or off
Intent.ACTION_TIMEZONE_CHANGED The time zone has changed

Full list: section "Standard Broadcast Actions" in the Intent reference

5 Implementing a Receiver

  • To receive a broadcast, extend BroadcastReceiver and override onReceive():
class PowerReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val message = when (intent.action) {
            Intent.ACTION_POWER_CONNECTED -> "Charging"
            Intent.ACTION_POWER_DISCONNECTED -> "On battery"
            else -> return
        }
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}
  • intent.action tells you which of the subscribed actions arrived, extras carry the payload
  • Two ways to register it: statically in the manifest or dynamically in code (next slides)

6 Manifest-Registered Receivers

  • Declared in AndroidManifest.xml; the system starts your app (even if it is not running) to deliver the broadcast
<receiver android:name=".BootReceiver" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
  • Restriction since Android 8.0: apps targeting API 26+ can no longer register for most implicit broadcasts in the manifest (they would wake up too many apps at once). ACTION_POWER_CONNECTED or ACTION_BATTERY_LOW are only delivered to receivers registered at runtime
  • Exceptions still allowed in the manifest: BOOT_COMPLETED, LOCALE_CHANGED, TIMEZONE_CHANGED, broadcasts addressed to your package, ... (list)
  • android:exported="true" is needed to receive broadcasts from the system or other apps, "false" for broadcasts from your own app only

7 Dynamically Registered Receivers

  • Registered in code with a Context; receive only while registered. Required for most system broadcasts since Android 8.0
  • Since Android 14 (targetSdk >= 34) you must say whether other apps may send to the receiver: RECEIVER_EXPORTED (system broadcasts, other apps) or RECEIVER_NOT_EXPORTED (own app only)
val receiver = PowerReceiver()
val filter = IntentFilter().apply {
    addAction(Intent.ACTION_POWER_CONNECTED)
    addAction(Intent.ACTION_POWER_DISCONNECTED)
}
// System broadcasts come from outside the app -> RECEIVER_EXPORTED
ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_EXPORTED)

// ... later, always:
context.unregisterReceiver(receiver)
  • ContextCompat.registerReceiver() (androidx.core) applies the flag correctly on all API levels
  • Always unregister in the matching lifecycle callback (onStart/onStop, onCreate/onDestroy), otherwise the receiver leaks the Context

8 Receivers in Compose

In a composable, DisposableEffect is the natural place to register and unregister (lecture 06):

@Composable
fun DownloadStatus() {
    val context = LocalContext.current
    var status by remember { mutableStateOf("idle") }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                status = intent.getStringExtra(DownloadService.EXTRA_STATUS) ?: "done"
            }
        }
        val filter = IntentFilter(DownloadService.ACTION_DOWNLOAD_FINISHED)
        // Own app only -> RECEIVER_NOT_EXPORTED
        ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
        onDispose { context.unregisterReceiver(receiver) }
    }

    Text(text = "Download: $status")
}

The service sends the broadcast to its own package only:

sendBroadcast(Intent(ACTION_DOWNLOAD_FINISHED).setPackage(packageName).putExtra(EXTRA_STATUS, "done"))

9 Legacy: LocalBroadcastManager

  • androidx.localbroadcastmanager was an in-process event bus: broadcasts that never left the app, faster and without security concerns. The library is deprecated, there will be no further releases (release notes)
  • The documentation recommends "LiveData or reactive streams" instead. In a Kotlin/Compose app this means StateFlow/SharedFlow (lecture 10):
    • A shared ViewModel or repository exposes a StateFlow<DownloadState>; the service or worker updates it, the UI collects it with collectAsStateWithLifecycle() (lecture 11)
    • Events that must not be replayed (e.g. "show a snackbar") go into a SharedFlow
  • Compared to broadcasts: type-safe (no string actions, no extras), observable state instead of fire-and-forget events, no registration/unregistration, testable without Android
  • Broadcasts remain the right tool for system events and for communication between apps

10 Summary

  • A Service runs without a UI on the main thread of your process; it is not a thread. Started services are controlled with startForegroundService()/stopSelf(), bound services expose an IBinder
  • Since Android 8.0 background services are stopped when the app is idle: use coroutines while the app is visible, WorkManager for deferrable work and a foreground service with a notification for user-visible long-running work
  • Foreground services need FOREGROUND_SERVICE, a foregroundServiceType with its permission (Android 14) and must call startForeground() within 5 seconds
  • Inside a Service, do the work in a CoroutineScope(SupervisorJob() + Dispatchers.IO) and cancel it in onDestroy()
  • Broadcast receivers subscribe to system or app events. Manifest receivers only work for a few implicit broadcasts, everything else is registered at runtime with ContextCompat.registerReceiver(..., RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED) and unregistered again (DisposableEffect in Compose)
  • LocalBroadcastManager and IntentService are deprecated: in-app communication uses StateFlow/SharedFlow in a shared ViewModel or repository

11 Recap Questions

  • When do you use a Service, when a coroutine and when WorkManager? Give one example each
  • Can a Service block the UI thread? Why?
  • What does Android 8.0 do with a background service a few minutes after the app left the foreground?
  • What must a foreground service do within 5 seconds of being started, and what does it need in the manifest since Android 14?
  • What is the difference between START_STICKY, START_NOT_STICKY and START_REDELIVER_INTENT?
  • How would you inform your UI that your Service has finished a download? Name two ways
  • Why can ACTION_POWER_CONNECTED not be received by a manifest-registered receiver?
  • What do RECEIVER_EXPORTED and RECEIVER_NOT_EXPORTED mean, and when do you use which?
  • Where do you register and unregister a receiver in a composable?
  • What replaces LocalBroadcastManager for in-app communication today?
  • Which software design pattern is behind broadcasts on Android?

Questions?