Check if WorkRequest has been previously enquequed by WorkManager Android - android

I am using PeriodicWorkRequest to perform a task for me every 15 minutes.
I would like to check, if this periodic work request has been previously scheduled. If not, schedule it.
if (!PreviouslyScheduled) {
PeriodicWorkRequest dataupdate = new PeriodicWorkRequest.Builder( DataUpdateWorker.class , 15 , TimeUnit.MINUTES).build();
WorkManager.getInstance().enqueue(dataupdate);
}
Previously when I was performing task using JobScheduler, I used to use
public static boolean isJobServiceScheduled(Context context, int JOB_ID ) {
JobScheduler scheduler = (JobScheduler) context.getSystemService( Context.JOB_SCHEDULER_SERVICE ) ;
boolean hasBeenScheduled = false ;
for ( JobInfo jobInfo : scheduler.getAllPendingJobs() ) {
if ( jobInfo.getId() == JOB_ID ) {
hasBeenScheduled = true ;
break ;
}
}
return hasBeenScheduled ;
}
Need help constructing a similar module for work request to help find scheduled/active workrequests.

Set some Tag to your PeriodicWorkRequest task:
PeriodicWorkRequest work =
new PeriodicWorkRequest.Builder(DataUpdateWorker.class, 15, TimeUnit.MINUTES)
.addTag(TAG)
.build();
Then check for tasks with the TAG before enqueue() work:
WorkManager wm = WorkManager.getInstance();
ListenableFuture<List<WorkStatus>> future = wm.getStatusesByTag(TAG);
List<WorkStatus> list = future.get();
// start only if no such tasks present
if((list == null) || (list.size() == 0)){
// shedule the task
wm.enqueue(work);
} else {
// this periodic task has been previously scheduled
}
But if you dont really need to know that it was previously scheduled or not, you could use:
static final String TASK_ID = "data_update"; // some unique string id for the task
PeriodicWorkRequest work =
new PeriodicWorkRequest.Builder(DataUpdateWorker.class,
15, TimeUnit.MINUTES)
.build();
WorkManager.getInstance().enqueueUniquePeriodicWork(TASK_ID,
ExistingPeriodicWorkPolicy.KEEP, work);
ExistingPeriodicWorkPolicy.KEEP means that the task will be scheduled only once and then work periodically even after device reboot. In case you need to re-schedule the task (for example in case you need to change some parameters of the task), you will need to use ExistingPeriodicWorkPolicy.REPLACE here

You need to add a unique tag to every WorkRequest. Check Tagged work.
You can group your tasks logically by assigning a tag string to any WorkRequest object. For that you need to call WorkRequest.Builder.addTag()
Check below Android doc example:
OneTimeWorkRequest cacheCleanupTask =
new OneTimeWorkRequest.Builder(MyCacheCleanupWorker.class)
.setConstraints(myConstraints)
.addTag("cleanup")
.build();
Same you can use for PeriodicWorkRequest
Then, You will get a list of all the WorkStatus for all tasks with that tag using WorkManager.getStatusesByTag().
Which gives you a LiveData list of WorkStatus for work tagged with a tag.
Then you can check status using WorkStatus as below:
WorkStatus workStatus = listOfWorkStatuses.get(0);
boolean finished = workStatus.getState().isFinished();
if (!finished) {
// Work InProgress
} else {
// Work Finished
}
You can check below google example for more details. Here they added how to add a tag to WorkRequest and get status of work by tag :
https://github.com/googlecodelabs/android-workmanager
Edits
Check below code and comment to how we can get WorkStatus by tag. And schedule our Work if WorkStatus results empty.
// Check work status by TAG
WorkManager.getInstance().getStatusesByTag("[TAG_STRING]").observe(this, listOfWorkStatuses -> {
// Note that we will get single WorkStatus if any tag (here [TAG_STRING]) related Work exists
// If there are no matching work statuses
// then we make sure that periodic work request has been previously not scheduled
if (listOfWorkStatuses == null || listOfWorkStatuses.isEmpty()) {
// we can schedule our WorkRequest here
PeriodicWorkRequest dataupdate = new PeriodicWorkRequest.Builder( DataUpdateWorker.class , 15 , TimeUnit.MINUTES)
.addTag("[TAG_STRING]")
.build();
WorkManager.getInstance().enqueue(dataupdate);
return;
}
WorkStatus workStatus = listOfWorkStatuses.get(0);
boolean finished = workStatus.getState().isFinished();
if (!finished) {
// Work InProgress
} else {
// Work Finished
}
});
I have not tested code. Please provide your feedback for the same.
Hope this helps you.

