Android Workers, uniqueWork input data bug - android

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.

Related

Android WorkManager network constraint retry without cancelling previous uncompleted work

I use Android WorkManager to download file from Firebase Storage.
The code is as the following
#NonNull
#Override
public Result doWork() {
dowloadFile-1
dowloadFile-2
dowloadFile-3
dowloadFile-4
return Result.success();
}
// Create and start an unique work.
// It mean there is only one work running at the time
public static void start(Context context) {
Constraints constraints = new Constraints.Builder()
// The Worker needs Network connectivity
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
int backoffDelay = context.getResources().getInteger(R.integer.sync_retry_backoff_delay);
OneTimeWorkRequest worker = new OneTimeWorkRequest.Builder(FirebaseSyncService.class)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, backoffDelay, TimeUnit.MINUTES)
.build();
WorkManager
.getInstance(context)
.enqueueUniqueWork(SYNC_WORK_ID, ExistingWorkPolicy.KEEP, worker);
}
I use getWorkInfosForUniqueWorkLiveData() to check state of unique work
And the scenario are as the following
Step-1. Connect internet then start work
Step-2. The program completed download file-1 and file-2
The work state now is RUNNING
Step-3. Disconnect internet
Work will be stopped and WorkManager will retry by adding new Work to queue
The work state now is ENQUEUED
Step-4. Re-connect internet
Expected:
After reconnecting internet, I expect the WorkManager will perform new work from beginning and the state is RUNING again.
I want to download file-1 and file-2 again.
Actual
WorkManager continue previous work and resume from downloading file-3. But the state is ENQUEUED.
Even if I change ExistingWorkPolicy.KEEP -> ExistingWorkPolicy.REPLACE, the behavior is the same.
Thank you for all supports
check these in your worker() Java file.
For doing network call you maybe using try catch, if not you have to use and then check in catch block you have to add return Result.Failure. Otherwise, even if you disconnect your internet it will continuous working(doWork() function),even work state is ENQUEUED and you will see exception in log.(Not sure reason, seems to be bug...)
I'm not faced these issue, when i use with Kotlin CoroutineWorker. its happening only if i use Worker
Note : I'll not suggest this type of coding, use Kotlin CoroutineWorker and forLoop for multiple series of file downloads. here i'm showing only for Demo
purpose.
#NonNull
#Override
public Result doWork() {
try {
dowloadFile-1
}catch (Exception e){
return Result.Failure; // this is important.Otherwise worker not finish,even work state is ENQUEUED
}
try {
dowloadFile-2
}catch (Exception e){
return Result.Failure;
}
try {
dowloadFile-3
}catch (Exception e){
return Result.Failure;
}
try {
dowloadFile-4
}catch (Exception e){
return Result.Failure;
}
//all success
return Result.success();
}
Quick Tip, In your case - Its better to use ExistingWorkPolicy.REPLACE, and Network call in background thread(use CoroutineWorker())

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();
}
});

workstatus observer always in enqueued state

