CategoriesBackground Task

Guide to background Task handling – Part 4 (WorkManager)

WorkManager is an Android component for handling background tasks that need to schedule and survive app restarts and system reboots. WorkManager also can be used for executing one-time background tasks which replaced the functionality of now deprecated IntentService and JobIntentService . WorkManger can handle Immediate, Long running and Deferrable types of background workloads.

WorkManger is a feature pack component compared to other background tasks handling components we discussed so far. Let’s look at the WorkManger features.

Robust scheduling

As I mentioned above briefly work-manager can handle all three types of background tasks. When it comes to scheduling a unique tag and name can be given to a worker to replace, restart, cancel or monitor it. Work-manger schedule data is stored in SQLite database which allows itself to run persistently. In addition to that work manager build in mind with power-saving features so as developers we do not need to worry about it.

Work constraints

WorkManger provides the ability to set up work constraints for which workers can only execute when a given constraint is fulfilled. As an example, you can define a worker to only execute when the phone is connected to the wifi.

Expedited work

A worker can be created as an expedited worker which marks that worker as important work which allows Android OS to run that task immediately.

Flexible retry policy

If a worker fails due to some reason WorkManager provides functionality to set up a retry policy. which allows you to define how the retry should be handled.

Work chaining

Just like in Coroutines few works can be changed together to execute and complete a particular task.

Built-In threading Support

WorkManager has built-in support for Coroutines which allows you to work with other coroutines code blocks with ease.

What WorkManger is not

WorkManager should only be used when the background task must be a persistence task. If the background task must not survive and continue when the application is closed then the most suitable thing to use is Coroutines. If you want to wake your phone from Doze mode and execute some task you need to use AlarmManager for that because WorkManager does not wake your phone from Doze mode.

Coding Example

Let’s take a look at a coding example. In this coding example, I have developed three examples to showcase the usages of WorkManager to run one-time immediate tasks, periodic Immediate tasks and long-running tasks.

One Time Immediate WorkManger

Let’s say you have a task that needs to execute one time in the background but that task may take a few minutes to complete. In that case, creating a WorkManager run only one time and handling your task inside it is ideal. In Android developer documentation defines a long-running task as something that might take more than 10 minutes. So any task less than 10 minutes should work fine in this WorkManager. Generally, I recommend implementing WorkManager to handle background tasks that only take 3~5 minutes because task execution time could exceed the 10-minute limit due to conditions out of our control like network delay. In this coding example, I’m using WorkManager to download an image. A fake network call layer is defined to simulate the network calls using Interceptor. Let’s create the ImageDownloadWorker class to perform an image download.

class ImageDownloadWorker(appContext: Context, workerParams: WorkerParameters): CoroutineWorker(appContext, workerParams) {

    private val thisApplication = appContext as ThisApplication
    private lateinit var fileDownloadState: Result

    override suspend fun doWork(): Result = coroutineScope {

        val networkService by lazy { thisApplication.networkService }
        val imageFileName =
            inputData.getString(IMAGE_NAME) ?: return@coroutineScope Result.failure()
        downloadImage(networkService,imageFileName)

        return@coroutineScope fileDownloadState

    }

    private suspend fun downloadImage(networkService: ApiService, imageFileName: String) {

        try {

            val filePath = File(
                thisApplication.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
                File.separator.toString() + "$imageFileName.jpg"
            ).toString()

            networkService.downloadImage(imageFileName).saveFile(filePath)
                .collect {

                    fileDownloadState = when (it) {
                        is DownloadState.Downloading -> {
                            delay(250L)
                            setProgress(workDataOf(
                                DOWNLOAD_PROGRESS to it.progress)
                            )
                            Result.success()
                        }
                        DownloadState.Failed() -> {
                            Result.failure(
                                workDataOf(
                                    ERROR_MESSAGE to "File Download Error"
                                )
                            )
                        }
                        DownloadState.Finished -> {
                            Result.success(
                                workDataOf(
                                    DOWNLOADED_FILE_NAME to filePath
                                )
                            )
                        }
                        else -> {
                            Result.failure(
                                workDataOf(
                                    ERROR_MESSAGE to "UnKnown Error"
                                )
                            )
                        }
                    }
                }

        } catch (exception: Exception) {

            fileDownloadState = when (exception) {
                is HttpException ->
                    Result.failure(workDataOf(ERROR_MESSAGE to "Network Error"))
                is ConnectivityInterceptor.NoConnectivityException ->
                    Result.failure(workDataOf(ERROR_MESSAGE to "No Connection Error"))
                is SocketTimeoutException ->
                    Result.retry()
                is IOException ->
                    Result.failure(workDataOf(ERROR_MESSAGE to "File Write Exception"))
                else ->
                    Result.failure(workDataOf(ERROR_MESSAGE to "UnKnown Error"))
            }
        }
    }

    companion object {
        const val DOWNLOAD_PROGRESS = "DOWNLOAD_PROGRESS"
        const val DOWNLOADED_FILE_NAME = "DOWNLOADED_FILE_NAME"
        const val IMAGE_NAME = "IMAGE_NAME"
        const val ERROR_MESSAGE = "ERROR_MESSAGE"
    }

}

A WorkManager class can be implemented simply by extending the Worker or CoroutineWorker class. Since the image download task connects with the internet and the network layer is implemented by using Coroutine, it is better to create this worker class by implementing CoroutineWorker since it makes it easier to handle a Coroutine workload. When the work manager is started it triggers the doWork() function. After implementing the function add coroutine scope which creates a new coroutine scope that waits for the suspend function and its children inside it to finish. This allows us to display a progress bar. Based on the execution result Result.success(), Result.failure() or Result.retry() can be called. With the result, it’s possible to send additional data that can be captured by the component that triggered the WorkManager. That’s how it’s showing the image download progress. It’s not necessary to understand how this image download part works to understand this WorkManager example. You can check out the whole project if you are interested. I’ll leave the repository link at the end of the article.

LaunchedEffect(key1 = Unit) {

        val uploadWorkRequest = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
            .setInputData(
                workDataOf(
                    ImageDownloadWorker.IMAGE_NAME to "wallpaper"
                )
            )
            .build()

        val workManager = WorkManager.getInstance(context.applicationContext)
        workManager.enqueue(uploadWorkRequest)
        workManager.getWorkInfoByIdLiveData(uploadWorkRequest.id)
            .observe(lifecycleOwner) { workInfo ->
                when (workInfo.state) {
                    WorkInfo.State.ENQUEUED -> {
                        Log.d(TAG, "file download worker is started")
                    }
                    WorkInfo.State.RUNNING -> {

                        val progress = workInfo.progress
                            .getInt(ImageDownloadWorker.DOWNLOAD_PROGRESS, 0)

                        fileDownloadProgress = progress / 100f

                        Log.d(TAG, "file download progress: $progress")

                    }
                    WorkInfo.State.SUCCEEDED -> {

                        val downloadFile =
                            workInfo.outputData.getString(ImageDownloadWorker.DOWNLOADED_FILE_NAME)

                        fileDownloadProgress = 1.0f
                        isFileDownloadOCompleted = true

                        Log.d(TAG, "downloaded file name: $downloadFile")
                    }
                    WorkInfo.State.FAILED -> {

                        val errorMessage =
                            workInfo.outputData.getString(ImageDownloadWorker.ERROR_MESSAGE)
                        Log.d(TAG, "downloaded error: $errorMessage")

                    }
                    else -> {
                        Log.d(TAG, "other work state: ${workInfo.state}")

                    }
                }
            }
    }

In the composer UI I have initialized the WorkManager. Since this WorkManager only gonna get executed one time OneTimeWorkRequestBuilder is used to initialize the ImageDownloadWorker . A set of input data can be provided to the WorkManger by providing the data as a key-value pair to the setInputData() builder function. In this example, the image name is sent to the ImageDownloadWorker which has been used to create the file with that name. When the enqueue() function on the WorkManager instance is called and passes the defined ImageDownloadWorker to it, The work manager will start its work. To keep observing the ImageDownloadWorker results the getWorkInfoByIdLiveData() function is used. After that, the process is identical to how we deal with the LiveData. Based on the WorkInfo.State I have updated the UI accordingly.

Any of the WorkManger by default set as an expedited work request which means the worker needs to run immediately even if it means temporarily stopping other works in the queue. However, sometimes the operating system will fail to execute a worker immediately. In those cases, setExpedited() function can be used to define OutOfQuotaPolicy. The WorkManger component is carefully constructed with proper resource management in mind. So every worker receives a quota of execution time. setExpedited() determine what should happen when a worker fails to execute the task within that quota. setting that value to RUN_AS_NON_EXPEDITED_WORK_REQUEST make it execute as a non-expedited work which means expedited workers will prioritize while this worker waits for its next quota. If the value of setExpedited() is set to DROP_WORK_REQUEST the worker will be dropped if the quota for the execution in not available.