I was also searching for the same condition.I couldn't find one.So in order to solve this problem i found a mechanism.First cancel all scheduled works and reschedule the work again. So that we can ensure that only one instance of your work will be maintained. Also please be ensure that you have to maintain your worker code logic like that.
For canceling a work. For more
UUID compressionWorkId = compressionWork.getId();
WorkManager.getInstance().cancelWorkById(compressionWorkId);

keep same taskID with ExistingPeriodicWorkPolicy.KEEP will not create new task each time .
WorkManager.getInstance().enqueueUniquePeriodicWork(TASK_ID,
ExistingPeriodicWorkPolicy.KEEP, work);

Related

Android Workers, uniqueWork input data bug

I have created two Worker classes. One for uploading images and second for calling processing for uploaded images.
I'm initializing WorkManager and starts chained work.
public void uploadAndProcess() {
workManager.beginUniqueWork("uploadAndProcessWork", ExistingWorkPolicy.APPEND, buildUploadRequest())
.then(buildProcessingRequest())
.enqueue();
}
private Data buildData() {
return new Data.Builder()
.putStringArray("vehicleIds", vehicleIds.toArray(new String[]{}))
.putString("targetId", targetId)
.putInt("position", position != null ? position : -1)
.build();
}
private Constraints buildConstraints() {
return new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
}
private OneTimeWorkRequest buildUploadRequest() {
return new OneTimeWorkRequest.Builder(UploadWorker.class)
.setInputMerger(OverwritingInputMerger.class)
.setInputData(buildData())
.setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.setConstraints(buildConstraints())
.build();
}
private OneTimeWorkRequest buildProcessingRequest() {
return new OneTimeWorkRequest.Builder(ProcessWorker.class)
.setInputMerger(OverwritingInputMerger.class)
.setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.setConstraints(buildConstraints())
.build();
}
When I call uploadAndProcess() for first object, everything is OK. UploadWorker gets valid vehicleIds parameter. Images are uploaded, output is passed to second worker.
But, when uploadAndProcess() is called second time, for second object, valid vehicleIds is set inside buildData(), but received vehicleIds inside of a UploadWorker are from first object, first called UploadWorker.
I don't know is this some strange expected behavior or some bug in Workers.
Is there any way to keep uniqueWork enqueue and have proper inputData parameters? With regular enqueue work method, inputData in Workers are valid, as it should be.

When does Android's WorkerManager stop a Worker?

