There are many options on Android for deferrable background work. This codelab covers WorkManager, a compatible, flexible and simple library for deferrable background work. WorkManager is currently in alpha - when stable, it will be the recommended task scheduler on Android.

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 a simple, but incredibly flexible library that has many additional benefits. These include:

When to use WorkManager

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:

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. For more details about when to use WorkManager, check out the Guide to background processing.

What you will build

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.

What you'll learn

What you'll need

Step 1 - Download the Code

Click the following link to download all the code for this codelab:

Download starting code

Or if you prefer you can clone the navigation codelab from GitHub:

$ git clone -b codelab_start https://github.com/googlecodelabs/android-workmanager

Step 2 - Get an Image

If you're using a device where you've already downloaded or taken pictures on the device, you're all set.

If you're using a brand new device (like a recently created emulator), you'll want to either take a picture or download an image from the web using your device. Pick something mysterious!

Step 3 - Run the app

Run the app. You should see the following screens (make sure you allow the permissions to access photos from the initial prompt):


You can select an image and get to the next screen, which has radio buttons where you can select how blurry you'd like your image to be. Pressing the Go button will eventually blur and save the image.

As of now, the app does not apply any blurring.

The starting code contains:

* These are the only files you'll write code in.

WorkManager requires the gradle dependency below. Go ahead and add it now:

app/build.gradle

dependencies {
    // Other dependencies
    implementation "android.arch.work:work-runtime:$versions.work"
}

You should get the most current version of work-runtime from here and put the correct version in:

build.gradle

versions.work = "1.0.0-alpha10"

Make sure to Sync Now to sync your project with the changed gradle files.

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.

WorkManager Basics

There are a few WorkManager classes you need to know about:

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.

Step 1 - Make BlurWorker

In the package workers, create a new class called BlurWorker.

It should extend Worker.

Step 2 - Add a constructor

Add a constructor to the BlurWorker class:

public BlurWorker(
        @NonNull Context appContext,
        @NonNull WorkerParameters workerParams) {
    super(appContext, workerParams);
}

Step 3 - Override and implement doWork()

Your Worker will blur the res/test.jpg image.

Override the doWork() method and then implement the following:

  1. Get a Context by calling getApplicationContext(). You'll need this for various bitmap manipulations you're about to do.
  2. Create a Bitmap from the test image:
Bitmap picture = BitmapFactory.decodeResource(
    applicationContext.getResources(),
    R.drawable.test);
  1. Get a blurred version of the bitmap by calling the static blurBitmap method from WorkerUtils.
  2. Write that bitmap to a temporary file by calling the static writeBitmapToFile method from WorkerUtils. Make sure to save the returned URI to a local variable.
  3. Make a Notification displaying the URI by calling the static makeStatusNotification method from WorkerUtils.
  4. Return Worker.Result.SUCCESS.
  5. Wrap the code from steps 2-6 in a try/catch statement. Catch a generic Throwable.
  6. In the catch statement, emit an error Log statement: Log.e(TAG, "Error applying blur", throwable);
  7. In the catch statement then return Worker.Result.FAILURE;

The completed code for this step is below.

BlurWorker.java

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 Worker.Result.SUCCESS;
        } catch (Throwable throwable) {

            // Technically WorkManager will return Worker.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 Worker.Result.FAILURE;
        }
    }
}

Step 4 - Get WorkManager in the ViewModel

Create a variable for a WorkManager instance in your ViewModel and instantiate it in the ViewModel's constructor:

BlurViewModel.java

private WorkManager mWorkManager;

// BlurViewModel constructor
public BlurViewModel() {
  mWorkManager = WorkManager.getInstance();
  //...rest of the constructor
}

Step 5 - Enqueue the WorkRequest in WorkManager

Alright, time to make a WorkRequest and tell WorkManager to run it. There are two types of WorkRequests:

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:

BlurViewModel.java

void applyBlur(int blurLevel) {
   mWorkManager.enqueue(OneTimeWorkRequest.from(BlurWorker.class));
}

Step 6 - Run your code!

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:

Then navigate to data>data>com.example.background>files>blur_filter_outputs><URI> 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.

Step 1 - Create Data input object

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 WorkRequests.

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:

  1. Create a Data.Builder object.
  2. If mImageUri is a non-null URI, then add it to the Data object using the putString method. This method takes a key and a value. You can use the String constant KEY_IMAGE_URI from the Constants class.
  3. Call build() on the Data.Builder object to make your Data object, and return it.

Below is the completed createInputDataForUri method:

BlurViewModel.java

