Search code examples
androidkotlinkotlin-coroutinesandroid-workmanager

Long Running Worker issue of Workmanger: Getting Exception kotlinx.coroutines.JobCancellationException: Job was cancelled in CoroutineWorker in Kotlin


I have created a simple CoroutineWorker that run loop for 1000 time with delay of 1000 milliseconds.

This worker is unique periodic worker with 15 minutes as repeat interval and with ExistingPeriodicWorkPolicy as KEEP

But When I start worker and after some time during execution worker gets cancelled with Exception JobCancellationException

The Full Exception:

Exception kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@ba57765
12:55:47.088 WM-Wor...rapper  I  Work [ id=4c44c3da-3c57-4cac-a40a-82c948125807, tags={ com.sk.workmanagerdemo1.DownloadingWorker } ] was cancelled
                                 java.util.concurrent.CancellationException: Task was cancelled.
                                    at androidx.work.impl.utils.futures.AbstractFuture.cancellationExceptionWithCause(AbstractFuture.java:1184)
                                    at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:514)
                                    at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:475)
                                    at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:311)
                                    at androidx.work.impl.utils.SerialExecutor$Task.run(SerialExecutor.java:91)
                                    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
                                    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
                                    at java.lang.Thread.run(Thread.java:923)

The Worker code:

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.*

class DownloadingWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return withContext(Dispatchers.IO) {
            Log.i("MYTAG", "Started ${getCurrentDateTime()}")
            return@withContext try {
                for (i in 0..1000) {
                    delay(1000)
                    Log.i("MYTAG", "Downloading $i")
                }
                Log.i("MYTAG", "Completed ${getCurrentDateTime()}")
                Result.success()
            } catch (e: Exception) {
                Log.i("MYTAG", "Exception $e")
                Result.failure()
            }
        }
    }

    private fun getCurrentDateTime(): String {
        val time = SimpleDateFormat("dd/M/yyyy hh:mm:ss")
        return time.format(Date())
    }
}

And starting of worker

private fun setPeriodicWorkRequest() {
        val downloadConstraints = Constraints.Builder()
            .setRequiresCharging(true)
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
        val periodicWorkRequest = PeriodicWorkRequest
            .Builder(DownloadingWorker::class.java, 15, TimeUnit.MINUTES)
            .setConstraints(downloadConstraints)
            .build()
        WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
            "DownloadWorker",
            ExistingPeriodicWorkPolicy.KEEP,
            periodicWorkRequest
        )
    }

I am calling the above function on a button click in the activity.

I am not sure why I am getting this exception automatically after some time around after 10 mins.

Thanks in advance. Please help me in this to identify cause and please let me know any input from my side.


Solution

  • For me I couldn't manage to reproduce that exception; though assuming that all the work constraints are met; there is a possibility of cancelling the worker process by the system in case it takes much long.

    For that case the documentation offers support for long-running workers which associates a Foreground service that indicates to the system/user that there is a long running processing in the background; so the system can keep the process and the user knows what is consuming their resources and that also can allow them to stop that worker.

    Especially when you mentioned that it's long with:

    I am not sure why I am getting this exception automatically after some time around after 10 mins.

    Where the documentation says something that I though that could be the reason:

    WorkManager has built-in support for long running workers. In such cases, WorkManager can provide a signal to the OS that the process should be kept alive if possible while this work is executing. These Workers can run longer than 10 minutes.

    To enable that you need to call startForeground() to update the notification associated with the foreground service; usually with the progress of the worker.

    An example is provided by the documentation showing how to use that in the worker; here I had customized that to your DownloadingWorker:

    
    class DownloadingWorker(context: Context, params: WorkerParameters) :
        CoroutineWorker(context, params) {
    
        private val notificationId: Int = 1
    
        override suspend fun doWork(): Result {
            return withContext(Dispatchers.IO) {
                Log.i("MYTAG", "Started ${getCurrentDateTime()}")
                return@withContext try {
                    for (i in 0..1000) {
                        delay(1000)
                        val progress = "Starting Download"
                        setForeground(createForegroundInfo(progress))
                        Log.i("MYTAG", "Downloading $i")
                        setForeground(createForegroundInfo("Downloading $i"))
                    }
                    Log.i("MYTAG", "Completed ${getCurrentDateTime()}")
                    setForeground(createForegroundInfo("Completed"))
                    Result.success()
                } catch (e: Exception) {
                    Log.i("MYTAG", "Exception $e")
                    Result.failure()
                }
            }
        }
    
        // Creates an instance of ForegroundInfo which can be used to update the
        // ongoing notification.
        private fun createForegroundInfo(progress: String): ForegroundInfo {
            val title = "notification title)"
            val cancel = "cancel download"
            val channelId = "notification id"
            // This PendingIntent can be used to cancel the worker
            val intent = WorkManager.getInstance(applicationContext)
                .createCancelPendingIntent(getId())
    
            // Create a Notification channel if necessary
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                createChannel(channelId)
            }
    
            val notification = NotificationCompat.Builder(applicationContext, channelId)
                .setContentTitle(title)
                .setTicker(title)
                .setContentText(progress)
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setOngoing(true)
                // Add the cancel action to the notification which can
                // be used to cancel the worker
                .addAction(android.R.drawable.ic_delete, cancel, intent)
                .build()
    
            return ForegroundInfo(notificationId, notification)
        }
    
    
        @RequiresApi(Build.VERSION_CODES.O)
        private fun createChannel(channelId: String) {
            // Create a Notification channel
            val serviceChannel = NotificationChannel(
                channelId,
                "Download Channel",
                NotificationManager.IMPORTANCE_DEFAULT
            )
    
            val notificationManager =
                applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as
                        NotificationManager
            notificationManager.createNotificationChannel(serviceChannel)
    
        }
    
    
        private fun getCurrentDateTime(): String {
            val time = SimpleDateFormat("dd/M/yyyy hh:mm:ss")
            return time.format(Date())
        }
    
    }
    

    Make sure to add the foreground service to the manifest within <application>:

    <application
        ....
        <service
           android:name="androidx.work.impl.foreground.SystemForegroundService"
           android:foregroundServiceType="dataSync" />
    
    </application>
    

    Also you need to request Manifest.permission.POST_NOTIFICATIONS programmatically in API 33+ and in manifest:

    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />