Notes: 17. Use IntentService
The student materials have been reviewed and are updated as of SEPTEMBER 2022.
The next type of service you’ll learn to implement serves as a one-off job. Similar to how you used the WorkManager, to start a piece of work, and then return a result, or not care about the result at all, you can do the same with a service.
There are two types of one-off services, and they are almost the same. The first is the IntentService, and the second is the JobIntentService You’re probably thinking they have the same name, so why are there two of them?
Historically, first came the IntentService, and it was used for everything you had to fire and forget, so to say. You would start the service, run some work, and then communicate the result to the rest of the app somehow, but you could also ignore the result if you’re working on synchronizing some data, or uploading a file.
But with the new restrictions that came in Android Oreo (API 26), IntentServices became restricted too. Because of this, similar to how the WorkManager solved the problem of backwards compatibility, and background restrictions, the JobIntentService came to life, as a solution which should work in all cases.
The JobIntentService is backed by two mechanisms. The first is the standard Service ecosystem, for Android devices which run versions prior to Android Oreo. The second is the JobScheduler ecosystem, from the Android Oreo version, and above.
The JobScheduler is in fact a legacy mechanism, which you shouldn’t use manually. It’s built into the WorkManager and JobIntentService, so you don’t have to schedule Jobs yourself, you can rely on other APIs to do that, if needed!
Without further ado, let’s implement both types of the Service, to download the image! :]
You don’t have to create new files for this episode, you’ll use the service you previously implemented. Open the DownloadService if you haven’t already. Change the code of the service to the following:
const val SERVICE_NAME = "Download Image Service"
class DownloadService : IntentService(SERVICE_NAME) {
override fun onHandleIntent(intent: Intent?) {
val imagePath = intent?.getStringExtra("image_path")
if (imagePath != null) {
downloadImage(imagePath)
}
}
private fun downloadImage(imagePath: String) {
val file = File(applicationContext.externalMediaDirs.first(), imagePath)
FileUtils.downloadImage(file, imagePath)
}
}
There have been a few changes here. First, you extend the IntentService, instead of the regular Service class.
It requires a name for the primary constructor, so you created a constant named SERVICE_NAME, to represent this download service.
Then you removed the onBind() function, as the IntentService cannot be bound to any component, it’s a one-off service, not something which should persist with other components.
The onStartCommand() function was replaced by the onHandleIntent() function, as this is the starting point for all IntentServices.
And finally, notice how you removed the threading code. This is because the IntentService runs in a background thread by default! Pretty cool! :] You don’t have to change anything else to start this service! Run the project, and you’ll see the image download still works.
But because of the new BG restrictions in Android, the IntentService has been deprecated. To use a newer, stable API, you have to use the JobIntentService. Change the code as such:
class DownloadService : JobIntentService() {
companion object {
private const val JOB_ID = 10
fun startWork(context: Context, intent: Intent) {
enqueueWork(context, DownloadService::class.java, JOB_ID, intent)
}
}
...
}
To refactor to a JobIntentService, you first have to change the extended class. You don’t need the service name anymore, so you removed the constant and the constructor argument.
You also created a companion object, and a startWork() function. This is because the JobIntentService should be started in a static way, and in turn you have to call the static enqueueWork() function, from within the JobIntentService you want to start.
The function takes in a context, the service class you want to start, its ID, as an Integer, and an intent to start the service with.
Now move onto implementing the rest of the code:
class DownloadService : JobIntentService() {
...
override fun onHandleWork(intent: Intent) {
val imagePath = intent.getStringExtra("image_path")
if (imagePath != null) {
downloadImage(imagePath)
} else {
Log.d("Missing image path", "Stopping service")
stopSelf()
}
}
...
}
Instead of using the onHandleIntent(), you have to use the onHandleWork() function. Its signature is the same, so not much changed here. Same goes for the downloadImage function:
class DownloadService : JobIntentService() {
companion object {
private const val JOB_ID = 10
fun startWork(context: Context, intent: Intent) {
enqueueWork(context, DownloadService::class.java, JOB_ID, intent)
}
}
override fun onHandleWork(intent: Intent) {
val imagePath = intent.getStringExtra("image_path")
if (imagePath != null) {
downloadImage(imagePath)
} else {
Log.d("Missing image path", "Stopping service")
stopSelf()
}
}
private fun downloadImage(imagePath: String) {
val file = File(applicationContext.externalMediaDirs.first(), imagePath)
FileUtils.downloadImage(file, imagePath)
}
}
The logic stays the same, and the threading is automatic. But to be fully compatible with older and newer versions of Android, you have to add a couple of permissions to the AndroidManifest. Open the AndroidManifest.xml file, and add the following code:
<uses-permission android:name="android.permission.WAKE_LOCK" />
<service
android:name=".service.DownloadService"
android:permission="android.permission.BIND_JOB_SERVICE" />
The WAKE_LOCK permission is used because the system will hold a wake lock for you, when you schedule your work. This is for pre-Oreo devices. The BIND_JOB_SERVICE permission is used to bind the JobIntentService, and run it with the JobScheduler API. This is for Oreo and above devices.
Finally, head over to the ImagesFragment, and change the startService code, to the following:
val intent = Intent()
intent.putExtra("image_path", imageUrl)
DownloadService.startWork(requireContext(), intent)
Instead of manually starting the service, you’ll enqueue work in the system, and the system will decide which API it will use - the JobScheduler, or the IntentService. Run the project, and everything should work as before! :]