bindServiceLocalBroadcastManagerContext.startService() / startForegroundService()Context.bindService(), the client gets an IBinder to call methods
onCreate() -> onStartCommand() (once per startService() call) -> runs until
stopSelf()/stopService() -> onDestroy()onCreate() -> onBind() -> runs while clients are bound -> onUnbind() -> onDestroy()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 |
IntentService is deprecated since API 30, it does not work with the limits on the next slideSince Android 8.0 (API 26) the system restricts what apps may do while they are in the background (background execution limits):
stopSelf() had been calledstartService() gets an IllegalStateExceptionstartForegroundService() 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 ANRA foreground service shows a persistent notification, so the user knows the app is working:
FOREGROUND_SERVICE (normal, API 28) in the manifesttargetSdk >= 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)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
}
}
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 hascreateNotificationChannel() is idempotent, calling it on every start is finesetProgress(max, progress, false) and NotificationManager.notify()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) |
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")
}
}
context.stopService(Intent(context, DownloadService::class.java)), or
let the service call stopSelf() when its work is donestartForegroundService() call results in one onStartCommand() call, the service instance
is created only oncegetSystemService(), e.g.NotificationManager to show notifications (previous slides)ConnectivityManager to check the network stateAlarmManager to fire an Intent at a certain timeLocationManager 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
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 10onBind() 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)IntentFilteronReceive() 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| 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
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 payloadAndroidManifest.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>
ACTION_POWER_CONNECTED
or ACTION_BATTERY_LOW are only delivered to receivers registered at runtimeBOOT_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 onlyContext; receive only while registered. Required for most system
broadcasts since Android 8.0targetSdk >= 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 levelsonStart/onStop, onCreate/onDestroy),
otherwise the receiver leaks the ContextIn 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"))
LocalBroadcastManagerandroidx.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)StateFlow/SharedFlow (lecture 10):ViewModel or repository exposes a StateFlow<DownloadState>; the service or
worker updates it, the UI collects it with collectAsStateWithLifecycle() (lecture 11)SharedFlowstartForegroundService()/stopSelf(), bound services expose an
IBinderFOREGROUND_SERVICE, a foregroundServiceType with its permission
(Android 14) and must call startForeground() within 5 secondsCoroutineScope(SupervisorJob() + Dispatchers.IO) and cancel it
in onDestroy()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 repositorySTART_STICKY, START_NOT_STICKY and START_REDELIVER_INTENT?ACTION_POWER_CONNECTED not be received by a manifest-registered receiver?RECEIVER_EXPORTED and RECEIVER_NOT_EXPORTED mean, and when do you use which?LocalBroadcastManager for in-app communication today?