39

Background

In the past, I used a foreground IntentService to handle various events that come one after another. Then it was deprecated when Android 11 came (Android R, API 30) and it was said to prefer to use Worker that uses setForegroundAsync instead, and so I did it.

val builder = NotificationCompat.Builder(context,...)...
setForegroundAsync(ForegroundInfo(notificationId, builder.build()))

The problem

As Android 12 came (Android S, API 31), just one version after , now setForegroundAsync is marked as deprecated, and I am told to use setExpedited instead:

* @deprecated Use {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)} and
* {@link ListenableWorker#getForegroundInfoAsync()} instead.

Thing is, I'm not sure how it works exactly. Before, we had a notification that should be shown to the user as it's using a foreground service (at least for "old" Android versions). Now it seems that getForegroundInfoAsync is used for it, but I don't think it will use it for Android 12 :

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.

Another clue about it is (here) :

Starting in WorkManager 2.7.0, your app can call setExpedited() to declare that a Worker should use an expedited job. This new API uses expedited jobs when running on Android 12, and the API uses foreground services on prior versions of Android to provide backward compatibility.

And the only snippet they have is of the scheduling itself :

OneTimeWorkRequestBuilder<T>().apply {
    setInputData(inputData)
    setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
}.build()

Even the docs of OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST seems weird:

When the app does not have any expedited job quota, the expedited work request will fallback to a regular work request.

All I wanted is just to do something right away, reliably, without interruption (or at least lowest chance possible), and use whatever the OS offers to do it.

I don't get why setExpedited was created.

The questions

  1. How does setExpedited work exactly ?
  2. What is this "expedited job quota" ? What happens on Android 12 when it reaches this situation? The worker won't do its job right away?
  3. Is setExpedited as reliable as a foreground service? Would it always be able to launch right away?
  4. What are the advantages, disadvantages and restrictions that setExpedited has ? Why should I prefer it over a foreground service?
  5. Does it mean that users won't see anything when the app is using this API on Android 12 ?
android developer
  • 114,585
  • 152
  • 739
  • 1,270

2 Answers2

13

Conceptually Foreground Services & Expedited Work are not the same thing.

Only a OneTimeWorkRequest can be run expedited as these are time sensitive. Any Worker can request to be run in the foreground. That might succeed depending on the app's foreground state.

A Worker can try to run its work in the foreground using setForeground[Async]() like this from WorkManager 2.7 onwards:

class DownloadWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    override suspend fun getForegroundInfo(): ForegroundInfo {
        TODO()
    }

    override suspend fun doWork(): Result {
        return try {
            setForeground(getForegroundInfo())
            Result.success()
        } catch(e: ForegroundServiceStartNotAllowedException) {
            // TODO handle according to your use case or fail.
            Result.fail()
        }
    }
}


You can request a WorkRequest to be run ASAP by using setExpedited when building it.

val request = OneTimeWorkRequestBuilder<SendMessageWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()


WorkManager.getInstance(context)
    .enqueue(request)

On Android versions before 12 expedited jobs will be run as a foreground service, showing a notification. On Android 12+ the notification might not be shown.

Diagram of when to use Foreground Services & Expedited Jobs

Ben Weiss
  • 17,182
  • 6
  • 67
  • 87
  • The diagram seems to get from android developer website, can you provide the link to the diagram? Thanks! – BobTheCat Dec 15 '21 at 07:59
  • 2
    The diagram is from the Droidcon London conference session on WorkManager. We will add it to d.android.com soon. – Ben Weiss Dec 16 '21 at 08:21
  • Appreciate for the great works! I just found out that you are the engineer I saw on the youtube video :D Back to bussiness, (In java, sdk< android 12) may I ask: 1. Is it necessary to override `getForegroundInfoAsync()` when I `setExpedited` ? becuase it will throw IllegalStateException if I don't do that. However from the d.android.com, they didnt' mentioned about that, it seems like the workManager will handle it for above android 12 or below it automatically. 2. If it is necessay to override `getForegroundInfoAsync()` , what is the proper way to do it (in Java) ? Thanks!! – BobTheCat Dec 16 '21 at 09:23
  • 1
    Thanks for the kind words. Yes it is necessary to override as WorkManager runs a Foreground Service for you even required. We're adding an API to make it easier but for now you'll need to use the `futures` artifact. https://maven.google.com/web/m_index.html#androidx.concurrent:concurrent-listenablefuture:1.0.0-beta01 – Ben Weiss Dec 17 '21 at 10:59
  • This sums it all https://stackoverflow.com/questions/69684656/upgrading-to-workmanager-2-7-0-how-to-implement-getforegroundinfoasync-for-rxwo – Gordon Freeman Mar 17 '22 at 13:53
  • it crashes on Android 11- if I set `.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)` but works fine for Androdi 12+ – user924 Sep 12 '22 at 14:59
  • Hi @BenWeiss, I implement my code based on guideline given in https://developer.android.com/topic/libraries/architecture/workmanager/advanced/long-running (Java example). I was wondering, is using `setExpedited` and having `try...catch` around `setForegroundAsync` sufficient to handle new API 31 limitation? thanks. – Cheok Yan Cheng Oct 24 '22 at 10:06
