Replace Android Foreground Services with WorkManager

Manish Bannur
9 min readAug 3, 2021

--

In this article you will learn

  • Basics of Foreground Service
  • Basics of JobScheduler
  • What’s new in Android 12
  • Expedited jobs
  • How to replace Foreground services with New WorkManager

Basics of Foreground Service

According to the text book definition from Official Android Developer Service and Official Android Developers Docs Guide

A foreground service performs some operation that is noticeable to the user. For example, an audio app would use a foreground service to play an audio track. Foreground services must display a Notification. Foreground services continue running even when the user isn’t interacting with the app.

When you use a foreground service, you must display a notification so that users are actively aware that the service is running. This notification cannot be dismissed unless the service is either stopped or removed from the foreground.

Some examples of foreground service — When you listen to music that is being played by Music Player application. So Music Player is basically using the foreground service to play the music. The foreground service always uses the notification to notify the user and using the notification you can actually interact with the service or the ongoing operation such as pause the music or play the next music.

Create a foreground service / Steps to create foreground service :

  • Extend the Service class and overriding onStartCommand, where we then promote our service to the foreground by calling startForeground and passing a notification to it, which we build with the NotificationCompat.Builder.
val pendingIntent: PendingIntent =
Intent(this, ExampleActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, 0)
}

val notification: Notification = Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
.setContentTitle(getText(R.string.notification_title))
.setContentText(getText(R.string.notification_message))
.setSmallIcon(R.drawable.icon)
.setContentIntent(pendingIntent)
.setTicker(getText(R.string.ticker_text))
.build()

// Notification ID cannot be 0.
startForeground(ONGOING_NOTIFICATION_ID, notification)
  • With target API level 26 (Android Oreo) or higher, we have to create notification channels in order to be able to display notifications, and we will do this is in the Application class.
  • Depending if we want to restart the service if the system kills it, we return either START_STICKY, START_NOT_STICKY or START_REDELIVER_INTENT from onStartCommand.
  • Additional we can override onCreate and onDestroy, where we can do an initial setup and a cleanup. We also have to override onBind, but since we are creating a started service and not a bound service, we can just ignore this method and return null.
  • We register our service in the AndroidManifest.xml file and then we can start it by calling startService, startForegroundService or ContextCompat.startForegroundService, where we have to pass an intent, which can optionally contain data in form of extras.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>

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

<application ...>
...
</application>
</manifest>

To Remember > If an app that targets API level 28 or higher attempts to create a foreground service without requesting the FOREGROUND_SERVICE permission, the system throws a SecurityException.

  • We stop the service by calling either stopSelf from within, or stopService from another app component.

Internals of foreground services

  • The Foreground Service in Android cannot be started without passing a Notification to it! (Creating a Notification is very very important to let the user know there is a service run by the App)
  • Service Lifecycle decides if a Service should be running or not. When it comes to overriding the onStartCommand() method, we are required to provide return modes which are
    - START_STICKY: This is used for services that are explicitly started and stopped as needed.
    - START_NOT_STICKY: This is used to let the OS know that there is a need to stay alive only when there is a command that has to be run. This can mean that the OS can kill the service if a higher priority Service has to be run.
    - START_REDELIVER_INTENT: This has the same property as that of the START_NOT_STICKY, except if the OS kills it, the Service gets fired once again! This is really important if you are going to be creating a long-running ForegroundService (Might also be a bounded service).

Basics of JobScheduler

An API for scheduling various types of jobs against the framework that will be executed in your application’s own process.

Steps to create JobScheduler

  • Construct the JobInfo objects and pass them to the JobScheduler with schedule(android.app.job.JobInfo).
    Container of data passed to the JobScheduler fully encapsulating the parameters required to schedule work against the calling application. These are constructed using the JobInfo.Builder. The goal here is to provide the scheduler with high-level semantics about the work you want to accomplish.
  • When the criteria declared are met, the system will execute this job on your application's JobService.
  • You identify the service component that implements the logic for your job when you construct the JobInfo using JobInfo.Builder.Builder(int, android.content.ComponentName).

Internals of JobScheduler

  • The framework will be intelligent about when it executes jobs, and attempt to batch and defer them as much as possible. Typically if you don’t specify a deadline on a job, it can be run at any moment depending on the current state of the JobScheduler’s internal queue.
  • While a job is running, the system holds a wakelock on behalf of your app. For this reason, you do not need to take any action to guarantee that the device stays awake for the duration of the job.

Restrictions on API level

  • Prior to Android version Build.VERSION_CODES#Q, you had to specify at least one constraint on the JobInfo object that you are creating. Otherwise, the builder would throw an exception when building. From Android version Build.VERSION_CODES#Q and onwards, it is valid to schedule jobs with no constraints.
  • Prior to Android version Build.VERSION_CODES.S, jobs could only have a maximum of 100 jobs scheduled at a time. Starting with Android version Build.VERSION_CODES.S, that limit has been increased to 150. Expedited jobs also count towards the limit.
  • In Android version Build.VERSION_CODES.LOLLIPOP, jobs had a maximum execution time of one minute. Starting with Android version Build.VERSION_CODES.M and ending with Android version Build.VERSION_CODES.R, jobs had a maximum execution time of 10 minutes.
  • Starting from Android version Build.VERSION_CODES.S, jobs will still be stopped after 10 minutes if the system is busy or needs the resources, but if not, jobs may continue running longer than 10 minutes.

