Android Service is one of the four application components of Android. It can perform longer-running work in the background, such as playing music, downloading files, etc. So Service does not provide user interface. This article will introduce the basic concepts of Service.
The complete code for this chapter can be found in .
Table of Contents
Overview
Service can perform longer-running work in the background, such as playing music, downloading files, etc. So Service does not provide user interface. It can also provide some functionalities for other apps to use. Once the Service is started, it will continue to run even if the user switches to another app.
Like Activity, Service also runs in the main thread. That is, if it is going to perform some CPU-intensive works, such as playing music, or blocking works, such as networking and downloading files, it needs to create a thread to handle those works.
As stated on the official website, Service is not a separate process. The Service object does not run in its own process (unless specified) but in the process of the app it belongs to. Service is also not a thread. The Service object does not run in its own thread but in the main thread of the app to which it belongs.
When the service is started, the Android system actually does it just to instantiate it and calls its onCreate() and other callbacks, such as onStartCommand(), onBind(), in the main thread. Next, it is up to the service to implement the appropriate behavior, such as creating a thread to perform the work.
Service provides two main functionalities:
- The app tell the system that it wants to perform some works in the background, even if the user is not interacting with the app directly. That is, the app is not in the foreground. In this case, the app will call Context.startService() to tell the system to schedule work for the service until the service stops itself, or another component stops it.
- The app can expose some functionalities for other apps to use. In this case, other apps will call Context.bindService() to create a long-standing connection with the service in order to interact with the service.
There are three types of Service to correspond to these two main functionalities. For details, please refer to the official website :
- Background Service: Started via Context.startService().
- Foreground Service: Started via Context.startService().
- Bound Service: Started via Context.bindService().
- Local Bound Service: A long-standing connection created by other components in the same app.
- Remote Bound Service: A long-standing connection created by other apps.
Lifecycle
Like Activity, Service also has a lifecycle. Other components start the service by calling Context.startService() or Context.bindService(). Like startActivity(), the intent is also passed when calling startService(). The official website has a detailed explanation of each callbacks.
- Context.startService(): The Android system will start the service and call Service.onCreate(). Then call Service.onStartCommand() and pass the intent. If the service has already been started and is running, the Android system will not start the service and will not call Service.onCreate() again. Instead, call Service.onStartCommand() directly.
- Context.bindService(): The Android system will start the service and call Service.onCreate(). Then call Service.onBind() and pass the intent. If the service has already been started and is running, the Android system will not start the service, nor will it call Service.onCreate() and Service.onBind() again.
When the service has finished its work, calls Service.stopSelf() to stop itself, or other components call Context.stopService() to stop the service. So, if the service handles multiple requests (Service.onStartCommand() is called multiple times), then the service should call Service.stopSelf(startId) where startId is delivered to onStartCommand().
If the service is started through Context.bindService(), when all other components call Context.unbindService() , the Android system will call Service.onUnbind() and stop the service.
Low Memory
The Android system tries to keep the process hosting a service around. Moreover, when the system runs low on memory and needs to kill some processes to release memory. It will decide which processes to kill based on the following conditions, please refer to the official website for details :
- If the service is executing onCreate(), onStartCommand(), or onDestroy(), the hosting process will not be killed to ensure that these methods can be fully executed.
- If the service is already started, its hosting process is less important than any process currently visible to the user on-screen. The system will kill these background hosting processes according to the priorities.
- If there are other apps bind the service, and the priority of the service’s hosting process is lower than the priority of this app. And this app is a foreground app, the hosting process of this service will also be considered as a foreground app, and the hosting process will not be killed.
- If the service calls startForeground(), it is a foreground service, The hosting process will not be killed.
So in most time the service is running. But when the system runs low on memory, it needs to kill some services. Later, the system may try to restart the the services.
So when we implement a service, we must take into account that the service may be killed by the system and then restarted. For example, when you implement onStartCommand(), you may want to use START_FLAG_REDELIVERY to tell the system to re-deliver the last intent when the service is restarted.
Choosing between Service and Thread
One of the main functionality of Service is to perform longer-running work in the background. When we implement a service to perform longer-running work, we create a background thread to run the work, because the service runs in the main thread. So why don’t we directly create a background thread to run the work, but create a background thread through Service? For example, create a thread directly in the Activity to run the work.
This problem is mentioned in detail on the official website. Simply put, it depends on the alive time of the background thread.
Suppose, you only want to play music when the activity is on the screen. When switching to other activities or other apps (that is, the activity is not displayed on the screen), stop playing the music. You should create a thread in the activity or use Kotlin coroutine to play music. Then, when the activity is stopped, you stop the thread or Kotlin coroutine to stop playing the music. That is, the background thread lives only when the activity is alive (that is, the activity is displayed on the screen).
Suppose, you want to play music when any activity is displayed while the app is running. Or, Play music even when the app is in the background (that is, without showing any activity). Then you should use Service to create a thread to play music. Because the alive time of this background thread has nothing to do with any activity, but is related to the alive time of the app. Therefore, we have to create a background thread through Service. In this case, the alive time of the background thread will be related to the alive time of the service.
If you want to play music when the app is running, you create a global background thread in an activity to play music, and this activity does not stop the thread when it ends. Doing so will leave the thread uncontrolled. Because when the Android system wants to stop the app, it will call the lifecycle callbacks of currently executing activities and services, and they can safely stop the background thread in these callbacks. When this thread is out of control, and the Android system wants to kill the app, there is no way for this background thread to end safely, such as writing data to a file, etc. Because this thread does not get informed that the system wants to kill the app. Also, if the app is killed due to low memory, the Android system may later try to restart the app’s services. But if it is an uncontrolled background thread, it will not be restarted.
Background Service
In the following example, we create a DownloadService to download files and update the download progress to the activity.
When the activity calls Context.startService(), if the service is not started, the service will be created. Then, onCreate() will be called. In onCreate(), we create a Handler as a background thread to download files.
Next, the system will call onStartCommand() and pass the intent. We get the ResultReceiver from the intent passed by the activity. The Service will update the download progress to the activity through the ResultReceiver. We send a message to the Handler, so the Handler will start working. Finally, onStartCommand() returns a constant.
This constant tells the system how to restart the service when it is killed. Android provides some constants:
- START_NOT_STICKY: After Android kills the service, do not restart the service unless there are pending intents.
- START_STICKY: After Android kills the service, restart the service. But instead of re-deliver the last intent, pass null unless there are pending intents.
- START_REDELIVER_INTENT: After Android kills the service, restart the service and pass the last intent. If there are pending intents, pass the pending intents in order.
In ServiceHandler.handleMessage(), after the download is complete, we call Service.stopSelf() to stop the service.
We don’t need to implement onBind() because DownloadService is not a Bound Service.
Finally, like Activity, remember to declare the service in AndroidManifest.xml.
package com.waynestalk.serviceexample import android.app.Service import android.content.Intent import android.os.* import android.util.Log class DownloadService : Service() { companion object { const val ARGUMENT_RECEIVER = "receiver" const val RESULT_CODE_UPDATE_PROGRESS = 6 const val RESULT_PROGRESS = "progress" private const val TAG = "DownloadService" } inner class ServiceHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { Log.d(TAG, "handleMessage: msg.arg1=${msg.arg1} => ${Thread.currentThread()}") for (i in 1..10) { try { Thread.sleep(1000) val resultData = Bundle().apply { putInt(RESULT_PROGRESS, i * 10) } receiver?.send(RESULT_CODE_UPDATE_PROGRESS, resultData) } catch (e: InterruptedException) { e.printStackTrace() } } stopSelf(msg.arg1) } } private var looper: Looper? = null private var serviceHandler: ServiceHandler? = null private var receiver: ResultReceiver? = null override fun onCreate() { Log.d(TAG, "onCreate => ${Thread.currentThread()}") val handlerThread = HandlerThread("DownloadService", Process.THREAD_PRIORITY_BACKGROUND) handlerThread.start() looper = handlerThread.looper serviceHandler = ServiceHandler(handlerThread.looper) } override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy => ${Thread.currentThread()}") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand=$startId => ${Thread.currentThread()}") receiver = intent?.getParcelableExtra(ARGUMENT_RECEIVER) serviceHandler?.obtainMessage()?.let { message -> message.arg1 = startId serviceHandler?.sendMessage(message) } return START_NOT_STICKY } override fun onBind(p0: Intent?): IBinder? { TODO("Not yet implemented") } }
package com.waynestalk.serviceexample import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.ResultReceiver import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.waynestalk.serviceexample.databinding.DownloadActivityBinding class DownloadActivity : AppCompatActivity() { companion object { private const val TAG = "DownloadActivity" } private var _binding: DownloadActivityBinding? = null private val binding: DownloadActivityBinding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = DownloadActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.downloadWithStartCommand.setOnClickListener { val intent = Intent(this, DownloadService::class.java) intent.putExtra(DownloadService.ARGUMENT_RECEIVER, UpdateReceiver(Handler(mainLooper))) startService(intent) } } inner class UpdateReceiver constructor(handler: Handler) : ResultReceiver(handler) { override fun onReceiveResult(resultCode: Int, resultData: Bundle) { super.onReceiveResult(resultCode, resultData) if (resultCode == DownloadService.RESULT_CODE_UPDATE_PROGRESS) { val progress = resultData.getInt(DownloadService.RESULT_PROGRESS) Log.d(TAG, "progress=" + progress + " => " + Thread.currentThread()) binding.progress.text = "$progress %" } } } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ServiceExample" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> <activity android:name=".DownloadActivity" /> <service android:name=".DownloadService" /> </application> </manifest>
Foreground Service
Foreground Service and Background Service are implemented in almost the same way. The Foreground Service must display a Notification even if the app is in the background, then call Service.startForeground(). Finally, in AndroidManifest.xml, also must declare android.permission.FOREGROUND_SERVICE.
Notifications will be displayed in the status bar and notification drawer, so the user will notice the service is running. Therefore, it is called Foreground Service. The user cannot dismiss this notification. But starting from Android 13 (API level 33), users can dismiss this notification. If you want the user not to dismiss the notification, you can call setOngoing(true) when creating the notification.
package com.waynestalk.serviceexample import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Intent import android.os.* import android.util.Log import androidx.core.app.NotificationCompat class DownloadForegroundService : Service() { companion object { const val ARGUMENT_RECEIVER = "receiver" const val RESULT_CODE_UPDATE_PROGRESS = 8 const val RESULT_PROGRESS = "progress" private const val CHANNEL_ID = "DownloadForegroundServiceChannel" private const val TAG = "ForegroundService" } inner class ServiceHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { Log.d(TAG, "handleMessage: msg.arg1=${msg.arg1} => ${Thread.currentThread()}") for (i in 1..10) { try { Thread.sleep(1000) val resultData = Bundle().apply { putInt(RESULT_PROGRESS, i * 10) } receiver?.send(RESULT_CODE_UPDATE_PROGRESS, resultData) } catch (e: InterruptedException) { e.printStackTrace() } } stopSelf(msg.arg1) } } private var looper: Looper? = null private var serviceHandler: ServiceHandler? = null private var receiver: ResultReceiver? = null override fun onCreate() { Log.d(TAG, "onCreate => ${Thread.currentThread()}") val handlerThread = HandlerThread("DownloadService", Process.THREAD_PRIORITY_BACKGROUND) handlerThread.start() looper = handlerThread.looper serviceHandler = ServiceHandler(handlerThread.looper) } override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy => ${Thread.currentThread()}") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand=$startId => ${Thread.currentThread()}") createNotificationChannel() val notificationIntent = Intent(this, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0) val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Download Foreground Service") .setContentText("Downloading ...") .setSmallIcon(android.R.drawable.stat_sys_download) .setContentIntent(pendingIntent) .setOngoing(true) .build() startForeground(startId, notification) receiver = intent?.getParcelableExtra(ARGUMENT_RECEIVER) serviceHandler?.obtainMessage()?.let { message -> message.arg1 = startId serviceHandler?.sendMessage(message) } return START_NOT_STICKY } override fun onBind(p0: Intent?): IBinder? { TODO("Not yet implemented") } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "Download Foreground Service", NotificationManager.IMPORTANCE_DEFAULT ) val notificationManager = getSystemService(NotificationManager::class.java) notificationManager.createNotificationChannel(channel) } } }
package com.waynestalk.serviceexample import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.ResultReceiver import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.waynestalk.serviceexample.databinding.DownloadForegroundActivityBinding class DownloadForegroundActivity : AppCompatActivity() { companion object { private const val TAG = "ForegroundActivity" } private var _binding: DownloadForegroundActivityBinding? = null private val binding: DownloadForegroundActivityBinding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = DownloadForegroundActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.downloadWithForegroundService.setOnClickListener { val intent = Intent(this, DownloadForegroundService::class.java) intent.putExtra( DownloadForegroundService.ARGUMENT_RECEIVER, UpdateReceiver(Handler(mainLooper)) ) startService(intent) } } inner class UpdateReceiver constructor(handler: Handler) : ResultReceiver(handler) { override fun onReceiveResult(resultCode: Int, resultData: Bundle) { super.onReceiveResult(resultCode, resultData) if (resultCode == DownloadForegroundService.RESULT_CODE_UPDATE_PROGRESS) { val progress = resultData.getInt(DownloadForegroundService.RESULT_PROGRESS) Log.d(TAG, "progress=" + progress + " => " + Thread.currentThread()) binding.progress.text = "$progress %" } } } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ServiceExample" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> <activity android:name=".DownloadForegroundActivity" /> <service android:name=".DownloadForegroundService" /> </application> </manifest>
Bound Service
Conclusion
Services can perform longer-running work in the background, or provide some functionalities for other apps to use. The Service does not provide user interface. The Service will continue to run while the app is in the background. Note that Service runs in the main thread, so it is necessary to create a background thread to perform those time-consuming tasks. When implementing a service, consider that the service may be killed by the system at any time, and may be restarted later. When killed, it should appropriately release resources and save important data to the storage.