/**
 * 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();
}

Step 2 - Pass the Data object to WorkRequest

You're going to want to change the applyBlur method so that it:

  1. Creates a new OneTimeWorkRequest.Builder.
  2. Calls setInputData, passing in the result from createInputDataForUri.
  3. Builds the OneTimeWorkRequest.
  4. Enqueues that request using WorkManager.

Below is the completed applyBlur method:

BlurViewModel.java

void applyBlur(int blurLevel) {
   OneTimeWorkRequest blurRequest =
                new OneTimeWorkRequest.Builder(BlurWorker.class)
                        .setInputData(createInputDataForUri())
                        .build();

   mWorkManager.enqueue(blurRequest);
}

Step 3 - Update BlurWorker's doWork() to get the input

Now let's update BlurWorker's doWork() method to get the URI we passed in from the Data object:

BlurWorker.java

public Worker.Result doWork() {

       Context applicationContext = getApplicationContext();
        
        // ADD THIS LINE
       String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);
         
        //... rest of doWork()
}

Step 4 - Blur the given URI

With the URI, you can blur the image the user selected:

BlurWorker.java

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

Step 5 - Output temporary URI

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:

  1. Create a new Data, just as you did with the input, and store outputUri as a String. Use the same key, KEY_IMAGE_URI.
  2. Pass this to Worker's setOutputData method.

BlurWorker.java

This line should follow the Uri outputUri line in doWork():

setOutputData(new Data.Builder().putString(
                    Constants.KEY_IMAGE_URI, outputUri.toString()).build());

Step 6 - Run your app

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><URI> 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:

We'll use a WorkManager chain of work to add this functionality.

WorkManager allows you to create separate WorkerRequests 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.

Step 1 - Create Cleanup and Save Workers

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.

Step 2 - Add a constructor

Add a constructor to the CleanupWorker class:

public CleanupWorker(
        @NonNull Context appContext,
        @NonNull WorkerParameters workerParams) {
    super(appContext, workerParams);
}

Step 3 - Override and implement doWork() for CleanupWorker

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:

CleanupWorker.java

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;
        }
    }
} 

Step 4 - Override and implement doWork() for SaveImageToFileWorker

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).

SaveImageToFileWorker.java

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 imageUrl = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, TITLE, DATE_FORMATTER.format(new Date()));
            if (TextUtils.isEmpty(imageUrl)) {
                Log.e(TAG, "Writing to MediaStore failed");
                return Worker.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;
        }
    }
}

Step 5 - Create a WorkRequest Chain

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:

BlurViewModel.java

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 WorkRequests:

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:

BlurViewModel.java

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:

Step 6 - Repeat the BlurWorker

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.

Try it yourself and then compare with the code below:

BlurViewModel.java

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 IMAGE_MANIPULATION_WORK_NAME as the key. You'll also need to pass in a ExisitingWorkPolicy. 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:

BlurViewModel.java

// 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 WorkStatus object. WorkStatus is an object that contains details about the current state of a WorkRequest, including:

The following table shows three different ways to get LiveData<WorkStatus> or LiveData<List<WorkStatus>> objects and what each does.

Type

WorkManager Method

Description

Get work using id

getStatusByIdLiveData

Each WorkRequest has a unique ID generated by WorkManager; you can use this to get a single LiveData<WorkStatus> for that exact WorkRequest.

Get work using unique chain name

getStatusesForUniqueWorkLiveData

As you've just seen, WorkRequests can be part of a unique chain. This returns LiveData<List<WorkStatus>> for all work in a single, unique chain of WorkRequests.

Get work using a tag

getStatusesByTagLiveData

Finally, you can optionally tag any WorkRequest with a String. You can tag multiple WorkRequests with the same tag to associate them. This returns the LiveData<List<WorkStatus>> for any single tag.

You'll be tagging the SaveImageToFileWorker WorkRequest, so that you can get it using getStatusesByTagLiveData. 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 WorkRequests will have the same tag but not the same ID. Also you are able to pick the tag.

You would not use getStatusesForUniqueWorkLiveData because that would return the WorkStatus for all of the blur WorkRequests and the cleanup WorkRequest as well; it would take extra logic to find the save image WorkRequest.

Step 1 - Tag your work

In applyBlur, when creating the SaveImageToFileWorker, tag your work using the String constant TAG_OUTPUT :

BlurViewModel.java

OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
        .addTag(TAG_OUTPUT) // This adds the tag
        .build();

Step 2 - Get the WorkStatus

Now that you've tagged the work, you can get the WorkStatus:

  1. Declare a new variable called mSavedWorkStatus which is a LiveData<List<WorkStatus>>
  2. In the BlurViewModel constructor, get the WorkStatus using WorkManager.getStatusesByTagLiveData
  3. Add a getter for mSavedWorkStatus

The code you need is below:

BlurViewModel.java

// New instance variable for the WorkStatus
private LiveData<List<WorkStatus>> mSavedWorkStatus;

//In the BlurViewModel constructor
mSavedWorkStatus = mWorkManager.getStatusesByTagLiveData(TAG_OUTPUT);  

// Add a getter method for mSavedWorkStatus
LiveData<List<WorkStatus>> getOutputStatus() { return mSavedWorkStatus; } 

Step 3 - Display the WorkStatus

Now that you have a LiveData for your WorkStatus, you can observe it in the BlurActivity. In the observer:

  1. Check if the list of WorkStatus is not null and if it has any WorkStatus objects in it - if not then the Go button has not been clicked yet, so return.
  2. Get the first WorkStatus in the list; there will only ever be one WorkStatus tagged with TAG_OUTPUT because we made the chain of work unique.
  3. Check whether the work status is finished, using workStatus.getState().isFinished();
  4. If it's not FINISHED, then call showWorkInProgress() which hides and shows the appropriate views.
  5. If it's FINISHED then call showWorkFinished()which hides and shows the appropriate views.

Here's the code:

BlurActivity.java

// Show work status, added in onCreate()
mViewModel.getOutputStatus().observe(this, listOfWorkStatuses -> {

    // If there are no matching work statuses, do nothing
    if (listOfWorkStatuses == null || listOfWorkStatuses.isEmpty()) {
        return;
    }

    // We only care about the one output status.
    // Every continuation has only one worker tagged TAG_OUTPUT
    WorkStatus workStatus = listOfWorkStatuses.get(0);

    boolean finished = workStatus.getState().isFinished();
    if (!finished) {
        showWorkInProgress();
    } else {
        showWorkFinished();
    }
});

Step 4 - Run your app

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 WorkStatus 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.

Step 1 - Create mOutputUri

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:

BlurViewModel.java

// New instance variable for the WorkStatus
private Uri mOutputUri;

// Add a getter and setter for mOutputUri
void setOutputUri(String outputImageUri) {
    mOutputUri = uriOrNull(outputImageUri);
}

Uri getOutputUri() { return mOutputUri; }

Step 2 - Create the See File button

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:

BlurActivity.java

// In 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);
        }
    }
});

Step 3 - Set the URI and show the button

There are a few final tweaks you need to apply to the WorkStatus observer to get this to work (no pun intended):

  1. If the WorkStatus is finished, get the output data, using workStatus.getOutputData().
  2. Then get the output URI, remember that it's stored with the Constants.KEY_IMAGE_URI key.
  3. Then if the URI isn't empty, it saved properly; show the mOutputButton and call setOutputUri on the view model with the uri.

BlurActivity.java

// Show work status
mViewModel.getOutputStatus().observe(this, listOfWorkStatuses -> {

    // If there are no matching work statuses, do nothing
    if (listOfWorkStatuses == null || listOfWorkStatuses.isEmpty()) {
        return;
    }

    // We only care about the one output status.
    // Every continuation has only one worker tagged TAG_OUTPUT
    WorkStatus workStatus = listOfWorkStatuses.get(0);

    boolean finished = workStatus.getState().isFinished();
    if (!finished) {
        showWorkInProgress();
    } else {
        showWorkFinished();
        Data outputData = // TODO get the output Data from the workStatus

        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
        }
    }
});

Step 4 - Run your code

Run your code. You should see your new, clickable See File button which takes you to the outputted file:

You added this 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.

Step 1 - Cancel the work by name

In the view model, write the method to cancel the work:

BlurViewModel.java

    /**
     * Cancel work using the work's unique name
     */
    void cancelWork() {
        mWorkManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME);
    }

Step 2 - Call cancel method

Then, hook up the button mCancelButton to call cancelWork:

BlurActivity.java

// In onCreate
        
// Hookup the Cancel button
mCancelButton.setOnClickListener(view -> mViewModel.cancelWork());

Step 3 - [OPTIONAL] Slow down work and show notifications

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 WorkRequests very quickly.

Place at the start of onWork for All Workers

WorkerUtils.makeStatusNotification("Doing <WORK NAME>", applicationContext);
WorkerUtils.sleep();

Step 4 - Run and cancel your work

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.

Step 1 - Create and add charging constraint

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:

BlurViewModel.java

// 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);

Step 2 - Test with emulator or device

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.

Congratulations! You've finished the Blur-O-Matic app and in the process learned about:

Excellent "work"! To see the end state of the code and all the changes check out:

WorkManager supports a lot more than we could cover in this codelab, including repeating work, a testing support library, parallel work requests and input mergers. To learn more, head over to the WorkManager documentation.