So far, we've talked about background tasks with Services and BroadcastReceivers. These are both good tools to have up your sleeve as an Android developer, but they both have their limitations. For BroadcastReceivers, they can only be registered and called while your app is running on API 26+. For Services, you might not want to implement all the logic needed to run code when certain conditions are met.
The WorkManager API makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts.
Key features:
- Backwards compatible up to API 14
- Uses JobScheduler on devices with API 23+
- Uses a combination of BroadcastReceiver + AlarmManager on devices with API 14-22
- Add work constraints like network availability or charging status
- Schedule asynchronous one-off or periodic tasks
- Monitor and manage scheduled tasks
- Chain tasks together
- Guarantees task execution, even if the app or device restarts
WorkManager is intended for tasks that are deferrable—that is, not required to run immediately—and required to run reliably even if 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 tasks that require immediate execution.
To learn more about WorkManager, we'll work through Google's provided WorkManager code lab together.
These days, smartphones are almost too good at taking pictures. Gone are the days a photographer could take a reliably blurry picture of something mysterious.
In this codelab you'll be working on Blur-O-Matic, an app that blurs photos and images and saves the result to a file. Was that the Loch Ness monster or evelopera toy submarine? With Blur-O-Matic, no-one will ever know.
In this step you will take an image in the res/drawable folder called test.jpg and run a few functions on it in the background. These functions will blur the image and save it to a temporary file.
There are a few WorkManager classes you need to know about:
- Worker: This is where you put the code for the actual work you want to perform in the background.
You'll extend this class and override the
doWork()
method. - WorkRequest: This represents a request to do some work. You'll pass in your
Worker
as part of creating yourWorkRequest
. When making theWorkRequest
you can also specify things like Constraints on when theWorker
should run. - WorkManager: This class actually schedules your
WorkRequest
and makes it run. It schedulesWorkRequest
s in a way that spreads out the load on system resources, while honoring the constraints you specify.
In your case, you'll define a new BlurWorker
which will contain the code to blur an image. When the
Go button is clicked, a WorkRequest
is created and then enqueued by WorkManager
.
In the package workers, create a new class called BlurWorker
.
It should extend Worker.
Add a constructor to the BlurWorker class:
public BlurWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
Your Worker
will blur the res/test.jpg
image.
Override the doWork()
method and then implement the following:
- Get a
Context
by callinggetApplicationContext()
. You'll need this for various bitmap manipulations you're about to do. - Create a Bitmap from the test image:
Bitmap picture = BitmapFactory.decodeResource(
applicationContext.getResources(),
R.drawable.test);
-
Get a blurred version of the bitmap by calling the static
blurBitmap
method fromWorkerUtils
. -
Write that bitmap to a temporary file by calling the static
writeBitmapToFile
method fromWorkerUtils
. Make sure to save the returned URI to a local variable. -
Make a Notification displaying the URI by calling the static
makeStatusNotification
method fromWorkerUtils
. -
Return
Result.success();
-
Wrap the code from steps 2-6 in a try/catch statement. Catch a generic
Throwable
. -
In the catch statement, emit an error Log statement:
Log.e(TAG, "Error applying blur", throwable);
-
In the catch statement then return
Result.failure();
The completed code for this step is below.
public class BlurWorker extends Worker {
public BlurWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
private static final String TAG = BlurWorker.class.getSimpleName();
@NonNull
@Override
public Worker.Result doWork() {
Context applicationContext = getApplicationContext();
try {
Bitmap picture = BitmapFactory.decodeResource(
applicationContext.getResources(),
R.drawable.test);
// Blur the bitmap
Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);
// Write bitmap to a temp file
Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);
WorkerUtils.makeStatusNotification("Output is "
+ outputUri.toString(), applicationContext);
// If there were no errors, return SUCCESS
return Result.success();
} catch (Throwable throwable) {
// Technically WorkManager will return Result.failure()
// but it's best to be explicit about it.
// Thus if there were errors, we're return FAILURE
Log.e(TAG, "Error applying blur", throwable);
return Result.failure();
}
}
}
Create a variable for a WorkManager
instance in your ViewModel
and instantiate it in the
ViewModel
's constructor:
private WorkManager mWorkManager;
// BlurViewModel constructor
public BlurViewModel() {
mWorkManager = WorkManager.getInstance();
//...rest of the constructor
}
Alright, time to make a WorkRequest
and tell WorkManager
to run it. There are two types of
WorkRequests
:
OneTimeWorkRequest
: AWorkRequest
that will only execute once.PeriodicWorkRequest
: AWorkRequest
that will repeat on a cycle.
We only want the image to be blurred once when the Go button is clicked. The applyBlur
method
is called when the Go button is clicked, so create a OneTimeWorkRequest
from BlurWorker
there. Then, using your WorkManager
instance enqueue your WorkRequest
.
Add the following line of code into BlurViewModel's applyBlur() method:
void applyBlur(int blurLevel) {
mWorkManager.enqueue(OneTimeWorkRequest.from(BlurWorker.class));
}
Run your code. It should compile and you should see the Notification when you press the Go button.
Optionally you can open the Device File Explorer in Android Studio. Using Command+Shift+A will be fastest, but if your prefer your mouse:
Then navigate to data>data>com.example.background>files>blur_filter_outputs> and confirm that the fish was in fact blurred:
Blurring that test image is all well and good, but for Blur-O-Matic to really be the revolutionary image editing app it's destined to be, you'll need to let users blur their own images.
To do this, we'll provide the URI of the user's selected image as input to our WorkRequest
.
Input and output is passed in and out via Data objects. Data
objects are lightweight containers
for key/value pairs. They are meant to store a small amount of data that might pass into and out from
WorkRequest
s.
You're going to pass in the URI for the user's image into a bundle. That URI is stored in a variable
called mImageUri
.
Create a private method called createInputDataForUri
. This method should:
-
Create a
Data.Builder
object. -
If
mImageUri
is a non-null URI, then add it to theData
object using theputString
method. This method takes a key and a value. You can use the String constantKEY_IMAGE_URI
from theConstants
class. -
Call
build()
on theData.Builder
object to make yourData
object, and return it.
Below is the completed createInputDataForUri
method:
/**
* Creates the input data bundle which includes the Uri to operate on
* @return Data which contains the Image Uri as a String
*/
private Data createInputDataForUri() {
Data.Builder builder = new Data.Builder();
if (mImageUri != null) {
builder.putString(KEY_IMAGE_URI, mImageUri.toString());
}
return builder.build();
}
You're going to want to change the applyBlur
method so that it:
-
Creates a new
OneTimeWorkRequest.Builder
. -
Calls
setInputData
, passing in the result fromcreateInputDataForUri
. -
Builds the
OneTimeWorkRequest
. -
Enqueues that request using
WorkManager
.
Below is the completed applyBlur
method:
void applyBlur(int blurLevel) {
OneTimeWorkRequest blurRequest =
new OneTimeWorkRequest.Builder(BlurWorker.class)
.setInputData(createInputDataForUri())
.build();
mWorkManager.enqueue(blurRequest);
}
Now let's update BlurWorker
's doWork()
method to get the URI we passed in from the Data
object:
public Worker.Result doWork() {
Context applicationContext = getApplicationContext();
// ADD THIS LINE
String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);
//... rest of doWork()
}
With the URI, you can blur the image the user selected:
public Worker.Result doWork() {
Context applicationContext = getApplicationContext();
String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);
try {
// REPLACE THIS CODE:
// Bitmap picture = BitmapFactory.decodeResource(
// applicationContext.getResources(),
// R.drawable.test);
// WITH
if (TextUtils.isEmpty(resourceUri)) {
Log.e(TAG, "Invalid input uri");
throw new IllegalArgumentException("Invalid input uri");
}
ContentResolver resolver = applicationContext.getContentResolver();
// Create a bitmap
Bitmap picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)));
//...rest of doWork
You won't be using this yet, but let's go ahead and provide an output Data for the temporary URI of our blurred photo. To do this:
-
Create a new
Data
, just as you did with the input, and storeoutputUri
as a String. Use the same key,KEY_IMAGE_URI
. -
Pass this to
Worker
'sResult.success()
method.
This line should follow the Uri outputUri
line and replace Result.success(outputData)
in
doWork()
:
Data outputData = new Data.Builder()
.putString(Constants.KEY_IMAGE_URI, outputUri.toString())
.build();
return Result.success(outputData);
At this point you should run your app. It should compile and have the same behavior.
Optionally, you can open the Device File Explorer in Android Studio and navigate to data>data>com.example.background>files>blur_filter_outputs> as you did in the last step.
Note that you might need to Synchronize to see your images:
Great work! You've blurred an input image using WorkManager!
Right now you're doing a single work task: blurring the image. This is a great first step, but is missing some core functionality:
- It doesn't clean up temporary files.
- It doesn't actually save the image to a permanent file.
- It always blurs the picture the same amount.
We'll use a WorkManager
chain of work to add this functionality.
WorkManager
allows you to create separate WorkerRequest
s that run in order or parallel. In this
step you'll create a chain of work that looks like this:
The WorkRequests
are represented as boxes.
Another really neat feature of chaining is that the output of one WorkRequest
becomes the input of
the next WorkRequest
in the chain. The input and output that is passed between each WorkRequest
is shown as blue text.
First, you'll define all the Worker
classes you need. You already have a Worker
for blurring an
image, but you also need a Worker
which cleans up temp files and a Worker
which saves the image
permanently.
Create two new classes in the worker
package which extend Worker
.
The first should be called CleanupWorker
, the second should be called SaveImageToFileWorker
.
Add a constructor to the CleanupWorker
class:
public CleanupWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
CleanupWorker
doesn't need to take any input or pass any output. It always deletes the temporary
files if they exist. Because this is not a codelab about file manipulation, you can copy the code
for the CleanupWorker
below:
public class CleanupWorker extends Worker {
public CleanupWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
private static final String TAG = CleanupWorker.class.getSimpleName();
@NonNull
@Override
public Worker.Result doWork() {
Context applicationContext = getApplicationContext();
try {
File outputDirectory = new File(applicationContext.getFilesDir(),
Constants.OUTPUT_PATH);
if (outputDirectory.exists()) {
File[] entries = outputDirectory.listFiles();
if (entries != null && entries.length > 0) {
for (File entry : entries) {
String name = entry.getName();
if (!TextUtils.isEmpty(name) && name.endsWith(".png")) {
boolean deleted = entry.delete();
Log.i(TAG, String.format("Deleted %s - %s",
name, deleted));
}
}
}
}
return Worker.Result.success();
} catch (Exception exception) {
Log.e(TAG, "Error cleaning up", exception);
return Worker.Result.failure();
}
}
}
SaveImageToFileWorker
will take input and output. The input is a String stored with the key
KEY_IMAGE_URI
. And the output will also be a String stored with the key KEY_IMAGE_URI.
Since this is still not a codelab about file manipulations, the code is below, with two TODOs for you to fill in the appropriate code for input and output. This is very similar to the code you wrote in the last step for input and output (it uses all the same keys).
public class SaveImageToFileWorker extends Worker {
public SaveImageToFileWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
private static final String TAG = SaveImageToFileWorker.class.getSimpleName();
private static final String TITLE = "Blurred Image";
private static final SimpleDateFormat DATE_FORMATTER =
new SimpleDateFormat("yyyy.MM.dd 'at' HH:mm:ss z", Locale.getDefault());
@NonNull
@Override
public Worker.Result doWork() {
Context applicationContext = getApplicationContext();
ContentResolver resolver = applicationContext.getContentResolver();
try {
String resourceUri = ;// TODO get the input Uri from the Data object
Bitmap bitmap = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)));
String outputUri = MediaStore.Images.Media.insertImage(
resolver, bitmap, TITLE, DATE_FORMATTER.format(new Date()));
if (TextUtils.isEmpty(outputUri)) {
Log.e(TAG, "Writing to MediaStore failed");
return Result.failure();
}
// TODO create and set the output Data object with the imageUri.
return Worker.Result.success();
} catch (Exception exception) {
Log.e(TAG, "Unable to save image to Gallery", exception);
return Worker.Result.failure();
}
}
}
You need to modify the BlurViewModel
's applyBlur
method to execute a chain of WorkRequests
instead of just one. Currently the code looks like this:
OneTimeWorkRequest blurRequest =
new OneTimeWorkRequest.Builder(BlurWorker.class)
.setInputData(createInputDataForUri())
.build();
mWorkManager.enqueue(blurRequest);
Instead of calling WorkManager.enqueue()
, call WorkManager.beginWith()
. This returns a
WorkContinuation, which defines a chain of WorkRequests. You can add to this chain of work
requests by calling then()
method, for example, if you have three WorkRequest
objects, workA
,
workB
, and workC
, you could do the following:
WorkContinuation continuation = mWorkManager.beginWith(workA);
continuation.then(workB) // FYI, then() returns a new WorkContinuation instance
.then(workC)
.enqueue(); // Enqueues the WorkContinuation which is a chain of work
This would produce and run the following chain of WorkRequest
s:
Create a chain of a CleanupWorker
WorkRequest
, a BlurImage
WorkRequest
and a
SaveImageToFile
WorkRequest
in applyBlur
. Pass input into the BlurImage
WorkRequest
.
The code for this is below:
void applyBlur(int blurLevel) {
// Add WorkRequest to Cleanup temporary images
WorkContinuation continuation =
mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// Add WorkRequest to blur the image
OneTimeWorkRequest blurRequest = new OneTimeWorkRequest.Builder(BlurWorker.class)
.setInputData(createInputDataForUri())
.build();
continuation = continuation.then(blurRequest);
// Add WorkRequest to save the image to the filesystem
OneTimeWorkRequest save =
new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
.build();
continuation = continuation.then(save);
// Actually start the work
continuation.enqueue();
}
This should compile and run. You should be able to see whatever image you choose to blur now saved in your Pictures folder:
Time to add the ability to blur the image different amounts. Take the blurLevel
parameter passed
into applyBlur
and add that many blur WorkRequest
operations to the chain. Only the first
WorkRequest
needs and should take in the uri input.
Note that this is a bit contrived for learning purposes. Calling our blur code three times is less
efficient than having BlurWorker
take in an input that controls the "level" of blur. But it's good
practice and shows the flexibility of WorkManager
chaining.
Try it yourself and then compare with the code below:
void applyBlur(int blurLevel) {
// Add WorkRequest to Cleanup temporary images
WorkContinuation continuation = mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// Add WorkRequests to blur the image the number of times requested
for (int i = 0; i < blurLevel; i++) {
OneTimeWorkRequest.Builder blurBuilder =
new OneTimeWorkRequest.Builder(BlurWorker.class);
// Input the Uri if this is the first blur operation
// After the first blur operation the input will be the output of previous
// blur operations.
if ( i == 0 ) {
blurBuilder.setInputData(createInputDataForUri());
}
continuation = continuation.then(blurBuilder.build());
}
// Add WorkRequest to save the image to the filesystem
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
.build();
continuation = continuation.then(save);
// Actually start the work
continuation.enqueue();
}
Superb "work"! Now you can blur an image as much or as little as you want! How mysterious!
Now that you've used chains, it's time to tackle another powerful feature of WorkManager - unique work chains.
Sometimes you only want one chain of work to run at a time. For example, perhaps you have a work
chain that syncs your local data with the server - you probably want to let the first data sync
finish before starting a new one. To do this, you would use beginUniqueWork
instead of
beginWith
; and you provide a unique String
name. This names the entire chain of work requests so
that you can refer to and query them together.
Ensure that your chain of work to blur your file is unique by using beginUniqueWork
. Pass in
Constants.IMAGE_MANIPULATION_WORK_NAME
as the key. You'll also need to pass in an
ExistingWorkPolicy. Your options are REPLACE
, KEEP
or APPEND
.
You'll use REPLACE
because if the user decides to blur another image before the current one is
finished, we want to stop the current one and start blurring the new image.
The code for starting your unique work continuation is below:
// REPLACE THIS CODE:
// WorkContinuation continuation =
// mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// WITH
WorkContinuation continuation =
mWorkManager
.beginUniqueWork(IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorker.class));
Blur-O-Matic will now only ever blur one picture at a time.
This section uses LiveData heavily, so to fully grasp what's going on you should be familiar with
LiveData
. LiveData
is an observable, lifecycle-aware data holder.
You can check out the documentation or the Android Lifecycle-aware components Codelab if this is
your first time working with LiveData
or observables.
The next big change you'll do is to actually change what's showing in the app as the Work executes.
You can get the status of any WorkRequest
by getting a LiveData
that holds a WorkInfo object.
WorkInfo
is an object that contains details about the current state of a WorkRequest
, including:
- Whether the work is
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
orSUCCEEDED
- If the
WorkRequest
is finished, any output data from the work.
The following table shows three different ways to get LiveData<WorkInfo>
or
LiveData<List<WorkInfo>>
objects and what each does.
Type | WorkManager Method | Description |
---|---|---|
Get work using id | getInfoByIdLiveData | Each WorkRequest has a unique ID generated by WorkManager ; you can use this to get a single LiveData<WorkInfo> for that exact WorkRequest . |
Get work using unique chain name | getWorkInfosForUniqueWorkLiveData | As you've just seen, WorkRequest s can be part of a unique chain. This returns LiveData<List<WorkInfo>> for all work in a single, unique chain of WorkRequest s. |
Get work using a tag | getWorkInfosByTagLiveData | Finally, you can optionally tag any WorkRequest with a String. You can tag multiple WorkRequest s with the same tag to associate them. This returns the LiveData<List<WorkInfo>> for any single tag. |
You'll be tagging the SaveImageToFileWorker
WorkRequest
, so that you can get it using
getWorkInfosByTagLiveData
. You'll use a tag to label your work instead of using the WorkManager
ID, because if your user blurs multiple images, all of the saving image WorkRequest
s will have the
same tag but not the same ID. Also you are able to pick the tag.
You would not use getWorkInfosForUniqueWorkLiveData
because that would return the WorkInfo
for
all of the blur WorkRequests
and the cleanup WorkRequest
as well; it would take extra logic to
find the save image WorkRequest
.
In applyBlur
, when creating the SaveImageToFileWorker
, tag your work using the String
constant
TAG_OUTPUT
:
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
.addTag(TAG_OUTPUT) // This adds the tag
.build();
Now that you've tagged the work, you can get the WorkInfo
:
-
Declare a new variable called
mSavedWorkInfo
which is aLiveData<List<WorkInfo>>
-
In the
BlurViewModel
constructor, get theWorkInfo
usingWorkManager.getWorkInfosByTagLiveData
-
Add a getter for
mSavedWorkInfo
The code you need is below:
// New instance variable for the WorkInfo
private LiveData<List<WorkInfo>> mSavedWorkInfo;
//In the BlurViewModel constructor
mSavedWorkInfo = mWorkManager.getWorkInfosByTagLiveData(TAG_OUTPUT);
// Add a getter method for mSavedWorkInfo
LiveData<List<WorkInfo>> getSavedWorkInfo() { return mSavedWorkInfo; }
Now that you have a LiveData
for your WorkInfo
, you can observe it in the BlurActivity
. In the
observer:
-
Check if the list of
WorkInfo
is not null and if it has anyWorkInfo
objects in it - if not then the Go button has not been clicked yet, so return. -
Get the first
WorkInfo
in the list; there will only ever be oneWorkInfo
tagged withTAG_OUTPUT
because we made the chain of work unique. -
Check whether the work status is finished, using
workInfo.getState().isFinished();
-
If it's not finished, then call
showWorkInProgress()
which hides and shows the appropriate views. -
If it's finished then call
showWorkFinished()
which hides and shows the appropriate views.
Here's the code:
// Show work status, added in onCreate()
mViewModel.getSavedWorkInfo().observe(this, listOfWorkInfos -> {
// If there are no matching work info, do nothing
if (listOfWorkInfos == null || listOfWorkInfos.isEmpty()) {
return;
}
// We only care about the first output status.
// Every continuation has only one worker tagged TAG_OUTPUT
WorkInfo workInfo = listOfWorkInfos.get(0);
boolean finished = workInfo.getState().isFinished();
if (!finished) {
showWorkInProgress();
} else {
showWorkFinished();
}
});
Run your app - it should compile and run, and now show a progress bar when it's working, as well as the cancel button:
Each WorkInfo
also has a getOutputData method which allows you to get the output Data
object
with the final saved image. Let's display a button that says See File whenever there's a blurred
image ready to show.
Create a variable in BlurViewModel
for the final URI and provide getters and setters for it. To
turn a String
into a Uri
, you can use the uriOrNull
method.
You can use the code below:
// New instance variable for the WorkInfo
private Uri mOutputUri;
// Add a getter and setter for mOutputUri
void setOutputUri(String outputImageUri) {
mOutputUri = uriOrNull(outputImageUri);
}
Uri getOutputUri() { return mOutputUri; }
There's already a button in the activity_blur.xml
layout that is hidden. It's in BlurActivity
and called mOutputButton
.
Setup the click listener for that button. It should get the URI and then open up an activity to view that URI. You can use the code below:
// Inside onCreate()
mOutputButton.setOnClickListener(view -> {
Uri currentUri = mViewModel.getOutputUri();
if (currentUri != null) {
Intent actionView = new Intent(Intent.ACTION_VIEW, currentUri);
if (actionView.resolveActivity(getPackageManager()) != null) {
startActivity(actionView);
}
}
});
There are a few final tweaks you need to apply to the WorkInfo
observer to get this to work (no
pun intended):
-
If the
WorkInfo
is finished, get the output data, usingworkInfo.getOutputData()
. -
Then get the output URI, remember that it's stored with the
Constants.KEY_IMAGE_URI
key. -
Then if the URI isn't empty, it saved properly; show the
mOutputButton
and callsetOutputUri
on the view model with the uri.
// Show work info, goes inside onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfo -> {
// If there are no matching work info, do nothing
if (listOfWorkInfo == null || listOfWorkInfo.isEmpty()) {
return;
}
// We only care about the first output status.
// Every continuation has only one worker tagged TAG_OUTPUT
WorkInfo workInfo = listOfWorkInfo.get(0);
boolean finished = workInfo.getState().isFinished();
if (!finished) {
showWorkInProgress();
} else {
showWorkFinished();
Data outputData = ;// TODO get the output Data from the workInfo
String outputImageUri = ;// TODO get the Uri from the Data using the
// Constants.KEY_IMAGE_URI key
// If there is an output file show "See File" button
if (!TextUtils.isEmpty(outputImageUri)) {
// TODO set the output Uri in the ViewModel
// TODO show mOutputButton
}
}
});
Run your code. You should see your new, clickable See File button which takes you to the outputted file:
You added the Cancel Work button, so let's add the code to make it do something. With
WorkManager
, you can cancel work using the id, by tag and by unique chain name.
In this case, you'll want to cancel work by unique chain name, because you want to cancel all work in the chain, not just a particular step.
In the view model, write the method to cancel the work:
/**
* Cancel work using the work's unique name
*/
void cancelWork() {
mWorkManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME);
}
Then, hook up the button mCancelButton to call cancelWork:
// In onCreate()
// Hookup the Cancel button
mCancelButton.setOnClickListener(view -> mViewModel.cancelWork());
Optionally, there are two static methods in the WorkerUtils
class which you can call to show a
notification when the work starts, and to artificially slow down the speed of the work. This is
helpful to see the work actually get cancelled and to slow things down on emulated devices which run
WorkRequest
s very quickly.
Place at the start of onWork for All Workers
WorkerUtils.makeStatusNotification("Doing <WORK_NAME>", applicationContext);
WorkerUtils.sleep();
Run your app. It should compile just fine. Start blurring a picture and then click the cancel button. The whole chain is cancelled!
Last but not least, WorkManager
support Constraints. For Blur-O-Matic, you'll use the constraint
that the device must be charging when it's saving.
To create a Constraints
object, you use a Constraints.Builder
. Then you set the constraints you
want and add it to the WorkRequest
, as shown below:
// In the applyBlur method
// Create charging constraint
Constraints constraints = new Constraints.Builder()
.setRequiresCharging(true)
.build();
// Add WorkRequest to save the image to the filesystem
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
.setConstraints(constraints) // This adds the Constraints
.addTag(TAG_OUTPUT)
.build();
continuation = continuation.then(save);
Now you can run Blur-O-Matic. If you're on a device, you can remove or plug in your device. On an emulator, you can change the charging status in the Extended controls window:
When the device is not charging, it should hang out in the loading state until you plug it in.
Another good constraint to add to Blur-O-Matic would be a setRequiresStorageNotLow
constraint when
saving. To see a full list of constraint options, check out the Constraints.Builder reference.
WorkManager
provides a work-testing
artifact which helps with unit testing of your workers for
Android Instrumentation tests.
To get started, include the following dependency:
dependencies {
androidTestImplementation "androidx.work:work-testing:$work_version"
}
You can get the latest version of WorkManager
here
work-testing
provides a special implementation of WorkManager
for test mode, which is
initialized using WorkManagerTestInitHelper
.
The work-testing
artifact also provides a SynchronousExecutor
which makes it easier to write
tests in a synchronous manner, without having to deal with multiple threads, locks or latches.
In your test setup method, you'll want to make sure that WorkManager is initialized for testing:
@RunWith(AndroidJUnit4::class)
class BasicInstrumentationTest {
@Before
fun setup() {
val context = InstrumentationRegistry.getTargetContext()
val config = Configuration.Builder()
// Set log level to Log.DEBUG to make it easier to debug
.setMinimumLoggingLevel(Log.DEBUG)
// Use a SynchronousExecutor here to make it easier to write tests
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
}
In your tests, you will use WorkManager
similar to how you use it in production code. The main
difference is that you'll use an instance of TestDriver to fake out things like conditions being
met or time delays.
@Test
@Throws(Exception::class)
fun testWithConstraints() {
// Define input data
val input = workDataOf(KEY_1 to 1, KEY_2 to 2) // uses KTX syntax, work-runtime-ktx needed
// Define constraint(s)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// Create request
val request = OneTimeWorkRequestBuilder<EchoWorker>()
.setInputData(input)
.setConstraints(constraints)
.build()
val workManager = WorkManager.getInstance()
val testDriver = WorkManagerTestInitHelper.getTestDriver()
// Enqueue and wait for result.
workManager.enqueue(request).result.get()
testDriver.setAllConstraintsMet(request.id)
// Get WorkInfo and outputData
val workInfo = workManager.getWorkInfoById(request.id).get()
val outputData = workInfo.outputData
// Assert
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
assertThat(outputData, `is`(input))
}