How does setExpedited work and is it as good&reliable as a foreground service?
Asked Answered
G

2

44

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 ?
Godgiven answered 29/5, 2021 at 8:24 Comment(4)
Did you find any solution? I also want to do the same thing in android 12.Dropsonde
@ChiragBhuva No idea yet how to use it properly. I'm not even sure I want to use it.Godgiven
Thanks. I was wondering, do you have a concrete answer for your question? As, I have a similar doubt as you - #74178077Prosector
@CheokYanCheng I don't know.Godgiven
S
15

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)
        }
    }
}
Sortilege answered 21/7, 2021 at 11:36 Comment(20)
- 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 implementedKrieg
@BogdanStolyarov ahh another great documentation quirk, good to know..Sortilege
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?Godgiven
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 answerSortilege
@androiddeveloper if it helps with your issue please mark my post as accepted answerSortilege
@Sortilege 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.Godgiven
@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 versionsSortilege
@Sortilege 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 ?Godgiven
@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 activitySortilege
@Sortilege 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?Godgiven
@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: developer.android.com/about/versions/12/…Sortilege
@Sortilege I see. Thanks. Marking as accepted answer now. Hope it won't cause new issues to use this new API.Godgiven
@Sortilege 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 :(Godgiven
@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 (guava.dev/releases/29.0-jre/api/docs/com/google/common/util/…), use .set function to set your foreground result and return this settable future as getForegroundInfoAsync result. That's allSortilege
@Sortilege 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: developer.android.com/about/versions/12/foreground-services . Really annoying...Godgiven
@Sortilege are you saying that we have to use setExpedited even if a task runs for a couple of second? why?Usurpation
@Usurpation Couple of seconds should be fine on a regular worker. But it all depends on app quota and system behavior. So no guarantees hereSortilege
@Sortilege seems documentation says about more than 10 minutes developer.android.com/topic/libraries/architecture/workmanager/…, 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 leastUsurpation
@Usurpation 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 manufacturerSortilege
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC If you set that on Android 11 the app crashes, it does not starting with Android 12, so the check if wrong I suppose.Ladon
I
15

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

Iosep answered 29/10, 2021 at 9:45 Comment(7)
The diagram seems to get from android developer website, can you provide the link to the diagram? Thanks!Dillingham
The diagram is from the Droidcon London conference session on WorkManager. We will add it to d.android.com soon.Iosep
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!!Dillingham
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. maven.google.com/web/…Iosep
This sums it all #69685156Steading
it crashes on Android 11- if I set .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) but works fine for Androdi 12+Usurpation
Hi @BenWeiss, I implement my code based on guideline given in developer.android.com/topic/libraries/architecture/workmanager/… (Java example). I was wondering, is using setExpedited and having try...catch around setForegroundAsync sufficient to handle new API 31 limitation? thanks.Prosector

© 2022 - 2024 — McMap. All rights reserved.