Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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;
    }
}
like image 337
Oliver Avatar asked Jan 08 '19 00:01

Oliver


1 Answers

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;
    }
}
like image 102
ianhanniballake Avatar answered Oct 23 '22 21:10

ianhanniballake