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:
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:Specifically, only the Worker subclass of
ListenableWorker
runs on a background thread provided by theExecutor
- not yourListenableWorker
implementation.As per the ListenableWorker.startWork() documentation:
Because you're using
ListenableWorker
, yourstartWork
method is being called on the main thread, as expected. Since you call your owndoWork()
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 callgetLastLocation()
on.Instead, you need to only call
set
on yourResolvableFuture
when you actually have a result - i.e., in theonSuccess()
oronFailure
callbacks. This is the signal toWorkManager
that you're actually done with your work: