How do I run a ListenableWorker work on a background thread?
L

2

5

Since I need to perform work asynchronously in WorkManager, I need to use the ListenableWorker, which by default runs on the main (UI) thread. Since this work could be a long processing tasks that could freeze the interface, I wanted to perform it on a background thread. In the Working with WorkManager (Android Dev Summit '18) video, the Google engineer showed how to manually configure WorkManager to run works on a custom Executor, so I followed his guidance:

1) Disable the default WorkManager initializer in the AndroidManifest:

<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="com.example.myapp.workmanager-init"
    tools:node="remove" />

2) In Application.onCreate, initialize WorkManager with the custom configuration, which in my case is this:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Configuration configuration = new Configuration.Builder().setExecutor(Executors.newSingleThreadExecutor()).build();
        WorkManager.initialize(this, configuration);
    }
}

Now my actual ListenableWorker is this:

@NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        Log.d(TAG, "Work started.");
        mFuture = ResolvableFuture.create();
        Result result = doWork();
        mFuture.set(result);
        return mFuture;
    }

    private Result doWork() {
        Log.d(TAG, "isMainThread? " + isMainThread());
        mFusedLocationProviderClient.getLastLocation().addOnSuccessListener(new OnSuccessListener<Location>() {
            @Override
            public void onSuccess(Location location) {
                if (location != null) {
                    // Since I still don't know how to communicate with the UI, I will just log the location
                    Log.d(TAG, "Last location: " + location);
                    return Result.success();
                } else {
                    return Result.failure();
                }
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                e.printStackTrace();
                return Result.failure();
            }
        });
    }

    private boolean isMainThread() {
        return Looper.getMainLooper().getThread() == Thread.currentThread();
    }

Why does the isMainThread() method return true even though I specified the Executor WorkManager should use as a new background thread and how can I actually run that piece of work on a background thread?

EDIT: ListenableWorker with the need of a CountDownLatch.
Since I need to reschedule the work everytime it succeeds (workaround for the 15 minutes minimum interval for PeriodicWorkRequest), I need to do it after the previous piece of work returns success, otherwise I have weird behavior. This is needed because, apparently, ExistingWorkPolicy.APPEND doesn't work as expected.
Use case is to request location updates with high accuracy at a pretty frequent interval (5-10s), even in background. Turned on and off by an SMS, even when the app is not running (but not force-stopped), or through a button (It's an university project).

public class LocationWorker extends ListenableWorker {

    static final String UNIQUE_WORK_NAME = "LocationWorker";
    static final String KEY_NEW_LOCATION = "new_location";
    private static final String TAG = "LocationWorker";
    private ResolvableFuture<Result> mFuture;
    private LocationCallback mLocationCallback;
    private CountDownLatch mLatch;
    private Context mContext;

    public LocationWorker(@NonNull final Context appContext, @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
        mContext = appContext;
        Utils.setRequestingLocationUpdates(mContext, true);
        mLatch = new CountDownLatch(1);
        mLocationCallback = new LocationCallback() {
            @Override
            public void onLocationResult(LocationResult locationResult) {
                LocationUtils.getInstance(mContext).removeLocationUpdates(this);
                Location location = locationResult.getLastLocation();
                Log.d(TAG, "Work " + getId() + " returned: " + location);
                mFuture.set(Result.success(Utils.getOutputData(location)));
                // Rescheduling work
                OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(LocationWorker.class).setInitialDelay(10, TimeUnit.SECONDS).build();
                WorkManager.getInstance().enqueueUniqueWork(LocationWorker.UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, request);
                Log.d(TAG, "Rescheduling work. New ID: " + request.getId());
                // Relase lock
                mLatch.countDown();
            }
        };
    }

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        Log.d(TAG, "Starting work " + getId());
        mFuture = ResolvableFuture.create();
        LocationUtils.getInstance(mContext).requestSingleUpdate(mLocationCallback, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                LocationUtils.getInstance(mContext).removeLocationUpdates(mLocationCallback);
                Utils.setRequestingLocationUpdates(mContext, false);
                WorkManager.getInstance().cancelUniqueWork(UNIQUE_WORK_NAME);
                mFuture.set(Result.failure());
                // Relase lock
                mLatch.countDown();
            }
        });
        try {
            mLatch.await(5L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return mFuture;
    }
}
Latinity answered 8/1, 2019 at 0:12 Comment(0)
T
16

