03 - Services
A Service is an application component that can perform long-running operations in the background. It does not provide a user interface. Once started, a service might continue running for some time, even after the user switches to another application.
In Android, services (e.g., Service) by default run on the main thread, so any long-running or blocking task directly executed in onStartCommand() will freeze the UI and potentially cause an Application Not Responding (ANR) error.
A service is simply a component that can run in the background, even when the user is not interacting with your application, so you should create a service only if that is what you need. For example, a thread in an activity would be more useful displaying data periodically or continuously in the UI.
The service lifecycle
All services extend the Service
class. Depending on whether the service want to comunicate with other components the service can be bounded (data can be requested from the service) or unbounded. Below you have the the list of callback methods available in the Service
class which define the service lifecycle.
Callback Method | Purpose |
---|---|
onCreate() | Initialize resources required by the service. This is called only once during the service lifecycle. |
onStartCommand() | Handle the service start request. Perform tasks that need to run in the foreground or background. |
onBind() | Return an IBinder to allow the client to communicate with the service. |
onUnbind() | Perform cleanup for the service when no clients remain bound. |
onRebind() | Handle cases where clients return to a previously unbound service. |
onDestroy() | Clean up any remaining resources, stop threads, and save final states. |
Services types
Background services
A background service performs an operation that isn't directly noticed by the user. For example, if an app used a service to compact its storage, that would usually be a background service.
Starting a background service
- Caller component side
val intent = Intent(this, BackgroundService::class.java)
startService(intent)
- Service side
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
//start task...
return START_STICKY
}
onStartCommand()
has to return one of the following constants
START_NOT_STICKY
If the system kills the service after onStartCommand() returns, do not recreate the service unless there are pending intents to deliver.START_STICKY
If the system kills the service after onStartCommand() returns, recreate the service and call onStartCommand(), but do not redeliver the last intent.START_REDELIVER_INTENT
If the system kills the service after onStartCommand() returns, recreate the service and call onStartCommand() with the last intent that was delivered to the service.
Stoping a background service
- Caller component side
val intent = Intent(this, BackgroundService::class.java)
stopService(intent)
OR
- Service side
stopSelf()
+
- Service side
override fun onDestroy() {
super.onDestroy()
//cleanup ...
Log.d(TAG, "Service stopped")
}
Running a task inside the service
//task
private fun startTask() {
Thread {
// do something...
Log.d("Task", "log progress")
}.start()
}
Foreground services
A foreground service performs some operation that is noticeable to the user. For example, an audio app would use a foreground service to play an audio track. Foreground services must display a Notification. Foreground services continue running even when the user isn't interacting with the app.
Starting a foreground service
- Caller component side
val intent = Intent(this, ForegroundService::class.java)
startService(intent)
- Service side
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// define a channel for the notification
// this is used so that you can group the types of notifications you want to have in your app
val channelId = "ForegroundServiceChannel"
val channelName = "Foreground Service"
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(channel)
//create a notification
val notification = Notification.Builder(this, channelId)
.setContentTitle("Notification title")
.setContentText("Content: $data")
.setSmallIcon(android.R.drawable.ic_notification_overlay)
.build()
//this is when the foreground service actually starts
startForeground(1, notification)
//start task ...
return START_STICKY
}
Stoping a foreground service
- Caller component side
val intent = Intent(this, ForegroundService::class.java)
stopService(intent)
- Service side
override fun onDestroy() {
super.onDestroy()
//cleanup...
Log.d(TAG, "Service stopped")
}
Updating data in the notification
//task
private fun startTask() {
Thread {
while (taskRunning) {
//do something...
updateNotification()
}
}.start()
}
private fun updateNotification() {
val notification = Notification.Builder(this, "ForegroundServiceChannel")
.setContentTitle("Notification title")
.setContentText("Content: $data")
.setSmallIcon(android.R.drawable.ic_notification_overlay)
.build()
//use notification manager to pass the updated notification
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(1, notification)
}
Aditional permissions need to be set in the AndroidManifest file
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ When you decleare the service, you need to specify the service type
Example
<service
android:name=".MyMediaPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="false">
</service>
You can find other types of foreground services here.
Bound services
A bound service offers a client-server interface that allows components to interact with the service, send requests, receive results, and even do so across processes with interprocess communication (IPC).
Preparing bound service
- Caller component side
private var boundService: BoundService? = null
private var isBound = false
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as BoundService.ServiceBinder
boundService = binder.getService()
isBound = true
}
override fun onServiceDisconnected(name: ComponentName?) {
isBound = false
boundService = null
}
}
- Service side
class BoundService : Service() {
private val binder = ServiceBinder()
private var data: String = ""
//...
inner class ServiceBinder : Binder() {
fun getService(): BoundService = this@BoundService
}
}
Binding a service
- Caller component side
val intent = Intent(this, BoundService::class.java)
bindService(intent, serviceConnection, BIND_AUTO_CREATE)
- Service side
override fun onBind(intent: Intent?): IBinder {
//call task
return binder
}
Unbiding a service
- Caller component side
if (isBound) {
unbindService(serviceConnection)
isBound = false
}
- Service side
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
override fun onDestroy() {
//cleanup...
super.onDestroy()
}
Running a task inside the service
//task
private fun startTask() {
Thread {
// do something...
// modify data...
Log.d("Task", "log progress")
}.start()
}
Retrieving data from the bound service
- Caller component side
val data = boundService?.getData() ?: "No data"
- Service side
fun getData(): String {
return data
}
Exercices
Clone the lab repository or dowload the project zip file and import the project template into android studio. For each task you will have a series of TODOs.
- The layout for the main activity is already created. All buttons have their
onClickListeners
declared. - All services classes are already created.
-
Using button [3], start the
BackgroundCounterService
from theMainActvity
. Using button [4], stop theBackgroundService
from theMainActvity
. In this service create a function that counts every second indefinetly (use a simple while loop) and logs the value of the counter in the logcat. Call this function inside theonStartCommand
.Run the app and click button [3]. Notice what happends with the interface.
-
Fix the issue in task 1.
-
Use buttons [1] and [2] to start/stop the
ForegroundService
. Create a similar counter function to the one in task 2. and use it to update each second a notification that shows the counter value. -
Use buttons [5] and [6] to bind/unbind the
QuoteFetchService
. Display the latest fetched quote in the MainActivity when button [7] is pressed.
Notice what happends if you press button [7], before button [6].