User-initiated Data Transfer Job
User-initiated data transfer job is only available in API level 34 (Android 14) or higher.
One option we can use to do long data operation is by using user-initiated data transfer job.
Characteristics​
User-initiated data transfer jobs have these characteristics:
- They are started by the user.
- They require a notification.
- They start immediately.
- They may be able to run for an extended period of time as system condition allow.
- They can be run concurrently with other user-initiated data transfer jobs.
- They can be stopped by user via Task Manager or by the system.
- They can only be started when the conditions are met.
- You can define the constraints for the job to be run by the system.
Basic Usage​
First, we need to add RUN_USER_INITIATED_JOBS
permission to our AndroidManifest.xml
file. We also need to these 2 additional permissions:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Add this permission: -->
<uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" />
<!-- We also need to add this permission to specify job network constraint: -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- We also need to add this permission to post notification: -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
</application>
</manifest>
The next thing you need to do is create a subclass of JobService
for the data transfer. Here's a simple JobService
example that pretends to download data from Internet:
package com.hanmajid.androidnotebook
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationManagerCompat
/**
* User-initiated data transfer job service for (dummy) downloading files.
*/
class DummyDownloadJobService : JobService() {
override fun onStartJob(params: JobParameters): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Create notification channel.
val name = "Download Data"
val importance = NotificationManager.IMPORTANCE_LOW
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
// Post initial notification.
postNotification(params, 0.0)
// Create a new thread for the actual work.
// In this example, we only count from 0 to 10 while updating the notification text.
val handlerThread = HandlerThread("MyDownloadThread").apply {
start()
}
val handler = Handler(handlerThread.looper)
handler.post {
for (i in 0..9) {
// Update progress to notification.
postNotification(params, i / 10.0)
Thread.sleep(1000L)
}
// Update completion to notification.
postNotification(params, 1.0)
// Finish the job.
jobFinished(params, false)
}
// Returns true so that the service is keep running.
return true
} else {
// Returns false so that the service is stopped.
return false
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun postNotification(params: JobParameters, progress: Double) {
val contentText = if (progress < 1) {
"Progress: ${progress * 100}%"
} else {
"Completed!"
}
val notification = Notification.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Downloading your file")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentText(contentText)
.build()
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
NotificationManagerCompat.from(this)
.notify(
NOTIFICATION_ID,
notification,
)
}
// Set notification to job.
setNotification(
params,
NOTIFICATION_ID,
notification,
JOB_END_NOTIFICATION_POLICY_DETACH,
)
}
override fun onStopJob(params: JobParameters): Boolean {
// Returns true so that the job can be retried rescheduled based on the retry criteria.
return true
}
companion object {
// Notification channel ID for downloading data.
const val NOTIFICATION_CHANNEL_ID = "download-data-channel"
// Notification ID for downloading data.
const val NOTIFICATION_ID = 123
}
}
Next, we need to register this class to our AndroidManifest.xml
file:
<?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>
<service
android:name=".DummyDownloadJobService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
</manifest>
Lastly, you need to schedule your job with JobScheduler
. For user-initiated data transfer jobs, its necessary to use setUserInitiated()
method:
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.content.ComponentName
import android.content.Context
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import androidx.annotation.RequiresApi
// Define network constraint for the job.
val networkRequestBuilder = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.build()
// Define the job and its constraints.
val jobInfo = JobInfo.Builder(1, ComponentName(context, DummyDownloadJobService::class.java))
.setUserInitiated(true)
.setRequiredNetwork(networkRequestBuilder)
.build()
// Schedule the job.
val jobScheduler = context.getSystemService(JobScheduler::class.java)
jobScheduler.schedule(jobInfo)
Here's what happen when we try schedule the data transfer job: