Work Manager - oppia/oppia-android GitHub Wiki
Table of Contents
- What is WorkManager
- When to use WorkManager
- How to use WorkManager
- Writing tests with WorkManager
- Debugging WorkManager
What is WorkManager
WorkManager is part of Android Jetpack and an Architecture Component for background work that needs a combination of opportunistic and guaranteed execution. Opportunistic execution means that WorkManager will do your background work as soon as it can. Guaranteed execution means that WorkManager will take care of the logic to start your work under a variety of situations, even if you navigate away from your app.
WorkManager is an incredibly flexible library that has many additional benefits. These include:
- Support for both asynchronous one-off and periodic tasks
- Support for constraints such as network conditions, storage space, and charging status
- Chaining of complex work requests, including running work in parallel
- Output from one work request used as input for the next
- Handling API level compatibility back to API level 14
- Working with or without Google Play services
- Following system health best practices
The WorkManager library is a good choice for tasks that are useful to complete, even if the user navigates away from the particular screen or your app. Some examples of tasks that are a good use of WorkManager:
- Uploading logs
- Periodically syncing local data with the network
WorkManager offers guaranteed execution, and not all tasks require that. As such, it is not a catch-all for running every task off of the main thread.
When to use WorkManager
WorkManager is used for long-running periodic tasks such as collecting or uploading analytics, and synchronizing platform parameters with the Oppia backend. These tasks can't be performed in any other way because they need to be able to run even when the user isn't actively using the device (particularly in the case of platform parameters which we want to synchronize for the next app open). Thus, only work that needs to be able to happen even when the app is closed, or under very specific network situations (like the device connecting to cellular or wifi triggering the app needing to do something) should use WorkManager.
Other types of background tasks (those are, tasks done off the main thread) are all handled using a background coroutine dispatcher and are generally coerced into a specific data flow pattern called DataProviders. For more context on those, see the corresponding wiki page.
How to use WorkManager
The Oppia Android app implements exactly one worker: BootstrapOppiaWorker. This worker is responsible for several things:
- Determining if an invalid worker is trying to be run (such as an old worker that's no longer valid) and, if so, cancelling it.
- Identifying the specific type of work a worker wants to do.
- Deferring worker construction until platform parameters are guaranteed to be loaded (so that both workers and their factories can depend on platform parameters either directly or via their other dependencies).
- Delegating work to an actual defined
OppiaWorkerimplementation.
With this architecture in place, you'll never need to directly implement a ListenableWorker. Instead, you must do the following:
- Introduce a new
OppiaWorkerimplementation (along with its factory and task type). - Introduce a scheduler class that can automatically schedule the new worker for periodic work.
- Introduce a module that creates the necessary Dagger bindings for both the worker and schedule to automatically be used. Note that this includes adding the new module to the corresponding application component classes (e.g.
DeveloperApplicationComponent) so that the bindings are enabled.
That's it! From there you can customize which tasks the job can do and adapt the work and scheduling accordingly. It's recommended to look at existing implementations for how they're set up, but the simplest is the debug worker which is designed to specifically analyze WorkManager behavior and can be seen in the org.oppia.android.domain.workmanager.debug package.
Writing tests with WorkManager
Writing worker tests generally means testing one of three things:
- The actual worker (i.e. the class that implements
OppiaWorker). - Testing the class that automatically schedules the worker on application startup (i.e. the class that implements
StartupWorkerScheduleReadinessListener). - Testing the Dagger module that correctly binds both the worker and the scheduler.
For all three of these, look at the following references (plus their tested class implementations):
These classes are specifically designed to serve as examples for creating and testing Oppia workers.
At a higher level, tests never interact with WorkManager directly. Instead, they make use of OppiaWorkManagerTestDriver to do all of:
- Initializing the test so that it can properly integrate with
WorkManager. - Running
OppiaWorkers as either one-off or periodic jobs. - Inspect the latest state of a job.
- Force job constraints to test constrained scenarios (such as when internet connectivity is required).
Cumulatively, these provide all the necessary functionality for testing both workers and worker schedulers. Validating the module is done using standard module testing techniques.
Setting up the test environment properly is highly complex which is why the helper must be used (and, in fact, file content checks prohibit using WorkManager or its testing utilities directly).
Debugging WorkManager
Verifying that WorkManager is behaving correctly can be difficult since it relies heavily on OS constraints and thus cannot necessarily guarantee certain behaviors, especially for periodic tasks. Here are some important aspects to note when trying to determine if a worker is running correctly:
WorkManagerwon't allow periodic tasks to run more frequently than 15 minutes unless they are explicitly initiated with a one-time work request.- Periodic tasks will run immediately upon opening the app if their constraints are met and they haven't yet run within their configured time period.
- Tasks will run even if the app is closed and this can be validated by force closing the app using:
adb shell am force-stop org.oppia.android - There's a debug worker that's configured to run every 15 minutes, 20 minutes, and 6 hours (to align with other long periodic workers). This only runs in
//:oppia_devbuilds but simply opening the developer app at least once and watching ADB (for debug logs) should provide a basis to verify thatWorkManageris actually running. - You can use tests to verify that your worker will run when expected. See
DebugWorkerTestfor an example of how to test periodic workers. - Finally, you can actually force a periodic task to run through ADB (see below).
Important: Many of the following commands may require using a root ADB shell. You can enable that using adb root but it may not be available on all Android devices. It should always be available for emulators so long as they're running a developer version of the Android OS (make sure you aren't using an image that has Google Play, but you may need Google APIs for testing certain jobs like Firebase analytics).
To force a specific worker to run we must first figure out what's currently enqueued. To do that run the following ADB command:
adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" org.oppia.android -f 32
(Note that the -f 32 will allow the diagnostic request to wake the app if it was force stopped or never opened).
View logcat for info logs. You should see something like the following:
Enqueued work:
Id Class Name Job Id State Unique Name Tags
8126cd00-2c35-4704-8239-77abda3dd12e org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2109 ENQUEUED DebugWorker.RUN_EVERY_SIX_HOURS_WITH_OR_WITHOUT_CONNECTIVITY DebugWorker.RUN_EVERY_SIX_HOURS_WITH_OR_WITHOUT_CONNECTIVITY,org.oppia.android.domain.workmanager.BootstrapOppiaWorker
e7b9b00c-e742-4688-b004-14cf07698405 org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2110 ENQUEUED PlatformParameterSyncUpWorker.refresh_platform_parameters org.oppia.android.domain.workmanager.BootstrapOppiaWorker,PlatformParameterSyncUpWorker.refresh_platform_parameters
240ffbc1-3001-478b-947b-3549f9e91007 org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2112 ENQUEUED LogUploadWorker.upload_performance_metrics org.oppia.android.domain.workmanager.BootstrapOppiaWorker,LogUploadWorker.upload_performance_metrics
21048af0-8b1b-4c85-899e-c252ecd4c2fa org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2113 ENQUEUED LogUploadWorker.upload_exceptions org.oppia.android.domain.workmanager.BootstrapOppiaWorker,LogUploadWorker.upload_exceptions
88be6627-bc44-45ab-993f-9dcb9b6a756d org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2114 ENQUEUED LogUploadWorker.upload_firestore_data org.oppia.android.domain.workmanager.BootstrapOppiaWorker,LogUploadWorker.upload_firestore_data
9aecbfd4-405c-4f29-9467-a6e434350ded org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2115 ENQUEUED LogUploadWorker.upload_events org.oppia.android.domain.workmanager.BootstrapOppiaWorker,LogUploadWorker.upload_events
9de8273f-6c12-4684-94a3-a75b4b36aa0b org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2123 ENQUEUED DebugWorker.RUN_EVERY_TWENTY_MINUTES_WITH_OR_WITHOUT_CONNECTIVITY org.oppia.android.domain.workmanager.BootstrapOppiaWorker,DebugWorker.RUN_EVERY_TWENTY_MINUTES_WITH_OR_WITHOUT_CONNECTIVITY
9e070daa-c323-499f-b45f-00d2932f8809 org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2131 ENQUEUED DebugWorker.RUN_EVERY_FIFTEEN_MINUTES_WITH_CONNECTIVITY org.oppia.android.domain.workmanager.BootstrapOppiaWorker,DebugWorker.RUN_EVERY_FIFTEEN_MINUTES_WITH_CONNECTIVITY
This forces WorkManager to reconcile its internal state which will automatically kick off stale jobs. We can leverage that to force a job that's run within its recent time period to run again by making WorkManager forget about it. This even bypasses its own internal 15 minute limit. The following command can be used to achieve this:
adb shell "run-as org.oppia.android sqlite3 no_backup/androidx.work.workdb \"UPDATE WorkSpec SET period_start_time = 0 WHERE id = '<ID>'; DELETE FROM SystemIdInfo WHERE work_spec_id = '<ID>';\""
<ID> is updated using one of the IDs in the diagnostics table above, for example 9e070daa-c323-499f-b45f-00d2932f8809 would correspond to the periodic 15-minute DebugWorker job. These IDs are long-lived unlike the job IDs above.
Once this command finishes you can re-run the diagnostics command above and the corresponding job should run.
For simplicity, here are Bash functions that take either a parameter of the worker's unique name (e.g. 'DebugWorker.RUN_EVERY_FIFTEEN_MINUTES_WITH_CONNECTIVITY') or the worker's ID performs all of the necessary commands:
function PrintOppiaWorkerDiagnostics() {
adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" org.oppia.android -f 32
}
function ForceRunOppiaJobById() {
echo "Attempting to force run Oppia worker with ID: $1"
local worker_id="$1"
adb shell "run-as org.oppia.android sqlite3 no_backup/androidx.work.workdb \"UPDATE WorkSpec SET period_start_time = 0 WHERE id = '$worker_id'; DELETE FROM SystemIdInfo WHERE work_spec_id = '$worker_id';\""
PrintOppiaWorkerDiagnostics
}
function ForceRunOppiaJobWithUniqueName() {
local unique_name="$1"
echo "Attempting to find an Oppia worker with unique name: $unique_name"
PrintOppiaWorkerDiagnostics
local app_pid=$(adb shell pidof -s org.oppia.android)
local worker_id=$(adb logcat --pid=$app_pid -d | grep "WM-DiagnosticsWrkr" | grep "$unique_name" | tail -1 | awk '{for(i=1;i<=NF;i++) if($i=="WM-DiagnosticsWrkr:") {print $(i+1); exit}}')
echo "Worker has unique ID: $worker_id"
ForceRunOppiaJobById $worker_id
}
Caveat: these may only run successfully if there's exactly one device available.