Custom Work Manager initialization

And how to implement it with handling one pitfall

Michael Spitsin
6 min readNov 18, 2018

About a half year ago Google introduced a new library called Work Manager for handling complex async work in Android. It should spare your from choosing, whether that work should be done with a JobScheduler, Firebase JobDispatcher or Alarm Manager. Work Manager is smart enough to choose it for you. So it acts as Facade for the programmer, giving to him(her) a concise API. That’s why it is a good library to help with image uploading, logs, analytics sending and e.t.c.

I will not cover all procs and cons of this library, because I want to talk about a pitfall that I faced recently working with that library.

The input data is next: we have an application and we need to use WorkManager in it. We want to start unique periodic job, when application is started and keep it running infinitely with some big period, like 8–10 hours. Need to mention that application uses libraries, that creates an additional process besides an application process. For example, our project used library Yandex Metrica.

So as we read this, we might think “Okey, I just create a PeriodicWorkRequest in Application.onCreate and then run it right here”. It is simple and straightforward and would work in an usual situation, when you have one process application. But not here.

In our multiprocess Application (because we have an application process and Yandex Metrica’s process) we will receive next exception:

Process: com.example:Metrica, PID: 12707
java.lang.IllegalStateException: WorkManager is not initialized properly. The most likely cause is that you disabled WorkManagerInitializer in your manifest but forgot to call WorkManager#initialize in your Application#onCreate or a ContentProvider.
at androidx.work.WorkManager.getInstance(WorkManager.java:134)

So why is that? Let’s take a closer look

Work Manager initialization

Aforementioned exception happens because of 2 reasons that present in the project simultaneously:

  1. By default, WorkManager initialized in a separate provider called WorkManagerInitializer
  2. We have two-process-application with starting WorkRequest in Application.onCreate method

As we have multi process application, each process needs to call Application.onCreate and don’t forget to mention, that ContentProvider called only once in a main process by default:

android:multiprocess — If the app runs in multiple processes, this attribute determines whether multiple instances of the content provder are created. If true, each of the app's processes has its own content provider object. If false, the app's processes share only one content provider object. The default value is false

android:process — The name of the process in which the content provider should run. Normally, all components of an application run in the default process created for the application. It has the same name as the application package…

So the situation is the next. Main process starts WorkManagerInitializer at the beginning, where WorkManager is initialized. Then Application.onCreate is called in the main process and so WorkRequest is started. Everything is good, since WorkRequest started after initialization of WorkManager. Then Second process (in our case Yandex Metrica’s process) is started and Application.onCreate is called for that process (no provider started). So another one WorkRequest is created, but for WorkManager that is not initialized in that process. Thus, aforementioned error occurs. Corresponding issue you can find here.

Solution

There are 3 proposals, but I will tell about them briefly, and then will introduce solution, that we using in our application.

❌Proposal 1: Use static flag

First proposal you can actually find in discussion thread of such error in Metrica’s repository:

For now, It possible to detect main process. Calling Application#onCreate() follow ContentProvider creation via ContentProvider#onCreate(). And ContentProvider creates only in related process. So you can make static variable in Application, like IS_MAIN_PROCESS. It should be false by default.

Then you add one line of code to ContentProvider where you initialize WorkManager. The code will assign true to variable in Application.

So, for all process except main it will be false. then you surround launching tasks with condition like

Here is snippet of code that will clear situation:

class MyApplication : Application() {

override fun onCreate() {
super.onCreate()

//do process independent tasks here

if (IS_MAIN_PROCESS) {
//do main process dependent tasks here
}
}

companion object {
var IS_MAIN_PROCESS = false
}
}

class ActivationContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
MyApplication.IS_MAIN_PROCESS = true

//do you other content provider stuff if needed
}

//implement other content provider methods
}

So for each process there will be it’s own Application instance and it’s own class declaration, so IS_MAIN_PROCESS will be true only for main process. And for others it will be false , since ActivationContentProvider will run only in main process.

Though solution is working, I don’t really like it, because you have static variable in the code (read global state flag) that is changed. For me it’s a code smell. Small, justified, but code smell.

❌Proposal 2: Setup Work Manager in Application.onCreate

As the next solution, you can disable WorkManagerInitializer provider from manifest file and input default initialization line into Application.onCreate method just before running your tasks:

//Manifest
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="com.gpn.azs.workmanager-init"
android:enabled="false"
/>
//Application
class MyApplication : Application() {

override fun onCreate() {
super.onCreate()
//...

WorkManager.initialize(this, Configuration.Builder().build())
//run work manager requests
}
}

Although this is working solution, it is not such a good solution in terms of multi process application, since each process will initialize its own instance of WorkManager and run needed task.

If you have, for example, some unique task that is needed to be run in application, then with this approach you can actually run it twice, and I’m sure it is not what you want. Method enqueuUniquePeriodicWork can help you to run only one task in fact, since it will have unique identifier, so next time work will not be started or will replace previous work. But still it is not clean.

❌/✔️Proposal 3: Do not run WorkRequest in Application.onCreate

This is not only proposal but also a way to resolve this issue. You just don’t need to execute WorkRequest in Application.onCreate method. Instead of that you can run it in first activity, for example. But here we need to be careful. If we have some periodic work to do (like once per day approximately), then we need to run it when application is started, so Application.onCreate looks pretty logic.

However there is other starting place. ContentProvider . This leads us to the next solution

✔️Actual Solution

Let’s introduce next class:

class MyWorkManagerInitializer : DummyContentProvider() {
override fun onCreate(): Boolean {
WorkManager.initialize(context!!, Configuration.Builder().build())
//run your tasks here return true
}
}
//where
abstract class DummyContentProvider : ContentProvider() {
override fun onCreate() = true

override fun
insert(uri: Uri, values: ContentValues?) = null
override fun
query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?) = null
override fun
update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?) = 0
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = 0

override fun getType(uri: Uri) = null
}

Please, do not pay much attention to DummyContentProvider, since it is just simple abstract inheritor, which provides dummy realisation of all required methods.

So our MyWorkManagerInitializer looks pretty similar to WorkManagerInitializer. Moreover it expands WorkManagerInitializer with initialization of required tasks, but we can not just extend WorkManagerInitializer since it has restricted access:

/**
* The {
@link ContentProvider} responsible for initializing {@link WorkManagerImpl}.
*
*
@hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class WorkManagerInitializer extends ContentProvider {
@Override
public boolean onCreate() {
// Initialize WorkManager with the default configuration.
WorkManager.initialize(getContext(), new Configuration.Builder().build());
return true;
}
//dummy implementations of all content provider abstract methods
}

Then we need to disable WorkManagerInitializer and enable our MyWorkManagerInitializer in manifest file:

<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="com.sample.workmanager-init"
android:enabled="false"
/>

<provider
android:name=".MyWorkManagerInitializer"
android:authorities="com.sample.MyWorkManagerInitializer"
/>

And that’s all. This solution is better for my opinion, and I prefer it, because:

  • You don’t need to introduce static variables
  • You will run you global tasks only once, not one per process
  • You will run global task in a global entry point, not in a local activity

Afterwords

Multi-process applications give us additional colors in a development life. :) I hope you liked this small post. Don’t forget to 👏 and click ‘Follow’. Also will be interesting to know your stories in multi-process environment. ;)

--

--

Michael Spitsin

Love being creative to solve some problems with an simple and elegant ways