What’s new in Android 12

Apps that target Android 12 (API level 31) can no longer start foreground services while running in the background, except for a few special cases.

If an app tries to start a foreground service while the app is running in the background, and the foreground service doesn’t satisfy one of the exceptional cases, the system throws a ForegroundServiceStartNotAllowedException.

Expedited jobs
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:

  • They’re meant for short tasks that complete within a few minutes. Unless your app has enough quota, the system might stop an expedited job if the job has already been running for at least 3 minutes.
  • They’re less affected by some of the system’s power management restrictions, including Battery Saver and Doze.
  • The system runs them immediately, provided that the system’s current workload makes it possible to do so.

Expedited jobs might be deferred

The system tries to execute a given expedited job as soon as possible after the job is invoked. However, as is the case with other types of jobs, the system might defer the start of new expedited jobs, such as in the following cases:

  • The system load is too high, which can occur when too many jobs are already running, or when the system doesn’t have enough memory.
  • The expedited job quota limit has been exceeded. Expedited jobs use 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 jobs are more restrictive than the ones used for other types of background jobs.

How to replace Foreground services with New WorkManager

WorkManager is an API that makes it easy to schedule reliable, asynchronous tasks that are expected to run even if the app exits or the device restarts. The WorkManager API is a suitable and recommended replacement for all previous Android background scheduling APIs, including FirebaseJobDispatcher, GcmNetworkManager, and Job Scheduler. WorkManager incorporates the features of its predecessors in a modern, consistent API that works back to API level 14 while also being conscious of battery life.

Under the hood WorkManager uses an underlying job dispatching service based on the following criteria.

Note: If your app targets Android 10 (API level 29) or above, your FirebaseJobDispatcher and GcmNetworkManager API calls will no longer work on devices running Android Marshmallow (6.0) and above. Follow the migration guides for FirebaseJobDispatcher and GcmNetworkManager for guidance on migrating. Also, see the Unifying Background Task Scheduling on Android announcement for more information regarding their deprecation.

Features

In addition to providing a simpler and consistent API, WorkManager has a number of other key benefits, including:

Work Constraints

Declaratively define the optimal conditions for your work to run using Work Constraints. (For example, run only when the device is Wi-Fi, when the device idle, or when it has sufficient storage space, etc.)

Robust Scheduling

WorkManager allows you to schedule work to run one- time or repeatedly using flexible scheduling windows. Work can be tagged and named as well, allowing you to schedule unique, replaceable work and monitor or cancel groups of work together. Scheduled work is stored in an internally managed SQLite database and WorkManager takes care of ensuring that this work persists and is rescheduled across device reboots. In addition, WorkManager adheres to power-saving features and best practices like Doze mode, so you don’t have to worry about it.

Flexible Retry Policy

Sometimes work fails. WorkManager offers flexible retry policies, including a configurable exponential backoff policy.

Work Chaining

For complex related work, chain individual work tasks together using a fluent, natural, interface that allows you to control which pieces run sequentially and which run in parallel.

KotlinJava

WorkManager.getInstance(...)
.beginWith(listOf(workA,workB))
.then(workC)
.enqueue()

For each work task, you can define input and output data for that work. When chaining work together, WorkManager automatically passes output data from one work task to the next.

Built-In Threading Interoperability

WorkManager integrates seamlessly with RxJava and Coroutines and provides the flexibility to plug in your own asynchronous APIs.

WorkManager is intended for work that 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

WorkManager is not intended for in-process background work that can safely be terminated if the app process goes away or for work that requires immediate execution. Please review the background processing guide to see which solution meets your needs.

Recommended solutions for background task

The following sections describe recommended solutions for each background task type.

Immediate tasks
We recommend Kotlin coroutines for tasks that should end when the user leaves a certain scope or finishes an interaction. Many Android KTX libraries contain ready-to-use coroutine scopes for common app components like ViewModel and common application lifecycles.
For Java programming language users, see Threading on Android for recommended options.
For tasks that should be executed immediately and need continued processing, even if the user puts the application in background or the device restarts, we recommend using WorkManager and its support for long-running tasks. In specific cases, such as with media playback or active navigation, you might want to use foreground Services directly.

Deferred tasks
Every task that is not directly connected to a user interaction and can run at any time in the future can be deferred. The recommended solution for deferred tasks is WorkManager.
WorkManager makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts. See the documentation for WorkManager to learn how to schedule these types of tasks.

Exact tasks
A task that needs to be executed at an exact point in time can use AlarmManager. To learn more about AlarmManager, see Schedule repeating alarms.

Replacing foreground task

WorkManager is the recommended solution for starting higher-priority background tasks.

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.

The following code snippet shows an example of how to use the setExpedited() method:

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

Because the CoroutineWorker.setForeground() and ListenableWorker.setForegroundAsync() methods are backed by foreground services, they're subject to the same foreground service launch restrictions and exemptions. You can use the API opportunistically, but be prepared to handle an exception if the system disallows your app from starting a foreground service. For a more consistent experience, use setExpedited().

References

Make sure you give this post 50 claps 👏 and follow if you enjoyed reading and want to read and learn more.

--

--