11

ad 1. I cannot explain that exactly (haven't looked into the code), but from our (developers) perspective it's like a queue for job requests that takes your apps job quota into account. So basically you can rely on it if your app behaves properly battery wise (whatever that means...)

ad 2. Every app gets some quota that it cannot exceed. The details of this quota are (and probably always will) an OEM internal implementation detail. As to reaching quota - that's exactly what the OutOfQuotaPolicy flag is for. You can set that if your app reaches its quota - job can run as normal job or it can be dropped

ad 3. That's a mistery. Here we can find vague statement:

Expedited jobs, new in Android 12, allow apps to execute short, important tasks while giving the system better control over access to resources. These jobs have a set of characteristics somewhere in between a foreground service and a regular JobScheduler job

Which (as i understand it) means that work manager will be only for short running jobs. So it seems like it won't be ANY official way to launch long running jobs from the background, period. Which (my opinions) is insane, but hey - it is what it is.

What we do know is that it should launch right away, but it might get deferred. source

ad 4. You cannot run foreground service from the background. So if you need to run a "few minutes task" when app is not in foreground - expedited job will the only way to do it in android 12. Want to run longer task? You need to get your into the foreground. It will probably require you to run job just to show notification that launches activity. Than from activity you can run foreground service! Excited already?

ad 5. On android 12 they won't see a thing. On earlier versions expedited job will fallback to foreground service. You need to override public open suspend fun getForegroundInfo(): ForegroundInfo to do custom notification. I'm not sure what will happen if you don't override that though.

Sample usage (log uploading worker):

@HiltWorker
class LogReporterWorker @AssistedInject constructor(
    @Assisted appContext: Context,
    @Assisted workerParams: WorkerParameters,
    private val loggingRepository: LoggingRepository,
) : CoroutineWorker(appContext, workerParams) {
    override suspend fun doWork(): Result {
        loggingRepository.uploadLogs()
    }

    override suspend fun getForegroundInfo(): ForegroundInfo {
        SystemNotificationsHandler.registerForegroundServiceChannel(applicationContext)
        val notification = NotificationCompat.Builder(
            applicationContext,
            SystemNotificationsHandler.FOREGROUND_SERVICE_CHANNEL
        )
            .setContentTitle(Res.string(R.string.uploading_logs))
            .setSmallIcon(R.drawable.ic_logo_small)
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .build()
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            ForegroundInfo(
                NOTIFICATION_ID,
                notification,
                ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
            )
        } else {
            ForegroundInfo(
                NOTIFICATION_ID,
                notification
            )
        }
    }

    companion object {
        private const val TAG = "LogReporterWorker"
        private const val INTERVAL_HOURS = 1L
        private const val NOTIFICATION_ID = 3562

        fun schedule(context: Context) {
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.UNMETERED)
                .build()
            val worker = PeriodicWorkRequestBuilder<LogReporterWorker>(
                INTERVAL_HOURS,
                TimeUnit.HOURS
            )
                .setConstraints(constraints)
                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                .addTag(TAG)
                .build()

            WorkManager.getInstance(context)
                .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, worker)
        }
    }
}
Jakoss
  • 4,647
  • 2
  • 26
  • 40
  • 13
    `- I'm not sure what will happen if you don't override that though` The app will be crashed with a message that `getForegroundInfo` isn't implemented – Bogdan Stolyarov Jul 28 '21 at 16:16
  • 2
    @BogdanStolyarov ahh another great documentation quirk, good to know.. – Jakoss Jul 29 '21 at 08:02
  • I see. So you say that if the operation is short, I should consider using it? Can you please show a sample of how to use it after the changes? – android developer Aug 02 '21 at 09:58
  • 1
    If you want to run short operation from the background you HAVE TO use that. If you want to run long operation you have to start running foreground service while having activity opened. I will add example to answer – Jakoss Aug 02 '21 at 14:15
  • @androiddeveloper if it helps with your issue please mark my post as accepted answer – Jakoss Aug 04 '21 at 09:21
  • @Jakoss How can I decide whether I need it or not? If it's less than 10 minutes, use it, and if not, use a foreground service? Would it become a foreground service with a notification, automatically? Or I have to choose? As for accepting the answer, I still don't know so I upvoted for now. – android developer Aug 04 '21 at 10:20
  • @androiddeveloper like i wrote, if you want to run job that takes longer than seconds, and you want to run it from backgroun - you HAVE TO use it on android 12 and further on. It will automatically use foreground service on older android versions – Jakoss Aug 04 '21 at 12:04
  • @Jakoss Wait, so this is how you start a foreground service now? Even if it starts from boot Intent or other Intents ? Or are you talking only about Worker ? – android developer Aug 04 '21 at 16:07
  • 1
    @androiddeveloper no, this is how you can run long running job from scheduler/background. You can run foreground service as before as long as you have a visible activity – Jakoss Aug 05 '21 at 06:13
  • @Jakoss I see. So it's only for Worker, and only for cases that you want to run something right-now, and it will decide whether to use a foreground service or not? Foreground services weren't changed, right? I can still open them as usual, such a from boot, right? – android developer Aug 05 '21 at 06:18
  • 1
    @androiddeveloper Foreground services are limited as to when they can be started. But i believe you still should be able to start it from boot, here are the exceptions for foreground services: https://developer.android.com/about/versions/12/foreground-services#cases-fgs-background-starts-allowed – Jakoss Aug 05 '21 at 06:26
  • @Jakoss I see. Thanks. Marking as accepted answer now. Hope it won't cause new issues to use this new API. – android developer Aug 05 '21 at 09:02
  • @Jakoss Can you please post a Github sample of this, but with no work actually being done? Without even kotlin coroutines ? Just the really bare minimal sample? I wanted to try it out on some POC, but I failed for some reason. Using Worker, all I get is getForegroundInfoAsync :( – android developer Aug 23 '21 at 21:10
  • @androiddeveloper That's because in Worker `getForegroundInfo` is suspending function. Something that you will not encounter in normal Worker that's simply synchronous. It requires you to return `ListenableFuture`. You could create a `SettableFuture` (https://guava.dev/releases/29.0-jre/api/docs/com/google/common/util/concurrent/SettableFuture.html), use `.set` function to set your foreground result and return this settable future as `getForegroundInfoAsync` result. That's all – Jakoss Aug 24 '21 at 06:29
  • @Jakoss OK thanks I think I got it with something similar to what you wrote. You just used some stuff that don't exist anywhere. I suggest to remove the not-needed stuff there. It's confusing. Anyway, I was hoping I could also start a foreground service (that lives for a much longer time) on this Worker but it seems it's impossible due to recent Android 12 restrictions: https://developer.android.com/about/versions/12/foreground-services . Really annoying... – android developer Aug 24 '21 at 07:25
  • @Jakoss are you saying that we have to use `setExpedited` even if a task runs for a couple of second? why? – user924 Sep 12 '22 at 15:03
  • @user924 Couple of seconds should be fine on a regular worker. But it all depends on app quota and system behavior. So no guarantees here – Jakoss Sep 12 '22 at 16:59
  • @Jakoss seems documentation says about more than 10 minutes https://developer.android.com/topic/libraries/architecture/workmanager/advanced/long-running, if a task runs for a couple of seconds than I guess it's totally safe to not use `setExpedited`. I mean that system should not trigger any crashes at least – user924 Sep 12 '22 at 17:06
  • 1
    @user924 yes, it seems reasonable, but there are no guarantees. Every background processing now relies on the quota i was writing about in the answer. If you app "misbehaves" few seconds might be too much. That's my whole problem with WorkManager. There are no guarantees and numbers, only vague "quota" that's pretty much up to manufacturer – Jakoss Sep 13 '22 at 08:08