I am trying to observe my workers but they are always in queued state or sometime it's RUNNING but never SUCCEED or FAILED.
is workStatus.state from return in doWork() or it's different?
this is my worker script:
package com.mockie.daikokuten.sync.workers
import androidx.work.Worker
class TestWorker:Worker()
{
override fun doWork():Worker.Result
{
return Worker.Result.SUCCESS
}
}
this is script to observe the workers :
val test = PeriodicWorkRequest.Builder(
TestWorker::class.java,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS)
.addTag("test_worker")
.build()
WorkManager.getInstance()?.enqueueUniquePeriodicWork("test_worker", ExistingPeriodicWorkPolicy.KEEP, test)
WorkManager.getInstance()!!.getStatusesByTag("test_worker")
.observe(this, Observer { workStatus ->
if (workStatus != null)
{
for(ws in workStatus)
{
Log.d(":dump2 id ", ws.id.toString())
Log.d(":dump2 tag", ws.tags.toString())
Log.d(":dump2 state", ws.state.toString())
}
}
})
this is the result in Logcat:
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 id: 5c6297f7-11d8-4f2f-a327-773672a7435c
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 tag: [test_worker, com.mockie.daikokuten.sync.workers.TestWorker]
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 state: ENQUEUED
For your periodic work request you should see
ENQUEUED - RUNNING - ENQUEUED
where the latter ENQUEUED is the state of the next work request.
You might get very briefly a SUCCEEDED between RUNNING and ENQUEUED, but I have never seen that.
For a onetime work request you see
ENQUEUED - RUNNING - SUCCEEDED
or whatever you return in doWork().
(Android 8.1 API 27, 1.0.0-alpha04)
This is for anyone who is having trouble getting their output data from periodic work.
It's more like a hack.
In your Worker, just define a static mutable Live Data.
At the place where you observe your work's state, observe this live data when your state turns to "RUNNING".
Here's a template :
The actual Worker:
public class SomeWorker extends Worker{
//This live data can be of any type. I'm setting Boolean
Public static MutableLiveData<Boolean> outputObservable = new MutableLiveData();
private boolean output_boolean;
try{
//Do you work here post your result to the live data
output_boolean = SomeTaskThatReturnsABoolean();
outputObservable.postValue(output_boolean);
return Result.Success();
}catch(Exception e){
e.printStackTrace();
outputObservable.postValue(!output_boolean);
return Result.Failure();
}
}
Your activity that observes this worker's info:
//In YourActivity class inside OnCreate
mWorkManager.getWorkInfoForUniqueWorkLiveData(YOUR_TAG).observe (this,
new Observer<List<WorkInfo>>(){
#Override
public void onChanged(#Nullable List<WorkInfo> workInfos) {
if(workInfos!=null && (!(workInfos.isEmpty()))) {
WorkInfo info = workInfos.get(0);
switch(info.getState()){
case ENQUEUED:
break;
case RUNNING:
SomeWorker.outputObservable.observe(YourActivity.this,
new Observer<Boolean>(){
#Override
public void onChanged(#Nullable Boolean aBoolean) {
//EDIT: Remove the observer of the worker otherwise
//before execution of your below code, the observation might switch
mWorkManager.getWorkInfoForUniqueWorkLiveData(YOUR_TAG).removeObservers(YourActivity.this);
if(aBoolean)
//Do whatever you have to if it's true
else
//Do whatever you have to if it's false
}
}
);
}
}
}
}
);
In this way you can observe your results when the state of the work is under running, before it gets switched back to enqueued.
The above answer is correct. For PeriodicWork you should see:
ENQUEUED -> RUNNING -> ENQUEUED
However, there is a bug in alpha04 which causes PeriodicWork to not run on API >= 23. This will be fixed in alpha05. For more info take a look at https://issuetracker.google.com/issues/111195153.
IMPORTANT: As of a couple of days ago: alpha05 has shipped. This bug is fixed.

Check if WorkRequest has been previously enquequed by WorkManager 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);

Android RxJava Cancel the old request

I am new to Rxjava and exploring the possibilities and need help in below described scenario.
Step 1 : Swipe the View
Step 2 : Make an API Call to fetch Data
The above steps are repeated for the number of Views.
Problem :
API_CALL_1 fetching the View_1 Results
User swipes a View_2
API_CALL_2 fetching the View_2 Results
View_1 results are returned and populated in View_2 along with View_2 results
I need to cancel the API_CALL_1 request when the API_CALL_2 request.
Thanks.
Class member:
Subscription mSubscription;
Your API call:
if(subscription != null && !subscription.isUnsubscribed()){
subscription.unsubscribe();
subscription = null;
}
subscription = doApiCall("some_id")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Object>() {
#Override
public void call(Object o) {
}
}, new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
}
});
So, the main idea of it, that you have to unsubscribe from previous call:
if(subscription != null && !subscription.isUnsubscribed()){
subscription.unsubscribe();
subscription = null;
}
Before starting new call
a better way is to use switchMap to cancel previous request
You are able to provide 'cancel' logic by using 'switchMap' operator. For example, you want to start/stop loading (Code written in Kotlin).
val loadingObservable = Observable.timer(10, TimeUnit.SECONDS)
val load = PublishSubject.create<Boolean>()
val resultObservable = load.switchMap { if(it) loadingObservable else Observable.never() }
Explanation:
'loadingObservable' is a timer that simulate long running operation
'load' is a command from user to start or stop loading. If you want
to start loading - send True else False
'switchMap' operator is
designed to dispose previous observable. So it you send True it will
start loading. If you send False it will dispose (cancel) previous
observable.

Categories

Resources