We have an Android app using WorkManager to handle background sync work. Our sync worker is like this:
public class SyncWorker extends Worker {
[...]
#NonNull
#Override
public Result doWork() {
if (canNotRetry(getRunAttemptCount())) {
// This could seem unreachable, consider removing... or not... because if stopped by the
// system, the work might be retried by design
CBlogger.INSTANCE.log([...]);
return Result.success();
}
boolean syncOk = false;
//Sync
try (Realm realm = Realm.getDefaultInstance()) {
// Doing sync related ops & network calls
// checking this.isStopped() between operations to quit
// sync activity when worker has to be stopped
syncOk = true;
} catch (Throwable throwable) {
CBlogger.INSTANCE.log([...]);
}
// On error, continue with following code to avoid any logic in catch
// This method must NOT throw any unhandled exception to avoid unique work to be marked as failed
try {
if (syncOk) {
return Result.success();
}
if (canNotRetry(getRunAttemptCount() + 1)) {
CBlogger.INSTANCE.log([...]);
return Result.success();
} else {
CBlogger.INSTANCE.log([...]);
return Result.retry();
}
} catch (Throwable e) {
CBlogger.INSTANCE.log([...]);
return Result.success();
}
}
private boolean canNotRetry(int tryNumber) {
// Check if the work has been retry too many times
if (tryNumber > MAX_SYNC_RETRY_COUNT) {
CBlogger.INSTANCE.log([...]);
return true;
} else {
return false;
}
}
#Override
public void onStopped() {
CBlogger.INSTANCE.log([...]);
}
}
The work is scheduled by a dedicate method of an helper class:
public static void scheduleWorker(Context context, String syncPolicy, ExistingWorkPolicy existingWorkingPolicy){
Constraints constraints = new Constraints.Builder()
.setRequiresCharging(false)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data.Builder data = new Data.Builder();
data.putString(context.getResources().getString(R.string.sync_worker_policy), syncPolicy);
Log.d(TAG, "Scheduling one-time sync request");
logger.info("Scheduling one-time sync request");
OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder
(SyncWorker.class)
.setInputData(data.build())
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build();
WorkManager.getInstance(context).enqueueUniqueWork("OneTimeSyncWorker", existingWorkingPolicy, oneTimeWorkRequest);
}
that is called when user clicks on "Sync" button or by another worker that is scheduled to run every 20' and calls the helper's function this way:
SyncWorkerManager.scheduleWorker(context, context.getResources().getString(R.string.sync_worker_policy_full), ExistingWorkPolicy.KEEP);
so that a new sync is queued only if not already waiting or running. Notice that sync work policy enforces that a connected network is required.
This strategy works all in all good, but sometimes we find in logs that Worker's onStopped() method is called a few seconds (about 10") after SyncWorker start.
Known that we never programmatically stop a specific Worker for the outside and we only call WorkManager.getInstance(context).cancelAllWork(); during logout procedure or before a new login (that also schedules del periodic Worker), when does the system can decide to stop the worker and call its onStopped() method?
I know that it can happen when:
Constraints are no longer satisfied (network connection dropped)
Worker runs over 10' limit imposed by JobScheduler implementation (our scenario is tested on Android 9 device)
New unique work enqueued with same name and REPLACE policy (we never use this policy in our app for the SyncWorker, only for PeriodicSyncWorker)
Spurious calls due to this bug (we work with "androidx.work:work-runtime:2.2.0")
Is there any other condition that can cause Worker's to be stopped? Something like:
Doze mode
App stand-by buckets
App background restrictions (Settings --> Apps --> My App --> Battery --> Allow Background)
App battery optimization (Settings --> Apps --> My App --> Battery --> Battery Optimization)
Thanks
There are multiple reasons a Worker can be stopped. You can explicitly ask for it to be cancelled or WorkManager might stop it for a variety of reasons which are documented here.

I am unable to getStatusesByTag of the enqueueUniquePeriodicWork through WorkManager

