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;
}
}
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With