Search code examples
androidandroid-jetpackandroid-workmanager

Expedited WorkRequests require a ListenableWorker to provide an implementation for getForegroundInfoAsync()


Doing a little Jeopardy style Q&A here.

I have some work I sometimes need to run as expedited as described in the version 2.7.0 of WorkManager:

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED).build()
val oneTimeWorkRequest = OneTimeWorkRequest.Builder(MyWorker::class.java)
    .setInitialDelay(2, TimeUnit.SECONDS)
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .setConstraints(constraints).build()
WorkManager.getInstance(context).enqueueUniqueWork("my-identifier", ExistingWorkPolicy.REPLACE, oneTimeWorkRequest)

I believe the code ran just fine on Android 12/S, but when the job is run on Android 11 I get the following error:

E/WM-WorkerWrapper: Work [ id=<UUID>, tags={ [WorkerTag] } ] failed because it threw an exception/error
     java.util.concurrent.ExecutionException: java.util.concurrent.ExecutionException: java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Expedited WorkRequests require a ListenableWorker to provide an implementation for `getForegroundInfoAsync()`
        at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:516)
        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)

What do I need to do?


Solution

  • The documentation for ListenableWorker.getForegroundInfoAsync() states this:

    Prior to Android S, WorkManager manages and runs a foreground service on your behalf to execute the WorkRequest, showing the notification provided in the ForegroundInfo. To update this notification subsequently, the application can use NotificationManager.

    Starting in Android S and above, WorkManager manages this WorkRequest using an immediate job.

    So in the class extending ListenableWorker it's necessary to override getForegroundInfoAsync().

    An alternative to directly overriding that method yourself is to use for example CoroutineWorker:

        class MyWorker(val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
    
        companion object {
            private const val NOTIFICATION_CHANNEL_ID = "11"
            private const val NOTIFICATION_CHANNEL_NAME = "Work Service"
        }
    
        override suspend fun doWork(): Result {
            // TODO: Do work here
            return Result.success()
        }
    
        override suspend fun getForegroundInfo(): ForegroundInfo {
            val notificationManager =
                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel = NotificationChannel(
                    NOTIFICATION_CHANNEL_ID,
                    NOTIFICATION_CHANNEL_NAME,
                    NotificationManager.IMPORTANCE_HIGH
                )
                notificationManager.createNotificationChannel(channel)
            }
    
            val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
                .setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE))
                .setSmallIcon(R.drawable.ic_refresh_24dp)
                .setOngoing(true)
                .setAutoCancel(true)
                .setOnlyAlertOnce(true)
                .setPriority(NotificationCompat.PRIORITY_MIN)
                .setContentTitle(context.getString(R.string.app_name))
                .setLocalOnly(true)
                .setVisibility(NotificationCompat.VISIBILITY_SECRET)
                .setContentText("Updating widget")
                .build()
            return ForegroundInfo(1337, notification)
        }
    
    }
    

    (That constant for the pending intent flag is really just val PENDING_INTENT_FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 to make stuff work with both Android 12/S and earlier.)