Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 ?
like image 321
android developer Avatar asked May 29 '21 08:05

android developer


People also ask

When should I use work manager?

Use WorkManager for reliable workWorkManager is intended for work that is required to run reliably even if the user navigates off a screen, the app exits, or the device restarts. For example: Sending logs or analytics to backend services. Periodically syncing application data with a server.

What is expedited work in Android?

Expedited work uses a quota system that's based on the App Standby Buckets and limits the maximum execution time within a rolling time window. The quotas used for expedited work is more restrictive than the ones used for other types of background jobs.

What's the best tool for handling work that is deferrable and expected to run even if your app restarts?

What is Work Manager? Work Manager is a library part of Android Jetpack which makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts i.e. even your app restarts due to any issue Work Manager makes sure the scheduled task executes again.

What is work manager and how work manager play an essential role in mobile application development?

Android WorkManager is a background processing library which is used to execute background tasks which should run in a guaranteed way but not necessarily immediately. With WorkManager we can enqueue our background processing even when the app is not running and the device is rebooted for some reason.


Video Answer


2 Answers

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)
        }
    }
}
like image 129
Jakoss Avatar answered Oct 19 '22 04:10

Jakoss


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

like image 29
keyboardsurfer Avatar answered Oct 19 '22 02:10

keyboardsurfer