I am using WorkManager via implementation "android.arch.work:work-runtime:1.0.1" to start a PeriodicWorkRequest to call a Rest API to pull some data at interval PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS which is 15 minutes, my code is:
PeriodicWorkRequest.Builder builder = new
PeriodicWorkRequest.Builder(MyWorker.class,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
.addTag(TAG_WORKER)
.setInputData(createInputData(config));
WorkManager.getInstance().enqueueUniquePeriodicWork(TAG_WORKER,
ExistingPeriodicWorkPolicy.KEEP, builder.build());
I want to know the status or the PeriodicWorkRequest enqueued by WorkManager by its TAG, through the code:
WorkManager.getInstance().getStatusesByTag(TAG_WORKER);
But I am unable to resolve the method getStatusesByTag(TAG_WORKER), please help if anyone did the workaround to get the Status of Request been enqueued by TAG.
Thanks in advance!
The API to get information on the WorkRequest's status changed last year with 1.0.0-alpha11. To know the status of your work you can use:
WorkManager.getInstance().getWorkInfosByTagLiveData(TAG_WORKER)
.observe(lifecycleOwner, new Observer<WorkInfo>() {
#Override
public void onChanged(#Nullable WorkInfo workInfo) {
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
displayMessage("Work finished!")
}
}
});
You can find more information on WorkManager's documentation page: "Work States and observing work"
You are using unique work. To observe unique work you should be using
getWorkInfosForUniqueWorkLiveData API.
Edit: Re-read your code snippet and realized that you are also adding a tag. You can also use the getWorkInfosByTagLiveData API mentioned in the answer below.

Firebase job dispatcher recurring job not working

I am scheduling a recurring job using firebase job dispatcher. But It is not executing.
Job job = builder.setService(FetchGCMNotificationService.class)
.setTag(FIREBASE_JOB_TAG)
.setRecurring(true)
.setLifetime(Lifetime.FOREVER)
.setReplaceCurrent(true)
.setConstraints(Constraint.ON_ANY_NETWORK)
.setTrigger(Trigger.executionWindow(TimeUnit.MINUTES.toSeconds(9),TimeUnit.MINUTES.toSeconds(10)))
.build();
firebaseJobDispatcher.mustSchedule(job);
After spending so much time on stackoverflow and firebase documentation I didn't get any solution. Then I looked into the firebase job dispatcher code and there is a condition for rescheduling jobs. The condition is in GooglePlayReceiver class :
private static boolean needsToBeRescheduled(JobParameters job, int result) {
return job.isRecurring()
&& job.getTrigger() instanceof ContentUriTrigger
&& result != JobService.RESULT_FAIL_RETRY;
}
According to this above condition
Job should be recurring which is true in my case
job.getTrigger() should be instanceof ContentUriTrigger. I am confused here because I want to execute my job based on ExecutionWindowTrigger. So how can I give ContentUriTrigger.
And the last condition is true in my case because when my job is getting done, I am passing false for needsReschedule param value in jobFinished method.
jobFinished method Implementation in JobService class is:
public final void jobFinished(#NonNull JobParameters job, boolean needsReschedule) {
if (job == null) {
Log.e(TAG, "jobFinished called with a null JobParameters");
return;
}
synchronized (runningJobs) {
JobCallback jobCallback = runningJobs.remove(job.getTag());
if (jobCallback != null) {
jobCallback.sendResult(needsReschedule ? RESULT_FAIL_RETRY : RESULT_SUCCESS);
}
}}
So in GooglePlayReceiver 3rd condition(result != JobService.RESULT_FAIL_RETRY;) is true based on this ternary condition needsReschedule ? RESULT_FAIL_RETRY : RESULT_SUCCESS
Please look into this issue and correct me if I am wrong.

Unique OneTimeWorkRequest in Workmanager

We are using OneTimeWorkRequest to start background task in our project.
At application start, we are starting the OneTimeWorkRequest (say req A)
Depends on user's action we start the same work request A.
At some cases, if the app gets killed when the work request A is in progress, Android automatically restarts the request A when the app restarts. Once again we are also starting the request A again. So two instances of the request A runs in parallel and leads to a deadlock.
To avoid this, I did below code in app start to check if the worker is running but this always returns false.
public static boolean isMyWorkerRunning(String tag) {
List<WorkStatus> status = WorkManager.getInstance().getStatusesByTag(tag).getValue();
return status != null;
}
Is there a better way to handle this?
I checked the beginUniqueWork(). Is it costlier if I have only one request?
Edit 2:
This question is about unique One time task. For starting unique Periodic task we had a separate API enqueueUniquePeriodicWork(). But we did not have an API for starting unique onetime work. I was confused to use between continuation object or manually check and start approach.
In recent build they Android added new api for this enqueueUniqueWork(). This is the exact reason they mentioned in their release notes.
Add WorkManager.enqueueUniqueWork() API to enqueue unique
OneTimeWorkRequests without having to create a WorkContinuation.
https://developer.android.com/jetpack/docs/release-notes
Edit 2:
Nov 8th release notes:
https://developer.android.com/jetpack/docs/release-notes
Add WorkManager.enqueueUniqueWork() API to enqueue unique
OneTimeWorkRequests without having to create a WorkContinuation.
This says, alpha11 has this new API to uniquely enqueue a onetimework.
I tried changing the code as follows:
OneTimeWorkRequest impWork = new OneTimeWorkRequest.Builder(WorkerNotesAttachment.class)
.addTag(RWORK_TAG_NOTES)
.build();
WorkManager.getInstance().enqueueUniqueWork(RWORK_TAG_NOTES, ExistingWorkPolicy.REPLACE, impWork);
I tried using the beginUniqueWork API. But it fails to run sometimes. So I ended up writing the following function.
public static boolean isMyWorkerRunning(String tag) {
List<WorkStatus> status = null;
try {
status = WorkManager.getInstance().getStatusesByTag(tag).get();
boolean running = false;
for (WorkStatus workStatus : status) {
if (workStatus.getState() == State.RUNNING
|| workStatus.getState() == State.ENQUEUED) {
return true;
}
}
return false;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return false;
}
We need to get all the WorkStatus objects and check if atleast one of them is in running or Enqueued state. As the system keeps all the completed works in the DB for few days (Refer pruneWork()), we need to check all the work instances.
Invoke this function before starting the OneTimeWorkRequest.
public static void startCacheWorker() {
String tag = RWORK_TAG_CACHE;
if (isMyWorkerRunning(tag)) {
log("worker", "RWORK: tag already scheduled, skipping " + tag);
return;
}
// Import contact for given network
OneTimeWorkRequest impWork = new OneTimeWorkRequest.Builder(WorkerCache.class)
.addTag(tag)
.build();
WorkManager.getInstance().enqueue(impWork);
}
You can use beginUniqueWork() with a unique name.
If you use ExistingWorkPolicy:
APPEND: the 2 requests will run serial.
KEEP: will not run the second request if the first is running.
REPLACE: the 2 requests will run parallel.
Using getStatusesByTag returns LiveData of List<WorkStatus>
it was made as LiveData because WorkStatus is kept in Room DB and WorkManger has to query it first on background thread then deliver the result.
so you must observe to get the real value when it's available .
calling getValue() will return last value of the LiveData which isn't available on the time you call it.
What you can do
public static LiveData<Boolean> isMyWorkerRunning(String tag) {
MediatorLiveData<Boolean> result = new MediatorLiveData<>();
LiveData<List<WorkStatus>> statusesByTag = WorkManager.getInstance().getStatusesByTag(tag);
result.addSource(statusesByTag, (workStatuses) -> {
boolean isWorking;
if (workStatuses == null || workStatuses.isEmpty())
isWorking = false;
else {
State workState = workStatuses.get(0).getState();
isWorking = !workState.isFinished();
}
result.setValue(isWorking);
//remove source so you don't get further updates of the status
result.removeSource(statusesByTag);
});
return result;
}
Now you don't start the task until you observe on the returning value of isMyWorkerRunning if it's true then it's safe to start it if not this mean that another task with the same tag is running
Since all of the answers are mostly outdated, you can listen for changes on a tagged worker like this:
LiveData<List<WorkInfo>> workInfosByTag = WorkManager.getInstance().getWorkInfosByTagLiveData(tag);
workInfosByTag.observeForever(workInfos -> {
for (WorkInfo workInfo : workInfos) {
workInfo.toString();
}
});

Categories

Resources