Long Running WorkManger

In the above example, we download a photo that we can guarantee to finish its work within a few minutes as long as the image is a reasonable size. But the same cannot be said for a video. If there is a need to download a large file like a video in background the using WorkManger things need to be handled a little differently. If you have been following up on the series you may recall when implementing a long-running service we need to display a notification to the user to notify him that the background task is running. When it comes to implementing long-running WorkMangers same should be done. For the most part, this example looks similar to the above example.

class VideoDownloadWorker (appContext: Context, workerParams: WorkerParameters): CoroutineWorker(appContext, workerParams)  {

    private val thisApplication = appContext as ThisApplication
    private lateinit var fileDownloadState: Result
    private lateinit var notification: Notification
    private val notificationManager by lazy { thisApplication.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }

    override suspend fun doWork(): Result = coroutineScope {...}

    private suspend fun downloadVideo (networkService: ApiService, videoFileName: String) {

        try {

            val filePath = File(
                thisApplication.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
                File.separator.toString() + "$videoFileName.mp4"
            ).toString()

            setForeground(createForegroundInfo())

            networkService.downloadVideo(videoFileName).saveFile(filePath)
                .collect{...}

        } catch (exception: Exception) {...}
    }

    private fun createForegroundInfo(): ForegroundInfo {

        val intent = WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)

        notification = NotificationCompat.Builder(thisApplication, CHANNEL_ID)
            .setContentTitle(thisApplication.getText(R.string.video_download_notification_title))
            .setContentText(thisApplication.getText(R.string.video_download_description))
            .setOngoing(true)
            .setSmallIcon(R.drawable.ic_stat_file_download)
            .addAction(android.R.drawable.ic_delete, thisApplication.getText(R.string.video_download_cancel), intent)
            .build()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                CHANNEL_ID,
                "Video Download Notification",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationChannel.description = "This is video download notification"
            notificationManager.createNotificationChannel(notificationChannel)
            notificationManager.notify(VIDEO_DOWNLOAD_NOTIFICATION_ID, notification)
        }

        return ForegroundInfo(VIDEO_DOWNLOAD_NOTIFICATION_ID, notification)
    }

    companion object {
        const val DOWNLOAD_PROGRESS = "DOWNLOAD_PROGRESS"
        const val DOWNLOADED_FILE_NAME = "DOWNLOADED_FILE_NAME"
        const val VIDEO_NAME = "VIDEO_NAME"
        const val ERROR_MESSAGE = "ERROR_MESSAGE"
    }
}

Let’s take a look at createForegroundInfo() function. Inside createForegroundInfo() fucntion a notification is defined. By using addAction() I have set up the ability to cancel the download anytime the user desires. After define the notificaion retrun notificaiotn as ForegroundInfo . Just before the download process starts the notification is setup and shown by calling setForeground(createForegroundInfo()) . The code in the UI is the same as the way it was implemented in the one-time immediate work manager example.

Periodical WorkManger

In this example, we are exploring how the Workmanger can be implemented to execute background tasks periodically. This is a common user case in mobile application development. If there is a data backup task that needs to be executed in your application this is the go-to solution to achieve that. In this example, I have created a scenario that is similar to logistic app. The person who’s delivering the packages needs to add some recodes to the system when they complete the delivery. However this data is not that important to execute in real-time. In this kind of scenario, we can keep those recodes in the SQLite database and execute a network call periodically to update the backend. In this example, I have collected the address, NIC and delivery type using a very simple UI. And those data will be saved into a SQLite table called “Deliveryhistory”. When the defined WorkManger class is triggered to upload the data it will use “backup/work_history” sample endpoint to update the Hypothetical backend. Let’s take a look at the WorkManger class implementation for this.

class DataBackupWorker(appContext: Context, workerParams: WorkerParameters): CoroutineWorker(appContext, workerParams) {

    private val thisApplication = appContext as ThisApplication

