Notes: 25. Use the JobScheduler
The student materials have been reviewed and are updated as of SEPTEMBER 2022.
The next legacy mechanism you’ll use for background processing is the JobScheduler. It’s actually very similar to how the WorkManager is implemented. But it has some key differences when it comes to communicating the Result output data.
JobScheduler is based on using services, which are backed by job requests. These services then run on the main thread, and behave as a “job” which has to be finished. You have to push your code to the background yourself, and you also have to notify the service that the job has finished, through special functions.
But you also have to communicate the result through BroadcastReceivers, which can turn out to be a lot of hoops you have to jump through. Let’s implement the JobScheduler API! :]
As you remember from the introduction, you need a JobService, in which you will run the background work. Create a new file named DownloadImageJobService, and add the following code:
class DownloadImageJobService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
val imagePath = params?.extras?.getString("image_path")
}
}
Now open the AndroidManifest.xml, and add the Service to the manifest:
<service
android:name=".DownloadImageJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
Then go back to the Service file. It’s similar to an IntentService, isn’t it? It receives a signal to start a job, and some parameters. Now add the following code, to the onStartJob() function:
class DownloadImageJobService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
val imagePath = params?.extras?.getString("image_path")
return if (imagePath != null) {
downloadImage(imagePath)
true
} else {
jobFinished(null, false)
false
}
}
private fun downloadImage(imageDownloadPath: String) {
Thread(Runnable {
}).start()
}
}
onStartJob() needs to return a Boolean, to tell the JobScheduler API if the job has started succesfully or not. In this case, if there is an image path available to download, you start the download job, and return true.
If not, you tell the job has completed, and return false. The jobFinished() function takes in JobParameters, if you have any data to send, and a Boolean which tells the API if you want to reschedule the job or not.
But in this case, you don’t need parameters, and you won’t reschedule the job. Now move onto implementing the rest of the downloadImage() function:
class DownloadImageJobService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
val imagePath = params?.extras?.getString("image_path")
return if (imagePath != null) {
downloadImage(imagePath)
true
} else {
jobFinished(null, false)
false
}
}
private fun downloadImage(imageDownloadPath: String) {
Thread(Runnable {
val imageUrl = URL(imageDownloadPath)
val connection = imageUrl.openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()
val imagePath = "owl_image_${System.currentTimeMillis()}.jpg"
val inputStream = connection.inputStream
val file = File(applicationContext.externalMediaDirs.first(), imagePath)
}).start()
}
}
You should already know this, as you’ve implemented this before. You’re using the HTTP connection to download an image, and creating a file to store the image in. Next, add the output stream for the file:
class DownloadImageJobService : JobService() {
...
private fun downloadImage(imageDownloadPath: String) {
Thread(Runnable {
try {
val imageUrl = URL(imageDownloadPath)
val connection = imageUrl.openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()
val imagePath = "owl_image_${System.currentTimeMillis()}.jpg"
val inputStream = connection.inputStream
val file = File(applicationContext.externalMediaDirs.first(), imagePath)
val outputStream = FileOutputStream(file)
outputStream.use { output ->
val buffer = ByteArray(4 * 1024)
var byteCount = inputStream.read(buffer)
while (byteCount > 0) {
output.write(buffer, 0, byteCount)
byteCount = inputStream.read(buffer)
}
output.flush()
}
}).start()
}
}
And finally, finish off with a try/catch block, and a broadcast that the image was downloaded:
class DownloadImageJobService : JobService() {
...
private fun downloadImage(imageDownloadPath: String) {
Thread(Runnable {
try {
...
sendBroadcast(Intent().apply {
action = ACTION_IMAGE_DOWNLOADED
putExtra("image_path", file.absolutePath)
})
} catch (error: Throwable) {
jobFinished(null, false)
}
}).start()
}
}
By using the following code to send a broadcast, you tell the receiver which you’ll build, that the image download was successful! Don’t worry about the constant here, you’ll create it in a moment.
sendBroadcast(Intent().apply {
action = ACTION_IMAGE_DOWNLOADED
putExtra("image_path", file.absolutePath)
})
One more thing to finish up here. Override the onStopJob(), and add the following code:
class DownloadImageJobService : JobService() {
...
override fun onStopJob(params: JobParameters?): Boolean {
return false
}
}
By returning false here, you tell the JobScheduler that you don’t want to reschedule the job, you just want to finish it.
Nicely done! Now you need to implement the BroadcastReceiver that will receive the signal from the JobService. Create a new file named ImageDownloadedReceiver, and add the following code:
const val ACTION_IMAGE_DOWNLOADED = "image_downloaded"
class ImageDownloadedReceiver(
private val onImageDownloaded: (String) -> Unit
) : BroadcastReceiver() {}
You first have to define that constant from before, to identify specific broadcasts for this receiver.
Then you define a callback property in the receiver, which you’ll use to send the image file path, to the MainActivity.kt. As with any receiver, override the onReceive() function:
const val ACTION_IMAGE_DOWNLOADED = "image_downloaded"
class ImageDownloadedReceiver(
private val onImageDownloaded: (String) -> Unit
) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
}
}
And fill in the function with the following code:
const val ACTION_IMAGE_DOWNLOADED = "image_downloaded"
class ImageDownloadedReceiver(
private val onImageDownloaded: (String) -> Unit
) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != ACTION_IMAGE_DOWNLOADED) {
return
}
val imagePath = intent.getStringExtra("image_path")
if (imagePath != null) {
onImageDownloaded(imagePath)
}
}
}
You have to check that the broadcast you’ve received is the correct one, and then you get the image path from the Intent.
If the path is not null, you pass it to the UI. Pretty simple! Finally, head over to the MainActivity.kt, and register and unregister the receiver as such:
private val receiver by lazy {
ImageDownloadedReceiver {
displayImage(it)
}
}
registerReceiver(receiver, IntentFilter().apply {
addAction(ACTION_IMAGE_DOWNLOADED)
})
override fun onDestroy() {
unregisterReceiver(receiver)
super.onDestroy()
}
Nothing new here. You have to create the receiver, and its lifecycle in the Activity. You also call the displayImage() function, when a broadcast is consumed by the receiver! All that’s left to do is start the JobService! Fetch the JobScheduler, from the system:
val jobScheduler = getSystemService(JobScheduler::class.java) ?: return
If you don’t manage to fetch the scheduler, you simply return. In production apps you’d probably want to default to a different service, but this will do for now. Now start scheduling the job like so:
const val LOAD_IMAGE_JOB_ID = 10
val jobScheduler = getSystemService(JobScheduler::class.java) ?: return
jobScheduler.schedule(JobInfo.Builder(LOAD_IMAGE_JOB_ID,
ComponentName(this, DownloadImageJobService::class.java))
.build())
You first create a constant to represent the job’s ID. Then you build a new job, which will start the DownloadImageJobService. But you still have to add necessary constraints and the input data:
val jobScheduler = getSystemService(JobScheduler::class.java) ?: return
jobScheduler.schedule(JobInfo.Builder(LOAD_IMAGE_JOB_ID,
ComponentName(this, DownloadImageJobService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.setExtras(PersistableBundle().apply {
putString("image_path", "https://wallpaperplay.com/walls/full/1/c/7/38027.jpg")
})
.setOverrideDeadline(1500)
.build())
By using setRequiredNetworkType() you can add the network constraint. Using setExtras() you pass in the bundle of parameters you want as the input data. And finally, setOverrideDeadline() adds the maximum latency or delay for the job to start, in milliseconds.
That’s all you need to setup the JobService. Launch the app now, and you should see your components working together, to display the owl image! :]