If you want to continuously (i.e., less than every 60 seconds), you absolutely should be using a foreground service and not WorkManager, which is for, as per the documentation:

deferrable, asynchronous tasks

And not something that needs to run near continously.

However, if you do proceed to incorrectly use WorkManager, you'd want to keep the following in mind:

Your custom doWork method runs on the main thread because as per the setExecutor() documentation:

An Executor for running Workers

Specifically, only the Worker subclass of ListenableWorker runs on a background thread provided by the Executor - not your ListenableWorker implementation.

As per the ListenableWorker.startWork() documentation:

This method is called on the main thread.

Because you're using ListenableWorker, your startWork method is being called on the main thread, as expected. Since you call your own doWork() method on the same thread, you'll still be on the main thread.

In your case, you don't need to care about what thread you're on and you don't need any Executor since it doesn't matter what thread you call getLastLocation() on.

Instead, you need to only call set on your ResolvableFuture when you actually have a result - i.e., in the onSuccess() or onFailure callbacks. This is the signal to WorkManager that you're actually done with your work:

public class LocationWorker extends ListenableWorker {

    static final String UNIQUE_WORK_NAME = "LocationWorker";
    static final String KEY_NEW_LOCATION = "new_location";
    private static final String TAG = "LocationWorker";
    private ResolvableFuture<Result> mFuture;
    private LocationCallback mLocationCallback;

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

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        Log.d(TAG, "Starting work " + getId());
        mFuture = ResolvableFuture.create();
        Utils.setRequestingLocationUpdates(getApplicationContext(), true);
        mLocationCallback = new LocationCallback() {
            @Override
            public void onLocationResult(LocationResult locationResult) {
                LocationUtils.getInstance(getApplicationContext()).removeLocationUpdates(this);
                Location location = locationResult.getLastLocation();
                Log.d(TAG, "Work " + getId() + " returned: " + location);
                // Rescheduling work
                OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(LocationWorker.class).setInitialDelay(10, TimeUnit.SECONDS).build();
                WorkManager.getInstance().enqueueUniqueWork(LocationWorker.UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, request);
                Log.d(TAG, "Rescheduling work. New ID: " + request.getId());

                // Always set the result as the last operation
                mFuture.set(Result.success(Utils.getOutputData(location)));
            }
        };
        LocationUtils.getInstance(getApplicationContext()).requestSingleUpdate(mLocationCallback, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                LocationUtils.getInstance(getApplicationContext()).removeLocationUpdates(mLocationCallback);
                Utils.setRequestingLocationUpdates(getApplicationContext(), false);
                WorkManager.getInstance().cancelUniqueWork(UNIQUE_WORK_NAME);
                mFuture.set(Result.failure());
            }
        });
        return mFuture;
    }
}
Transfinite answered 8/1, 2019 at 0:33 Comment(14)
So since Worker is a subclass of ListenableWorker, setExecutor() only sets the custom Executor Workers and not ListenableWorkers, like in my case? Having said that, it's impossible to perform work in ListenableWorker on a background thread? As per WorkManager's code, Worker is able to perform work on background by using getBackgroundExecutor() and I would like to do the same in my ListenableWorker... is it possible? I used setExecutor() because another Google engineer suggested here: issuetracker.google.com/issues/122066789 Thanks.Latinity
You don't need any Executor for what you're doing - the FusedLocationProviderClient is taking care of that for you.Transfinite
FusedLocationProviderClient automatically runs on a separate thread regardless of how you called it? If, like I do, I have another ListenableWorker for a Retrofit request, I think I would need to manually define the background executor, wouldn't I?Latinity
If you're already using an API that has a callback, they're either already doing the threading for you (since they obviously can't block your thread) or they offer an optional API that takes an Executor as an overload.Transfinite
Ok now I understood better what you meant: the actual computation happens on the thread specified by that API, only the callback is executed on my thread, correct? I've got one last question: as I previously said, the Worker class, which is the synchronous implementation of WorkManager, execute whatever code you put in doWork() on a background thread (proven by the isMainThread() method in the question and **). Well, how can I run whatever code I put in ListenableWorker's startWork()? This is the actual question since the beginning, still unanswered. Thank you for understanding.Latinity
** by looking at the source codeLatinity
I've updated my answer to point out the documentation on startWork() that is explicitly always the main thread. In your case, because you're using an API that has a callback style API, it doesn't matter at all what thread you're on.Transfinite
Now I am in the need of using a CountDownLatch so I definitely need to run the ListenableWorker code in another thread to avoid busy waiting on the UI thread, so maybe I could get an answer to the original question 😅Latinity
No, you don't. Please update your question with your fixed ListenableWorker (only calling set when you actually have a result) and where you are attempting to use a CountDownLatch.Transfinite
I've updated my answer with an explanation that WorkManager is absolutely not the API you want to be using for your particular problem. I also added code that explains exactly what I mean by calling set() only when you have a result. I think the point you were missing is that you don't need to wait for set() to be called to return the ResolvableFuture - you return the ResolvableFuture immediately, and WorkManager will wait for you to call set() at some later time.Transfinite
Thank you for the answer. I already did a version of the app which used a foreground service, but I don't know why, it felt ugly to me. So I looked at the new Google WorkManager API and it looked interesting. But since a Google engineer explicitly told me the best solution to fetch the user location is to use a foreground service, I will trust him and use it. I'll just use WorkManager when uploading the user location to my server because I think the use case here fits well WorkManager. Thanks again!Latinity
Two questions on the foreground Service, if I may ask: 1) The service should be promoted to foreground only if the Activity is in background, and vice versa (startForeground() shows the notification even if started when the Activity is in foreground): considering it's all inside my process, should I implement this logic extending the Binder class? 2) What is the best practice on updating the UI when the Service gets a new location? I'm thinking of a LocalBroadcastManager but was wondering what was the advice of a Google engineer. Thanks for clearing my doubts and improving me!Latinity
Thank you @Transfinite for a very useful example of how to use ListenableWorker rather than Worker... would be great if the official docs could include such an example rather than focussing on Worker and just stating that you should use ListenableWorker when async work needs to be done. Appreciate that this API is still in beta, but it's a really really useful API that seems to tie all the loose ends up nicely.Stroke
@Transfinite The official docs say : 'A ListenableWorker is given a maximum of ten minutes to finish its execution and return a Result.' Any suggestions how to execute some work which takes more time maybe hours using a ListenableWorker, not able to use Foreground Notification Service directly since my work starts from background and Android 12 has restrictions on running NotificationService from background.Volcanism
M
0

Well - As the other answers mentioned, if you want to do frequent work (like every 60 sec) you should use foreground service.

Regardless, I'd use coroutines to get out of the main thread. Like:

        runBlocking(Dispatchers.Default) {
           //If you want to run blocking your code
        }

Or using the GlobalScope Launch

        GlobalScope.launch {
         //Work in a separated thread (running not blocking)
        }

Here is a practical example of getting a location using ListenableWorker and setting up the listeners out of the main thread.

https://github.com/febaisi/ListenableWorkerExample/blob/master/app/src/main/java/com/febaisi/listenableworkerexample/data/LocationListenableWorker.kt#L28

Mystical answered 8/10, 2019 at 4:39 Comment(1)
Please nobody use GlobalScope: medium.com/@elizarov/…Refrigeration

© 2022 - 2024 — McMap. All rights reserved.