    override suspend fun doWork(): Result = coroutineScope {

        val networkService by lazy { thisApplication.networkService }
        val database by lazy { thisApplication.database }

        val allUnSync = database.deliveryHistoryDao().getAllUnSync()

        if(allUnSync.isNotEmpty()){

            val deliveryRecodeList = mutableListOf<DeliveryRecodeRequest>()

            allUnSync.forEach {

                val deliveryRecodeRequest = DeliveryRecodeRequest(
                    id = it.id,
                    address = it.address,
                    nic = it.nic,
                    deliveryType = it.type
                )

                deliveryRecodeList.add(deliveryRecodeRequest)
            }

            try {

                val updateDeliveryRequestData =
                    networkService.updateDeliveryRequestData(deliveryRecodeList)

                if(updateDeliveryRequestData.status){

                    database.deliveryHistoryDao().updateSyncStatus()
                    return@coroutineScope Result.success()
                }else{
                    return@coroutineScope Result.failure(
                        workDataOf(ImageDownloadWorker.ERROR_MESSAGE to updateDeliveryRequestData.message))
                }

            }catch (exception: Exception){
                return@coroutineScope when (exception) {
                    is HttpException ->
                        Result.failure(workDataOf(ERROR_MESSAGE to "Network Error"))
                    is ConnectivityInterceptor.NoConnectivityException ->
                        Result.failure(workDataOf(ERROR_MESSAGE to "No Connection Error"))
                    is SocketTimeoutException ->
                        Result.retry()
                    is IOException ->
                        Result.failure(workDataOf(ERROR_MESSAGE to "Processing Error"))
                    else ->
                        Result.failure(workDataOf(ERROR_MESSAGE to "UnKnown Error"))
                }

            }

        }

        return@coroutineScope Result.success()
    }

    companion object {
        const val ERROR_MESSAGE = "ERROR_MESSAGE"
    }
}

In “Deliveryhistory” table I have defined a column called “is_sync” to keep track of the data that has already been pushed to the backend. In DataBackupWorker class doWork() fuction first, I access the database and get all the non-sync data. Secondly, I execute the network call to back up those data to the server. Finally, when all the work is done I change the status of “is_sync” value in all updated rows. Let’s see how we can initiate this DataBackupWorker calss to execute periodically.

LaunchedEffect(key1 = Unit) {

        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()

        val dataBackupWorkRequest =
            PeriodicWorkRequestBuilder<DataBackupWorker>(
                1, TimeUnit.HOURS, 15, TimeUnit.MINUTES)
                .setConstraints(constraints)
                .build()  

        val workManager = WorkManager.getInstance(applicationContext)
        workManager.enqueueUniquePeriodicWork(TAG,ExistingPeriodicWorkPolicy.KEEP ,dataBackupWorkRequest)
        workManager.getWorkInfoByIdLiveData(dataBackupWorkRequest.id)
            .observe(lifecycleOwner) { workInfo ->
                when (workInfo.state) {
                    WorkInfo.State.ENQUEUED -> {
                        Log.d(TAG, "data backup worker is started")
                    }
                    WorkInfo.State.RUNNING -> {
                        Log.d(TAG, "data backup worker is running")

                    }
                    WorkInfo.State.SUCCEEDED -> {
                        Log.d(TAG, "data backup worker completed it's work")
                    }
                    WorkInfo.State.FAILED -> {
                        val errorMessage =
                            workInfo.outputData.getString(DataBackupWorker.ERROR_MESSAGE)
                        Log.d(TAG, "data backup worker error: $errorMessage")
                    }
                    else -> {
                        Log.d(TAG, "other work state: ${workInfo.state}")

                    }
                }
            }
    }

One of the flexibilities of WorkManager is the ability to provide the conditions that workers should be executed. In a data backup scenario, a network connection has to be there for the DataBackupWorker to access the back end. Therefore I have defined a constraint for the worker using Constraints.Builder() to make sure the work is only getting executed when there is an active network connection. To initialize DataBackupWorker to execute periodically PeriodicWorkRequestBuilder is used. PeriodicWorkRequestBuilder constructor takes two parameters. The first parameter is repeat interval which defines the time interval we need the worker to get triggered. Even though the repeat interval is set as a fixed time it won’t be triggering the worker class every 1 hour exactly. The trigger interval depends on the constraints that you are using in your WorkRequest object and on the optimizations performed by the system. The second parameter is flex interval which defines in what period the execution happens within the defined repeat interval. In the above example, the task will the work manager will trigger every hour and execute within the last 15 minutes of that hour.

This example concludes the article about WorkManger. You can find the code in this repository. Refer to work_manager_example branch for the start point of the application.

Published by Chathuranga Shan

I am a mobile application developer who loves to get into new technologies in the Industry. Specialized in Android mobile application development with 5+ years’ experience working for different types of products across many business domains.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.